diff options
| -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 |