upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--src/client.rs72
-rw-r--r--src/config.rs62
-rw-r--r--src/key_handling/users.rs683
-rw-r--r--src/login.rs153
-rw-r--r--src/main.rs4
-rw-r--r--src/sub_commands/login.rs30
-rw-r--r--src/sub_commands/prs/create.rs3
-rw-r--r--test_utils/Cargo.toml1
-rw-r--r--test_utils/src/lib.rs62
-rw-r--r--test_utils/src/relay.rs126
-rw-r--r--tests/login.rs1100
-rw-r--r--tests/prs_create.rs22
13 files changed, 1959 insertions, 360 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b8c0f04..e977d30 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2717,6 +2717,7 @@ dependencies = [
2717 "rexpect 0.5.0 (git+https://github.com/phaer/rexpect.git?branch=skip-ansi-escape-codes)", 2717 "rexpect 0.5.0 (git+https://github.com/phaer/rexpect.git?branch=skip-ansi-escape-codes)",
2718 "simple-websockets", 2718 "simple-websockets",
2719 "strip-ansi-escapes", 2719 "strip-ansi-escapes",
2720 "tungstenite 0.20.1",
2720] 2721]
2721 2722
2722[[package]] 2723[[package]]
diff --git a/src/client.rs b/src/client.rs
index e0e0494..5ddf742 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -10,25 +10,32 @@
10// which is currently in nightly. alternatively we can use nightly as it looks 10// which is currently in nightly. alternatively we can use nightly as it looks
11// certain that the implementation is going to make it to stable but we don't 11// certain that the implementation is going to make it to stable but we don't
12// want to inadvertlty use other features of nightly that might be removed. 12// want to inadvertlty use other features of nightly that might be removed.
13use anyhow::Result; 13use anyhow::{Context, Result};
14use async_trait::async_trait; 14use async_trait::async_trait;
15use futures::future::join_all;
15#[cfg(test)] 16#[cfg(test)]
16use mockall::*; 17use mockall::*;
17use nostr::Event; 18use nostr::Event;
18 19
19pub struct Client { 20pub struct Client {
20 client: nostr_sdk::Client, 21 client: nostr_sdk::Client,
21 pub fallback_relays: Vec<String>, 22 fallback_relays: Vec<String>,
22} 23}
23 24
24#[async_trait]
25#[cfg_attr(test, automock)] 25#[cfg_attr(test, automock)]
26#[async_trait]
26pub trait Connect { 27pub trait Connect {
27 fn default() -> Self; 28 fn default() -> Self;
28 fn new(opts: Params) -> Self; 29 fn new(opts: Params) -> Self;
29 async fn connect(&self) -> Result<()>; 30 async fn connect(&self) -> Result<()>;
30 async fn disconnect(&self) -> Result<()>; 31 async fn disconnect(&self) -> Result<()>;
32 fn get_fallback_relays(&self) -> &Vec<String>;
31 async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>; 33 async fn send_event_to(&self, url: &str, event: nostr::event::Event) -> Result<nostr::EventId>;
34 async fn get_events(
35 &self,
36 relays: Vec<String>,
37 filters: Vec<nostr::Filter>,
38 ) -> Result<Vec<nostr::Event>>;
32} 39}
33 40
34#[async_trait] 41#[async_trait]
@@ -37,7 +44,7 @@ impl Connect for Client {
37 Client { 44 Client {
38 client: nostr_sdk::Client::new(&nostr::Keys::generate()), 45 client: nostr_sdk::Client::new(&nostr::Keys::generate()),
39 fallback_relays: vec![ 46 fallback_relays: vec![
40 "ws://localhost:8080".to_string(), 47 "ws://localhost:8051".to_string(),
41 "ws://localhost:8052".to_string(), 48 "ws://localhost:8052".to_string(),
42 ], 49 ],
43 } 50 }
@@ -61,9 +68,52 @@ impl Connect for Client {
61 Ok(()) 68 Ok(())
62 } 69 }
63 70
71 fn get_fallback_relays(&self) -> &Vec<String> {
72 &self.fallback_relays
73 }
74
64 async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> { 75 async fn send_event_to(&self, url: &str, event: Event) -> Result<nostr::EventId> {
65 Ok(self.client.send_event_to(url, event).await?) 76 Ok(self.client.send_event_to(url, event).await?)
66 } 77 }
78
79 async fn get_events(
80 &self,
81 relays: Vec<String>,
82 filters: Vec<nostr::Filter>,
83 ) -> Result<Vec<nostr::Event>> {
84 // add relays
85 for relay in &relays {
86 self.client
87 .add_relay(relay.as_str(), None)
88 .await
89 .context("cannot add relay")?;
90 }
91
92 let relays_map = self.client.relays().await;
93
94 let relay_results = join_all(
95 relays
96 .clone()
97 .iter()
98 .map(|r| {
99 (
100 relays_map.get(&nostr::Url::parse(r).unwrap()).unwrap(),
101 filters.clone(),
102 )
103 })
104 .map(|(relay, filters)| {
105 relay.get_events_of(
106 filters,
107 // 20 is nostr_sdk default
108 std::time::Duration::from_secs(20),
109 nostr_sdk::FilterOptions::ExitOnEOSE,
110 )
111 }),
112 )
113 .await;
114
115 Ok(get_dedup_events(relay_results))
116 }
67} 117}
68 118
69#[derive(Default)] 119#[derive(Default)]
@@ -82,3 +132,17 @@ impl Params {
82 self 132 self
83 } 133 }
84} 134}
135
136fn get_dedup_events(
137 relay_results: Vec<Result<Vec<nostr::Event>, nostr_sdk::relay::Error>>,
138) -> Vec<Event> {
139 let mut dedup_events: Vec<Event> = vec![];
140 for events in relay_results.into_iter().flatten() {
141 for event in events {
142 if !dedup_events.iter().any(|e| event.id.eq(&e.id)) {
143 dedup_events.push(event);
144 }
145 }
146 }
147 dedup_events
148}
diff --git a/src/config.rs b/src/config.rs
index f410934..2370e34 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -4,7 +4,7 @@ use anyhow::{anyhow, Context, Result};
4use directories::ProjectDirs; 4use directories::ProjectDirs;
5#[cfg(test)] 5#[cfg(test)]
6use mockall::*; 6use mockall::*;
7use nostr::secp256k1::XOnlyPublicKey; 7use nostr::{secp256k1::XOnlyPublicKey, ToBech32};
8use serde::{self, Deserialize, Serialize}; 8use serde::{self, Deserialize, Serialize};
9 9
10#[derive(Default)] 10#[derive(Default)]
@@ -71,6 +71,66 @@ pub struct MyConfig {
71pub struct UserRef { 71pub struct UserRef {
72 pub public_key: XOnlyPublicKey, 72 pub public_key: XOnlyPublicKey,
73 pub encrypted_key: String, 73 pub encrypted_key: String,
74 pub metadata: UserMetadata,
75 pub relays: UserRelays,
76 pub last_checked: u64,
77}
78
79impl UserRef {
80 pub fn new(public_key: XOnlyPublicKey, encrypted_key: String) -> Self {
81 Self {
82 public_key,
83 encrypted_key,
84 relays: UserRelays {
85 relays: vec![],
86 created_at: 0,
87 },
88 metadata: UserMetadata {
89 #[allow(clippy::expect_used)]
90 name: public_key
91 .to_bech32()
92 .expect("public key should always produce bech32"),
93 // name: format!(
94 // "{}",
95 // public_key
96 // .to_bech32()
97 // .expect("public key should always produce bech32"),
98 // )
99 // .as_str()[..10].to_string(),
100 created_at: 0,
101 },
102 last_checked: 0,
103 }
104 }
105}
106
107#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
108pub struct UserMetadata {
109 pub name: String,
110 pub created_at: u64,
111}
112
113#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
114pub struct UserRelays {
115 pub relays: Vec<UserRelayRef>,
116 pub created_at: u64,
117}
118
119impl UserRelays {
120 pub fn write(&self) -> Vec<String> {
121 self.relays
122 .iter()
123 .filter(|r| r.write)
124 .map(|r| r.url.clone())
125 .collect()
126 }
127}
128
129#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
130pub struct UserRelayRef {
131 pub url: String,
132 pub read: bool,
133 pub write: bool,
74} 134}
75 135
76#[cfg(test)] 136#[cfg(test)]
diff --git a/src/key_handling/users.rs b/src/key_handling/users.rs
index 1d2cc34..91519bc 100644
--- a/src/key_handling/users.rs
+++ b/src/key_handling/users.rs
@@ -1,11 +1,21 @@
1use std::time::SystemTime;
2
1use anyhow::{Context, Result}; 3use anyhow::{Context, Result};
4use async_trait::async_trait;
2use nostr::prelude::*; 5use nostr::prelude::*;
3use zeroize::Zeroize; 6use zeroize::Zeroize;
4 7
5use super::encryption::{EncryptDecrypt, Encryptor}; 8use super::encryption::{EncryptDecrypt, Encryptor};
9#[cfg(not(test))]
10use crate::client::Client;
11#[cfg(test)]
12use crate::client::MockConnect;
6use crate::{ 13use crate::{
7 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms}, 14 cli_interactor::{Interactor, InteractorPrompt, PromptInputParms, PromptPasswordParms},
8 config::{self, ConfigManagement, ConfigManager}, 15 client::Connect,
16 config::{
17 self, ConfigManagement, ConfigManager, UserMetadata, UserRef, UserRelayRef, UserRelays,
18 },
9}; 19};
10 20
11#[derive(Default)] 21#[derive(Default)]
@@ -15,13 +25,29 @@ pub struct UserManager {
15 encryptor: Encryptor, 25 encryptor: Encryptor,
16} 26}
17 27
28#[async_trait]
18pub trait UserManagement { 29pub trait UserManagement {
19 fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys>; 30 fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys>;
31 async fn get_user(
32 &self,
33 #[cfg(test)] client: &MockConnect,
34 #[cfg(not(test))] client: &Client,
35 public_key: &XOnlyPublicKey,
36 after: u64,
37 ) -> Result<UserRef>;
38 fn get_user_from_cache(&self, public_key: &XOnlyPublicKey) -> Result<UserRef>;
39 fn add_user_to_config(
40 &self,
41 public_key: XOnlyPublicKey,
42 encrypted_secret_key: Option<String>,
43 overwrite: bool,
44 ) -> Result<()>;
20} 45}
21 46
22#[cfg(test)] 47#[cfg(test)]
23use duplicate::duplicate_item; 48use duplicate::duplicate_item;
24#[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))] 49#[cfg_attr(test, duplicate_item(UserManager; [UserManager]; [self::tests::MockUserManager]))]
50#[async_trait]
25impl UserManagement for UserManager { 51impl UserManagement for UserManager {
26 fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { 52 fn add(&self, nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> {
27 let mut prompt = "login with nsec (or hex private key)"; 53 let mut prompt = "login with nsec (or hex private key)";
@@ -66,9 +92,152 @@ impl UserManagement for UserManager {
66 .context("failed to encrypt nsec with password.")?; 92 .context("failed to encrypt nsec with password.")?;
67 pass.zeroize(); 93 pass.zeroize();
68 94
69 let user_ref = config::UserRef { 95 self.add_user_to_config(keys.public_key(), Some(encrypted_secret_key), true)?;
70 public_key: keys.public_key(), 96
71 encrypted_key: encrypted_secret_key, 97 Ok(keys)
98 }
99
100 fn add_user_to_config(
101 &self,
102 public_key: XOnlyPublicKey,
103 encrypted_secret_key: Option<String>,
104 overwrite: bool,
105 ) -> Result<()> {
106 let user_ref =
107 config::UserRef::new(public_key, encrypted_secret_key.unwrap_or(String::new()));
108
109 let mut cfg = self.config_manager.load().context("failed to load application config to find and remove any old versions of the user's encrypted key")?;
110 // don't overwrite unless specified
111 if !overwrite
112 && cfg
113 .users
114 .clone()
115 .into_iter()
116 .any(|r| r.public_key.eq(&public_key))
117 {
118 return Ok(());
119 }
120 // if overwrite remove any duplicate entries for key before adding it to config
121 cfg.users = cfg
122 .users
123 .clone()
124 .into_iter()
125 .filter(|r| !r.public_key.eq(&public_key))
126 .collect();
127 cfg.users.push(user_ref);
128 self.config_manager
129 .save(&cfg)
130 .context("failed to save application configuration with new user details in")
131 }
132
133 fn get_user_from_cache(&self, public_key: &XOnlyPublicKey) -> Result<UserRef> {
134 let cfg = self
135 .config_manager
136 .load()
137 .context("failed to load application config")?;
138 Ok(cfg
139 .users
140 .iter()
141 .find(|u| u.public_key.eq(public_key))
142 .context(format!("pubkey isn't a current user: {public_key}"))?
143 .clone())
144 }
145 /// get UserRef fetching most recent user relays and metadata infomation
146 /// from relays
147 async fn get_user(
148 &self,
149 #[cfg(test)] client: &MockConnect,
150 #[cfg(not(test))] client: &Client,
151 public_key: &XOnlyPublicKey,
152 use_cache_unless_checked_more_than_x_secs_ago: u64,
153 ) -> Result<UserRef> {
154 let cfg = self
155 .config_manager
156 .load()
157 .context("failed to load application config")?;
158 let user_ref = cfg
159 .users
160 .iter()
161 .find(|u| u.public_key.eq(public_key))
162 .context(format!("pubkey isn't a current user: {public_key}"))?;
163 // return cache if last fetched was within X minutes
164 if !unix_timestamp_after_now_plus_secs(
165 user_ref.last_checked,
166 use_cache_unless_checked_more_than_x_secs_ago,
167 ) {
168 return Ok(user_ref.clone());
169 }
170 let events: Vec<Event> = match client
171 .get_events(
172 if user_ref.relays.write().is_empty() {
173 client.get_fallback_relays().clone()
174 } else {
175 user_ref.relays.write()
176 },
177 vec![
178 nostr::Filter::default()
179 .author(public_key.to_string())
180 .since(nostr::Timestamp::from(user_ref.metadata.created_at + 1))
181 .kind(Kind::Metadata),
182 nostr::Filter::default()
183 .author(public_key.to_string())
184 .since(nostr::Timestamp::from(user_ref.relays.created_at + 1))
185 .kind(Kind::RelayList),
186 ],
187 )
188 .await
189 {
190 Ok(events) => events,
191 Err(_) => {
192 // TODO error reporting
193 return Ok(user_ref.clone());
194 }
195 };
196
197 let mut user_ref = user_ref.clone();
198
199 user_ref.last_checked = SystemTime::now()
200 .duration_since(SystemTime::UNIX_EPOCH)
201 .context("system time should be after the year 1970")?
202 .as_secs();
203
204 if let Some(new_metadata_event) = events
205 .iter()
206 .filter(|e| e.kind.eq(&nostr::Kind::Metadata) && e.pubkey.eq(public_key))
207 .max_by_key(|e| e.created_at)
208 {
209 if new_metadata_event.created_at.as_u64() > user_ref.metadata.created_at {
210 let metadata = nostr::Metadata::from_json(new_metadata_event.content.clone())
211 .context("metadata cannot be found in kind 0 event content")?;
212 user_ref.metadata = UserMetadata {
213 name: metadata
214 .name
215 .context("user metadata should always have name")?,
216 created_at: new_metadata_event.created_at.as_u64(),
217 };
218 }
219 };
220
221 if let Some(new_relays_event) = events
222 .iter()
223 .filter(|e| e.kind.eq(&nostr::Kind::RelayList) && e.pubkey.eq(public_key))
224 .max_by_key(|e| e.created_at)
225 {
226 if new_relays_event.created_at.as_u64() > user_ref.relays.created_at {
227 user_ref.relays = UserRelays {
228 relays: new_relays_event
229 .tags
230 .iter()
231 .filter(|t| t.kind().eq(&nostr::TagKind::R))
232 .map(|t| UserRelayRef {
233 url: t.as_vec()[1].clone(),
234 read: t.as_vec().len() == 2 || t.as_vec()[2].eq("read"),
235 write: t.as_vec().len() == 2 || t.as_vec()[2].eq("write"),
236 })
237 .collect(),
238 created_at: new_relays_event.created_at.as_u64(),
239 };
240 }
72 }; 241 };
73 242
74 // remove any duplicate entries for key before adding it to config 243 // remove any duplicate entries for key before adding it to config
@@ -77,19 +246,27 @@ impl UserManagement for UserManager {
77 .users 246 .users
78 .clone() 247 .clone()
79 .into_iter() 248 .into_iter()
80 .filter(|r| !r.public_key.eq(&keys.public_key())) 249 .filter(|r| !r.public_key.eq(public_key))
81 .collect(); 250 .collect();
82 cfg.users.push(user_ref); 251 cfg.users.push(user_ref.clone());
83 self.config_manager 252 self.config_manager
84 .save(&cfg) 253 .save(&cfg)
85 .context("failed to save application configuration with new user details in")?; 254 .context("failed to save application configuration with new user details in")?;
255 Ok(user_ref)
256 }
257}
86 258
87 Ok(keys) 259fn unix_timestamp_after_now_plus_secs(timestamp: u64, secs: u64) -> bool {
260 if let Ok(now) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
261 now.as_secs() > (timestamp + secs)
262 } else {
263 true
88 } 264 }
89} 265}
90 266
91#[cfg(test)] 267#[cfg(test)]
92mod tests { 268mod tests {
269 use nostr;
93 use test_utils::*; 270 use test_utils::*;
94 271
95 use super::*; 272 use super::*;
@@ -278,11 +455,10 @@ mod tests {
278 m.config_manager = MockConfigManagement::default(); 455 m.config_manager = MockConfigManagement::default();
279 m.config_manager.expect_load().returning(|| { 456 m.config_manager.expect_load().returning(|| {
280 Ok(MyConfig { 457 Ok(MyConfig {
281 users: vec![UserRef { 458 users: vec![UserRef::new(
282 public_key: TEST_KEY_1_KEYS.public_key(), 459 TEST_KEY_1_KEYS.public_key(),
283 // different key to TEST_KEY_1_ENCYPTED 460 TEST_KEY_2_ENCRYPTED.into(),
284 encrypted_key: TEST_KEY_2_ENCRYPTED.into(), 461 )],
285 }],
286 ..MyConfig::default() 462 ..MyConfig::default()
287 }) 463 })
288 }); 464 });
@@ -308,10 +484,10 @@ mod tests {
308 m.config_manager = MockConfigManagement::default(); 484 m.config_manager = MockConfigManagement::default();
309 m.config_manager.expect_load().returning(|| { 485 m.config_manager.expect_load().returning(|| {
310 Ok(MyConfig { 486 Ok(MyConfig {
311 users: vec![UserRef { 487 users: vec![UserRef::new(
312 public_key: TEST_KEY_2_KEYS.public_key(), 488 TEST_KEY_2_KEYS.public_key(),
313 encrypted_key: TEST_KEY_2_ENCRYPTED.into(), 489 TEST_KEY_2_ENCRYPTED.into(),
314 }], 490 )],
315 ..MyConfig::default() 491 ..MyConfig::default()
316 }) 492 })
317 }); 493 });
@@ -329,4 +505,479 @@ mod tests {
329 } 505 }
330 } 506 }
331 } 507 }
508
509 fn now_timestamp() -> u64 {
510 SystemTime::now()
511 .duration_since(SystemTime::UNIX_EPOCH)
512 .unwrap()
513 .as_secs()
514 }
515 fn roughly_now(timestamp: u64) -> bool {
516 let now = now_timestamp();
517 timestamp < now + 100 && timestamp > now - 100
518 }
519
520 mod get_user {
521 use anyhow::anyhow;
522
523 use super::*;
524 use crate::client::MockConnect;
525
526 fn generate_relaylist_event() -> nostr::Event {
527 nostr::event::EventBuilder::new(
528 nostr::Kind::RelayList,
529 "",
530 &[
531 nostr::Tag::RelayMetadata(
532 "wss://fredswrite1.relay".into(),
533 Some(nostr::RelayMetadata::Write),
534 ),
535 nostr::Tag::RelayMetadata(
536 "wss://fredsread1.relay".into(),
537 Some(nostr::RelayMetadata::Read),
538 ),
539 nostr::Tag::RelayMetadata("wss://fredsreadwrite.relay".into(), None),
540 ],
541 )
542 .to_event(&TEST_KEY_1_KEYS)
543 .unwrap()
544 }
545
546 fn generate_relaylist_event_user_2() -> nostr::Event {
547 nostr::event::EventBuilder::new(
548 nostr::Kind::RelayList,
549 "",
550 &[
551 nostr::Tag::RelayMetadata(
552 "wss://carolswrite1.relay".into(),
553 Some(nostr::RelayMetadata::Write),
554 ),
555 nostr::Tag::RelayMetadata(
556 "wss://carolsread1.relay".into(),
557 Some(nostr::RelayMetadata::Read),
558 ),
559 nostr::Tag::RelayMetadata("wss://carolsreadwrite.relay".into(), None),
560 ],
561 )
562 .to_event(&TEST_KEY_2_KEYS)
563 .unwrap()
564 }
565
566 fn fallback_relays() -> Vec<String> {
567 vec!["ws://fallback1".to_string(), "ws://fallback2".to_string()].clone()
568 }
569
570 fn generate_mock_client() -> MockConnect {
571 let mut client = <MockConnect as std::default::Default>::default();
572 client
573 .expect_get_fallback_relays()
574 .return_const(fallback_relays());
575 client
576 }
577
578 fn generate_standard_config() -> MyConfig {
579 MyConfig {
580 users: vec![UserRef {
581 public_key: TEST_KEY_1_KEYS.public_key(),
582 encrypted_key: TEST_KEY_1_ENCRYPTED.to_string(),
583 metadata: UserMetadata {
584 name: "Fred".to_string(),
585 created_at: 10,
586 },
587 relays: UserRelays {
588 relays: vec![
589 UserRelayRef {
590 url: "ws://existingread".to_string(),
591 read: true,
592 write: false,
593 },
594 UserRelayRef {
595 url: "ws://existingreadwrite".to_string(),
596 read: true,
597 write: true,
598 },
599 UserRelayRef {
600 url: "ws://existingwrite".to_string(),
601 read: false,
602 write: true,
603 },
604 ],
605 created_at: 10,
606 },
607 last_checked: now_timestamp() - (60 * 60), // 1h ago
608 }],
609 ..MyConfig::default()
610 }
611 .clone()
612 }
613
614 fn expected_userrelayrefs() -> Vec<UserRelayRef> {
615 vec![
616 UserRelayRef {
617 url: "wss://fredswrite1.relay".into(),
618 read: false,
619 write: true,
620 },
621 UserRelayRef {
622 url: "wss://fredsread1.relay".into(),
623 read: true,
624 write: false,
625 },
626 UserRelayRef {
627 url: "wss://fredsreadwrite.relay".into(),
628 read: true,
629 write: true,
630 },
631 ]
632 }
633
634 mod when_within_caching_time_window {
635 use super::*;
636
637 #[test]
638 fn returns_cached_details_without_checking_relays_or_updaing_config() -> Result<()> {
639 let mut m = MockUserManager::default();
640 let client = generate_mock_client();
641 m.config_manager
642 .expect_load()
643 .returning(|| Ok(generate_standard_config()));
644 let res = futures::executor::block_on(m.get_user(
645 &client,
646 &TEST_KEY_1_KEYS.public_key(),
647 24 * 60 * 60, // within 24 hours
648 ))?;
649 assert_eq!(res.metadata.name, "Fred");
650 assert_eq!(res.relays.relays[0].url, "ws://existingread");
651 Ok(())
652 }
653 }
654
655 mod returns_userref_with_latest_details_from_events_on_relays {
656 use super::*;
657
658 #[test]
659 fn name() -> Result<()> {
660 let mut m = MockUserManager::default();
661 let mut client = generate_mock_client();
662 m.config_manager
663 .expect_load()
664 .returning(|| Ok(generate_standard_config()));
665 m.config_manager.expect_save().returning(|_| Ok(()));
666 client
667 .expect_get_events()
668 .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")]));
669
670 let res = futures::executor::block_on(m.get_user(
671 &client,
672 &TEST_KEY_1_KEYS.public_key(),
673 5 * 60, // 5 mins ago
674 ))?;
675 assert_eq!(res.metadata.name, "fred");
676 Ok(())
677 }
678
679 #[test]
680 fn name_ignoring_other_users_events() -> Result<()> {
681 let mut m = MockUserManager::default();
682 let mut client = generate_mock_client();
683 m.config_manager
684 .expect_load()
685 .returning(|| Ok(generate_standard_config()));
686 m.config_manager.expect_save().returning(|_| Ok(()));
687 client.expect_get_events().returning(|_, _| {
688 Ok(vec![
689 generate_test_key_2_metadata_event("carole"),
690 generate_test_key_1_metadata_event_old("fred"),
691 ])
692 });
693
694 let res = futures::executor::block_on(m.get_user(
695 &client,
696 &TEST_KEY_1_KEYS.public_key(),
697 5 * 60, // 5 mins ago
698 ))?;
699 assert_eq!(res.metadata.name, "fred");
700 Ok(())
701 }
702
703 #[test]
704 fn relays() -> Result<()> {
705 let mut m = MockUserManager::default();
706 let mut client = generate_mock_client();
707 m.config_manager
708 .expect_load()
709 .returning(|| Ok(generate_standard_config()));
710 m.config_manager.expect_save().returning(|_| Ok(()));
711 client.expect_get_events().returning(|_, _| {
712 Ok(vec![
713 generate_test_key_1_metadata_event("fred"),
714 generate_relaylist_event(),
715 ])
716 });
717
718 let res = futures::executor::block_on(m.get_user(
719 &client,
720 &TEST_KEY_1_KEYS.public_key(),
721 5 * 60, // 5 mins ago
722 ))?;
723 assert_eq!(res.relays.relays, expected_userrelayrefs(),);
724 Ok(())
725 }
726
727 #[test]
728 fn relays_ignoring_other_users_events() -> Result<()> {
729 let mut m = MockUserManager::default();
730 let mut client = generate_mock_client();
731 m.config_manager
732 .expect_load()
733 .returning(|| Ok(generate_standard_config()));
734 m.config_manager.expect_save().returning(|_| Ok(()));
735 client.expect_get_events().returning(|_, _| {
736 Ok(vec![
737 make_event_old_or_change_user(
738 generate_relaylist_event(),
739 &TEST_KEY_1_KEYS,
740 10000,
741 ),
742 generate_relaylist_event_user_2(),
743 ])
744 });
745
746 let res = futures::executor::block_on(m.get_user(
747 &client,
748 &TEST_KEY_1_KEYS.public_key(),
749 5 * 60, // 5 mins ago
750 ))?;
751 assert_eq!(res.relays.relays, expected_userrelayrefs(),);
752 Ok(())
753 }
754 }
755
756 mod saves_updates_to_config {
757 use super::*;
758
759 #[test]
760 fn saves_name_to_config() -> Result<()> {
761 let mut m = MockUserManager::default();
762 let mut client = generate_mock_client();
763 m.config_manager
764 .expect_load()
765 .returning(|| Ok(generate_standard_config()));
766 m.config_manager
767 .expect_save()
768 .once()
769 .withf(|cfg| cfg.users[0].metadata.name.eq("fred"))
770 .returning(|_| Ok(()));
771 client
772 .expect_get_events()
773 .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")]));
774
775 futures::executor::block_on(m.get_user(
776 &client,
777 &TEST_KEY_1_KEYS.public_key(),
778 5 * 60, // 5 mins ago
779 ))?;
780 Ok(())
781 }
782
783 #[test]
784 fn updates_metadata_created_at() -> Result<()> {
785 let mut m = MockUserManager::default();
786 let mut client = generate_mock_client();
787 m.config_manager
788 .expect_load()
789 .returning(|| Ok(generate_standard_config()));
790 m.config_manager
791 .expect_save()
792 .once()
793 .withf(|cfg| roughly_now(cfg.users[0].metadata.created_at))
794 .returning(|_| Ok(()));
795 client
796 .expect_get_events()
797 .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")]));
798
799 futures::executor::block_on(m.get_user(
800 &client,
801 &TEST_KEY_1_KEYS.public_key(),
802 5 * 60, // 5 mins ago
803 ))?;
804 Ok(())
805 }
806
807 #[test]
808 fn saves_relays_to_config() -> Result<()> {
809 let mut m = MockUserManager::default();
810 let mut client = generate_mock_client();
811 m.config_manager
812 .expect_load()
813 .returning(|| Ok(generate_standard_config()));
814 m.config_manager
815 .expect_save()
816 .once()
817 .withf(|cfg| expected_userrelayrefs().eq(&cfg.users[0].relays.relays))
818 .returning(|_| Ok(()));
819 client
820 .expect_get_events()
821 .returning(|_, _| Ok(vec![generate_relaylist_event()]));
822
823 futures::executor::block_on(m.get_user(
824 &client,
825 &TEST_KEY_1_KEYS.public_key(),
826 5 * 60, // 5 mins ago
827 ))?;
828 Ok(())
829 }
830
831 #[test]
832 fn updates_relays_created_at() -> Result<()> {
833 let mut m = MockUserManager::default();
834 let mut client = generate_mock_client();
835 m.config_manager
836 .expect_load()
837 .returning(|| Ok(generate_standard_config()));
838 m.config_manager
839 .expect_save()
840 .once()
841 .withf(|cfg| roughly_now(cfg.users[0].relays.created_at))
842 .returning(|_| Ok(()));
843 client
844 .expect_get_events()
845 .returning(|_, _| Ok(vec![generate_relaylist_event()]));
846
847 futures::executor::block_on(m.get_user(
848 &client,
849 &TEST_KEY_1_KEYS.public_key(),
850 5 * 60, // 5 mins ago
851 ))?;
852 Ok(())
853 }
854
855 #[test]
856 fn when_no_changes_updates_last_updated() -> Result<()> {
857 let mut m = MockUserManager::default();
858 let mut client = generate_mock_client();
859 m.config_manager
860 .expect_load()
861 .returning(|| Ok(generate_standard_config()));
862 m.config_manager
863 .expect_save()
864 .once()
865 .withf(|cfg| roughly_now(cfg.users[0].last_checked))
866 .returning(|_| Ok(()));
867 client.expect_get_events().returning(|_, _| Ok(vec![]));
868
869 futures::executor::block_on(m.get_user(
870 &client,
871 &TEST_KEY_1_KEYS.public_key(),
872 5 * 60, // 5 mins ago
873 ))?;
874 Ok(())
875 }
876
877 #[test]
878 fn when_changes_updates_last_updated() -> Result<()> {
879 let mut m = MockUserManager::default();
880 let mut client = generate_mock_client();
881 m.config_manager
882 .expect_load()
883 .returning(|| Ok(generate_standard_config()));
884 m.config_manager
885 .expect_save()
886 .once()
887 .withf(|cfg| roughly_now(cfg.users[0].last_checked))
888 .returning(|_| Ok(()));
889 client
890 .expect_get_events()
891 .returning(|_, _| Ok(vec![generate_test_key_1_metadata_event("fred")]));
892
893 futures::executor::block_on(m.get_user(
894 &client,
895 &TEST_KEY_1_KEYS.public_key(),
896 5 * 60, // 5 mins ago
897 ))?;
898 Ok(())
899 }
900 }
901
902 mod fetches_from_correct_relays {
903 use super::*;
904 #[test]
905 fn when_userref_write_relays_present_fetches_only_from_them() -> Result<()> {
906 let mut m = MockUserManager::default();
907 let mut client = generate_mock_client();
908 m.config_manager
909 .expect_load()
910 .returning(|| Ok(generate_standard_config()));
911 m.config_manager.expect_save().returning(|_| Ok(()));
912 client
913 .expect_get_events()
914 .once()
915 .withf(move |relays, _filters| {
916 vec![
917 "ws://existingreadwrite".to_string(),
918 "ws://existingwrite".to_string(),
919 ]
920 .eq(relays)
921 })
922 .returning(|_, _| Ok(vec![]));
923
924 futures::executor::block_on(m.get_user(
925 &client,
926 &TEST_KEY_1_KEYS.public_key(),
927 5 * 60, // 5 mins ago
928 ))?;
929 Ok(())
930 }
931 #[test]
932 fn when_userref_write_relays_not_present_fetches_from_fallback_relays() -> Result<()> {
933 let mut m = MockUserManager::default();
934 let mut client = generate_mock_client();
935 m.config_manager.expect_load().returning(|| {
936 Ok(MyConfig {
937 users: vec![UserRef {
938 relays: UserRelays {
939 relays: vec![],
940 created_at: 0,
941 },
942 ..generate_standard_config().users[0].clone()
943 }],
944 ..generate_standard_config()
945 })
946 });
947 m.config_manager.expect_save().returning(|_| Ok(()));
948 client
949 .expect_get_events()
950 .once()
951 .withf(move |relays, _filters| fallback_relays().eq(relays))
952 .returning(|_, _| Ok(vec![]));
953
954 futures::executor::block_on(m.get_user(
955 &client,
956 &TEST_KEY_1_KEYS.public_key(),
957 5 * 60, // 5 mins ago
958 ))?;
959 Ok(())
960 }
961 }
962
963 #[test]
964 fn when_failed_to_fetch_events_returns_cached_details() -> Result<()> {
965 let mut m = MockUserManager::default();
966 let mut client = generate_mock_client();
967 m.config_manager
968 .expect_load()
969 .returning(|| Ok(generate_standard_config()));
970 client
971 .expect_get_events()
972 .returning(|_, _| Err(anyhow!("test error")));
973
974 let res = futures::executor::block_on(m.get_user(
975 &client,
976 &TEST_KEY_1_KEYS.public_key(),
977 5 * 60, // 10 mins ago
978 ))?;
979 assert_eq!(res.metadata.name, "Fred");
980 Ok(())
981 }
982 }
332} 983}
diff --git a/src/login.rs b/src/login.rs
index 12fe76e..e73373a 100644
--- a/src/login.rs
+++ b/src/login.rs
@@ -1,10 +1,14 @@
1use anyhow::{bail, Context, Result}; 1use anyhow::{bail, Context, Result};
2use nostr::prelude::{FromSkStr, ToBech32}; 2use nostr::{prelude::FromSkStr, secp256k1::XOnlyPublicKey};
3use zeroize::Zeroize; 3use zeroize::Zeroize;
4 4
5#[cfg(not(test))]
6use crate::client::Client;
7#[cfg(test)]
8use crate::client::MockConnect;
5use crate::{ 9use crate::{
6 cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, 10 cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms},
7 config::{ConfigManagement, ConfigManager}, 11 config::{ConfigManagement, ConfigManager, UserRef},
8 key_handling::{ 12 key_handling::{
9 encryption::{EncryptDecrypt, Encryptor}, 13 encryption::{EncryptDecrypt, Encryptor},
10 users::{UserManagement, UserManager}, 14 users::{UserManagement, UserManager},
@@ -12,77 +16,114 @@ use crate::{
12}; 16};
13 17
14/// handles the encrpytion and storage of key material 18/// handles the encrpytion and storage of key material
15pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { 19pub async fn launch(
20 nsec: &Option<String>,
21 password: &Option<String>,
22 #[cfg(test)] client: Option<&MockConnect>,
23 #[cfg(not(test))] client: Option<&Client>,
24) -> Result<nostr::Keys> {
16 // if nsec parameter 25 // if nsec parameter
17 if let Some(nsec_unwrapped) = nsec { 26 let key = if let Some(nsec_unwrapped) = nsec {
18 // get key or fail without prompts 27 // get key or fail without prompts
19 let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; 28 let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?;
20 println!(
21 "logged in as {}",
22 &key.public_key()
23 .to_bech32()
24 .context("public key should always produce bech32")?
25 );
26 29
27 // if password, add user to enable password login in future 30 // if password, add user to enable password login in future
28 if password.is_some() { 31 if password.is_some() {
29 UserManager::default() 32 UserManager::default()
30 .add(nsec, password) 33 .add(nsec, password)
31 .context("could not store identity")?; 34 .context("could not store identity")?;
35 } else {
36 UserManager::default().add_user_to_config(key.public_key(), None, false)?;
32 } 37 }
33 return Ok(key); 38 key
34 } 39 } else {
40 let cfg = ConfigManager
41 .load()
42 .context("failed to load application config")?;
43 // if encrypted nsec present
44 if cfg.users.last().is_some() && !cfg.users.last().unwrap().encrypted_key.is_empty() {
45 // unfortunately this line is unstable in rust:
46 // if let Some(user) = cfg.users.last() && !user.encrypted_key.is_empty() {
47 let user = cfg.users.last().unwrap();
48 let mut pass = if let Some(p) = password.clone() {
49 p
50 } else {
51 println!("login as {}", &user.metadata.name);
52 Interactor::default()
53 .password(PromptPasswordParms::default().with_prompt("password"))
54 .context("failed to get password input from interactor.password")?
55 };
35 56
36 // if encrypted nsec stored, attempt password 57 let key_result = Encryptor
37 let cfg = ConfigManager 58 .decrypt_key(&user.encrypted_key, pass.as_str())
38 .load() 59 .context("failed to decrypt key with provided password");
39 .context("failed to load application config")?; 60 pass.zeroize();
40 let key = if let Some(user) = cfg.users.last() {
41 let mut pass = if let Some(p) = password.clone() {
42 p
43 } else {
44 println!(
45 "login as {}",
46 &user
47 .public_key
48 .to_bech32()
49 .context("public key should always produce bech32")?
50 );
51 Interactor::default()
52 .password(PromptPasswordParms::default().with_prompt("password"))
53 .context("failed to get password input from interactor.password")?
54 };
55 61
56 let key_result = Encryptor 62 key_result.context(format!("failed to log in as {}", &user.metadata.name))?
57 .decrypt_key(&user.encrypted_key, pass.as_str()) 63 }
58 .context("failed to decrypt key with provided password"); 64 // no encrypted nsec present
59 pass.zeroize(); 65 else {
66 // no nsec but password supplied
67 if password.is_some() {
68 bail!("no nsec available to decrypt with specified password");
69 }
70 // otherwise add new user with nsec and password prompts
71 UserManager::default()
72 .add(nsec, password)
73 .context("failed to add user")?
74 }
75 };
60 76
61 key_result.context(format!( 77 // get user details
62 "failed to log in as {}", 78 let user_ref = if let Some(client) = client {
63 &user 79 get_user_details(&key.public_key(), client).await?
64 .public_key
65 .to_bech32()
66 .context("public key should always produce bech32")?
67 ))?
68 } else { 80 } else {
69 // no nsec but password supplied 81 // this will get user details with name as npub
70 if password.is_some() {
71 bail!("no nsec available to decrypt with specified password");
72 }
73 // otherwise add new user with nsec and password prompts
74 UserManager::default() 82 UserManager::default()
75 .add(nsec, password) 83 .get_user_from_cache(&key.public_key())?
76 .context("failed to add user")? 84 .clone()
77 }; 85 };
78 println!(
79 "logged in as {}",
80 &key.public_key()
81 .to_bech32()
82 .context("public key should always produce bech32")?
83 );
84 86
85 // fetching metdata 87 // print logged in
88 println!("logged in as {}", user_ref.metadata.name);
86 89
87 Ok(key) 90 Ok(key)
88} 91}
92
93async fn get_user_details(
94 public_key: &XOnlyPublicKey,
95 #[cfg(test)] client: &crate::client::MockConnect,
96 #[cfg(not(test))] client: &Client,
97) -> Result<UserRef> {
98 let term = console::Term::stdout();
99 term.write_line("searching for your details...")?;
100 let user_manager = UserManager::default();
101 let user_ref = user_manager
102 .get_user(
103 client,
104 public_key,
105 // 1 hour
106 60 * 60,
107 )
108 .await?;
109 term.clear_last_lines(1)?;
110 if user_ref.metadata.created_at.eq(&0) {
111 println!("cannot find your account metadata (name, etc) on relays",);
112 // TODO use secondary fallback list of relays.
113 // TODO better reporting of what relays were checked and what the user
114 // here is a starter:
115 // cannot find account details on relays:
116 // - purplepages.xyz
117 // - fallbackrelay1
118 // - ...
119 // would you like to:
120 // [-] proceed anyway
121 // - add custom fallback relays
122 } else if user_ref.relays.created_at.eq(&0) {
123 println!(
124 "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience."
125 );
126 // TODO better guidance on how to do this
127 }
128 Ok(user_ref)
129}
diff --git a/src/main.rs b/src/main.rs
index 5c8518b..68b0ed6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -41,7 +41,9 @@ enum Commands {
41async fn main() -> Result<()> { 41async fn main() -> Result<()> {
42 let cli = Cli::parse(); 42 let cli = Cli::parse();
43 match &cli.command { 43 match &cli.command {
44 Commands::Login(args) => sub_commands::login::launch(&cli, args), 44 Commands::Login(args) => {
45 futures::executor::block_on(sub_commands::login::launch(&cli, args))
46 }
45 Commands::Prs(args) => futures::executor::block_on(sub_commands::prs::launch(&cli, args)), 47 Commands::Prs(args) => futures::executor::block_on(sub_commands::prs::launch(&cli, args)),
46 } 48 }
47} 49}
diff --git a/src/sub_commands/login.rs b/src/sub_commands/login.rs
index 5391024..b93e9bc 100644
--- a/src/sub_commands/login.rs
+++ b/src/sub_commands/login.rs
@@ -1,12 +1,32 @@
1use anyhow::Result; 1use anyhow::Result;
2use clap; 2use clap;
3 3
4use crate::{login, Cli}; 4#[cfg(not(test))]
5use crate::client::Client;
6#[cfg(test)]
7use crate::client::MockConnect;
8use crate::{client::Connect, login, Cli};
5 9
6#[derive(clap::Args)] 10#[derive(clap::Args)]
7pub struct SubCommandArgs; 11pub struct SubCommandArgs {
12 /// don't fetch user metadata and relay list from relays
13 #[arg(long, action)]
14 offline: bool,
15}
16
17pub async fn launch(args: &Cli, command_args: &SubCommandArgs) -> Result<()> {
18 if command_args.offline {
19 login::launch(&args.nsec, &args.password, None).await?;
20 Ok(())
21 } else {
22 #[cfg(not(test))]
23 let client = Client::default();
24 #[cfg(test)]
25 let client = <MockConnect as std::default::Default>::default();
8 26
9pub fn launch(args: &Cli, _command_args: &SubCommandArgs) -> Result<()> { 27 client.connect().await?;
10 let _ = login::launch(&args.nsec, &args.password)?; 28 login::launch(&args.nsec, &args.password, Some(&client)).await?;
11 Ok(()) 29 client.disconnect().await?;
30 Ok(())
31 }
12} 32}
diff --git a/src/sub_commands/prs/create.rs b/src/sub_commands/prs/create.rs
index 3047e92..0249488 100644
--- a/src/sub_commands/prs/create.rs
+++ b/src/sub_commands/prs/create.rs
@@ -86,7 +86,8 @@ pub async fn launch(
86 86
87 // create PR event 87 // create PR event
88 88
89 let keys = login::launch(&cli_args.nsec, &cli_args.password)?; 89 // TODO add client here
90 let keys = login::launch(&cli_args.nsec, &cli_args.password, None).await?;
90 91
91 let events = 92 let events =
92 generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, keys)?; 93 generate_pr_and_patch_events(&title, &description, &to_branch, &git_repo, &ahead, keys)?;
diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml
index 2d3555b..6a3fff8 100644
--- a/test_utils/Cargo.toml
+++ b/test_utils/Cargo.toml
@@ -15,3 +15,4 @@ rand = "0.8"
15rexpect = { git = "https://github.com/phaer/rexpect.git", branch= "skip-ansi-escape-codes" } 15rexpect = { git = "https://github.com/phaer/rexpect.git", branch= "skip-ansi-escape-codes" }
16simple-websockets = "0.1.6" 16simple-websockets = "0.1.6"
17strip-ansi-escapes = "0.2.0" 17strip-ansi-escapes = "0.2.0"
18tungstenite = "0.20.1"
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs
index a9d818c..2a06357 100644
--- a/test_utils/src/lib.rs
+++ b/test_utils/src/lib.rs
@@ -23,6 +23,39 @@ pub static TEST_KEY_1_ENCRYPTED_WEAK: &str = "ncryptsec1qy8ke0tjqnn8wt3w6lnc86c2
23pub static TEST_KEY_1_KEYS: Lazy<nostr::Keys> = 23pub static TEST_KEY_1_KEYS: Lazy<nostr::Keys> =
24 Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_1_NSEC).unwrap()); 24 Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_1_NSEC).unwrap());
25 25
26pub fn generate_test_key_1_metadata_event(name: &str) -> nostr::Event {
27 nostr::event::EventBuilder::set_metadata(nostr::Metadata::new().name(name))
28 .to_event(&TEST_KEY_1_KEYS)
29 .unwrap()
30}
31pub fn generate_test_key_1_metadata_event_old(name: &str) -> nostr::Event {
32 make_event_old_or_change_user(
33 generate_test_key_1_metadata_event(name),
34 &TEST_KEY_1_KEYS,
35 10000,
36 )
37}
38
39pub fn generate_test_key_1_relay_list_event() -> nostr::Event {
40 nostr::event::EventBuilder::new(
41 nostr::Kind::RelayList,
42 "",
43 &[
44 nostr::Tag::RelayMetadata(
45 "ws://localhost:8053".into(),
46 Some(nostr::RelayMetadata::Write),
47 ),
48 nostr::Tag::RelayMetadata(
49 "ws://localhost:8054".into(),
50 Some(nostr::RelayMetadata::Read),
51 ),
52 nostr::Tag::RelayMetadata("ws://localhost:8055".into(), None),
53 ],
54 )
55 .to_event(&TEST_KEY_1_KEYS)
56 .unwrap()
57}
58
26pub static TEST_KEY_2_NSEC: &str = 59pub static TEST_KEY_2_NSEC: &str =
27 "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm"; 60 "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm";
28pub static TEST_KEY_2_NPUB: &str = 61pub static TEST_KEY_2_NPUB: &str =
@@ -33,12 +66,41 @@ pub static TEST_KEY_2_ENCRYPTED: &str = "...2";
33pub static TEST_KEY_2_KEYS: Lazy<nostr::Keys> = 66pub static TEST_KEY_2_KEYS: Lazy<nostr::Keys> =
34 Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_2_NSEC).unwrap()); 67 Lazy::new(|| nostr::Keys::from_sk_str(TEST_KEY_2_NSEC).unwrap());
35 68
69pub fn generate_test_key_2_metadata_event(name: &str) -> nostr::Event {
70 nostr::event::EventBuilder::set_metadata(nostr::Metadata::new().name(name))
71 .to_event(&TEST_KEY_2_KEYS)
72 .unwrap()
73}
74
36pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex"; 75pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex";
37pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t"; 76pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t";
38pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!"; 77pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!";
39pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe"; 78pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe";
40pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg"; 79pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg";
41 80
81pub fn make_event_old_or_change_user(
82 event: nostr::Event,
83 keys: &nostr::Keys,
84 how_old_in_secs: u64,
85) -> nostr::Event {
86 let mut unsigned = nostr::event::EventBuilder::new(event.kind, event.content, &event.tags)
87 .to_unsigned_event(keys.public_key());
88
89 unsigned.created_at = nostr::types::Timestamp::try_from(
90 nostr::types::Timestamp::now().as_u64() - how_old_in_secs,
91 )
92 .unwrap();
93 unsigned.id = nostr::EventId::new(
94 &keys.public_key(),
95 unsigned.created_at,
96 &unsigned.kind,
97 &unsigned.tags,
98 &unsigned.content,
99 );
100
101 unsigned.sign(keys).unwrap()
102}
103
42/// wrapper for a cli testing tool - currently wraps rexpect and dialoguer 104/// wrapper for a cli testing tool - currently wraps rexpect and dialoguer
43/// 105///
44/// 1. allow more accurate articulation of expected behaviour 106/// 1. allow more accurate articulation of expected behaviour
diff --git a/test_utils/src/relay.rs b/test_utils/src/relay.rs
index 6de3618..ce618a3 100644
--- a/test_utils/src/relay.rs
+++ b/test_utils/src/relay.rs
@@ -5,26 +5,36 @@ use nostr::{ClientMessage, RelayMessage};
5 5
6use crate::CliTester; 6use crate::CliTester;
7 7
8type ListenerFunc<'a> = &'a dyn Fn(&mut Relay, u64, nostr::Event) -> Result<()>; 8type ListenerEventFunc<'a> = &'a dyn Fn(&mut Relay, u64, nostr::Event) -> Result<()>;
9pub type ListenerReqFunc<'a> =
10 &'a dyn Fn(&mut Relay, u64, nostr::SubscriptionId, Vec<nostr::Filter>) -> Result<()>;
9 11
10pub struct Relay<'a> { 12pub struct Relay<'a> {
11 port: u16, 13 port: u16,
12 event_hub: simple_websockets::EventHub, 14 event_hub: simple_websockets::EventHub,
13 clients: HashMap<u64, simple_websockets::Responder>, 15 clients: HashMap<u64, simple_websockets::Responder>,
14 pub events: Vec<nostr::Event>, 16 pub events: Vec<nostr::Event>,
15 event_listener: Option<ListenerFunc<'a>>, 17 pub reqs: Vec<Vec<nostr::Filter>>,
18 event_listener: Option<ListenerEventFunc<'a>>,
19 req_listener: Option<ListenerReqFunc<'a>>,
16} 20}
17 21
18impl<'a> Relay<'a> { 22impl<'a> Relay<'a> {
19 pub fn new(port: u16, event_listener: Option<ListenerFunc<'a>>) -> Self { 23 pub fn new(
24 port: u16,
25 event_listener: Option<ListenerEventFunc<'a>>,
26 req_listener: Option<ListenerReqFunc<'a>>,
27 ) -> Self {
20 let event_hub = simple_websockets::launch(port) 28 let event_hub = simple_websockets::launch(port)
21 .unwrap_or_else(|_| panic!("failed to listen on port {port}")); 29 .unwrap_or_else(|_| panic!("failed to listen on port {port}"));
22 Self { 30 Self {
23 port, 31 port,
24 events: vec![], 32 events: vec![],
33 reqs: vec![],
25 event_hub, 34 event_hub,
26 clients: HashMap::new(), 35 clients: HashMap::new(),
27 event_listener, 36 event_listener,
37 req_listener,
28 } 38 }
29 } 39 }
30 pub fn respond_ok( 40 pub fn respond_ok(
@@ -44,11 +54,54 @@ impl<'a> Relay<'a> {
44 // bail!(format!("{}", &ok_json)); 54 // bail!(format!("{}", &ok_json));
45 Ok(responder.send(simple_websockets::Message::Text(ok_json))) 55 Ok(responder.send(simple_websockets::Message::Text(ok_json)))
46 } 56 }
57
58 pub fn respond_eose(
59 &self,
60 client_id: u64,
61 subscription_id: nostr::SubscriptionId,
62 ) -> Result<bool> {
63 let responder = self.clients.get(&client_id).unwrap();
64
65 Ok(responder.send(simple_websockets::Message::Text(
66 RelayMessage::EndOfStoredEvents(subscription_id).as_json(),
67 )))
68 }
69
70 /// send events and eose
71 pub fn respond_events(
72 &self,
73 client_id: u64,
74 subscription_id: &nostr::SubscriptionId,
75 events: &Vec<nostr::Event>,
76 ) -> Result<bool> {
77 let responder = self.clients.get(&client_id).unwrap();
78
79 for event in events {
80 let res = responder.send(simple_websockets::Message::Text(
81 RelayMessage::Event {
82 subscription_id: subscription_id.clone(),
83 event: Box::new(event.clone()),
84 }
85 .as_json(),
86 ));
87 if !res {
88 return Ok(false);
89 }
90 }
91 self.respond_eose(client_id, subscription_id.clone())
92 }
93
94 pub fn shutdown(&mut self) -> Result<()> {
95 let (mut socket, _) = tungstenite::connect(format!("ws://localhost:{}", self.port))?;
96 socket.write(tungstenite::Message::text("shut me down"))?;
97 socket.close(None)?;
98 Ok(())
99 }
47 /// listen, collect events and responds with event_listener to events or 100 /// listen, collect events and responds with event_listener to events or
48 /// Ok(eventid) if event_listner is None 101 /// Ok(eventid) if event_listner is None
49 pub async fn listen_until_close(&mut self) -> Result<()> { 102 pub async fn listen_until_close(&mut self) -> Result<()> {
50 loop { 103 loop {
51 println!("polling"); 104 println!("{} polling", self.port);
52 match self.event_hub.poll_async().await { 105 match self.event_hub.poll_async().await {
53 simple_websockets::Event::Connect(client_id, responder) => { 106 simple_websockets::Event::Connect(client_id, responder) => {
54 // add their Responder to our `clients` map: 107 // add their Responder to our `clients` map:
@@ -65,8 +118,13 @@ impl<'a> Relay<'a> {
65 "Received a message from client #{}: {:?}", 118 "Received a message from client #{}: {:?}",
66 client_id, message 119 client_id, message
67 ); 120 );
68 121 if let simple_websockets::Message::Text(s) = message.clone() {
69 if let Ok(event) = get_nevent(message) { 122 if s.eq("shut me down") {
123 println!("{} recieved shut me down", self.port);
124 break;
125 }
126 }
127 if let Ok(event) = get_nevent(&message) {
70 self.events.push(event.clone()); 128 self.events.push(event.clone());
71 if let Some(listner) = self.event_listener { 129 if let Some(listner) = self.event_listener {
72 listner(self, client_id, event)?; 130 listner(self, client_id, event)?;
@@ -74,16 +132,40 @@ impl<'a> Relay<'a> {
74 self.respond_ok(client_id, event, None)?; 132 self.respond_ok(client_id, event, None)?;
75 } 133 }
76 } 134 }
135
136 if let Ok((subscription_id, filters)) = get_nreq(&message) {
137 self.reqs.push(filters.clone());
138 if let Some(listner) = self.req_listener {
139 listner(self, client_id, subscription_id, filters)?;
140 } else {
141 self.respond_eose(client_id, subscription_id)?;
142 }
143 // respond with events
144 // respond with EOSE
145 }
146 if is_nclose(&message) {
147 println!("{} recieved nostr close", self.port);
148 break;
149 }
77 } 150 }
78 } 151 }
79 } 152 }
80 println!("stop polling"); 153 println!(
81 println!("we may not be polling but the tcplistner is still listening"); 154 "{} stop polling. we may not be polling but the tcplistner is still listening",
155 self.port
156 );
82 Ok(()) 157 Ok(())
83 } 158 }
84} 159}
85 160
86fn get_nevent(message: simple_websockets::Message) -> Result<nostr::Event> { 161pub fn shutdown_relay(port: u64) -> Result<()> {
162 let (mut socket, _) = tungstenite::connect(format!("ws://localhost:{}", port))?;
163 socket.write(tungstenite::Message::text("shut me down"))?;
164 socket.close(None)?;
165 Ok(())
166}
167
168fn get_nevent(message: &simple_websockets::Message) -> Result<nostr::Event> {
87 if let simple_websockets::Message::Text(s) = message.clone() { 169 if let simple_websockets::Message::Text(s) = message.clone() {
88 let cm_result = ClientMessage::from_json(s); 170 let cm_result = ClientMessage::from_json(s);
89 if let Ok(ClientMessage::Event(event)) = cm_result { 171 if let Ok(ClientMessage::Event(event)) = cm_result {
@@ -94,6 +176,32 @@ fn get_nevent(message: simple_websockets::Message) -> Result<nostr::Event> {
94 bail!("not nostr event") 176 bail!("not nostr event")
95} 177}
96 178
179fn get_nreq(
180 message: &simple_websockets::Message,
181) -> Result<(nostr::SubscriptionId, Vec<nostr::Filter>)> {
182 if let simple_websockets::Message::Text(s) = message.clone() {
183 let cm_result = ClientMessage::from_json(s);
184 if let Ok(ClientMessage::Req {
185 subscription_id,
186 filters,
187 }) = cm_result
188 {
189 return Ok((subscription_id, filters));
190 }
191 }
192 bail!("not nostr event")
193}
194
195fn is_nclose(message: &simple_websockets::Message) -> bool {
196 if let simple_websockets::Message::Text(s) = message.clone() {
197 let cm_result = ClientMessage::from_json(s);
198 if let Ok(ClientMessage::Close(_)) = cm_result {
199 return true;
200 }
201 }
202 false
203}
204
97pub enum Message { 205pub enum Message {
98 Event, 206 Event,
99 // Request, 207 // Request,
diff --git a/tests/login.rs b/tests/login.rs
index a75608d..d565620 100644
--- a/tests/login.rs
+++ b/tests/login.rs
@@ -8,7 +8,7 @@ static EXPECTED_SET_PASSWORD_CONFIRM_PROMPT: &str = "confirm password";
8static EXPECTED_PASSWORD_PROMPT: &str = "password"; 8static EXPECTED_PASSWORD_PROMPT: &str = "password";
9 9
10fn standard_login() -> Result<CliTester> { 10fn standard_login() -> Result<CliTester> {
11 let mut p = CliTester::new(["login"]); 11 let mut p = CliTester::new(["login", "--offline"]);
12 12
13 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)? 13 p.expect_input_eventually(EXPECTED_NSEC_PROMPT)?
14 .succeeds_with(TEST_KEY_1_NSEC)?; 14 .succeeds_with(TEST_KEY_1_NSEC)?;
@@ -20,74 +20,602 @@ fn standard_login() -> Result<CliTester> {
20 p.expect_end_eventually()?; 20 p.expect_end_eventually()?;
21 Ok(p) 21 Ok(p)
22} 22}
23mod with_relays {
24 use anyhow::Ok;
25 use futures::join;
26 use test_utils::relay::{shutdown_relay, ListenerReqFunc, Relay};
23 27
24mod when_first_time_login {
25 use super::*; 28 use super::*;
26 29
27 #[test] 30 mod when_first_time_login {
28 #[serial] 31 use super::*;
29 fn prompts_for_nsec_and_password() -> Result<()> {
30 before()?;
31 standard_login()?;
32 after()
33 }
34
35 #[test]
36 #[serial]
37 fn succeeds_with_text_logged_in_as_npub() -> Result<()> {
38 with_fresh_config(|| {
39 let mut p = CliTester::new(["login"]);
40 32
41 p.expect_input(EXPECTED_NSEC_PROMPT)? 33 // falls_back_to_fallback_relays - this is implict in the tests
42 .succeeds_with(TEST_KEY_1_NSEC)?; 34
35 mod dislays_logged_in_with_correct_name {
36
37 use super::*;
38
39 async fn run_test_displays_correct_name(
40 relay_listener1: Option<ListenerReqFunc<'_>>,
41 relay_listener2: Option<ListenerReqFunc<'_>>,
42 ) -> Result<()> {
43 let (mut r51, mut r52) = (
44 Relay::new(8051, None, relay_listener1),
45 Relay::new(8052, None, relay_listener2),
46 );
47
48 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
49 with_fresh_config(|| {
50 let mut p = CliTester::new(["login"]);
51
52 p.expect_input(EXPECTED_NSEC_PROMPT)?
53 .succeeds_with(TEST_KEY_1_NSEC)?;
54
55 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
56 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
57 .succeeds_with(TEST_PASSWORD)?;
58
59 p.expect("searching for your details...\r\n")?;
60 p.expect("\r")?;
61
62 p.expect_end_with("logged in as fred\r\n")?;
63 for p in [51, 52] {
64 shutdown_relay(8000 + p)?;
65 }
66 Ok(())
67 })
68 });
69
70 // launch relay
71 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
72
73 cli_tester_handle.join().unwrap()?;
74 Ok(())
75 }
76
77 #[test]
78 #[serial]
79 fn when_latest_metadata_and_relay_list_on_all_relays() -> Result<()> {
80 futures::executor::block_on(run_test_displays_correct_name(
81 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
82 relay.respond_events(
83 client_id,
84 &subscription_id,
85 &vec![
86 generate_test_key_1_metadata_event("fred"),
87 generate_test_key_1_relay_list_event(),
88 ],
89 )?;
90 Ok(())
91 }),
92 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
93 relay.respond_events(
94 client_id,
95 &subscription_id,
96 &vec![
97 generate_test_key_1_metadata_event("fred"),
98 generate_test_key_1_relay_list_event(),
99 ],
100 )?;
101 Ok(())
102 }),
103 ))
104 }
105
106 #[test]
107 #[serial]
108 fn when_latest_metadata_and_relay_list_on_some_relays_but_others_have_none()
109 -> Result<()> {
110 futures::executor::block_on(run_test_displays_correct_name(
111 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
112 relay.respond_events(
113 client_id,
114 &subscription_id,
115 &vec![
116 generate_test_key_1_metadata_event("fred"),
117 generate_test_key_1_relay_list_event(),
118 ],
119 )?;
120 Ok(())
121 }),
122 None,
123 ))
124 }
125
126 #[test]
127 #[serial]
128 fn when_latest_metadata_only_on_relay_and_relay_list_on_another() -> Result<()> {
129 futures::executor::block_on(run_test_displays_correct_name(
130 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
131 relay.respond_events(
132 client_id,
133 &subscription_id,
134 &vec![generate_test_key_1_metadata_event("fred")],
135 )?;
136 Ok(())
137 }),
138 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
139 relay.respond_events(
140 client_id,
141 &subscription_id,
142 &vec![generate_test_key_1_relay_list_event()],
143 )?;
144 Ok(())
145 }),
146 ))
147 }
148
149 #[test]
150 #[serial]
151 fn when_some_relays_return_old_metadata_event() -> Result<()> {
152 futures::executor::block_on(run_test_displays_correct_name(
153 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
154 relay.respond_events(
155 client_id,
156 &subscription_id,
157 &vec![
158 generate_test_key_1_metadata_event("fred"),
159 generate_test_key_1_relay_list_event(),
160 ],
161 )?;
162 Ok(())
163 }),
164 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
165 relay.respond_events(
166 client_id,
167 &subscription_id,
168 &vec![generate_test_key_1_metadata_event_old("fred old")],
169 )?;
170 Ok(())
171 }),
172 ))
173 }
174
175 #[test]
176 #[serial]
177 fn when_some_relays_return_other_users_metadata() -> Result<()> {
178 futures::executor::block_on(run_test_displays_correct_name(
179 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
180 relay.respond_events(
181 client_id,
182 &subscription_id,
183 &vec![generate_test_key_2_metadata_event("carole")],
184 )?;
185 Ok(())
186 }),
187 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
188 relay.respond_events(
189 client_id,
190 &subscription_id,
191 &vec![
192 generate_test_key_1_metadata_event_old("fred"),
193 generate_test_key_1_relay_list_event(),
194 ],
195 )?;
196 Ok(())
197 }),
198 ))
199 }
200
201 #[test]
202 #[serial]
203 fn when_some_relays_return_other_event_kinds() -> Result<()> {
204 futures::executor::block_on(run_test_displays_correct_name(
205 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
206 let mut event = generate_test_key_1_metadata_event("Fred");
207 event.kind = nostr::Kind::TextNote;
208 relay.respond_events(
209 client_id,
210 &subscription_id,
211 &vec![make_event_old_or_change_user(event, &TEST_KEY_1_KEYS, 0)],
212 )?;
213 Ok(())
214 }),
215 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
216 relay.respond_events(
217 client_id,
218 &subscription_id,
219 &vec![
220 generate_test_key_1_metadata_event_old("fred"),
221 generate_test_key_1_relay_list_event(),
222 ],
223 )?;
224 Ok(())
225 }),
226 ))
227 }
228
229 mod when_specifying_command_line_nsec_only {
230 use super::*;
231
232 #[test]
233 #[serial]
234 fn displays_correct_name() -> Result<()> {
235 futures::executor::block_on(
236 run_test_when_specifying_command_line_nsec_only_displays_correct_name(
237 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
238 relay.respond_events(
239 client_id,
240 &subscription_id,
241 &vec![
242 generate_test_key_1_metadata_event("fred"),
243 generate_test_key_1_relay_list_event(),
244 ],
245 )?;
246 Ok(())
247 }),
248 None,
249 ),
250 )
251 }
252 async fn run_test_when_specifying_command_line_nsec_only_displays_correct_name(
253 relay_listener1: Option<ListenerReqFunc<'_>>,
254 relay_listener2: Option<ListenerReqFunc<'_>>,
255 ) -> Result<()> {
256 let (mut r51, mut r52) = (
257 Relay::new(8051, None, relay_listener1),
258 Relay::new(8052, None, relay_listener2),
259 );
260
261 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
262 with_fresh_config(|| {
263 let mut p = CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC]);
264
265 p.expect("searching for your details...\r\n")?;
266 p.expect("\r")?;
267
268 p.expect_end_with("logged in as fred\r\n")?;
269 for p in [51, 52] {
270 shutdown_relay(8000 + p)?;
271 }
272 Ok(())
273 })
274 });
275
276 // launch relay
277 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
278
279 cli_tester_handle.join().unwrap()?;
280 Ok(())
281 }
282 }
283 mod when_specifying_command_line_password_only {
284 use super::*;
285
286 #[test]
287 #[serial]
288 fn displays_correct_name() -> Result<()> {
289 futures::executor::block_on(
290 run_test_when_specifying_command_line_password_only_displays_correct_name(
291 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
292 relay.respond_events(
293 client_id,
294 &subscription_id,
295 &vec![
296 generate_test_key_1_metadata_event("fred"),
297 generate_test_key_1_relay_list_event(),
298 ],
299 )?;
300 Ok(())
301 }),
302 None,
303 ),
304 )
305 }
306 async fn run_test_when_specifying_command_line_password_only_displays_correct_name(
307 relay_listener1: Option<ListenerReqFunc<'_>>,
308 relay_listener2: Option<ListenerReqFunc<'_>>,
309 ) -> Result<()> {
310 let (mut r51, mut r52) = (
311 Relay::new(8051, None, relay_listener1),
312 Relay::new(8052, None, relay_listener2),
313 );
314
315 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
316 with_fresh_config(|| {
317 CliTester::new([
318 "login",
319 "--offline",
320 "--nsec",
321 TEST_KEY_1_NSEC,
322 "--password",
323 TEST_PASSWORD,
324 ])
325 .expect_end_eventually()?;
326
327 let mut p = CliTester::new(["login", "--password", TEST_PASSWORD]);
328
329 p.expect("searching for your details...\r\n")?;
330 p.expect("\r")?;
331
332 p.expect_end_with("logged in as fred\r\n")?;
333 for p in [51, 52] {
334 shutdown_relay(8000 + p)?;
335 }
336 Ok(())
337 })
338 });
339
340 // launch relay
341 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
342
343 cli_tester_handle.join().unwrap()?;
344 Ok(())
345 }
346 }
347
348 mod when_specifying_command_line_nsec_and_password {
349 use super::*;
350
351 #[test]
352 #[serial]
353 fn displays_correct_name() -> Result<()> {
354 futures::executor::block_on(
355 run_test_when_specifying_command_line_nsec_and_password_displays_correct_name(
356 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
357 relay.respond_events(
358 client_id,
359 &subscription_id,
360 &vec![
361 generate_test_key_1_metadata_event("fred"),
362 generate_test_key_1_relay_list_event(),
363 ],
364 )?;
365 Ok(())
366 }),
367 None,
368 ),
369 )
370 }
371 async fn run_test_when_specifying_command_line_nsec_and_password_displays_correct_name(
372 relay_listener1: Option<ListenerReqFunc<'_>>,
373 relay_listener2: Option<ListenerReqFunc<'_>>,
374 ) -> Result<()> {
375 let (mut r51, mut r52) = (
376 Relay::new(8051, None, relay_listener1),
377 Relay::new(8052, None, relay_listener2),
378 );
379
380 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
381 with_fresh_config(|| {
382 let mut p = CliTester::new([
383 "login",
384 "--nsec",
385 TEST_KEY_1_NSEC,
386 "--password",
387 TEST_PASSWORD,
388 ]);
389
390 p.expect("searching for your details...\r\n")?;
391 p.expect("\r")?;
392
393 p.expect_end_with("logged in as fred\r\n")?;
394 for p in [51, 52] {
395 shutdown_relay(8000 + p)?;
396 }
397 Ok(())
398 })
399 });
400
401 // launch relay
402 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
403
404 cli_tester_handle.join().unwrap()?;
405 Ok(())
406 }
407 }
408 }
43 409
44 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? 410 mod when_no_metadata_found {
45 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? 411 use super::*;
46 .succeeds_with(TEST_PASSWORD)?; 412
413 #[test]
414 #[serial]
415 fn warm_user_and_displays_npub() -> Result<()> {
416 futures::executor::block_on(
417 run_test_when_no_metadata_found_warns_user_and_uses_npub(None, None),
418 )
419 }
420
421 async fn run_test_when_no_metadata_found_warns_user_and_uses_npub(
422 relay_listener1: Option<ListenerReqFunc<'_>>,
423 relay_listener2: Option<ListenerReqFunc<'_>>,
424 ) -> Result<()> {
425 let (mut r51, mut r52) = (
426 Relay::new(8051, None, relay_listener1),
427 Relay::new(8052, None, relay_listener2),
428 );
429
430 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
431 with_fresh_config(|| {
432 let mut p = CliTester::new(["login"]);
433
434 p.expect_input(EXPECTED_NSEC_PROMPT)?
435 .succeeds_with(TEST_KEY_1_NSEC)?;
436
437 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
438 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
439 .succeeds_with(TEST_PASSWORD)?;
440
441 p.expect("searching for your details...\r\n")?;
442 p.expect("\r")?;
443 p.expect("cannot find your account metadata (name, etc) on relays\r\n")?;
444
445 p.expect_end_with(format!("logged in as {TEST_KEY_1_NPUB}\r\n").as_str())?;
446 for p in [51, 52] {
447 shutdown_relay(8000 + p)?;
448 }
449 Ok(())
450 })
451 });
452
453 // launch relay
454 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
455
456 cli_tester_handle.join().unwrap()?;
457 Ok(())
458 }
459 }
47 460
48 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 461 mod when_metadata_but_no_relay_list_found {
49 }) 462 use super::*;
463
464 #[test]
465 #[serial]
466 fn warm_user_and_displays_name() -> Result<()> {
467 futures::executor::block_on(
468 run_test_when_no_relay_list_found_warns_user_and_uses_npub(
469 Some(&|relay, client_id, subscription_id, _| -> Result<()> {
470 relay.respond_events(
471 client_id,
472 &subscription_id,
473 &vec![generate_test_key_1_metadata_event("fred")],
474 )?;
475 Ok(())
476 }),
477 None,
478 ),
479 )
480 }
481
482 async fn run_test_when_no_relay_list_found_warns_user_and_uses_npub(
483 relay_listener1: Option<ListenerReqFunc<'_>>,
484 relay_listener2: Option<ListenerReqFunc<'_>>,
485 ) -> Result<()> {
486 let (mut r51, mut r52) = (
487 Relay::new(8051, None, relay_listener1),
488 Relay::new(8052, None, relay_listener2),
489 );
490
491 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
492 with_fresh_config(|| {
493 let mut p = CliTester::new(["login"]);
494
495 p.expect_input(EXPECTED_NSEC_PROMPT)?
496 .succeeds_with(TEST_KEY_1_NSEC)?;
497
498 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
499 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
500 .succeeds_with(TEST_PASSWORD)?;
501
502 p.expect("searching for your details...\r\n")?;
503 p.expect("\r")?;
504 p.expect("cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience.\r\n")?;
505
506 p.expect_end_with("logged in as fred\r\n")?;
507 for p in [51, 52] {
508 shutdown_relay(8000 + p)?;
509 }
510 Ok(())
511 })
512 });
513
514 // launch relay
515 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
516
517 cli_tester_handle.join().unwrap()?;
518 Ok(())
519 }
520 }
50 } 521 }
51 522
52 #[test] 523 mod when_second_time_login_and_details_already_fetched {
53 #[serial] 524 use super::*;
54 fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> {
55 with_fresh_config(|| {
56 let mut p = CliTester::new(["login"]);
57
58 p.expect_input(EXPECTED_NSEC_PROMPT)?
59 .succeeds_with(TEST_KEY_1_SK_HEX)?;
60
61 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
62 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
63 .succeeds_with(TEST_PASSWORD)?;
64 525
65 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 526 // TODO: the following two tests would require a fake config file or
66 }) 527 // fake time
528 // - uses_relays_from_user_relay_list
529 // - dislays_correct_name - when_local_metadata_is_the_most_recent
530
531 mod uses_cache {
532 use super::*;
533
534 #[test]
535 #[serial]
536 fn dislays_logged_in_with_correct_name() -> Result<()> {
537 futures::executor::block_on(run_test_dislays_logged_in_with_correct_name(Some(
538 &|relay, client_id, subscription_id, _| -> Result<()> {
539 relay.respond_events(
540 client_id,
541 &subscription_id,
542 &vec![
543 generate_test_key_1_metadata_event("fred"),
544 generate_test_key_1_relay_list_event(),
545 ],
546 )?;
547 Ok(())
548 },
549 )))
550 }
551 async fn run_test_dislays_logged_in_with_correct_name(
552 relay_listener: Option<ListenerReqFunc<'_>>,
553 ) -> Result<()> {
554 let (mut r51, mut r52) = (
555 Relay::new(8051, None, relay_listener),
556 Relay::new(8052, None, None),
557 );
558
559 let cli_tester_handle = std::thread::spawn(move || -> Result<()> {
560 with_fresh_config(|| {
561 let mut p = CliTester::new([
562 "login",
563 "--nsec",
564 TEST_KEY_1_NSEC,
565 "--password",
566 TEST_PASSWORD,
567 ]);
568
569 p.expect_end_eventually_with("logged in as fred\r\n")?;
570
571 for p in [51, 52] {
572 shutdown_relay(8000 + p)?;
573 }
574
575 let mut p = CliTester::new(["login", "--password", TEST_PASSWORD]);
576
577 p.expect("searching for your details...\r\n")?;
578 p.expect("\r")?;
579
580 p.expect_end_eventually_with("logged in as fred\r\n")?;
581
582 Ok(())
583 })
584 });
585
586 // launch relay
587 let _ = join!(r51.listen_until_close(), r52.listen_until_close(),);
588
589 cli_tester_handle.join().unwrap()?;
590
591 Ok(())
592 }
593 }
67 } 594 }
595}
68 596
69 mod when_invalid_nsec { 597/// using the offline flag simplifies the test. relay interaction is tested
598/// seperately
599mod with_offline_flag {
600 use super::*;
601 mod when_first_time_login {
70 use super::*; 602 use super::*;
71 603
72 #[test] 604 #[test]
73 #[serial] 605 #[serial]
74 fn prompts_for_nsec_until_valid() -> Result<()> { 606 fn prompts_for_nsec_and_password() -> Result<()> {
75 with_fresh_config(|| { 607 before()?;
76 let invalid_nsec_response = 608 standard_login()?;
77 "invalid nsec. try again with nsec (or hex private key)"; 609 after()
610 }
78 611
79 let mut p = CliTester::new(["login"]); 612 #[test]
613 #[serial]
614 fn succeeds_with_text_logged_in_as_npub() -> Result<()> {
615 with_fresh_config(|| {
616 let mut p = CliTester::new(["login", "--offline"]);
80 617
81 p.expect_input(EXPECTED_NSEC_PROMPT)? 618 p.expect_input(EXPECTED_NSEC_PROMPT)?
82 // this behaviour is intentional. rejecting the response with dialoguer hides
83 // the original input from the user so they cannot see the
84 // mistake they made.
85 .succeeds_with(TEST_INVALID_NSEC)?;
86
87 p.expect_input(invalid_nsec_response)?
88 .succeeds_with(TEST_INVALID_NSEC)?;
89
90 p.expect_input(invalid_nsec_response)?
91 .succeeds_with(TEST_KEY_1_NSEC)?; 619 .succeeds_with(TEST_KEY_1_NSEC)?;
92 620
93 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? 621 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
@@ -97,178 +625,174 @@ mod when_first_time_login {
97 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 625 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
98 }) 626 })
99 } 627 }
100 }
101}
102 628
103mod when_second_time_login { 629 #[test]
104 use super::*; 630 #[serial]
631 fn succeeds_with_hex_secret_key_in_place_of_nsec() -> Result<()> {
632 with_fresh_config(|| {
633 let mut p = CliTester::new(["login", "--offline"]);
105 634
106 #[test] 635 p.expect_input(EXPECTED_NSEC_PROMPT)?
107 #[serial] 636 .succeeds_with(TEST_KEY_1_SK_HEX)?;
108 fn prints_login_as_npub() -> Result<()> {
109 with_fresh_config(|| {
110 standard_login()?.exit()?;
111 637
112 CliTester::new(["login"]) 638 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
113 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? 639 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
114 .exit() 640 .succeeds_with(TEST_PASSWORD)?;
115 })
116 }
117 641
118 #[test] 642 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
119 #[serial] 643 })
120 fn prompts_for_password_and_succeeds_with_logged_in_as_npub() -> Result<()> { 644 }
121 with_fresh_config(|| {
122 standard_login()?.exit()?;
123 645
124 let mut p = CliTester::new(["login"]); 646 mod when_invalid_nsec {
647 use super::*;
125 648
126 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? 649 #[test]
127 .expect_password(EXPECTED_PASSWORD_PROMPT)? 650 #[serial]
128 .succeeds_with(TEST_PASSWORD)?; 651 fn prompts_for_nsec_until_valid() -> Result<()> {
652 with_fresh_config(|| {
653 let invalid_nsec_response =
654 "invalid nsec. try again with nsec (or hex private key)";
129 655
130 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 656 let mut p = CliTester::new(["login", "--offline"]);
131 })
132 }
133 657
134 #[test] 658 p.expect_input(EXPECTED_NSEC_PROMPT)?
135 #[serial] 659 // this behaviour is intentional. rejecting the response with dialoguer
136 fn when_invalid_password_exit_with_error() -> Result<()> { 660 // hides the original input from the user so they
137 with_fresh_config(|| { 661 // cannot see the mistake they made.
138 standard_login()?.exit()?; 662 .succeeds_with(TEST_INVALID_NSEC)?;
139 663
140 let mut p = CliTester::new(["login"]); 664 p.expect_input(invalid_nsec_response)?
665 .succeeds_with(TEST_INVALID_NSEC)?;
141 666
142 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? 667 p.expect_input(invalid_nsec_response)?
143 .expect_password(EXPECTED_PASSWORD_PROMPT)? 668 .succeeds_with(TEST_KEY_1_NSEC)?;
144 .succeeds_with(TEST_INVALID_PASSWORD)?;
145 p.expect_end_with(format!("Error: failed to log in as {}\r\n\r\nCaused by:\r\n 0: failed to decrypt key with provided password\r\n 1: failed to decrypt\r\n", TEST_KEY_1_NPUB).as_str())
146 })
147 }
148}
149 669
150mod when_called_with_nsec_parameter_only { 670 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
151 use super::*; 671 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
672 .succeeds_with(TEST_PASSWORD)?;
152 673
153 #[test] 674 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
154 #[serial] 675 })
155 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { 676 }
156 with_fresh_config(|| { 677 }
157 CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC])
158 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
159 })
160 } 678 }
161 679
162 #[test] 680 mod when_second_time_login {
163 #[serial] 681 use super::*;
164 fn forgets_identity() -> Result<()> {
165 with_fresh_config(|| {
166 CliTester::new(["login", "--nsec", TEST_KEY_1_NSEC])
167 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
168 682
169 let mut p = CliTester::new(["login"]); 683 #[test]
684 #[serial]
685 fn prints_login_as_npub() -> Result<()> {
686 with_fresh_config(|| {
687 standard_login()?.exit()?;
170 688
171 p.expect_input(EXPECTED_NSEC_PROMPT)? 689 CliTester::new(["login", "--offline"])
172 .succeeds_with(TEST_KEY_1_NSEC)?; 690 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
691 .exit()
692 })
693 }
173 694
174 p.exit() 695 #[test]
175 }) 696 #[serial]
176 } 697 fn prompts_for_password_and_succeeds_with_logged_in_as_npub() -> Result<()> {
698 with_fresh_config(|| {
699 standard_login()?.exit()?;
177 700
178 mod when_logging_in_as_different_nsec { 701 let mut p = CliTester::new(["login", "--offline"]);
179 use super::*; 702
703 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
704 .expect_password(EXPECTED_PASSWORD_PROMPT)?
705 .succeeds_with(TEST_PASSWORD)?;
706
707 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
708 })
709 }
180 710
181 #[test] 711 #[test]
182 #[serial] 712 #[serial]
183 fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { 713 fn when_invalid_password_exit_with_error() -> Result<()> {
184 with_fresh_config(|| { 714 with_fresh_config(|| {
185 standard_login()?.exit()?; 715 standard_login()?.exit()?;
186 716
187 CliTester::new(["login", "--nsec", TEST_KEY_2_NSEC]) 717 let mut p = CliTester::new(["login", "--offline"]);
188 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) 718
719 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
720 .expect_password(EXPECTED_PASSWORD_PROMPT)?
721 .succeeds_with(TEST_INVALID_PASSWORD)?;
722 p.expect_end_with(format!("Error: failed to log in as {}\r\n\r\nCaused by:\r\n 0: failed to decrypt key with provided password\r\n 1: failed to decrypt\r\n", TEST_KEY_1_NPUB).as_str())
189 }) 723 })
190 } 724 }
191 } 725 }
192 #[test]
193 #[serial]
194 fn invalid_nsec_param_fails_without_prompts() -> Result<()> {
195 with_fresh_config(|| {
196 CliTester::new(["login", "--nsec", TEST_INVALID_NSEC]).expect_end_with(
197 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n",
198 )
199 })
200 }
201}
202 726
203mod when_called_with_nsec_and_password_parameter { 727 mod when_called_with_nsec_parameter_only {
204 use super::*; 728 use super::*;
205 729
206 #[test] 730 #[test]
207 #[serial] 731 #[serial]
208 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> { 732 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> {
209 with_fresh_config(|| { 733 with_fresh_config(|| {
210 CliTester::new([ 734 CliTester::new(["login", "--offline", "--nsec", TEST_KEY_1_NSEC])
211 "login", 735 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
212 "--nsec", 736 })
213 TEST_KEY_1_NSEC, 737 }
214 "--password",
215 TEST_PASSWORD,
216 ])
217 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
218 })
219 }
220 738
221 #[test] 739 #[test]
222 #[serial] 740 #[serial]
223 fn remembers_identity() -> Result<()> { 741 fn forgets_identity() -> Result<()> {
224 with_fresh_config(|| { 742 with_fresh_config(|| {
225 CliTester::new([ 743 CliTester::new(["login", "--offline", "--nsec", TEST_KEY_1_NSEC])
226 "login", 744 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
227 "--nsec", 745
228 TEST_KEY_1_NSEC, 746 let mut p = CliTester::new(["login", "--offline"]);
229 "--password",
230 TEST_PASSWORD,
231 ])
232 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
233
234 CliTester::new(["login"])
235 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
236 .exit()
237 })
238 }
239 747
240 #[test] 748 p.expect_input(EXPECTED_NSEC_PROMPT)?
241 #[serial] 749 .succeeds_with(TEST_KEY_1_NSEC)?;
242 fn parameters_can_be_called_globally() -> Result<()> { 750
243 with_fresh_config(|| { 751 p.exit()
244 CliTester::new([ 752 })
245 "--nsec", 753 }
246 TEST_KEY_1_NSEC, 754
247 "--password", 755 mod when_logging_in_as_different_nsec {
248 TEST_PASSWORD, 756 use super::*;
249 "login", 757
250 ]) 758 #[test]
251 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 759 #[serial]
252 }) 760 fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> {
761 with_fresh_config(|| {
762 standard_login()?.exit()?;
763
764 CliTester::new(["login", "--offline", "--nsec", TEST_KEY_2_NSEC])
765 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())
766 })
767 }
768 }
769 #[test]
770 #[serial]
771 fn invalid_nsec_param_fails_without_prompts() -> Result<()> {
772 with_fresh_config(|| {
773 CliTester::new(["login", "--offline", "--nsec", TEST_INVALID_NSEC]).expect_end_with(
774 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n",
775 )
776 })
777 }
253 } 778 }
254 779
255 mod when_logging_in_as_different_nsec { 780 mod when_called_with_nsec_and_password_parameter {
256 use super::*; 781 use super::*;
257 782
258 #[test] 783 #[test]
259 #[serial] 784 #[serial]
260 fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> { 785 fn valid_nsec_param_succeeds_without_prompts() -> Result<()> {
261 with_fresh_config(|| { 786 with_fresh_config(|| {
262 standard_login()?.exit()?;
263
264 CliTester::new([ 787 CliTester::new([
265 "login", 788 "login",
789 "--offline",
266 "--nsec", 790 "--nsec",
267 TEST_KEY_2_NSEC, 791 TEST_KEY_1_NSEC,
268 "--password", 792 "--password",
269 TEST_PASSWORD, 793 TEST_PASSWORD,
270 ]) 794 ])
271 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str()) 795 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
272 }) 796 })
273 } 797 }
274 798
@@ -276,120 +800,182 @@ mod when_called_with_nsec_and_password_parameter {
276 #[serial] 800 #[serial]
277 fn remembers_identity() -> Result<()> { 801 fn remembers_identity() -> Result<()> {
278 with_fresh_config(|| { 802 with_fresh_config(|| {
279 standard_login()?.exit()?;
280
281 CliTester::new([ 803 CliTester::new([
282 "login", 804 "login",
805 "--offline",
283 "--nsec", 806 "--nsec",
284 TEST_KEY_2_NSEC, 807 TEST_KEY_1_NSEC,
285 "--password", 808 "--password",
286 TEST_PASSWORD, 809 TEST_PASSWORD,
287 ]) 810 ])
288 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())?; 811 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
289 812
290 CliTester::new(["login"]) 813 CliTester::new(["login", "--offline"])
291 .expect(format!("login as {}\r\n", TEST_KEY_2_NPUB).as_str())? 814 .expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
292 .exit() 815 .exit()
293 }) 816 })
294 } 817 }
295 }
296
297 mod when_provided_with_new_password {
298 use super::*;
299 818
300 #[test] 819 #[test]
301 #[serial] 820 #[serial]
302 fn password_changes() -> Result<()> { 821 fn parameters_can_be_called_globally() -> Result<()> {
303 with_fresh_config(|| { 822 with_fresh_config(|| {
304 standard_login()?.exit()?;
305
306 CliTester::new([ 823 CliTester::new([
307 "login",
308 "--nsec", 824 "--nsec",
309 TEST_KEY_1_NSEC, 825 TEST_KEY_1_NSEC,
310 "--password", 826 "--password",
311 TEST_INVALID_PASSWORD, 827 TEST_PASSWORD,
828 "login",
829 "--offline",
312 ]) 830 ])
313 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; 831 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
314
315 CliTester::new(["--password", TEST_INVALID_PASSWORD, "login"])
316 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
317 }) 832 })
318 } 833 }
319 }
320 834
321 #[test] 835 mod when_logging_in_as_different_nsec {
322 #[serial] 836 use super::*;
323 fn invalid_nsec_param_fails_without_prompts() -> Result<()> { 837
324 with_fresh_config(|| { 838 #[test]
325 CliTester::new([ 839 #[serial]
326 "login", 840 fn valid_nsec_param_succeeds_without_prompts_and_logs_in() -> Result<()> {
327 "--nsec", 841 with_fresh_config(|| {
328 TEST_INVALID_NSEC, 842 standard_login()?.exit()?;
329 "--password", 843
330 TEST_PASSWORD, 844 CliTester::new([
331 ]) 845 "login",
332 .expect_end_with( 846 "--offline",
333 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n", 847 "--nsec",
334 ) 848 TEST_KEY_2_NSEC,
335 }) 849 "--password",
850 TEST_PASSWORD,
851 ])
852 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())
853 })
854 }
855
856 #[test]
857 #[serial]
858 fn remembers_identity() -> Result<()> {
859 with_fresh_config(|| {
860 standard_login()?.exit()?;
861
862 CliTester::new([
863 "login",
864 "--offline",
865 "--nsec",
866 TEST_KEY_2_NSEC,
867 "--password",
868 TEST_PASSWORD,
869 ])
870 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_2_NPUB).as_str())?;
871
872 CliTester::new(["login", "--offline"])
873 .expect(format!("login as {}\r\n", TEST_KEY_2_NPUB).as_str())?
874 .exit()
875 })
876 }
877 }
878
879 mod when_provided_with_new_password {
880 use super::*;
881
882 #[test]
883 #[serial]
884 fn password_changes() -> Result<()> {
885 with_fresh_config(|| {
886 standard_login()?.exit()?;
887
888 CliTester::new([
889 "login",
890 "--offline",
891 "--nsec",
892 TEST_KEY_1_NSEC,
893 "--password",
894 TEST_INVALID_PASSWORD,
895 ])
896 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
897
898 CliTester::new(["--password", TEST_INVALID_PASSWORD, "login", "--offline"])
899 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
900 })
901 }
902 }
903
904 #[test]
905 #[serial]
906 fn invalid_nsec_param_fails_without_prompts() -> Result<()> {
907 with_fresh_config(|| {
908 CliTester::new([
909 "login",
910 "--offline",
911 "--nsec",
912 TEST_INVALID_NSEC,
913 "--password",
914 TEST_PASSWORD,
915 ])
916 .expect_end_with(
917 "Error: invalid nsec parameter\r\n\r\nCaused by:\r\n Invalid secret key\r\n",
918 )
919 })
920 }
336 } 921 }
337}
338 922
339mod when_called_with_password_parameter_only { 923 mod when_called_with_password_parameter_only {
340 use super::*; 924 use super::*;
341 925
342 #[test] 926 #[test]
343 #[serial] 927 #[serial]
344 fn when_nsec_stored_logs_in_without_prompts() -> Result<()> { 928 fn when_nsec_stored_logs_in_without_prompts() -> Result<()> {
345 with_fresh_config(|| { 929 with_fresh_config(|| {
346 standard_login()?.exit()?; 930 standard_login()?.exit()?;
347 931
348 CliTester::new(["login", "--password", TEST_PASSWORD]) 932 CliTester::new(["login", "--offline", "--password", TEST_PASSWORD])
349 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 933 .expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
350 }) 934 })
351 } 935 }
352 936
353 #[test] 937 #[test]
354 #[serial] 938 #[serial]
355 fn when_no_nsec_stored_logs_error() -> Result<()> { 939 fn when_no_nsec_stored_logs_error() -> Result<()> {
356 with_fresh_config(|| { 940 with_fresh_config(|| {
357 CliTester::new(["login", "--password", TEST_PASSWORD]) 941 CliTester::new(["login", "--offline", "--password", TEST_PASSWORD]).expect_end_with(
358 .expect_end_with("Error: no nsec available to decrypt with specified password\r\n") 942 "Error: no nsec available to decrypt with specified password\r\n",
359 }) 943 )
944 })
945 }
360 } 946 }
361}
362 947
363mod when_weak_password { 948 mod when_weak_password {
364 use super::*; 949 use super::*;
365 950
366 #[test] 951 #[test]
367 #[serial] 952 #[serial]
368 // combined into a single test as it is computationally expensive to run 953 // combined into a single test as it is computationally expensive to run
369 fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds() 954 fn warns_it_might_take_a_few_seconds_then_succeeds_then_second_login_prompts_for_password_then_warns_again_then_succeeds()
370 -> Result<()> { 955 -> Result<()> {
371 with_fresh_config(|| { 956 with_fresh_config(|| {
372 let mut p = CliTester::new_with_timeout(10000, ["login"]); 957 let mut p = CliTester::new_with_timeout(10000, ["login", "--offline"]);
373 p.expect_input(EXPECTED_NSEC_PROMPT)? 958 p.expect_input(EXPECTED_NSEC_PROMPT)?
374 .succeeds_with(TEST_KEY_1_NSEC)?; 959 .succeeds_with(TEST_KEY_1_NSEC)?;
375 960
376 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)? 961 p.expect_password(EXPECTED_SET_PASSWORD_PROMPT)?
377 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)? 962 .with_confirmation(EXPECTED_SET_PASSWORD_CONFIRM_PROMPT)?
378 .succeeds_with(TEST_WEAK_PASSWORD)?; 963 .succeeds_with(TEST_WEAK_PASSWORD)?;
379 964
380 p.expect("this may take a few seconds...\r\n")?; 965 p.expect("this may take a few seconds...\r\n")?;
381 966
382 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?; 967 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())?;
383 968
384 p = CliTester::new_with_timeout(10000, ["login"]); 969 p = CliTester::new_with_timeout(10000, ["login", "--offline"]);
385 970
386 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())? 971 p.expect(format!("login as {}\r\n", TEST_KEY_1_NPUB).as_str())?
387 .expect_password(EXPECTED_PASSWORD_PROMPT)? 972 .expect_password(EXPECTED_PASSWORD_PROMPT)?
388 .succeeds_with(TEST_WEAK_PASSWORD)?; 973 .succeeds_with(TEST_WEAK_PASSWORD)?;
389 974
390 p.expect("this may take a few seconds...\r\n")?; 975 p.expect("this may take a few seconds...\r\n")?;
391 976
392 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str()) 977 p.expect_end_with(format!("logged in as {}\r\n", TEST_KEY_1_NPUB).as_str())
393 }) 978 })
979 }
394 } 980 }
395} 981}
diff --git a/tests/prs_create.rs b/tests/prs_create.rs
index 0863496..564ef16 100644
--- a/tests/prs_create.rs
+++ b/tests/prs_create.rs
@@ -195,9 +195,9 @@ mod sends_pr_and_2_patches_to_3_relays {
195 let git_repo = prep_git_repo()?; 195 let git_repo = prep_git_repo()?;
196 196
197 let (mut r51, mut r52, mut r53) = ( 197 let (mut r51, mut r52, mut r53) = (
198 Relay::new(8051, None), 198 Relay::new(8051, None, None),
199 Relay::new(8052, None), 199 Relay::new(8052, None, None),
200 Relay::new(8053, None), 200 Relay::new(8053, None, None),
201 ); 201 );
202 202
203 // // check relay had the right number of events 203 // // check relay had the right number of events
@@ -427,9 +427,9 @@ mod sends_pr_and_2_patches_to_3_relays {
427 let git_repo = prep_git_repo()?; 427 let git_repo = prep_git_repo()?;
428 428
429 let (mut r51, mut r52, mut r53) = ( 429 let (mut r51, mut r52, mut r53) = (
430 Relay::new(8051, None), 430 Relay::new(8051, None, None),
431 Relay::new(8052, None), 431 Relay::new(8052, None, None),
432 Relay::new(8053, None), 432 Relay::new(8053, None, None),
433 ); 433 );
434 434
435 // // check relay had the right number of events 435 // // check relay had the right number of events
@@ -477,15 +477,16 @@ mod sends_pr_and_2_patches_to_3_relays {
477 let git_repo = prep_git_repo()?; 477 let git_repo = prep_git_repo()?;
478 478
479 let (mut r51, mut r52, mut r53) = ( 479 let (mut r51, mut r52, mut r53) = (
480 Relay::new(8051, None), 480 Relay::new(8051, None, None),
481 Relay::new( 481 Relay::new(
482 8052, 482 8052,
483 Some(&|relay, client_id, event| -> Result<()> { 483 Some(&|relay, client_id, event| -> Result<()> {
484 relay.respond_ok(client_id, event, Some("Payment Required"))?; 484 relay.respond_ok(client_id, event, Some("Payment Required"))?;
485 Ok(()) 485 Ok(())
486 }), 486 }),
487 None,
487 ), 488 ),
488 Relay::new(8053, None), 489 Relay::new(8053, None, None),
489 ); 490 );
490 491
491 // // check relay had the right number of events 492 // // check relay had the right number of events
@@ -523,15 +524,16 @@ mod sends_pr_and_2_patches_to_3_relays {
523 let git_repo = prep_git_repo()?; 524 let git_repo = prep_git_repo()?;
524 525
525 let (mut r51, mut r52, mut r53) = ( 526 let (mut r51, mut r52, mut r53) = (
526 Relay::new(8051, None), 527 Relay::new(8051, None, None),
527 Relay::new( 528 Relay::new(
528 8052, 529 8052,
529 Some(&|relay, client_id, event| -> Result<()> { 530 Some(&|relay, client_id, event| -> Result<()> {
530 relay.respond_ok(client_id, event, Some("Payment Required"))?; 531 relay.respond_ok(client_id, event, Some("Payment Required"))?;
531 Ok(()) 532 Ok(())
532 }), 533 }),
534 None,
533 ), 535 ),
534 Relay::new(8053, None), 536 Relay::new(8053, None, None),
535 ); 537 );
536 538
537 // // check relay had the right number of events 539 // // check relay had the right number of events