Add support for querying NIP-05 on /.well-known/nostr.json?name=username

This commit is contained in:
Tony Klink 2024-01-30 11:43:03 -06:00
parent e1306608ef
commit 377da44eed
Signed by: klink
GPG key ID: 85175567C4D19231
12 changed files with 278 additions and 101 deletions

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
noose::sled::BanInfo, noose::sled::BanInfo,
noose::user::{User, UserRow}, noose::user::{User, UserRow, Nip05Profile},
utils::{error::Error, structs::Subscription}, utils::{error::Error, structs::Subscription},
}; };
use nostr::secp256k1::XOnlyPublicKey; use nostr::secp256k1::XOnlyPublicKey;
@ -25,10 +25,14 @@ pub enum Command {
// Old messages // Old messages
DbReqInsertUser(UserRow), DbReqInsertUser(UserRow),
DbReqGetUser(User),
DbReqCreateAccount(XOnlyPublicKey, String, String), DbReqCreateAccount(XOnlyPublicKey, String, String),
DbReqGetAccount(String), DbReqGetAccount(String),
DbReqClear, DbReqClear,
// NIP-05 related messages
DbReqGetUser(String),
DbResUser(Nip05Profile),
// DbResponse // DbResponse
DbResRelayMessages( DbResRelayMessages(
/* client_id*/ uuid::Uuid, /* client_id*/ uuid::Uuid,
@ -38,7 +42,6 @@ pub enum Command {
DbResOk, DbResOk,
DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage), DbResOkWithStatus(/* client_id */ uuid::Uuid, nostr::RelayMessage),
DbResAccount, // TODO: Add Account DTO as a param DbResAccount, // TODO: Add Account DTO as a param
DbResUser(UserRow),
DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage), DbResEventCounts(/* client_id */ uuid::Uuid, nostr::RelayMessage),
// Event Pipeline // Event Pipeline
PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>), PipelineReqEvent(/* client_id */ uuid::Uuid, Box<nostr::Event>),

View file

@ -6,6 +6,8 @@ use crate::{
use nostr::{Event, RelayMessage}; use nostr::{Event, RelayMessage};
use std::sync::Arc; use std::sync::Arc;
use super::user::Nip05Profile;
pub trait Noose: Send + Sync { pub trait Noose: Send + Sync {
async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>; async fn start(&mut self, pubsub: Arc<PubSub>) -> Result<(), Error>;
@ -14,4 +16,6 @@ pub trait Noose: Send + Sync {
async fn find_event(&self, subscription: Subscription) -> Result<Vec<RelayMessage>, Error>; async fn find_event(&self, subscription: Subscription) -> Result<Vec<RelayMessage>, Error>;
async fn counts(&self, subscription: Subscription) -> Result<RelayMessage, Error>; async fn counts(&self, subscription: Subscription) -> Result<RelayMessage, Error>;
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error>;
} }

View file

@ -11,7 +11,7 @@ impl MigrationRunner {
let m_events_fts = include_str!("./1697410223576_events_fts.sql"); let m_events_fts = include_str!("./1697410223576_events_fts.sql");
let m_users = include_str!("./1697410294265_users.sql"); let m_users = include_str!("./1697410294265_users.sql");
let m_unattached_media = include_str!("./1697410480767_unattached_media.sql"); let m_unattached_media = include_str!("./1697410480767_unattached_media.sql");
let m_pragma = include_str!("./1697410424624_pragma.sql"); let m_nip05 = include_str!("./1706575155557_nip05.sql");
let migrations = Migrations::new(vec![ let migrations = Migrations::new(vec![
M::up(m_create_events), M::up(m_create_events),
@ -20,7 +20,7 @@ impl MigrationRunner {
M::up(m_events_fts), M::up(m_events_fts),
M::up(m_users), M::up(m_users),
M::up(m_unattached_media), M::up(m_unattached_media),
M::up(m_pragma), M::up(m_nip05),
]); ]);
match migrations.to_latest(connection) { match migrations.to_latest(connection) {

View file

@ -1,4 +1,8 @@
use super::{db::Noose, migrations::MigrationRunner}; use super::{
db::Noose,
migrations::MigrationRunner,
user::{Nip05Profile, Nip05Table, UserRow},
};
use crate::{ use crate::{
bussy::{channels, Command, Message, PubSub}, bussy::{channels, Command, Message, PubSub},
utils::{config::Config as ServiceConfig, error::Error, structs::Subscription}, utils::{config::Config as ServiceConfig, error::Error, structs::Subscription},
@ -149,27 +153,6 @@ impl sea_query::Iden for TagsTable {
} }
} }
// enum DeletedCoordinatesTable {
// Table,
// Coordinate,
// CreatedAt,
// }
// impl sea_query::Iden for DeletedCoordinatesTable {
// fn unquoted(&self, s: &mut dyn std::fmt::Write) {
// write!(
// s,
// "{}",
// match self {
// Self::Table => "deleted_coordinates",
// Self::Coordinate => "coordinate",
// Self::CreatedAt => "created_at",
// }
// )
// .unwrap()
// }
// }
enum EventSeenByRelaysTable { enum EventSeenByRelaysTable {
Table, Table,
Id, Id,
@ -212,7 +195,22 @@ impl NostrSqlite {
async fn run_migrations(pool: &Pool) -> bool { async fn run_migrations(pool: &Pool) -> bool {
let connection = pool.get().await.unwrap(); let connection = pool.get().await.unwrap();
connection.interact(MigrationRunner::up).await.unwrap() connection.interact(MigrationRunner::up).await.unwrap();
connection
.interact(|conn| {
conn.pragma_update(None, "encoding", "UTF-8").unwrap();
conn.pragma_update(None, "journal_mode", "WAL").unwrap();
conn.pragma_update(None, "foreign_keys", "ON").unwrap();
conn.pragma_update(None, "auto_vacuum", "FULL").unwrap();
conn.pragma_update(None, "journal_size_limit", "32768")
.unwrap();
conn.pragma_update(None, "mmap_size", "17179869184")
.unwrap();
})
.await
.unwrap();
true
} }
async fn get_connection(&self) -> Result<Object, Error> { async fn get_connection(&self) -> Result<Object, Error> {
@ -1225,6 +1223,48 @@ impl NostrSqlite {
query_result query_result
} }
async fn get_nip05_profile(&self, username: String) -> Result<Nip05Profile, Error> {
let Ok(connection) = self.get_connection().await else {
return Err(Error::internal_with_message("Unable to get DB connection"));
};
let Ok(query_result) = connection
.interact(
move |conn: &mut rusqlite::Connection| -> Result<Nip05Profile, Error> {
let (sql, value) = Query::select()
.from(Nip05Table::Table)
.columns([
Nip05Table::Pubkey,
Nip05Table::Username,
Nip05Table::Relays,
Nip05Table::JoinedAt,
])
.and_where(sea_query::Expr::col(Nip05Table::Username).eq(&username))
.limit(1)
.build_rusqlite(SqliteQueryBuilder);
let Ok(res) = conn.query_row(sql.as_str(), &*value.as_params(), |row| {
let nip05_row: UserRow = row.into();
let nip05 = Nip05Profile::from(nip05_row);
Ok(nip05)
}) else {
return Err(Error::not_found("user", username));
};
Ok(res)
},
)
.await
else {
return Err(Error::internal_with_message(
"Failed to execute query 'get_nip05_profile'",
));
};
query_result
}
} }
impl From<nostr_database::DatabaseError> for Error { impl From<nostr_database::DatabaseError> for Error {
@ -1351,6 +1391,7 @@ impl Noose for NostrSqlite {
while let Ok(message) = subscriber.recv().await { while let Ok(message) = subscriber.recv().await {
log::info!("[Noose] received message: {:?}", message); log::info!("[Noose] received message: {:?}", message);
let command = match message.content { let command = match message.content {
// Relay Events
Command::DbReqWriteEvent(client_id, event) => match self.write_event(event).await { Command::DbReqWriteEvent(client_id, event) => match self.write_event(event).await {
Ok(status) => Command::DbResOkWithStatus(client_id, status), Ok(status) => Command::DbResOkWithStatus(client_id, status),
Err(e) => Command::ServiceError(e), Err(e) => Command::ServiceError(e),
@ -1375,6 +1416,11 @@ impl Noose for NostrSqlite {
Err(e) => Command::ServiceError(e), Err(e) => Command::ServiceError(e),
} }
} }
// NIP-05
Command::DbReqGetUser(username) => match self.get_nip05(username).await {
Ok(user) => Command::DbResUser(user),
Err(e) => Command::ServiceError(e),
},
_ => Command::Noop, _ => Command::Noop,
}; };
if command != Command::Noop { if command != Command::Noop {
@ -1439,6 +1485,10 @@ impl Noose for NostrSqlite {
Err(err) => Err(Error::internal_with_message(err.to_string())), Err(err) => Err(Error::internal_with_message(err.to_string())),
} }
} }
async fn get_nip05(&self, username: String) -> Result<Nip05Profile, Error> {
self.get_nip05_profile(username).await
}
} }
#[cfg(test)] #[cfg(test)]
@ -1708,4 +1758,14 @@ mod tests {
dbg!(res); dbg!(res);
} }
// #[tokio::test]
// async fn get_nip05() {
// let config = Arc::new(ServiceConfig::new());
// let db = NostrSqlite::new(config).await;
// let res = db.get_nip05("test".to_string()).await.unwrap();
// dbg!(serde_json::to_value(res).unwrap().to_string());
// }
} }

View file

@ -1,27 +1,103 @@
use chrono::Utc;
use regex::Regex; use regex::Regex;
use rusqlite::Row;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use validator::{Validate, ValidationError}; use validator::{Validate, ValidationError};
lazy_static! { lazy_static! {
static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap(); static ref VALID_CHARACTERS: Regex = Regex::new(r"^[a-zA-Z0-9\_]+$").unwrap();
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Validate)] pub enum Nip05Table {
Table,
Pubkey,
Username,
Relays,
JoinedAt,
}
impl sea_query::Iden for Nip05Table {
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
write!(
s,
"{}",
match self {
Self::Table => "nip05",
Self::Pubkey => "pubkey",
Self::Username => "username",
Self::Relays => "relays",
Self::JoinedAt => "joined_at",
}
)
.unwrap()
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct UserRow { pub struct UserRow {
pub pubkey: String, pub pubkey: String,
pub username: String, pub username: String,
inserted_at: i64, relays: Vec<String>,
admin: bool, joined_at: i64,
} }
impl UserRow { impl UserRow {
pub fn new(pubkey: String, username: String, admin: bool) -> Self { pub fn new(pubkey: String, username: String, relays: Vec<String>) -> Self {
Self { Self {
pubkey, pubkey,
username, username,
inserted_at: Utc::now().timestamp(), relays,
admin, joined_at: nostr::Timestamp::now().as_i64(),
}
}
}
impl From<&Row<'_>> for UserRow {
fn from(row: &Row) -> Self {
let pubkey: String = row.get("pubkey").unwrap();
let username: String = row.get("username").unwrap();
let relays_raw: String = row.get("relays").unwrap_or_default();
let joined_at: i64 = row.get("joined_at").unwrap();
let relays: Vec<String> = match serde_json::from_str(&relays_raw) {
Ok(val) => val,
Err(_) => vec![],
};
Self {
pubkey,
username,
relays,
joined_at,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Nip05Profile {
names: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
relays: Option<HashMap<String, Vec<String>>>,
}
impl From<UserRow> for Nip05Profile {
fn from(value: UserRow) -> Self {
let mut name: HashMap<String, String> = HashMap::new();
name.insert(value.username, value.pubkey.clone());
let relay = match value.relays.is_empty() {
true => None,
false => {
let mut relay: HashMap<String, Vec<String>> = HashMap::new();
relay.insert(value.pubkey.clone(), value.relays);
Some(relay)
}
};
Self {
names: name,
relays: relay,
} }
} }
} }
@ -41,3 +117,33 @@ pub fn validate_pubkey(value: &str) -> Result<(), ValidationError> {
Err(_) => Err(ValidationError::new("Unable to parse pubkey")), Err(_) => Err(ValidationError::new("Unable to parse pubkey")),
} }
} }
#[cfg(test)]
mod tests {
use super::{Nip05Profile, UserRow};
#[test]
fn make_nip05() {
let user_row = UserRow::new(
"npub1m2w0ckmkgj4wtvl8muwjynh56j3qd4nddca4exdg4mdrkepvfnhsmusy54".to_string(),
"test_user".to_string(),
vec![],
);
let nip05 = Nip05Profile::from(user_row);
dbg!(&nip05);
dbg!(serde_json::to_string(&nip05).unwrap());
}
#[test]
fn parse_relay_vec() {
let relays_raw = "";
let relays: Vec<String> = match serde_json::from_str(relays_raw) {
Ok(val) => val,
Err(_) => vec![],
};
dbg!(relays);
}
}

View file

@ -6,19 +6,24 @@ use warp::{Filter, Rejection, Reply};
pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let cors = warp::cors().allow_any_origin(); let cors = warp::cors().allow_any_origin();
static_files().or(index(context)).with(cors) static_files().or(index(context)).with(&cors)
} }
fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { fn index(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
// let real_client_ip = warp::header::optional::<std::net::SocketAddr>("X-Real-IP"); // let real_client_ip = warp::header::optional::<std::net::SocketAddr>("X-Real-IP");
let real_client_ip = warp::addr::remote(); let real_client_ip = warp::addr::remote();
let cors = warp::cors().allow_any_origin();
let relay_information_document_path = warp::path::end().and(warp::header::header("Accept").and(with_context(context.clone())).and_then(handler::relay_config)).with(&cors); let relay_information_document_path = warp::path::end().and(
let nostr_relay_path = warp::path::end().and(warp::ws().and(with_context(context.clone())) warp::header::header("Accept")
.and(with_context(context.clone()))
.and_then(handler::relay_config),
);
let nostr_relay_path = warp::path::end().and(
warp::ws()
.and(with_context(context.clone()))
.and(real_client_ip) .and(real_client_ip)
.and_then(handler::ws_handler) .and_then(handler::ws_handler),
.with(&cors)); );
relay_information_document_path.or(nostr_relay_path) relay_information_document_path.or(nostr_relay_path)
} }

View file

@ -36,7 +36,7 @@ pub struct Nip05 {
#[derive(Serialize, Deserialize, Debug, Validate, Clone)] #[derive(Serialize, Deserialize, Debug, Validate, Clone)]
pub struct UserQuery { pub struct UserQuery {
#[validate(length(min = 1))] #[validate(length(min = 1))]
pub user: String, pub name: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone, Validate)] #[derive(Serialize, Deserialize, Debug, Clone, Validate)]

View file

@ -45,14 +45,9 @@ pub async fn get_account(
} }
pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> { pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Reply, Rejection> {
let name = user_query.user;
let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await; let mut subscriber = context.pubsub.subscribe(channels::MSG_NIP05).await;
let user = User { let command = Command::DbReqGetUser(user_query.name);
name: Some(name),
pubkey: None,
};
let command = Command::DbReqGetUser(user);
context context
.pubsub .pubsub
.publish( .publish(
@ -65,18 +60,12 @@ pub async fn get_user(user_query: UserQuery, context: Context) -> Result<impl Re
.await; .await;
if let Ok(message) = subscriber.recv().await { if let Ok(message) = subscriber.recv().await {
let mut response = json!({"names": {}, "relays": {}});
match message.content { match message.content {
Command::DbResUser(user) => { Command::DbResUser(profile) => {
response = json!({ let response = serde_json::to_value(profile).unwrap();
"names": {
user.username: user.pubkey
},
"relays": {}
});
Ok(warp::reply::json(&response)) Ok(warp::reply::json(&response))
} }
Command::ServiceError(e) => Ok(warp::reply::json(&response)), Command::ServiceError(e) => Err(warp::reject::custom(e)),
_ => Err(warp::reject::custom(Error::internal_with_message( _ => Err(warp::reject::custom(Error::internal_with_message(
"Unhandeled message type", "Unhandeled message type",
))), ))),

View file

@ -1,4 +1,4 @@
mod accounts; // mod accounts;
pub mod dto; pub mod dto;
mod filter; mod filter;
mod handler; mod handler;

View file

@ -1,6 +1,6 @@
use crate::noose::user::User; use crate::noose::user::User;
use super::accounts::create_account; // use super::accounts::create_account;
use super::dto::{Account, UserQuery}; use super::dto::{Account, UserQuery};
use super::filter::{validate_body_filter, validate_query_filter}; use super::filter::{validate_body_filter, validate_query_filter};
use super::handler::{get_account, get_user}; use super::handler::{get_account, get_user};
@ -9,41 +9,49 @@ use crate::utils::structs::Context;
use warp::{Filter, Rejection, Reply}; use warp::{Filter, Rejection, Reply};
pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn routes(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
let cors = warp::cors().allow_any_origin();
let index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>")); let index = warp::path::end().map(|| warp::reply::html("<h1>SNEED!</h1>"));
index index
.or(nip05_get(context.clone())) .or(nip05_get(context.clone()))
.or(account_create(context.clone())) // .or(account_create(context.clone()))
.with(&cors)
}
fn well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
warp::get().and(warp::path(".well-known"))
}
fn nostr_well_known() -> impl Filter<Extract = (), Error = Rejection> + Clone {
well_known().and(warp::path("nostr.json"))
} }
pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn nip05_get(context: Context) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::get() nostr_well_known()
.and(warp::path(".well-known"))
.and(warp::path("nostr.json"))
.and(validate_query_filter::<UserQuery>()) .and(validate_query_filter::<UserQuery>())
.and(with_context(context)) .and(with_context(context.clone()))
.and_then(get_user) .and_then(get_user)
} }
pub fn account_create( // pub fn account_create(
context: Context, // context: Context,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { // ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path("account") // warp::path("account")
.and(warp::post()) // .and(warp::post())
.and(validate_body_filter::<User>()) // .and(validate_body_filter::<User>())
.and(with_context(context)) // .and(with_context(context))
.and_then(create_account) // .and_then(create_account)
} // }
pub fn account_get( // pub fn account_get(
context: Context, // context: Context,
) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { // ) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path("account") // warp::path("account")
.and(warp::get()) // .and(warp::get())
.and(validate_body_filter::<Account>()) // .and(validate_body_filter::<Account>())
.and(with_context(context)) // .and(with_context(context))
.and_then(get_account) // .and_then(get_account)
} // }
// pub fn account_update( // pub fn account_update(
// context: Context, // context: Context,

View file

@ -43,7 +43,7 @@ impl Config {
"contact": "klink@zhitno.st", "contact": "klink@zhitno.st",
"name": "zhitno.st", "name": "zhitno.st",
"description": "Very *special* nostr relay", "description": "Very *special* nostr relay",
"supported_nips": [ 1, 2, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 45 ], "supported_nips": [ 1, 2, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 45, 50 ],
"software": "git+https://git.zhitno.st/Klink/sneedstr.git", "software": "git+https://git.zhitno.st/Klink/sneedstr.git",
"version": "0.1.1" "version": "0.1.1"
}) })

View file

@ -8,13 +8,13 @@ use std::{
use validator::ValidationErrors; use validator::ValidationErrors;
use warp::{http::StatusCode, reject::Reject}; use warp::{http::StatusCode, reject::Reject};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
pub struct Error { pub struct Error {
pub code: u16, pub code: u16,
pub message: String, pub message: String,
/// Sneedstr version. pub sneedstr_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sneedstr_version: Option<u16>,
} }
impl StdError for Error { impl StdError for Error {
@ -32,7 +32,7 @@ impl Error {
Self { Self {
code: code.as_u16(), code: code.as_u16(),
message, message,
sneedstr_version: None, sneedstr_version: VERSION.to_string(),
} }
} }
@ -54,12 +54,11 @@ impl Error {
Self::new(StatusCode::BAD_REQUEST, message) Self::new(StatusCode::BAD_REQUEST, message)
} }
pub fn not_found<S: Display>(resource: &str, identifier: S, service_version: u16) -> Self { pub fn not_found<S: Display>(resource: &str, identifier: S) -> Self {
Self::new( Self::new(
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
format!("{} not found by {}", resource, identifier), format!("{} not found by {}", resource, identifier),
) )
.sneedstr_version(service_version)
} }
pub fn invalid_param<S: Display>(name: &str, value: S) -> Self { pub fn invalid_param<S: Display>(name: &str, value: S) -> Self {
@ -77,11 +76,6 @@ impl Error {
pub fn status_code(&self) -> StatusCode { pub fn status_code(&self) -> StatusCode {
StatusCode::from_u16(self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) StatusCode::from_u16(self.code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
} }
pub fn sneedstr_version(mut self, service_version: u16) -> Self {
self.sneedstr_version = Some(service_version);
self
}
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -116,28 +110,36 @@ mod tests {
#[test] #[test]
fn test_to_string() { fn test_to_string() {
let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()); let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned());
assert_eq!(err.to_string(), "400 Bad Request: invalid address") assert_eq!(
err.to_string(),
"Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }"
)
} }
#[test] #[test]
fn test_from_anyhow_error_as_internal_error() { fn test_from_anyhow_error_as_internal_error() {
let err = Error::from(anyhow::format_err!("hello")); let err = Error::from(anyhow::format_err!("hello"));
assert_eq!(err.to_string(), "500 Internal Server Error: hello") assert_eq!(
err.to_string(),
"Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }"
)
} }
#[test] #[test]
fn test_to_string_with_sneedstr_version() { fn test_to_string_with_sneedstr_version() {
let err = let err = Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned());
Error::new(StatusCode::BAD_REQUEST, "invalid address".to_owned()).sneedstr_version(123);
assert_eq!( assert_eq!(
err.to_string(), err.to_string(),
"400 Bad Request: invalid address\ndiem ledger version: 123" "Error { code: 400, message: 'invalid address', sneedstr_version: \"0.1.1\" }"
) )
} }
#[test] #[test]
fn test_internal_error() { fn test_internal_error() {
let err = Error::internal(anyhow::format_err!("hello")); let err = Error::internal(anyhow::format_err!("hello"));
assert_eq!(err.to_string(), "500 Internal Server Error: hello") assert_eq!(
err.to_string(),
"Error { code: 500, message: 'hello', sneedstr_version: \"0.1.1\" }"
)
} }
} }