diff options
| -rw-r--r-- | Cargo.lock | 1 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/client.rs | 31 | ||||
| -rw-r--r-- | src/git.rs | 93 | ||||
| -rw-r--r-- | src/login.rs | 374 | ||||
| -rw-r--r-- | src/main.rs | 7 | ||||
| -rw-r--r-- | src/repo_ref.rs | 14 | ||||
| -rw-r--r-- | src/sub_commands/init.rs | 2 | ||||
| -rw-r--r-- | src/sub_commands/login.rs | 22 | ||||
| -rw-r--r-- | src/sub_commands/push.rs | 2 | ||||
| -rw-r--r-- | src/sub_commands/send.rs | 12 | ||||
| -rw-r--r-- | test_utils/src/lib.rs | 5 | ||||
| -rw-r--r-- | tests/login.rs | 314 |
13 files changed, 674 insertions, 204 deletions
| @@ -1761,6 +1761,7 @@ dependencies = [ | |||
| 1761 | "nostr", | 1761 | "nostr", |
| 1762 | "nostr-database", | 1762 | "nostr-database", |
| 1763 | "nostr-sdk", | 1763 | "nostr-sdk", |
| 1764 | "nostr-signer", | ||
| 1764 | "nostr-sqlite", | 1765 | "nostr-sqlite", |
| 1765 | "once_cell", | 1766 | "once_cell", |
| 1766 | "passwords", | 1767 | "passwords", |
| @@ -26,6 +26,7 @@ keyring = "2.0.5" | |||
| 26 | nostr = "0.32.0" | 26 | nostr = "0.32.0" |
| 27 | nostr-database = "0.32.0" | 27 | nostr-database = "0.32.0" |
| 28 | nostr-sdk = "0.32.0" | 28 | nostr-sdk = "0.32.0" |
| 29 | nostr-signer = "0.32.0" | ||
| 29 | nostr-sqlite = "0.32.0" | 30 | nostr-sqlite = "0.32.0" |
| 30 | passwords = "3.1.13" | 31 | passwords = "3.1.13" |
| 31 | scrypt = "0.11.0" | 32 | scrypt = "0.11.0" |
diff --git a/src/client.rs b/src/client.rs index 9dba528..44abb29 100644 --- a/src/client.rs +++ b/src/client.rs | |||
| @@ -19,7 +19,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; | |||
| 19 | #[cfg(test)] | 19 | #[cfg(test)] |
| 20 | use mockall::*; | 20 | use mockall::*; |
| 21 | use nostr::Event; | 21 | use nostr::Event; |
| 22 | use nostr_sdk::NostrSigner; | 22 | use nostr_sdk::{EventBuilder, NostrSigner}; |
| 23 | 23 | ||
| 24 | #[allow(clippy::struct_field_names)] | 24 | #[allow(clippy::struct_field_names)] |
| 25 | pub struct Client { | 25 | pub struct Client { |
| @@ -292,3 +292,32 @@ fn get_dedup_events(relay_results: Vec<Result<Vec<nostr::Event>>>) -> Vec<Event> | |||
| 292 | } | 292 | } |
| 293 | dedup_events | 293 | dedup_events |
| 294 | } | 294 | } |
| 295 | |||
| 296 | pub async fn sign_event(event_builder: EventBuilder, signer: &NostrSigner) -> Result<nostr::Event> { | ||
| 297 | if signer.r#type().eq(&nostr_signer::NostrSignerType::NIP46) { | ||
| 298 | let term = console::Term::stderr(); | ||
| 299 | term.write_line("signing event with remote signer...")?; | ||
| 300 | let event = signer | ||
| 301 | .sign_event_builder(event_builder) | ||
| 302 | .await | ||
| 303 | .context("failed to sign event")?; | ||
| 304 | term.clear_last_lines(1)?; | ||
| 305 | Ok(event) | ||
| 306 | } else { | ||
| 307 | signer | ||
| 308 | .sign_event_builder(event_builder) | ||
| 309 | .await | ||
| 310 | .context("failed to sign event") | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | pub async fn fetch_public_key(signer: &NostrSigner) -> Result<nostr::PublicKey> { | ||
| 315 | let term = console::Term::stderr(); | ||
| 316 | term.write_line("fetching npub from remote signer...")?; | ||
| 317 | let public_key = signer | ||
| 318 | .public_key() | ||
| 319 | .await | ||
| 320 | .context("failed to get npub from remote signer")?; | ||
| 321 | term.clear_last_lines(1)?; | ||
| 322 | Ok(public_key) | ||
| 323 | } | ||
| @@ -76,8 +76,9 @@ pub trait RepoActions { | |||
| 76 | ) -> Result<Vec<nostr::Event>>; | 76 | ) -> Result<Vec<nostr::Event>>; |
| 77 | fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>; | 77 | fn parse_starting_commits(&self, starting_commits: &str) -> Result<Vec<Sha1Hash>>; |
| 78 | fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>; | 78 | fn ancestor_of(&self, decendant: &Sha1Hash, ancestor: &Sha1Hash) -> Result<bool>; |
| 79 | fn get_git_config_item(&self, item: &str, global: bool) -> Result<Option<String>>; | 79 | fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>>; |
| 80 | fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; | 80 | fn save_git_config_item(&self, item: &str, value: &str, global: bool) -> Result<()>; |
| 81 | fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool>; | ||
| 81 | } | 82 | } |
| 82 | 83 | ||
| 83 | impl RepoActions for Repo { | 84 | impl RepoActions for Repo { |
| @@ -581,8 +582,15 @@ impl RepoActions for Repo { | |||
| 581 | } | 582 | } |
| 582 | } | 583 | } |
| 583 | 584 | ||
| 584 | fn get_git_config_item(&self, item: &str, global: bool) -> Result<Option<String>> { | 585 | /// setting global to None will suppliment local config with global items |
| 585 | match if global { | 586 | /// not in local |
| 587 | fn get_git_config_item(&self, item: &str, global: Option<bool>) -> Result<Option<String>> { | ||
| 588 | let just_global = if let Some(just_global) = global { | ||
| 589 | just_global | ||
| 590 | } else { | ||
| 591 | false | ||
| 592 | }; | ||
| 593 | match if just_global { | ||
| 586 | self.git_repo | 594 | self.git_repo |
| 587 | .config() | 595 | .config() |
| 588 | .context("cannot open git config")? | 596 | .context("cannot open git config")? |
| @@ -593,11 +601,22 @@ impl RepoActions for Repo { | |||
| 593 | } | 601 | } |
| 594 | .get_entry(item) | 602 | .get_entry(item) |
| 595 | { | 603 | { |
| 596 | Ok(item) => Ok(Some( | 604 | Ok(item) => { |
| 597 | item.value() | 605 | if let Some(global) = global { |
| 598 | .context("cannot find git config item")? | 606 | if item.level().eq(&git2::ConfigLevel::Local) { |
| 599 | .to_string(), | 607 | if global { |
| 600 | )), | 608 | bail!("only local repository login available") |
| 609 | } | ||
| 610 | } else if !global { | ||
| 611 | bail!("only global repository login available") | ||
| 612 | } | ||
| 613 | } | ||
| 614 | Ok(Some( | ||
| 615 | item.value() | ||
| 616 | .context("cannot find git config item")? | ||
| 617 | .to_string(), | ||
| 618 | )) | ||
| 619 | } | ||
| 601 | Err(_) => Ok(None), | 620 | Err(_) => Ok(None), |
| 602 | } | 621 | } |
| 603 | } | 622 | } |
| @@ -613,9 +632,33 @@ impl RepoActions for Repo { | |||
| 613 | self.git_repo.config().context("cannot open git config")? | 632 | self.git_repo.config().context("cannot open git config")? |
| 614 | } | 633 | } |
| 615 | .set_str(item, value) | 634 | .set_str(item, value) |
| 616 | .context("cannot set git config value")?; | 635 | .context(format!( |
| 636 | "cannot set {} git config item {}", | ||
| 637 | if global { "global" } else { "local" }, | ||
| 638 | item | ||
| 639 | ))?; | ||
| 617 | Ok(()) | 640 | Ok(()) |
| 618 | } | 641 | } |
| 642 | |||
| 643 | /// returns false if item doesn't exist | ||
| 644 | fn remove_git_config_item(&self, item: &str, global: bool) -> Result<bool> { | ||
| 645 | if self.get_git_config_item(item, Some(global))?.is_none() { | ||
| 646 | Ok(false) | ||
| 647 | } else { | ||
| 648 | if global { | ||
| 649 | self.git_repo | ||
| 650 | .config() | ||
| 651 | .context("cannot open git config")? | ||
| 652 | .open_global() | ||
| 653 | .context("cannot open global git config")? | ||
| 654 | } else { | ||
| 655 | self.git_repo.config().context("cannot open git config")? | ||
| 656 | } | ||
| 657 | .remove(item) | ||
| 658 | .context("cannot remove existing git config item")?; | ||
| 659 | Ok(true) | ||
| 660 | } | ||
| 661 | } | ||
| 619 | } | 662 | } |
| 620 | 663 | ||
| 621 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { | 664 | fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { |
| @@ -849,7 +892,9 @@ mod tests { | |||
| 849 | let git_repo = Repo::from_path(&test_repo.dir)?; | 892 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 850 | git_repo.save_git_config_item("test.item", "testvalue", false)?; | 893 | git_repo.save_git_config_item("test.item", "testvalue", false)?; |
| 851 | assert_eq!( | 894 | assert_eq!( |
| 852 | git_repo.get_git_config_item("test.item", false)?.unwrap(), | 895 | git_repo |
| 896 | .get_git_config_item("test.item", Some(false))? | ||
| 897 | .unwrap(), | ||
| 853 | "testvalue", | 898 | "testvalue", |
| 854 | ); | 899 | ); |
| 855 | Ok(()) | 900 | Ok(()) |
| @@ -859,7 +904,10 @@ mod tests { | |||
| 859 | fn get_git_config_item_returns_none_if_not_present() -> Result<()> { | 904 | fn get_git_config_item_returns_none_if_not_present() -> Result<()> { |
| 860 | let test_repo = GitTestRepo::default(); | 905 | let test_repo = GitTestRepo::default(); |
| 861 | let git_repo = Repo::from_path(&test_repo.dir)?; | 906 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 862 | assert_eq!(git_repo.get_git_config_item("test.item", false)?, None); | 907 | assert_eq!( |
| 908 | git_repo.get_git_config_item("test.item", Some(false))?, | ||
| 909 | None | ||
| 910 | ); | ||
| 863 | Ok(()) | 911 | Ok(()) |
| 864 | } | 912 | } |
| 865 | 913 | ||
| @@ -869,11 +917,32 @@ mod tests { | |||
| 869 | let git_repo = Repo::from_path(&test_repo.dir)?; | 917 | let git_repo = Repo::from_path(&test_repo.dir)?; |
| 870 | git_repo.save_git_config_item("test.item", "", false)?; | 918 | git_repo.save_git_config_item("test.item", "", false)?; |
| 871 | assert_eq!( | 919 | assert_eq!( |
| 872 | git_repo.get_git_config_item("test.item", false)?, | 920 | git_repo.get_git_config_item("test.item", Some(false))?, |
| 873 | Some("".to_string()), | 921 | Some("".to_string()), |
| 874 | ); | 922 | ); |
| 875 | Ok(()) | 923 | Ok(()) |
| 876 | } | 924 | } |
| 925 | |||
| 926 | #[test] | ||
| 927 | fn remove_local_git_config_item() -> Result<()> { | ||
| 928 | let test_repo = GitTestRepo::default(); | ||
| 929 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 930 | git_repo.save_git_config_item("test.item", "testvalue", false)?; | ||
| 931 | assert!(git_repo.remove_git_config_item("test.item", false)?); | ||
| 932 | assert_eq!( | ||
| 933 | git_repo.get_git_config_item("test.item", Some(false))?, | ||
| 934 | None, | ||
| 935 | ); | ||
| 936 | Ok(()) | ||
| 937 | } | ||
| 938 | |||
| 939 | #[test] | ||
| 940 | fn remove_git_config_item_returns_false_if_item_wasnt_set() -> Result<()> { | ||
| 941 | let test_repo = GitTestRepo::default(); | ||
| 942 | let git_repo = Repo::from_path(&test_repo.dir)?; | ||
| 943 | assert!(!(git_repo.remove_git_config_item("test.item", false)?)); | ||
| 944 | Ok(()) | ||
| 945 | } | ||
| 877 | } | 946 | } |
| 878 | 947 | ||
| 879 | #[test] | 948 | #[test] |
diff --git a/src/login.rs b/src/login.rs index e1669c1..218a079 100644 --- a/src/login.rs +++ b/src/login.rs | |||
| @@ -1,11 +1,13 @@ | |||
| 1 | use std::str::FromStr; | 1 | use std::{fs::create_dir_all, str::FromStr, time::Duration}; |
| 2 | 2 | ||
| 3 | use anyhow::{bail, Context, Result}; | 3 | use anyhow::{bail, Context, Result}; |
| 4 | use nostr::PublicKey; | 4 | use nostr::{nips::nip46::NostrConnectURI, PublicKey}; |
| 5 | use nostr_database::Order; | 5 | use nostr_database::Order; |
| 6 | use nostr_sdk::{ | 6 | use nostr_sdk::{ |
| 7 | Alphabet, FromBech32, JsonUtil, Kind, NostrDatabase, NostrSigner, SingleLetterTag, ToBech32, | 7 | Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrDatabase, NostrSigner, SingleLetterTag, |
| 8 | ToBech32, | ||
| 8 | }; | 9 | }; |
| 10 | use nostr_signer::Nip46Signer; | ||
| 9 | use nostr_sqlite::SQLiteDatabase; | 11 | use nostr_sqlite::SQLiteDatabase; |
| 10 | 12 | ||
| 11 | #[cfg(not(test))] | 13 | #[cfg(not(test))] |
| @@ -16,7 +18,7 @@ use crate::{ | |||
| 16 | cli_interactor::{ | 18 | cli_interactor::{ |
| 17 | Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, | 19 | Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, |
| 18 | }, | 20 | }, |
| 19 | client::Connect, | 21 | client::{fetch_public_key, Connect}, |
| 20 | config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays}, | 22 | config::{get_dirs, UserMetadata, UserRef, UserRelayRef, UserRelays}, |
| 21 | git::{Repo, RepoActions}, | 23 | git::{Repo, RepoActions}, |
| 22 | key_handling::encryption::{decrypt_key, encrypt_key}, | 24 | key_handling::encryption::{decrypt_key, encrypt_key}, |
| @@ -25,14 +27,25 @@ use crate::{ | |||
| 25 | /// handles the encrpytion and storage of key material | 27 | /// handles the encrpytion and storage of key material |
| 26 | pub async fn launch( | 28 | pub async fn launch( |
| 27 | git_repo: &Repo, | 29 | git_repo: &Repo, |
| 30 | bunker_uri: &Option<String>, | ||
| 31 | bunker_app_key: &Option<String>, | ||
| 28 | nsec: &Option<String>, | 32 | nsec: &Option<String>, |
| 29 | password: &Option<String>, | 33 | password: &Option<String>, |
| 30 | #[cfg(test)] client: Option<&MockConnect>, | 34 | #[cfg(test)] client: Option<&MockConnect>, |
| 31 | #[cfg(not(test))] client: Option<&Client>, | 35 | #[cfg(not(test))] client: Option<&Client>, |
| 32 | change_user: bool, | 36 | change_user: bool, |
| 33 | ) -> Result<(NostrSigner, UserRef)> { | 37 | ) -> Result<(NostrSigner, UserRef)> { |
| 34 | if let Ok(keys) = match get_keys_without_prompts(git_repo, nsec, password, change_user) { | 38 | if let Ok(signer) = match get_signer_without_prompts( |
| 35 | Ok(keys) => Ok(keys), | 39 | git_repo, |
| 40 | bunker_uri, | ||
| 41 | bunker_app_key, | ||
| 42 | nsec, | ||
| 43 | password, | ||
| 44 | change_user, | ||
| 45 | ) | ||
| 46 | .await | ||
| 47 | { | ||
| 48 | Ok(signer) => Ok(signer), | ||
| 36 | Err(error) => { | 49 | Err(error) => { |
| 37 | if error | 50 | if error |
| 38 | .to_string() | 51 | .to_string() |
| @@ -60,7 +73,7 @@ pub async fn launch( | |||
| 60 | .password(PromptPasswordParms::default().with_prompt("password")) | 73 | .password(PromptPasswordParms::default().with_prompt("password")) |
| 61 | .context("failed to get password input from interactor.password")?; | 74 | .context("failed to get password input from interactor.password")?; |
| 62 | if let Ok(keys) = get_keys_with_password(git_repo, &password) { | 75 | if let Ok(keys) = get_keys_with_password(git_repo, &password) { |
| 63 | break Ok(keys); | 76 | break Ok(NostrSigner::Keys(keys)); |
| 64 | } | 77 | } |
| 65 | println!("incorrect password"); | 78 | println!("incorrect password"); |
| 66 | } | 79 | } |
| @@ -73,9 +86,17 @@ pub async fn launch( | |||
| 73 | } | 86 | } |
| 74 | } { | 87 | } { |
| 75 | // get user ref | 88 | // get user ref |
| 76 | let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?; | 89 | let user_ref = get_user_details( |
| 90 | &signer | ||
| 91 | .public_key() | ||
| 92 | .await | ||
| 93 | .context("cannot get public key from signer")?, | ||
| 94 | client, | ||
| 95 | git_repo, | ||
| 96 | ) | ||
| 97 | .await?; | ||
| 77 | print_logged_in_as(&user_ref, client.is_none())?; | 98 | print_logged_in_as(&user_ref, client.is_none())?; |
| 78 | Ok((NostrSigner::Keys(keys), user_ref)) | 99 | Ok((signer, user_ref)) |
| 79 | } else { | 100 | } else { |
| 80 | fresh_login(git_repo, client, change_user).await | 101 | fresh_login(git_repo, client, change_user).await |
| 81 | } | 102 | } |
| @@ -95,18 +116,45 @@ fn print_logged_in_as(user_ref: &UserRef, offline_mode: bool) -> Result<()> { | |||
| 95 | Ok(()) | 116 | Ok(()) |
| 96 | } | 117 | } |
| 97 | 118 | ||
| 98 | fn get_keys_without_prompts( | 119 | async fn get_signer_without_prompts( |
| 99 | git_repo: &Repo, | 120 | git_repo: &Repo, |
| 121 | bunker_uri: &Option<String>, | ||
| 122 | bunker_app_key: &Option<String>, | ||
| 100 | nsec: &Option<String>, | 123 | nsec: &Option<String>, |
| 101 | password: &Option<String>, | 124 | password: &Option<String>, |
| 102 | save_local: bool, | 125 | save_local: bool, |
| 103 | ) -> Result<nostr::Keys> { | 126 | ) -> Result<NostrSigner> { |
| 104 | if let Some(nsec) = nsec { | 127 | if let Some(nsec) = nsec { |
| 105 | get_keys_from_nsec(git_repo, nsec, password, save_local) | 128 | Ok(NostrSigner::Keys(get_keys_from_nsec( |
| 129 | git_repo, nsec, password, save_local, | ||
| 130 | )?)) | ||
| 106 | } else if let Some(password) = password { | 131 | } else if let Some(password) = password { |
| 107 | get_keys_with_password(git_repo, password) | 132 | Ok(NostrSigner::Keys(get_keys_with_password( |
| 133 | git_repo, password, | ||
| 134 | )?)) | ||
| 135 | } else if let Some(bunker_uri) = bunker_uri { | ||
| 136 | if let Some(bunker_app_key) = bunker_app_key { | ||
| 137 | let signer = get_nip46_signer_from_uri_and_key(bunker_uri, bunker_app_key) | ||
| 138 | .await | ||
| 139 | .context("failed to connect with remote signer")?; | ||
| 140 | if save_local { | ||
| 141 | save_to_git_config( | ||
| 142 | git_repo, | ||
| 143 | &signer.public_key().await?.to_bech32()?, | ||
| 144 | &None, | ||
| 145 | &Some((bunker_uri.to_string(),bunker_app_key.to_string())), | ||
| 146 | false, | ||
| 147 | ) | ||
| 148 | .context("failed to save bunker details local git config nostr.bunker-uri and nostr.bunker-app-key")?; | ||
| 149 | } | ||
| 150 | Ok(signer) | ||
| 151 | } else { | ||
| 152 | bail!( | ||
| 153 | "bunker-app-key parameter must be provided alongside bunker-uri. if unknown, login interactively." | ||
| 154 | ) | ||
| 155 | } | ||
| 108 | } else if !save_local { | 156 | } else if !save_local { |
| 109 | get_keys_with_git_config_nsec_without_prompts(git_repo) | 157 | get_signer_with_git_config_nsec_or_bunker_without_prompts(git_repo).await |
| 110 | } else { | 158 | } else { |
| 111 | bail!("user wants prompts to specify new keys") | 159 | bail!("user wants prompts to specify new keys") |
| 112 | } | 160 | } |
| @@ -139,18 +187,82 @@ fn get_keys_from_nsec( | |||
| 139 | if let Some(password) = password { | 187 | if let Some(password) = password { |
| 140 | s = encrypt_key(&keys, password)?; | 188 | s = encrypt_key(&keys, password)?; |
| 141 | } | 189 | } |
| 142 | git_repo | 190 | save_to_git_config( |
| 143 | .save_git_config_item("nostr.nsec", &s, false) | 191 | git_repo, |
| 144 | .context("failed to save encrypted nsec in local git config nostr.nsec")?; | 192 | &keys.public_key().to_bech32()?, |
| 145 | git_repo.save_git_config_item("nostr.npub", &keys.public_key().to_bech32()?, false)?; | 193 | &Some(s), |
| 194 | &None, | ||
| 195 | false, | ||
| 196 | ) | ||
| 197 | .context("failed to save encrypted nsec in local git config nostr.nsec")?; | ||
| 146 | } | 198 | } |
| 147 | Ok(keys) | 199 | Ok(keys) |
| 148 | } | 200 | } |
| 149 | 201 | ||
| 202 | fn save_to_git_config( | ||
| 203 | git_repo: &Repo, | ||
| 204 | npub: &str, | ||
| 205 | nsec: &Option<String>, | ||
| 206 | bunker: &Option<(String, String)>, | ||
| 207 | global: bool, | ||
| 208 | ) -> Result<()> { | ||
| 209 | if let Err(error) = silently_save_to_git_config(git_repo, npub, nsec, bunker, global) { | ||
| 210 | println!( | ||
| 211 | "failed to save login details to {} git config", | ||
| 212 | if global { "global" } else { "local" } | ||
| 213 | ); | ||
| 214 | if let Some(nsec) = nsec { | ||
| 215 | if nsec.contains("ncryptsec") { | ||
| 216 | println!("manually set git config nostr.nsec to: {nsec}"); | ||
| 217 | } else { | ||
| 218 | println!("manually set git config nostr.nsec"); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | if let Some(bunker) = bunker { | ||
| 222 | println!("manually set git config as follows:"); | ||
| 223 | println!("nostr.bunker-uri: {}", bunker.0); | ||
| 224 | println!("nostr.bunker-app-key: {}", bunker.1); | ||
| 225 | } | ||
| 226 | Err(error) | ||
| 227 | } else { | ||
| 228 | println!( | ||
| 229 | "saved login details to {} git config", | ||
| 230 | if global { "global" } else { "local" } | ||
| 231 | ); | ||
| 232 | Ok(()) | ||
| 233 | } | ||
| 234 | } | ||
| 235 | fn silently_save_to_git_config( | ||
| 236 | git_repo: &Repo, | ||
| 237 | npub: &str, | ||
| 238 | nsec: &Option<String>, | ||
| 239 | bunker: &Option<(String, String)>, | ||
| 240 | global: bool, | ||
| 241 | ) -> Result<()> { | ||
| 242 | // must do this first otherwise it might remove the global items just added | ||
| 243 | if global { | ||
| 244 | git_repo.remove_git_config_item("nostr.npub", false)?; | ||
| 245 | git_repo.remove_git_config_item("nostr.nsec", false)?; | ||
| 246 | git_repo.remove_git_config_item("nostr.bunker-uri", false)?; | ||
| 247 | git_repo.remove_git_config_item("nostr.bunker-app-key", false)?; | ||
| 248 | } | ||
| 249 | if let Some(bunker) = bunker { | ||
| 250 | git_repo.remove_git_config_item("nostr.nsec", global)?; | ||
| 251 | git_repo.save_git_config_item("nostr.bunker-uri", &bunker.0, global)?; | ||
| 252 | git_repo.save_git_config_item("nostr.bunker-app-key", &bunker.1, global)?; | ||
| 253 | } | ||
| 254 | if let Some(nsec) = nsec { | ||
| 255 | git_repo.save_git_config_item("nostr.nsec", nsec, global)?; | ||
| 256 | git_repo.remove_git_config_item("nostr.bunker-uri", global)?; | ||
| 257 | git_repo.remove_git_config_item("nostr.bunker-app-key", global)?; | ||
| 258 | } | ||
| 259 | git_repo.save_git_config_item("nostr.npub", npub, global) | ||
| 260 | } | ||
| 261 | |||
| 150 | fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { | 262 | fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { |
| 151 | decrypt_key( | 263 | decrypt_key( |
| 152 | &git_repo | 264 | &git_repo |
| 153 | .get_git_config_item("nostr.nsec", false) | 265 | .get_git_config_item("nostr.nsec", None) |
| 154 | .context("failed get git config")? | 266 | .context("failed get git config")? |
| 155 | .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?, | 267 | .context("git config item nostr.nsec doesn't exist so cannot decrypt it")?, |
| 156 | password, | 268 | password, |
| @@ -158,15 +270,74 @@ fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys | |||
| 158 | .context("failed to decrypt stored nsec key with provided password") | 270 | .context("failed to decrypt stored nsec key with provided password") |
| 159 | } | 271 | } |
| 160 | 272 | ||
| 161 | fn get_keys_with_git_config_nsec_without_prompts(git_repo: &Repo) -> Result<nostr::Keys> { | 273 | async fn get_nip46_signer_from_uri_and_key(uri: &str, app_key: &str) -> Result<NostrSigner> { |
| 162 | let nsec = &git_repo | 274 | let term = console::Term::stderr(); |
| 163 | .get_git_config_item("nostr.nsec", false) | 275 | term.write_line("connecting to remote signer...")?; |
| 164 | .context("failed get git config")? | 276 | let uri = NostrConnectURI::parse(uri)?; |
| 165 | .context("git config item nostr.nsec doesn't exist")?; | 277 | let signer = NostrSigner::nip46( |
| 166 | if nsec.contains("ncryptsec") { | 278 | Nip46Signer::new( |
| 167 | bail!("git config item nostr.nsec is an ncryptsec") | 279 | uri, |
| 280 | nostr::Keys::from_str(app_key).context("invalid app key")?, | ||
| 281 | Duration::from_secs(30), | ||
| 282 | None, | ||
| 283 | ) | ||
| 284 | .await?, | ||
| 285 | ); | ||
| 286 | term.clear_last_lines(1)?; | ||
| 287 | Ok(signer) | ||
| 288 | } | ||
| 289 | |||
| 290 | async fn get_signer_with_git_config_nsec_or_bunker_without_prompts( | ||
| 291 | git_repo: &Repo, | ||
| 292 | ) -> Result<NostrSigner> { | ||
| 293 | if let Ok(local_nsec) = &git_repo | ||
| 294 | .get_git_config_item("nostr.nsec", Some(false)) | ||
| 295 | .context("failed get local git config")? | ||
| 296 | .context("git local config item nostr.nsec doesn't exist") | ||
| 297 | { | ||
| 298 | if local_nsec.contains("ncryptsec") { | ||
| 299 | bail!("git global config item nostr.nsec is an ncryptsec") | ||
| 300 | } | ||
| 301 | Ok(NostrSigner::Keys( | ||
| 302 | nostr::Keys::from_str(local_nsec).context("invalid nsec parameter")?, | ||
| 303 | )) | ||
| 304 | } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(false)) | ||
| 305 | { | ||
| 306 | get_nip46_signer_from_uri_and_key(&uri, &app_key).await | ||
| 307 | } else if let Ok(global_nsec) = &git_repo | ||
| 308 | .get_git_config_item("nostr.nsec", Some(true)) | ||
| 309 | .context("failed get global git config")? | ||
| 310 | .context("git global config item nostr.nsec doesn't exist") | ||
| 311 | { | ||
| 312 | if global_nsec.contains("ncryptsec") { | ||
| 313 | bail!("git global config item nostr.nsec is an ncryptsec") | ||
| 314 | } | ||
| 315 | Ok(NostrSigner::Keys( | ||
| 316 | nostr::Keys::from_str(global_nsec).context("invalid nsec parameter")?, | ||
| 317 | )) | ||
| 318 | } else if let Ok((uri, app_key)) = get_git_config_bunker_uri_and_app_key(git_repo, Some(true)) { | ||
| 319 | get_nip46_signer_from_uri_and_key(&uri, &app_key).await | ||
| 320 | } else { | ||
| 321 | bail!("cannot get nsec or bunker from git config") | ||
| 168 | } | 322 | } |
| 169 | nostr::Keys::from_str(nsec).context("invalid nsec parameter") | 323 | } |
| 324 | |||
| 325 | fn get_git_config_bunker_uri_and_app_key( | ||
| 326 | git_repo: &Repo, | ||
| 327 | global: Option<bool>, | ||
| 328 | ) -> Result<(String, String)> { | ||
| 329 | Ok(( | ||
| 330 | git_repo | ||
| 331 | .get_git_config_item("nostr.bunker_url", global) | ||
| 332 | .context("failed get local git config")? | ||
| 333 | .context("git local config item nostr.bunker_url doesn't exist")? | ||
| 334 | .to_string(), | ||
| 335 | git_repo | ||
| 336 | .get_git_config_item("nostr.bunker-app-key", global) | ||
| 337 | .context("failed get local git config")? | ||
| 338 | .context("git local config item nostr.bunker-app-key doesn't exist")? | ||
| 339 | .to_string(), | ||
| 340 | )) | ||
| 170 | } | 341 | } |
| 171 | 342 | ||
| 172 | async fn fresh_login( | 343 | async fn fresh_login( |
| @@ -175,50 +346,119 @@ async fn fresh_login( | |||
| 175 | #[cfg(not(test))] client: Option<&Client>, | 346 | #[cfg(not(test))] client: Option<&Client>, |
| 176 | always_save: bool, | 347 | always_save: bool, |
| 177 | ) -> Result<(NostrSigner, UserRef)> { | 348 | ) -> Result<(NostrSigner, UserRef)> { |
| 349 | let mut public_key: Option<PublicKey> = None; | ||
| 178 | // prompt for nsec | 350 | // prompt for nsec |
| 179 | let mut prompt = "login with nsec"; | 351 | let mut prompt = "login with bunker uri / nsec"; |
| 180 | let keys = loop { | 352 | let signer = loop { |
| 181 | match nostr::Keys::from_str( | 353 | let input = Interactor::default() |
| 182 | &Interactor::default() | 354 | .input(PromptInputParms::default().with_prompt(prompt)) |
| 183 | .input(PromptInputParms::default().with_prompt(prompt)) | 355 | .context("failed to get nsec input from interactor")?; |
| 184 | .context("failed to get nsec input from interactor")?, | 356 | match nostr::Keys::from_str(&input) { |
| 185 | ) { | ||
| 186 | Ok(key) => { | 357 | Ok(key) => { |
| 187 | break key; | 358 | if let Err(error) = save_keys(git_repo, &key, always_save) { |
| 188 | } | 359 | println!("{error}"); |
| 189 | Err(_) => { | 360 | } |
| 190 | prompt = "invalid nsec. try again with nsec (or hex private key)"; | 361 | break NostrSigner::Keys(key); |
| 191 | } | 362 | } |
| 363 | Err(_) => match NostrConnectURI::parse(&input) { | ||
| 364 | Ok(_) => { | ||
| 365 | let app_key = Keys::generate().secret_key()?.to_secret_hex(); | ||
| 366 | match get_nip46_signer_from_uri_and_key(&input, &app_key).await { | ||
| 367 | Ok(signer) => { | ||
| 368 | let pub_key = fetch_public_key(&signer).await?; | ||
| 369 | if let Err(error) = | ||
| 370 | save_bunker(git_repo, &pub_key, &input, &app_key, always_save) | ||
| 371 | { | ||
| 372 | println!("{error}"); | ||
| 373 | } | ||
| 374 | public_key = Some(pub_key); | ||
| 375 | break signer; | ||
| 376 | } | ||
| 377 | Err(_) => { | ||
| 378 | prompt = "invalid. try again with nostr address / nsec"; | ||
| 379 | } | ||
| 380 | } | ||
| 381 | } | ||
| 382 | Err(_) => { | ||
| 383 | prompt = "invalid. try again with nostr address / nsec"; | ||
| 384 | } | ||
| 385 | }, | ||
| 192 | } | 386 | } |
| 193 | }; | 387 | }; |
| 388 | let public_key = if let Some(public_key) = public_key { | ||
| 389 | public_key | ||
| 390 | } else { | ||
| 391 | signer.public_key().await? | ||
| 392 | }; | ||
| 194 | // lookup profile | 393 | // lookup profile |
| 195 | // save keys | 394 | let user_ref = get_user_details(&public_key, client, git_repo).await?; |
| 196 | if let Err(error) = save_keys(git_repo, &keys, always_save) { | ||
| 197 | println!("{error}"); | ||
| 198 | } | ||
| 199 | let user_ref = get_user_details(&keys.public_key(), client, git_repo).await?; | ||
| 200 | print_logged_in_as(&user_ref, client.is_none())?; | 395 | print_logged_in_as(&user_ref, client.is_none())?; |
| 201 | Ok((NostrSigner::Keys(keys), user_ref)) | 396 | Ok((signer, user_ref)) |
| 202 | } | 397 | } |
| 203 | 398 | ||
| 204 | fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { | 399 | fn save_bunker( |
| 205 | let store = always_save | 400 | git_repo: &Repo, |
| 401 | public_key: &PublicKey, | ||
| 402 | uri: &str, | ||
| 403 | app_key: &str, | ||
| 404 | always_save: bool, | ||
| 405 | ) -> Result<()> { | ||
| 406 | if always_save | ||
| 206 | || Interactor::default() | 407 | || Interactor::default() |
| 207 | .confirm(PromptConfirmParms::default().with_prompt("save login details?"))?; | 408 | .confirm(PromptConfirmParms::default().with_prompt("save login details?"))? |
| 409 | { | ||
| 410 | let global = !Interactor::default().confirm( | ||
| 411 | PromptConfirmParms::default() | ||
| 412 | .with_prompt("just for this repository?") | ||
| 413 | .with_default(false), | ||
| 414 | )?; | ||
| 415 | let npub = public_key.to_bech32()?; | ||
| 416 | if let Err(error) = save_to_git_config( | ||
| 417 | git_repo, | ||
| 418 | &npub, | ||
| 419 | &None, | ||
| 420 | &Some((uri.to_string(), app_key.to_string())), | ||
| 421 | global, | ||
| 422 | ) { | ||
| 423 | if global { | ||
| 424 | if Interactor::default().confirm( | ||
| 425 | PromptConfirmParms::default() | ||
| 426 | .with_prompt("save in repository git config?") | ||
| 427 | .with_default(true), | ||
| 428 | )? { | ||
| 429 | save_to_git_config( | ||
| 430 | git_repo, | ||
| 431 | &npub, | ||
| 432 | &None, | ||
| 433 | &Some((uri.to_string(), app_key.to_string())), | ||
| 434 | false, | ||
| 435 | )?; | ||
| 436 | } | ||
| 437 | } else { | ||
| 438 | Err(error)?; | ||
| 439 | } | ||
| 440 | }; | ||
| 441 | } | ||
| 442 | Ok(()) | ||
| 443 | } | ||
| 208 | 444 | ||
| 209 | let global = !Interactor::default().confirm( | 445 | fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { |
| 210 | PromptConfirmParms::default() | 446 | if always_save |
| 211 | .with_prompt("just for this repository?") | 447 | || Interactor::default() |
| 212 | .with_default(false), | 448 | .confirm(PromptConfirmParms::default().with_prompt("save login details?"))? |
| 213 | )?; | 449 | { |
| 450 | let global = !Interactor::default().confirm( | ||
| 451 | PromptConfirmParms::default() | ||
| 452 | .with_prompt("just for this repository?") | ||
| 453 | .with_default(false), | ||
| 454 | )?; | ||
| 214 | 455 | ||
| 215 | let encrypt = Interactor::default().confirm( | 456 | let encrypt = Interactor::default().confirm( |
| 216 | PromptConfirmParms::default() | 457 | PromptConfirmParms::default() |
| 217 | .with_prompt("require password?") | 458 | .with_prompt("require password?") |
| 218 | .with_default(false), | 459 | .with_default(false), |
| 219 | )?; | 460 | )?; |
| 220 | 461 | ||
| 221 | if store { | ||
| 222 | let npub = keys.public_key().to_bech32()?; | 462 | let npub = keys.public_key().to_bech32()?; |
| 223 | let nsec_string = if encrypt { | 463 | let nsec_string = if encrypt { |
| 224 | let password = Interactor::default() | 464 | let password = Interactor::default() |
| @@ -233,22 +473,20 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<( | |||
| 233 | keys.secret_key()?.to_bech32()? | 473 | keys.secret_key()?.to_bech32()? |
| 234 | }; | 474 | }; |
| 235 | 475 | ||
| 236 | if let Err(error) = git_repo.save_git_config_item("nostr.nsec", &nsec_string, global) { | 476 | if let Err(error) = |
| 477 | save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, global) | ||
| 478 | { | ||
| 237 | if global { | 479 | if global { |
| 238 | println!("failed to edit global git config instead"); | ||
| 239 | if Interactor::default().confirm( | 480 | if Interactor::default().confirm( |
| 240 | PromptConfirmParms::default() | 481 | PromptConfirmParms::default() |
| 241 | .with_prompt("save in repository git config?") | 482 | .with_prompt("save in repository git config?") |
| 242 | .with_default(true), | 483 | .with_default(true), |
| 243 | )? { | 484 | )? { |
| 244 | git_repo.save_git_config_item("nostr.nsec", &nsec_string, false)?; | 485 | save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?; |
| 245 | git_repo.save_git_config_item("nostr.npub", &npub, false)?; | ||
| 246 | } | 486 | } |
| 247 | } else { | 487 | } else { |
| 248 | bail!(error) | 488 | Err(error)?; |
| 249 | } | 489 | } |
| 250 | } else { | ||
| 251 | git_repo.save_git_config_item("nostr.npub", &npub, global)?; | ||
| 252 | }; | 490 | }; |
| 253 | }; | 491 | }; |
| 254 | Ok(()) | 492 | Ok(()) |
| @@ -256,7 +494,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<( | |||
| 256 | 494 | ||
| 257 | fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { | 495 | fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { |
| 258 | git_repo | 496 | git_repo |
| 259 | .get_git_config_item(name, false) | 497 | .get_git_config_item(name, None) |
| 260 | .context("failed get git config")? | 498 | .context("failed get git config")? |
| 261 | .context(format!("git config item {name} doesn't exist")) | 499 | .context(format!("git config item {name} doesn't exist")) |
| 262 | } | 500 | } |
| @@ -350,6 +588,10 @@ async fn get_user_details( | |||
| 350 | println!("searching for profile and relay updates..."); | 588 | println!("searching for profile and relay updates..."); |
| 351 | } | 589 | } |
| 352 | let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() { | 590 | let database = SQLiteDatabase::open(if std::env::var("NGITTEST").is_err() { |
| 591 | create_dir_all(get_dirs()?.config_dir()).context(format!( | ||
| 592 | "cannot create cache directory in: {:?}", | ||
| 593 | get_dirs()?.config_dir() | ||
| 594 | ))?; | ||
| 353 | get_dirs()?.config_dir().join("cache.sqlite") | 595 | get_dirs()?.config_dir().join("cache.sqlite") |
| 354 | } else { | 596 | } else { |
| 355 | git_repo.get_path()?.join(".git/test-global-cache.sqlite") | 597 | git_repo.get_path()?.join(".git/test-global-cache.sqlite") |
diff --git a/src/main.rs b/src/main.rs index 30ecea3..9f53084 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -1,4 +1,5 @@ | |||
| 1 | #![cfg_attr(not(test), warn(clippy::pedantic))] | 1 | #![cfg_attr(not(test), warn(clippy::pedantic))] |
| 2 | #![allow(clippy::large_futures)] | ||
| 2 | #![cfg_attr(not(test), warn(clippy::expect_used))] | 3 | #![cfg_attr(not(test), warn(clippy::expect_used))] |
| 3 | 4 | ||
| 4 | use anyhow::Result; | 5 | use anyhow::Result; |
| @@ -19,6 +20,12 @@ mod sub_commands; | |||
| 19 | pub struct Cli { | 20 | pub struct Cli { |
| 20 | #[command(subcommand)] | 21 | #[command(subcommand)] |
| 21 | command: Commands, | 22 | command: Commands, |
| 23 | /// remote signer address | ||
| 24 | #[arg(long, global = true)] | ||
| 25 | bunker_uri: Option<String>, | ||
| 26 | /// remote signer app secret key | ||
| 27 | #[arg(long, global = true)] | ||
| 28 | bunker_app_key: Option<String>, | ||
| 22 | /// nsec or hex private key | 29 | /// nsec or hex private key |
| 23 | #[arg(short, long, global = true)] | 30 | #[arg(short, long, global = true)] |
| 24 | nsec: Option<String>, | 31 | nsec: Option<String>, |
diff --git a/src/repo_ref.rs b/src/repo_ref.rs index 2b0d024..426640f 100644 --- a/src/repo_ref.rs +++ b/src/repo_ref.rs | |||
| @@ -11,7 +11,7 @@ use crate::client::Client; | |||
| 11 | use crate::client::MockConnect; | 11 | use crate::client::MockConnect; |
| 12 | use crate::{ | 12 | use crate::{ |
| 13 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, | 13 | cli_interactor::{Interactor, InteractorPrompt, PromptInputParms}, |
| 14 | client::Connect, | 14 | client::{sign_event, Connect}, |
| 15 | git::{Repo, RepoActions}, | 15 | git::{Repo, RepoActions}, |
| 16 | }; | 16 | }; |
| 17 | 17 | ||
| @@ -95,8 +95,8 @@ pub static REPO_REF_KIND: u16 = 30_617; | |||
| 95 | 95 | ||
| 96 | impl RepoRef { | 96 | impl RepoRef { |
| 97 | pub async fn to_event(&self, signer: &NostrSigner) -> Result<nostr::Event> { | 97 | pub async fn to_event(&self, signer: &NostrSigner) -> Result<nostr::Event> { |
| 98 | signer | 98 | sign_event( |
| 99 | .sign_event_builder(nostr_sdk::EventBuilder::new( | 99 | nostr_sdk::EventBuilder::new( |
| 100 | nostr::event::Kind::Custom(REPO_REF_KIND), | 100 | nostr::event::Kind::Custom(REPO_REF_KIND), |
| 101 | "", | 101 | "", |
| 102 | [ | 102 | [ |
| @@ -152,9 +152,11 @@ impl RepoRef { | |||
| 152 | // code languages and hashtags | 152 | // code languages and hashtags |
| 153 | ] | 153 | ] |
| 154 | .concat(), | 154 | .concat(), |
| 155 | )) | 155 | ), |
| 156 | .await | 156 | signer, |
| 157 | .context("failed to create repository reference event") | 157 | ) |
| 158 | .await | ||
| 159 | .context("failed to create repository reference event") | ||
| 158 | } | 160 | } |
| 159 | } | 161 | } |
| 160 | 162 | ||
diff --git a/src/sub_commands/init.rs b/src/sub_commands/init.rs index 4afe83c..57785db 100644 --- a/src/sub_commands/init.rs +++ b/src/sub_commands/init.rs | |||
| @@ -61,6 +61,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 61 | 61 | ||
| 62 | let (signer, user_ref) = login::launch( | 62 | let (signer, user_ref) = login::launch( |
| 63 | &git_repo, | 63 | &git_repo, |
| 64 | &cli_args.bunker_uri, | ||
| 65 | &cli_args.bunker_app_key, | ||
| 64 | &cli_args.nsec, | 66 | &cli_args.nsec, |
| 65 | &cli_args.password, | 67 | &cli_args.password, |
| 66 | Some(&client), | 68 | Some(&client), |
diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs index e71d431..6f49ba8 100644 --- a/src/sub_commands/login.rs +++ b/src/sub_commands/login.rs | |||
| @@ -17,7 +17,16 @@ pub struct SubCommandArgs { | |||
| 17 | pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { | 17 | pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { |
| 18 | let git_repo = Repo::discover().context("cannot find a git repository")?; | 18 | let git_repo = Repo::discover().context("cannot find a git repository")?; |
| 19 | if command_args.offline { | 19 | if command_args.offline { |
| 20 | login::launch(&git_repo, &args.nsec, &args.password, None, true).await?; | 20 | login::launch( |
| 21 | &git_repo, | ||
| 22 | &args.bunker_uri, | ||
| 23 | &args.bunker_app_key, | ||
| 24 | &args.nsec, | ||
| 25 | &args.password, | ||
| 26 | None, | ||
| 27 | true, | ||
| 28 | ) | ||
| 29 | .await?; | ||
| 21 | Ok(()) | 30 | Ok(()) |
| 22 | } else { | 31 | } else { |
| 23 | #[cfg(not(test))] | 32 | #[cfg(not(test))] |
| @@ -25,7 +34,16 @@ pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { | |||
| 25 | #[cfg(test)] | 34 | #[cfg(test)] |
| 26 | let client = <MockConnect as std::default::Default>::default(); | 35 | let client = <MockConnect as std::default::Default>::default(); |
| 27 | 36 | ||
| 28 | login::launch(&git_repo, &args.nsec, &args.password, Some(&client), true).await?; | 37 | login::launch( |
| 38 | &git_repo, | ||
| 39 | &args.bunker_uri, | ||
| 40 | &args.bunker_app_key, | ||
| 41 | &args.nsec, | ||
| 42 | &args.password, | ||
| 43 | Some(&client), | ||
| 44 | true, | ||
| 45 | ) | ||
| 46 | .await?; | ||
| 29 | client.disconnect().await?; | 47 | client.disconnect().await?; |
| 30 | Ok(()) | 48 | Ok(()) |
| 31 | } | 49 | } |
diff --git a/src/sub_commands/push.rs b/src/sub_commands/push.rs index 92c1c18..3c471c0 100644 --- a/src/sub_commands/push.rs +++ b/src/sub_commands/push.rs | |||
| @@ -150,6 +150,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 150 | 150 | ||
| 151 | let (signer, user_ref) = login::launch( | 151 | let (signer, user_ref) = login::launch( |
| 152 | &git_repo, | 152 | &git_repo, |
| 153 | &cli_args.bunker_uri, | ||
| 154 | &cli_args.bunker_app_key, | ||
| 153 | &cli_args.nsec, | 155 | &cli_args.nsec, |
| 154 | &cli_args.password, | 156 | &cli_args.password, |
| 155 | Some(&client), | 157 | Some(&client), |
diff --git a/src/sub_commands/send.rs b/src/sub_commands/send.rs index 1d20e90..7c8f2ee 100644 --- a/src/sub_commands/send.rs +++ b/src/sub_commands/send.rs | |||
| @@ -19,7 +19,7 @@ use crate::{ | |||
| 19 | cli_interactor::{ | 19 | cli_interactor::{ |
| 20 | Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms, | 20 | Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptMultiChoiceParms, |
| 21 | }, | 21 | }, |
| 22 | client::Connect, | 22 | client::{sign_event, Connect}, |
| 23 | git::{Repo, RepoActions}, | 23 | git::{Repo, RepoActions}, |
| 24 | login, | 24 | login, |
| 25 | repo_ref::{self, RepoRef, REPO_REF_KIND}, | 25 | repo_ref::{self, RepoRef, REPO_REF_KIND}, |
| @@ -180,6 +180,8 @@ pub async fn launch(cli_args: &Cli, args: &SubCommandArgs) -> Result<()> { | |||
| 180 | }; | 180 | }; |
| 181 | let (signer, user_ref) = login::launch( | 181 | let (signer, user_ref) = login::launch( |
| 182 | &git_repo, | 182 | &git_repo, |
| 183 | &cli_args.bunker_uri, | ||
| 184 | &cli_args.bunker_app_key, | ||
| 183 | &cli_args.nsec, | 185 | &cli_args.nsec, |
| 184 | &cli_args.password, | 186 | &cli_args.password, |
| 185 | Some(&client), | 187 | Some(&client), |
| @@ -593,7 +595,7 @@ pub async fn generate_cover_letter_and_patch_events( | |||
| 593 | let mut events = vec![]; | 595 | let mut events = vec![]; |
| 594 | 596 | ||
| 595 | if let Some((title, description)) = cover_letter_title_description { | 597 | if let Some((title, description)) = cover_letter_title_description { |
| 596 | events.push(signer.sign_event_builder(EventBuilder::new( | 598 | events.push(sign_event(EventBuilder::new( |
| 597 | nostr::event::Kind::Custom(PATCH_KIND), | 599 | nostr::event::Kind::Custom(PATCH_KIND), |
| 598 | format!( | 600 | format!( |
| 599 | "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}", | 601 | "From {} Mon Sep 17 00:00:00 2001\nSubject: [PATCH 0/{}] {title}\n\n{description}", |
| @@ -656,7 +658,7 @@ pub async fn generate_cover_letter_and_patch_events( | |||
| 656 | .map(|pk| Tag::public_key(*pk)) | 658 | .map(|pk| Tag::public_key(*pk)) |
| 657 | .collect(), | 659 | .collect(), |
| 658 | ].concat(), | 660 | ].concat(), |
| 659 | )).await | 661 | ), signer).await |
| 660 | .context("failed to create cover-letter event")?); | 662 | .context("failed to create cover-letter event")?); |
| 661 | } | 663 | } |
| 662 | 664 | ||
| @@ -883,7 +885,7 @@ pub async fn generate_patch_event( | |||
| 883 | .context("failed to get parent commit")?; | 885 | .context("failed to get parent commit")?; |
| 884 | let relay_hint = repo_ref.relays.first().map(nostr::UncheckedUrl::from); | 886 | let relay_hint = repo_ref.relays.first().map(nostr::UncheckedUrl::from); |
| 885 | 887 | ||
| 886 | signer.sign_event_builder(EventBuilder::new( | 888 | sign_event(EventBuilder::new( |
| 887 | nostr::event::Kind::Custom(PATCH_KIND), | 889 | nostr::event::Kind::Custom(PATCH_KIND), |
| 888 | git_repo | 890 | git_repo |
| 889 | .make_patch_from_commit(commit,&series_count) | 891 | .make_patch_from_commit(commit,&series_count) |
| @@ -1000,7 +1002,7 @@ pub async fn generate_patch_event( | |||
| 1000 | ], | 1002 | ], |
| 1001 | ] | 1003 | ] |
| 1002 | .concat(), | 1004 | .concat(), |
| 1003 | )).await | 1005 | ), signer).await |
| 1004 | .context("failed to sign event") | 1006 | .context("failed to sign event") |
| 1005 | } | 1007 | } |
| 1006 | // TODO | 1008 | // TODO |
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs index 64e3fff..ba9d30c 100644 --- a/test_utils/src/lib.rs +++ b/test_utils/src/lib.rs | |||
| @@ -708,13 +708,14 @@ impl CliTester { | |||
| 708 | formatter: ColorfulTheme::default(), | 708 | formatter: ColorfulTheme::default(), |
| 709 | } | 709 | } |
| 710 | } | 710 | } |
| 711 | pub fn new_with_timeout<I, S>(timeout_ms: u64, args: I) -> Self | 711 | pub fn new_with_timeout_from_dir<I, S>(timeout_ms: u64, dir: &PathBuf, args: I) -> Self |
| 712 | where | 712 | where |
| 713 | I: IntoIterator<Item = S>, | 713 | I: IntoIterator<Item = S>, |
| 714 | S: AsRef<OsStr>, | 714 | S: AsRef<OsStr>, |
| 715 | { | 715 | { |
| 716 | Self { | 716 | Self { |
| 717 | rexpect_session: rexpect_with(args, timeout_ms).expect("rexpect to spawn new process"), | 717 | rexpect_session: rexpect_with_from_dir(dir, args, timeout_ms) |
| 718 | .expect("rexpect to spawn new process"), | ||
| 718 | formatter: ColorfulTheme::default(), | 719 | formatter: ColorfulTheme::default(), |
| 719 | } | 720 | } |
| 720 | } | 721 | } |
diff --git a/tests/login.rs b/tests/login.rs index 03fc2a9..166941e 100644 --- a/tests/login.rs +++ b/tests/login.rs | |||
| @@ -1,15 +1,17 @@ | |||
| 1 | use anyhow::Result; | 1 | use anyhow::Result; |
| 2 | use git::GitTestRepo; | ||
| 2 | use serial_test::serial; | 3 | use serial_test::serial; |
| 3 | use test_utils::*; | 4 | use test_utils::*; |
| 4 | 5 | ||
| 5 | static EXPECTED_NSEC_PROMPT: &str = "login with nsec"; | 6 | static EXPECTED_NSEC_PROMPT: &str = "login with bunker uri / nsec"; |
| 6 | static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?"; | 7 | static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?"; |
| 7 | static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?"; | 8 | static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?"; |
| 8 | static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password"; | 9 | static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password"; |
| 9 | static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password"; | 10 | static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password"; |
| 10 | 11 | ||
| 11 | fn standard_first_time_login_encrypting_nsec() -> Result<CliTester> { | 12 | fn standard_first_time_login_encrypting_nsec() -> Result<CliTester> { |
| 12 | let mut p = CliTester::new(["login", "--offline"]); | 13 | let test_repo = GitTestRepo::default(); |
| 14 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login", "--offline"]); | ||
| 13 | 15 | ||
| 14 | p.expect_input_eventually(EXPECTED_NSEC_PROMPT)? | 16 | p.expect_input_eventually(EXPECTED_NSEC_PROMPT)? |
| 15 | .succeeds_with(TEST_KEY_1_NSEC)?; | 17 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -57,7 +59,8 @@ mod with_relays { | |||
| 57 | ); | 59 | ); |
| 58 | 60 | ||
| 59 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 61 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 60 | let mut p = CliTester::new(["login"]); | 62 | let test_repo = GitTestRepo::default(); |
| 63 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); | ||
| 61 | 64 | ||
| 62 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 65 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 63 | .succeeds_with(TEST_KEY_1_NSEC)?; | 66 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -68,6 +71,8 @@ mod with_relays { | |||
| 68 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? | 71 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? |
| 69 | .succeeds_with(Some(false))?; | 72 | .succeeds_with(Some(false))?; |
| 70 | 73 | ||
| 74 | p.expect("saved login details to local git config\r\n")?; | ||
| 75 | |||
| 71 | p.expect("searching for profile and relay updates...\r\n")?; | 76 | p.expect("searching for profile and relay updates...\r\n")?; |
| 72 | 77 | ||
| 73 | p.expect_end_with("logged in as fred\r\n")?; | 78 | p.expect_end_with("logged in as fred\r\n")?; |
| @@ -94,7 +99,8 @@ mod with_relays { | |||
| 94 | ); | 99 | ); |
| 95 | 100 | ||
| 96 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 101 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 97 | let mut p = CliTester::new(["login"]); | 102 | let test_repo = GitTestRepo::default(); |
| 103 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); | ||
| 98 | 104 | ||
| 99 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 105 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 100 | .succeeds_with(TEST_KEY_1_NSEC)?; | 106 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -105,6 +111,8 @@ mod with_relays { | |||
| 105 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? | 111 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? |
| 106 | .succeeds_with(Some(false))?; | 112 | .succeeds_with(Some(false))?; |
| 107 | 113 | ||
| 114 | p.expect("saved login details to local git config\r\n")?; | ||
| 115 | |||
| 108 | p.expect("searching for profile and relay updates...\r\n")?; | 116 | p.expect("searching for profile and relay updates...\r\n")?; |
| 109 | 117 | ||
| 110 | p.expect("cannot extract account name from account metadata...\r\n")?; | 118 | p.expect("cannot extract account name from account metadata...\r\n")?; |
| @@ -406,7 +414,13 @@ mod with_relays { | |||
| 406 | ); | 414 | ); |
| 407 | 415 | ||
| 408 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 416 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 409 | let mut p = CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC]); | 417 | let test_repo = GitTestRepo::default(); |
| 418 | let mut p = CliTester::new_from_dir( | ||
| 419 | &test_repo.dir, | ||
| 420 | ["login", "--nsec", TEST_KEY_1_NSEC], | ||
| 421 | ); | ||
| 422 | |||
| 423 | p.expect("saved login details to local git config\r\n")?; | ||
| 410 | 424 | ||
| 411 | p.expect("searching for profile and relay updates...\r\n")?; | 425 | p.expect("searching for profile and relay updates...\r\n")?; |
| 412 | 426 | ||
| @@ -456,17 +470,24 @@ mod with_relays { | |||
| 456 | ); | 470 | ); |
| 457 | 471 | ||
| 458 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 472 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 459 | CliTester::new([ | 473 | let test_repo = GitTestRepo::default(); |
| 460 | "login", | 474 | CliTester::new_from_dir( |
| 461 | "--offline", | 475 | &test_repo.dir, |
| 462 | "--nsec", | 476 | [ |
| 463 | TEST_KEY_1_NSEC, | 477 | "login", |
| 464 | "--password", | 478 | "--offline", |
| 465 | TEST_PASSWORD, | 479 | "--nsec", |
| 466 | ]) | 480 | TEST_KEY_1_NSEC, |
| 481 | "--password", | ||
| 482 | TEST_PASSWORD, | ||
| 483 | ], | ||
| 484 | ) | ||
| 467 | .expect_end_eventually()?; | 485 | .expect_end_eventually()?; |
| 468 | 486 | ||
| 469 | let mut p = CliTester::new(["login", "--password", TEST_PASSWORD]); | 487 | let mut p = CliTester::new_from_dir( |
| 488 | &test_repo.dir, | ||
| 489 | ["login", "--password", TEST_PASSWORD], | ||
| 490 | ); | ||
| 470 | 491 | ||
| 471 | p.expect("searching for profile and relay updates...\r\n")?; | 492 | p.expect("searching for profile and relay updates...\r\n")?; |
| 472 | 493 | ||
| @@ -516,13 +537,19 @@ mod with_relays { | |||
| 516 | ); | 537 | ); |
| 517 | 538 | ||
| 518 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 539 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 519 | let mut p = CliTester::new([ | 540 | let test_repo = GitTestRepo::default(); |
| 520 | "login", | 541 | let mut p = CliTester::new_from_dir( |
| 521 | "--nsec", | 542 | &test_repo.dir, |
| 522 | TEST_KEY_1_NSEC, | 543 | [ |
| 523 | "--password", | 544 | "login", |
| 524 | TEST_PASSWORD, | 545 | "--nsec", |
| 525 | ]); | 546 | TEST_KEY_1_NSEC, |
| 547 | "--password", | ||
| 548 | TEST_PASSWORD, | ||
| 549 | ], | ||
| 550 | ); | ||
| 551 | |||
| 552 | p.expect("saved login details to local git config\r\n")?; | ||
| 526 | 553 | ||
| 527 | p.expect("searching for profile and relay updates...\r\n")?; | 554 | p.expect("searching for profile and relay updates...\r\n")?; |
| 528 | 555 | ||
| @@ -561,7 +588,8 @@ mod with_relays { | |||
| 561 | ); | 588 | ); |
| 562 | 589 | ||
| 563 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 590 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 564 | let mut p = CliTester::new(["login"]); | 591 | let test_repo = GitTestRepo::default(); |
| 592 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); | ||
| 565 | 593 | ||
| 566 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 594 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 567 | .succeeds_with(TEST_KEY_1_NSEC)?; | 595 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -572,6 +600,8 @@ mod with_relays { | |||
| 572 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? | 600 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? |
| 573 | .succeeds_with(Some(false))?; | 601 | .succeeds_with(Some(false))?; |
| 574 | 602 | ||
| 603 | p.expect("saved login details to local git config\r\n")?; | ||
| 604 | |||
| 575 | p.expect("searching for profile and relay updates...\r\n")?; | 605 | p.expect("searching for profile and relay updates...\r\n")?; |
| 576 | 606 | ||
| 577 | p.expect("cannot find your account metadata (name, etc) on relays\r\n")?; | 607 | p.expect("cannot find your account metadata (name, etc) on relays\r\n")?; |
| @@ -621,7 +651,8 @@ mod with_relays { | |||
| 621 | ); | 651 | ); |
| 622 | 652 | ||
| 623 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 653 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 624 | let mut p = CliTester::new(["login"]); | 654 | let test_repo = GitTestRepo::default(); |
| 655 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); | ||
| 625 | 656 | ||
| 626 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 657 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 627 | .succeeds_with(TEST_KEY_1_NSEC)?; | 658 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -632,6 +663,8 @@ mod with_relays { | |||
| 632 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? | 663 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? |
| 633 | .succeeds_with(Some(false))?; | 664 | .succeeds_with(Some(false))?; |
| 634 | 665 | ||
| 666 | p.expect("saved login details to local git config\r\n")?; | ||
| 667 | |||
| 635 | p.expect("searching for profile and relay updates...\r\n")?; | 668 | p.expect("searching for profile and relay updates...\r\n")?; |
| 636 | 669 | ||
| 637 | p.expect("cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience.\r\n")?; | 670 | p.expect("cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience.\r\n")?; |
| @@ -655,7 +688,7 @@ mod with_relays { | |||
| 655 | mod when_second_time_login_and_details_already_fetched { | 688 | mod when_second_time_login_and_details_already_fetched { |
| 656 | use super::*; | 689 | use super::*; |
| 657 | 690 | ||
| 658 | mod uses_cache { | 691 | mod uses_cache_and_stores_and_retrieves_ncryptsec_from_local_git_config { |
| 659 | use super::*; | 692 | use super::*; |
| 660 | 693 | ||
| 661 | #[tokio::test] | 694 | #[tokio::test] |
| @@ -685,13 +718,19 @@ mod with_relays { | |||
| 685 | ); | 718 | ); |
| 686 | 719 | ||
| 687 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 720 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 688 | let mut p = CliTester::new([ | 721 | let test_repo = GitTestRepo::default(); |
| 689 | "login", | 722 | let mut p = CliTester::new_from_dir( |
| 690 | "--nsec", | 723 | &test_repo.dir, |
| 691 | TEST_KEY_1_NSEC, | 724 | [ |
| 692 | "--password", | 725 | "login", |
| 693 | TEST_PASSWORD, | 726 | "--nsec", |
| 694 | ]); | 727 | TEST_KEY_1_NSEC, |
| 728 | "--password", | ||
| 729 | TEST_PASSWORD, | ||
| 730 | ], | ||
| 731 | ); | ||
| 732 | |||
| 733 | p.expect("saved login details to local git config\r\n")?; | ||
| 695 | 734 | ||
| 696 | p.expect_end_eventually_with("logged in as fred\r\n")?; | 735 | p.expect_end_eventually_with("logged in as fred\r\n")?; |
| 697 | 736 | ||
| @@ -699,7 +738,10 @@ mod with_relays { | |||
| 699 | shutdown_relay(8000 + p)?; | 738 | shutdown_relay(8000 + p)?; |
| 700 | } | 739 | } |
| 701 | 740 | ||
| 702 | let mut p = CliTester::new(["login", "--password", TEST_PASSWORD]); | 741 | let mut p = CliTester::new_from_dir( |
| 742 | &test_repo.dir, | ||
| 743 | ["login", "--password", TEST_PASSWORD], | ||
| 744 | ); | ||
| 703 | 745 | ||
| 704 | p.expect("searching for profile and relay updates...\r\n")?; | 746 | p.expect("searching for profile and relay updates...\r\n")?; |
| 705 | 747 | ||
| @@ -734,7 +776,8 @@ mod with_relays { | |||
| 734 | ); | 776 | ); |
| 735 | 777 | ||
| 736 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { | 778 | let cli_tester_handle = std::thread::spawn(move || -> Result<()> { |
| 737 | let mut p = CliTester::new(["login"]); | 779 | let test_repo = GitTestRepo::default(); |
| 780 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login"]); | ||
| 738 | 781 | ||
| 739 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 782 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 740 | .succeeds_with(TEST_KEY_1_NSEC)?; | 783 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -745,6 +788,8 @@ mod with_relays { | |||
| 745 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? | 788 | p.expect_confirm(EXPECTED_REQUIRE_PASSWORD_PROMPT, Some(false))? |
| 746 | .succeeds_with(Some(false))?; | 789 | .succeeds_with(Some(false))?; |
| 747 | 790 | ||
| 791 | p.expect("saved login details to local git config\r\n")?; | ||
| 792 | |||
| 748 | p.expect("searching for profile and relay updates...\r\n")?; | 793 | p.expect("searching for profile and relay updates...\r\n")?; |
| 749 | 794 | ||
| 750 | p.expect_end_with("logged in as fred\r\n")?; | 795 | p.expect_end_with("logged in as fred\r\n")?; |
| @@ -808,16 +853,15 @@ mod with_offline_flag { | |||
| 808 | use super::*; | 853 | use super::*; |
| 809 | 854 | ||
| 810 | #[test] | 855 | #[test] |
| 811 | #[serial] | ||
| 812 | fn prompts_for_nsec_and_password() -> Result<()> { | 856 | fn prompts_for_nsec_and_password() -> Result<()> { |
| 813 | standard_first_time_login_encrypting_nsec()?; | 857 | standard_first_time_login_encrypting_nsec()?; |
| 814 | Ok(()) | 858 | Ok(()) |
| 815 | } | 859 | } |
| 816 | 860 | ||
| 817 | #[test] | 861 | #[test] |
| 818 | #[serial] | ||
| 819 | fn succeeds_with_text_logged_in_as_npub() -> Result<()> { | 862 | fn succeeds_with_text_logged_in_as_npub() -> Result<()> { |
| 820 | let mut p = CliTester::new(["login", "--offline"]); | 863 | let test_repo = GitTestRepo::default(); |
| 864 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login", "--offline"]); | ||
| 821 | 865 | ||
| 822 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 866 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 823 | .succeeds_with(TEST_KEY_1_NSEC)?; | 867 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -832,13 +876,15 @@ mod with_offline_flag { | |||
| 832 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? | 876 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? |
| 833 | .succeeds_with(TEST_PASSWORD)?; | 877 | .succeeds_with(TEST_PASSWORD)?; |
| 834 | 878 | ||
| 879 | p.expect("saved login details to local git config\r\n")?; | ||
| 880 | |||
| 835 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 881 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) |
| 836 | } | 882 | } |
| 837 | 883 | ||
| 838 | #[test] | 884 | #[test] |
| 839 | #[serial] | ||
| 840 | fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> { | 885 | fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> { |
| 841 | let mut p = CliTester::new(["login", "--offline"]); | 886 | let test_repo = GitTestRepo::default(); |
| 887 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login", "--offline"]); | ||
| 842 | 888 | ||
| 843 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 889 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 844 | .succeeds_with(TEST_KEY_1_SK_HEX)?; | 890 | .succeeds_with(TEST_KEY_1_SK_HEX)?; |
| @@ -853,6 +899,8 @@ mod with_offline_flag { | |||
| 853 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? | 899 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? |
| 854 | .succeeds_with(TEST_PASSWORD)?; | 900 | .succeeds_with(TEST_PASSWORD)?; |
| 855 | 901 | ||
| 902 | p.expect("saved login details to local git config\r\n")?; | ||
| 903 | |||
| 856 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 904 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) |
| 857 | } | 905 | } |
| 858 | 906 | ||
| @@ -860,12 +908,11 @@ mod with_offline_flag { | |||
| 860 | use super::*; | 908 | use super::*; |
| 861 | 909 | ||
| 862 | #[test] | 910 | #[test] |
| 863 | #[serial] | ||
| 864 | fn prompts_for_nsec_until_valid() -> Result<()> { | 911 | fn prompts_for_nsec_until_valid() -> Result<()> { |
| 865 | let invalid_nsec_response = | 912 | let invalid_nsec_response = "invalid. try again with nostr address / nsec"; |
| 866 | "invalid nsec. try again with nsec (or hex private key)"; | ||
| 867 | 913 | ||
| 868 | let mut p = CliTester::new(["login", "--offline"]); | 914 | let test_repo = GitTestRepo::default(); |
| 915 | let mut p = CliTester::new_from_dir(&test_repo.dir, ["login", "--offline"]); | ||
| 869 | 916 | ||
| 870 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 917 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 871 | // this behaviour is intentional. rejecting the response with dialoguer | 918 | // this behaviour is intentional. rejecting the response with dialoguer |
| @@ -889,6 +936,8 @@ mod with_offline_flag { | |||
| 889 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? | 936 | .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? |
| 890 | .succeeds_with(TEST_PASSWORD)?; | 937 | .succeeds_with(TEST_PASSWORD)?; |
| 891 | 938 | ||
| 939 | p.expect("saved login details to local git config\r\n")?; | ||
| 940 | |||
| 892 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 941 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) |
| 893 | } | 942 | } |
| 894 | } | 943 | } |
| @@ -898,19 +947,31 @@ mod with_offline_flag { | |||
| 898 | use super::*; | 947 | use super::*; |
| 899 | 948 | ||
| 900 | #[test] | 949 | #[test] |
| 901 | #[serial] | ||
| 902 | fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { | 950 | fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { |
| 903 | CliTester::new(["login", "--offline", "--nsec", TEST_KEY_1_NSEC]) | 951 | let test_repo = GitTestRepo::default(); |
| 904 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 952 | let mut p = CliTester::new_from_dir( |
| 953 | &test_repo.dir, | ||
| 954 | ["login", "--offline", "--nsec", TEST_KEY_1_NSEC], | ||
| 955 | ); | ||
| 956 | |||
| 957 | p.expect("saved login details to local git config\r\n")?; | ||
| 958 | |||
| 959 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | ||
| 905 | } | 960 | } |
| 906 | 961 | ||
| 907 | #[test] | 962 | #[test] |
| 908 | #[serial] | ||
| 909 | fn forgets_identity() -> Result<()> { | 963 | fn forgets_identity() -> Result<()> { |
| 910 | CliTester::new(["login", "--offline", "--nsec", TEST_KEY_1_NSEC]) | 964 | let test_repo = GitTestRepo::default(); |
| 911 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; | 965 | let mut p = CliTester::new_from_dir( |
| 966 | &test_repo.dir, | ||
| 967 | ["login", "--offline", "--nsec", TEST_KEY_1_NSEC], | ||
| 968 | ); | ||
| 969 | |||
| 970 | p.expect("saved login details to local git config\r\n")?; | ||
| 971 | |||
| 972 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; | ||
| 912 | 973 | ||
| 913 | let mut p = CliTester::new(["login", "--offline"]); | 974 | p = CliTester::new_from_dir(&test_repo.dir, ["login", "--offline"]); |
| 914 | 975 | ||
| 915 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 976 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 916 | .succeeds_with(TEST_KEY_1_NSEC)?; | 977 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| @@ -922,18 +983,28 @@ mod with_offline_flag { | |||
| 922 | use super::*; | 983 | use super::*; |
| 923 | 984 | ||
| 924 | #[test] | 985 | #[test] |
| 925 | #[serial] | ||
| 926 | fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { | 986 | fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { |
| 927 | standard_first_time_login_encrypting_nsec()?.exit()?; | 987 | standard_first_time_login_encrypting_nsec()?.exit()?; |
| 988 | let test_repo = GitTestRepo::default(); | ||
| 989 | let mut p = CliTester::new_from_dir( | ||
| 990 | &test_repo.dir, | ||
| 991 | ["login", "--offline", "--nsec", TEST_KEY_2_NSEC], | ||
| 992 | ); | ||
| 928 | 993 | ||
| 929 | CliTester::new(["login", "--offline", "--nsec", TEST_KEY_2_NSEC]) | 994 | p.expect("saved login details to local git config\r\n")?; |
| 930 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) | 995 | |
| 996 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) | ||
| 931 | } | 997 | } |
| 932 | } | 998 | } |
| 933 | #[test] | 999 | #[test] |
| 934 | #[serial] | ||
| 935 | fn invalid_nsec_param_fails_without_prompts() -> Result<()> { | 1000 | fn invalid_nsec_param_fails_without_prompts() -> Result<()> { |
| 936 | CliTester::new(["login", "--offline", "--nsec", TEST_INVALID_NSEC]).expect_end_with( | 1001 | let test_repo = GitTestRepo::default(); |
| 1002 | let mut p = CliTester::new_from_dir( | ||
| 1003 | &test_repo.dir, | ||
| 1004 | ["login", "--offline", "--nsec", TEST_INVALID_NSEC], | ||
| 1005 | ); | ||
| 1006 | |||
| 1007 | p.expect_end_with( | ||
| 937 | "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", | 1008 | "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", |
| 938 | ) | 1009 | ) |
| 939 | } | 1010 | } |
| @@ -943,50 +1014,61 @@ mod with_offline_flag { | |||
| 943 | use super::*; | 1014 | use super::*; |
| 944 | 1015 | ||
| 945 | #[test] | 1016 | #[test] |
| 946 | #[serial] | ||
| 947 | fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { | 1017 | fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { |
| 948 | CliTester::new([ | 1018 | let test_repo = GitTestRepo::default(); |
| 949 | "login", | 1019 | let mut p = CliTester::new_from_dir( |
| 950 | "--offline", | 1020 | &test_repo.dir, |
| 951 | "--nsec", | 1021 | [ |
| 952 | TEST_KEY_1_NSEC, | 1022 | "login", |
| 953 | "--password", | 1023 | "--offline", |
| 954 | TEST_PASSWORD, | 1024 | "--nsec", |
| 955 | ]) | 1025 | TEST_KEY_1_NSEC, |
| 956 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 1026 | "--password", |
| 1027 | TEST_PASSWORD, | ||
| 1028 | ], | ||
| 1029 | ); | ||
| 1030 | p.expect("saved login details to local git config\r\n")?; | ||
| 1031 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | ||
| 957 | } | 1032 | } |
| 958 | 1033 | ||
| 959 | #[test] | 1034 | #[test] |
| 960 | #[serial] | ||
| 961 | fn parameters_can_be_called_globally() -> Result<()> { | 1035 | fn parameters_can_be_called_globally() -> Result<()> { |
| 962 | CliTester::new([ | 1036 | let test_repo = GitTestRepo::default(); |
| 963 | "--nsec", | 1037 | let mut p = CliTester::new_from_dir( |
| 964 | TEST_KEY_1_NSEC, | 1038 | &test_repo.dir, |
| 965 | "--password", | 1039 | [ |
| 966 | TEST_PASSWORD, | 1040 | "--nsec", |
| 967 | "login", | 1041 | TEST_KEY_1_NSEC, |
| 968 | "--offline", | 1042 | "--password", |
| 969 | ]) | 1043 | TEST_PASSWORD, |
| 970 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 1044 | "login", |
| 1045 | "--offline", | ||
| 1046 | ], | ||
| 1047 | ); | ||
| 1048 | p.expect("saved login details to local git config\r\n")?; | ||
| 1049 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | ||
| 971 | } | 1050 | } |
| 972 | 1051 | ||
| 973 | mod when_logging_in_as_different_nsec { | 1052 | mod when_logging_in_as_different_nsec { |
| 974 | use super::*; | 1053 | use super::*; |
| 975 | 1054 | ||
| 976 | #[test] | 1055 | #[test] |
| 977 | #[serial] | ||
| 978 | fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { | 1056 | fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { |
| 979 | standard_first_time_login_encrypting_nsec()?.exit()?; | 1057 | standard_first_time_login_encrypting_nsec()?.exit()?; |
| 980 | 1058 | let test_repo = GitTestRepo::default(); | |
| 981 | CliTester::new([ | 1059 | let mut p = CliTester::new_from_dir( |
| 982 | "login", | 1060 | &test_repo.dir, |
| 983 | "--offline", | 1061 | [ |
| 984 | "--nsec", | 1062 | "login", |
| 985 | TEST_KEY_2_NSEC, | 1063 | "--offline", |
| 986 | "--password", | 1064 | "--nsec", |
| 987 | TEST_PASSWORD, | 1065 | TEST_KEY_2_NSEC, |
| 988 | ]) | 1066 | "--password", |
| 989 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) | 1067 | TEST_PASSWORD, |
| 1068 | ], | ||
| 1069 | ); | ||
| 1070 | p.expect("saved login details to local git config\r\n")?; | ||
| 1071 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) | ||
| 990 | } | 1072 | } |
| 991 | } | 1073 | } |
| 992 | 1074 | ||
| @@ -994,37 +1076,46 @@ mod with_offline_flag { | |||
| 994 | use super::*; | 1076 | use super::*; |
| 995 | 1077 | ||
| 996 | #[test] | 1078 | #[test] |
| 997 | #[serial] | ||
| 998 | fn password_changes() -> Result<()> { | 1079 | fn password_changes() -> Result<()> { |
| 999 | standard_first_time_login_encrypting_nsec()?.exit()?; | 1080 | standard_first_time_login_encrypting_nsec()?.exit()?; |
| 1081 | let test_repo = GitTestRepo::default(); | ||
| 1082 | let mut p = CliTester::new_from_dir( | ||
| 1083 | &test_repo.dir, | ||
| 1084 | [ | ||
| 1085 | "login", | ||
| 1086 | "--offline", | ||
| 1087 | "--nsec", | ||
| 1088 | TEST_KEY_1_NSEC, | ||
| 1089 | "--password", | ||
| 1090 | TEST_INVALID_PASSWORD, | ||
| 1091 | ], | ||
| 1092 | ); | ||
| 1093 | p.expect("saved login details to local git config\r\n")?; | ||
| 1094 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; | ||
| 1000 | 1095 | ||
| 1001 | CliTester::new([ | 1096 | CliTester::new_from_dir( |
| 1002 | "login", | 1097 | &test_repo.dir, |
| 1003 | "--offline", | 1098 | ["--password", TEST_INVALID_PASSWORD, "login", "--offline"], |
| 1004 | "--nsec", | 1099 | ) |
| 1005 | TEST_KEY_1_NSEC, | 1100 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) |
| 1006 | "--password", | ||
| 1007 | TEST_INVALID_PASSWORD, | ||
| 1008 | ]) | ||
| 1009 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; | ||
| 1010 | |||
| 1011 | CliTester::new(["--password", TEST_INVALID_PASSWORD, "login", "--offline"]) | ||
| 1012 | .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | ||
| 1013 | } | 1101 | } |
| 1014 | } | 1102 | } |
| 1015 | 1103 | ||
| 1016 | #[test] | 1104 | #[test] |
| 1017 | #[serial] | ||
| 1018 | fn invalid_nsec_param_fails_without_prompts() -> Result<()> { | 1105 | fn invalid_nsec_param_fails_without_prompts() -> Result<()> { |
| 1019 | CliTester::new([ | 1106 | let test_repo = GitTestRepo::default(); |
| 1020 | "login", | 1107 | let mut p = CliTester::new_from_dir( |
| 1021 | "--offline", | 1108 | &test_repo.dir, |
| 1022 | "--nsec", | 1109 | [ |
| 1023 | TEST_INVALID_NSEC, | 1110 | "login", |
| 1024 | "--password", | 1111 | "--offline", |
| 1025 | TEST_PASSWORD, | 1112 | "--nsec", |
| 1026 | ]) | 1113 | TEST_INVALID_NSEC, |
| 1027 | .expect_end_with( | 1114 | "--password", |
| 1115 | TEST_PASSWORD, | ||
| 1116 | ], | ||
| 1117 | ); | ||
| 1118 | p.expect_end_with( | ||
| 1028 | "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", | 1119 | "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", |
| 1029 | ) | 1120 | ) |
| 1030 | } | 1121 | } |
| @@ -1034,11 +1125,12 @@ mod with_offline_flag { | |||
| 1034 | use super::*; | 1125 | use super::*; |
| 1035 | 1126 | ||
| 1036 | #[test] | 1127 | #[test] |
| 1037 | #[serial] | ||
| 1038 | // combined into a single test as it is computationally expensive to run | 1128 | // combined into a single test as it is computationally expensive to run |
| 1039 | fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds() | 1129 | fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds() |
| 1040 | -> Result<()> { | 1130 | -> Result<()> { |
| 1041 | let mut p = CliTester::new_with_timeout(10000, ["login", "--offline"]); | 1131 | let test_repo = GitTestRepo::default(); |
| 1132 | let mut p = | ||
| 1133 | CliTester::new_with_timeout_from_dir(10000, &test_repo.dir, ["login", "--offline"]); | ||
| 1042 | p.expect_input(EXPECTED_NSEC_PROMPT)? | 1134 | p.expect_input(EXPECTED_NSEC_PROMPT)? |
| 1043 | .succeeds_with(TEST_KEY_1_NSEC)?; | 1135 | .succeeds_with(TEST_KEY_1_NSEC)?; |
| 1044 | 1136 | ||
| @@ -1054,6 +1146,8 @@ mod with_offline_flag { | |||
| 1054 | 1146 | ||
| 1055 | p.expect("this may take a few seconds...\r\n")?; | 1147 | p.expect("this may take a few seconds...\r\n")?; |
| 1056 | 1148 | ||
| 1149 | p.expect("saved login details to local git config\r\n")?; | ||
| 1150 | |||
| 1057 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) | 1151 | p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) |
| 1058 | 1152 | ||
| 1059 | // commented out as 'login' command now assumes you want to | 1153 | // commented out as 'login' command now assumes you want to |