diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
| commit | e237328ec611a5891586530c1d3cb26c16c1093b (patch) | |
| tree | 22ac36baa240354d06ae82eb070609fa3e3fcb82 | |
| parent | 000901c0cbca8464b5a89bcc93c5474f6564bafd (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.lock | 1 | ||||
| -rw-r--r-- | src/client.rs | 72 | ||||
| -rw-r--r-- | src/config.rs | 62 | ||||
| -rw-r--r-- | src/key_handling/users.rs | 683 | ||||
| -rw-r--r-- | src/login.rs | 153 | ||||
| -rw-r--r-- | src/main.rs | 4 | ||||
| -rw-r--r-- | src/sub_commands/login.rs | 30 | ||||
| -rw-r--r-- | src/sub_commands/prs/create.rs | 3 | ||||
| -rw-r--r-- | test_utils/Cargo.toml | 1 | ||||
| -rw-r--r-- | test_utils/src/lib.rs | 62 | ||||
| -rw-r--r-- | test_utils/src/relay.rs | 126 | ||||
| -rw-r--r-- | tests/login.rs | 1100 | ||||
| -rw-r--r-- | tests/prs_create.rs | 22 |
13 files changed, 1959 insertions, 360 deletions
| @@ -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. |
| 13 | use anyhow::Result; | 13 | use anyhow::{Context, Result}; |
| 14 | use async_trait::async_trait; | 14 | use async_trait::async_trait; |
| 15 | use futures::future::join_all; | ||
| 15 | #[cfg(test)] | 16 | #[cfg(test)] |
| 16 | use mockall::*; | 17 | use mockall::*; |
| 17 | use nostr::Event; | 18 | use nostr::Event; |
| 18 | 19 | ||
| 19 | pub struct Client { | 20 | pub 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] | ||
| 26 | pub trait Connect { | 27 | pub 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 | |||
| 136 | fn 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}; | |||
| 4 | use directories::ProjectDirs; | 4 | use directories::ProjectDirs; |
| 5 | #[cfg(test)] | 5 | #[cfg(test)] |
| 6 | use mockall::*; | 6 | use mockall::*; |
| 7 | use nostr::secp256k1::XOnlyPublicKey; | 7 | use nostr::{secp256k1::XOnlyPublicKey, ToBech32}; |
| 8 | use serde::{self, Deserialize, Serialize}; | 8 | use serde::{self, Deserialize, Serialize}; |
| 9 | 9 | ||
| 10 | #[derive(Default)] | 10 | #[derive(Default)] |
| @@ -71,6 +71,66 @@ pub struct MyConfig { | |||
| 71 | pub struct UserRef { | 71 | pub 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 | |||
| 79 | impl 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)] | ||
| 108 | pub struct UserMetadata { | ||
| 109 | pub name: String, | ||
| 110 | pub created_at: u64, | ||
| 111 | } | ||
| 112 | |||
| 113 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 114 | pub struct UserRelays { | ||
| 115 | pub relays: Vec<UserRelayRef>, | ||
| 116 | pub created_at: u64, | ||
| 117 | } | ||
| 118 | |||
| 119 | impl 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)] | ||
| 130 | pub 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 @@ | |||
| 1 | use std::time::SystemTime; | ||
| 2 | |||
| 1 | use anyhow::{Context, Result}; | 3 | use anyhow::{Context, Result}; |
| 4 | use async_trait::async_trait; | ||
| 2 | use nostr::prelude::*; | 5 | use nostr::prelude::*; |
| 3 | use zeroize::Zeroize; | 6 | use zeroize::Zeroize; |
| 4 | 7 | ||
| 5 | use super::encryption::{EncryptDecrypt, Encryptor}; | 8 | use super::encryption::{EncryptDecrypt, Encryptor}; |
| 9 | #[cfg(not(test))] | ||
| 10 | use crate::client::Client; | ||
| 11 | #[cfg(test)] | ||
| 12 | use crate::client::MockConnect; | ||
| 6 | use crate::{ | 13 | use 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] | ||
| 18 | pub trait UserManagement { | 29 | pub 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)] |
| 23 | use duplicate::duplicate_item; | 48 | use 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] | ||
| 25 | impl UserManagement for UserManager { | 51 | impl 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) | 259 | fn 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)] |
| 92 | mod tests { | 268 | mod 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 @@ | |||
| 1 | use anyhow::{bail, Context, Result}; | 1 | use anyhow::{bail, Context, Result}; |
| 2 | use nostr::prelude::{FromSkStr, ToBech32}; | 2 | use nostr::{prelude::FromSkStr, secp256k1::XOnlyPublicKey}; |
| 3 | use zeroize::Zeroize; | 3 | use zeroize::Zeroize; |
| 4 | 4 | ||
| 5 | #[cfg(not(test))] | ||
| 6 | use crate::client::Client; | ||
| 7 | #[cfg(test)] | ||
| 8 | use crate::client::MockConnect; | ||
| 5 | use crate::{ | 9 | use 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 |
| 15 | pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { | 19 | pub 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 | |||
| 93 | async 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 { | |||
| 41 | async fn main() -> Result<()> { | 41 | async 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 @@ | |||
| 1 | use anyhow::Result; | 1 | use anyhow::Result; |
| 2 | use clap; | 2 | use clap; |
| 3 | 3 | ||
| 4 | use crate::{login, Cli}; | 4 | #[cfg(not(test))] |
| 5 | use crate::client::Client; | ||
| 6 | #[cfg(test)] | ||
| 7 | use crate::client::MockConnect; | ||
| 8 | use crate::{client::Connect, login, Cli}; | ||
| 5 | 9 | ||
| 6 | #[derive(clap::Args)] | 10 | #[derive(clap::Args)] |
| 7 | pub struct SubCommandArgs; | 11 | pub struct SubCommandArgs { |
| 12 | /// don't fetch user metadata and relay list from relays | ||
| 13 | #[arg(long, action)] | ||
| 14 | offline: bool, | ||
| 15 | } | ||
| 16 | |||
| 17 | pub 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 | ||
| 9 | pub 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" | |||
| 15 | rexpect = { git = "https://github.com/phaer/rexpect.git", branch= "skip-ansi-escape-codes" } | 15 | rexpect = { git = "https://github.com/phaer/rexpect.git", branch= "skip-ansi-escape-codes" } |
| 16 | simple-websockets = "0.1.6" | 16 | simple-websockets = "0.1.6" |
| 17 | strip-ansi-escapes = "0.2.0" | 17 | strip-ansi-escapes = "0.2.0" |
| 18 | tungstenite = "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 | |||
| 23 | pub static TEST_KEY_1_KEYS: Lazy<nostr::Keys> = | 23 | pub 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 | ||
| 26 | pub 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 | } | ||
| 31 | pub 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 | |||
| 39 | pub 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 | |||
| 26 | pub static TEST_KEY_2_NSEC: &str = | 59 | pub static TEST_KEY_2_NSEC: &str = |
| 27 | "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm"; | 60 | "nsec1ypglg6nj6ep0g2qmyfqcv2al502gje3jvpwye6mthmkvj93tqkesknv6qm"; |
| 28 | pub static TEST_KEY_2_NPUB: &str = | 61 | pub static TEST_KEY_2_NPUB: &str = |
| @@ -33,12 +66,41 @@ pub static TEST_KEY_2_ENCRYPTED: &str = "...2"; | |||
| 33 | pub static TEST_KEY_2_KEYS: Lazy<nostr::Keys> = | 66 | pub 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 | ||
| 69 | pub 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 | |||
| 36 | pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex"; | 75 | pub static TEST_INVALID_NSEC: &str = "nsec1ppsg5sm2aex"; |
| 37 | pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t"; | 76 | pub static TEST_PASSWORD: &str = "769dfd£pwega8SHGv3!#Bsfd5t"; |
| 38 | pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!"; | 77 | pub static TEST_INVALID_PASSWORD: &str = "INVALID769dfd£pwega8SHGv3!"; |
| 39 | pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe"; | 78 | pub static TEST_WEAK_PASSWORD: &str = "fhaiuhfwe"; |
| 40 | pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg"; | 79 | pub static TEST_RANDOM_TOKEN: &str = "lkjh2398HLKJ43hrweiJ6FaPfdssgtrg"; |
| 41 | 80 | ||
| 81 | pub 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 | ||
| 6 | use crate::CliTester; | 6 | use crate::CliTester; |
| 7 | 7 | ||
| 8 | type ListenerFunc<'a> = &'a dyn Fn(&mut Relay, u64, nostr::Event) -> Result<()>; | 8 | type ListenerEventFunc<'a> = &'a dyn Fn(&mut Relay, u64, nostr::Event) -> Result<()>; |
| 9 | pub type ListenerReqFunc<'a> = | ||
| 10 | &'a dyn Fn(&mut Relay, u64, nostr::SubscriptionId, Vec<nostr::Filter>) -> Result<()>; | ||
| 9 | 11 | ||
| 10 | pub struct Relay<'a> { | 12 | pub 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 | ||
| 18 | impl<'a> Relay<'a> { | 22 | impl<'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 | ||
| 86 | fn get_nevent(message: simple_websockets::Message) -> Result<nostr::Event> { | 161 | pub 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 | |||
| 168 | fn 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 | ||
| 179 | fn 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 | |||
| 195 | fn 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 | |||
| 97 | pub enum Message { | 205 | pub 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"; | |||
| 8 | static EXPECTED_PASSWORD_PROMPT: &str = "password"; | 8 | static EXPECTED_PASSWORD_PROMPT: &str = "password"; |
| 9 | 9 | ||
| 10 | fn standard_login() -> Result<CliTester> { | 10 | fn 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 | } |
| 23 | mod with_relays { | ||
| 24 | use anyhow::Ok; | ||
| 25 | use futures::join; | ||
| 26 | use test_utils::relay::{shutdown_relay, ListenerReqFunc, Relay}; | ||
| 23 | 27 | ||
| 24 | mod 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 | ||
| 599 | mod 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 | ||
| 103 | mod 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 | ||
| 150 | mod 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 | ||
| 203 | mod 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 | ||
| 339 | mod 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 | ||
| 363 | mod 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 |