upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2024-06-28 15:16:43 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2024-06-28 15:16:43 +0100
commita82546b70303000b4fc053a1ee21d3d8c7d6ad66 (patch)
treef8c4238a5ae27759b3c1a6adb5c865b07e339a66
parent6b06e874119ceca1a9dac1b94dcfe6e06aacd7b9 (diff)
feat(login): login with nip46 remote signer
and save details in git config
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--src/client.rs31
-rw-r--r--src/git.rs93
-rw-r--r--src/login.rs374
-rw-r--r--src/main.rs7
-rw-r--r--src/repo_ref.rs14
-rw-r--r--src/sub_commands/init.rs2
-rw-r--r--src/sub_commands/login.rs22
-rw-r--r--src/sub_commands/push.rs2
-rw-r--r--src/sub_commands/send.rs12
-rw-r--r--test_utils/src/lib.rs5
-rw-r--r--tests/login.rs314
13 files changed, 674 insertions, 204 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 019e051..a5a8fa0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index e25fd51..d41d870 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ keyring = "2.0.5"
26nostr = "0.32.0" 26nostr = "0.32.0"
27nostr-database = "0.32.0" 27nostr-database = "0.32.0"
28nostr-sdk = "0.32.0" 28nostr-sdk = "0.32.0"
29nostr-signer = "0.32.0"
29nostr-sqlite = "0.32.0" 30nostr-sqlite = "0.32.0"
30passwords = "3.1.13" 31passwords = "3.1.13"
31scrypt = "0.11.0" 32scrypt = "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)]
20use mockall::*; 20use mockall::*;
21use nostr::Event; 21use nostr::Event;
22use nostr_sdk::NostrSigner; 22use nostr_sdk::{EventBuilder, NostrSigner};
23 23
24#[allow(clippy::struct_field_names)] 24#[allow(clippy::struct_field_names)]
25pub struct Client { 25pub 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
296pub 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
314pub 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}
diff --git a/src/git.rs b/src/git.rs
index 46687ae..bb943a9 100644
--- a/src/git.rs
+++ b/src/git.rs
@@ -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
83impl RepoActions for Repo { 84impl 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
621fn oid_to_u8_20_bytes(oid: &Oid) -> [u8; 20] { 664fn 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 @@
1use std::str::FromStr; 1use std::{fs::create_dir_all, str::FromStr, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use nostr::PublicKey; 4use nostr::{nips::nip46::NostrConnectURI, PublicKey};
5use nostr_database::Order; 5use nostr_database::Order;
6use nostr_sdk::{ 6use nostr_sdk::{
7 Alphabet, FromBech32, JsonUtil, Kind, NostrDatabase, NostrSigner, SingleLetterTag, ToBech32, 7 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrDatabase, NostrSigner, SingleLetterTag,
8 ToBech32,
8}; 9};
10use nostr_signer::Nip46Signer;
9use nostr_sqlite::SQLiteDatabase; 11use 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
26pub async fn launch( 28pub 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
98fn get_keys_without_prompts( 119async 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
202fn 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}
235fn 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
150fn get_keys_with_password(git_repo: &Repo, password: &str) -> Result<nostr::Keys> { 262fn 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
161fn get_keys_with_git_config_nsec_without_prompts(git_repo: &Repo) -> Result<nostr::Keys> { 273async 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
290async 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
325fn 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
172async fn fresh_login( 343async 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
204fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<()> { 399fn 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( 445fn 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
257fn get_config_item(git_repo: &Repo, name: &str) -> Result<String> { 495fn 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
4use anyhow::Result; 5use anyhow::Result;
@@ -19,6 +20,12 @@ mod sub_commands;
19pub struct Cli { 20pub 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;
11use crate::client::MockConnect; 11use crate::client::MockConnect;
12use crate::{ 12use 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
96impl RepoRef { 96impl 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 {
17pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> { 17pub 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 @@
1use anyhow::Result; 1use anyhow::Result;
2use git::GitTestRepo;
2use serial_test::serial; 3use serial_test::serial;
3use test_utils::*; 4use test_utils::*;
4 5
5static EXPECTED_NSEC_PROMPT: &str = "login with nsec"; 6static EXPECTED_NSEC_PROMPT: &str = "login with bunker uri / nsec";
6static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?"; 7static EXPECTED_LOCAL_REPOSITORY_PROMPT: &str = "just for this repository?";
7static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?"; 8static EXPECTED_REQUIRE_PASSWORD_PROMPT: &str = "require password?";
8static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password"; 9static EXPECTED_SET_PASSWORD_PROMPT: &str = "encrypt with password";
9static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password"; 10static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password";
10 11
11fn standard_first_time_login_encrypting_nsec() -> Result<CliTester> { 12fn 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