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>2023-10-01 00:00:00 +0100
committerDanConwayDev <DanConwayDev@protonmail.com>2023-10-01 00:00:00 +0100
commite237328ec611a5891586530c1d3cb26c16c1093b (patch)
tree22ac36baa240354d06ae82eb070609fa3e3fcb82
parent000901c0cbca8464b5a89bcc93c5474f6564bafd (diff)
feat(login) fetch user relays and metadata
get user relay list and metadata events from relays when keys are used and last fetch attempt was more than an hour ago uses user's write relays if known, otherwise uses fallback relays to achieve this a method for intergration testing event fetching from relays was added
-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