Add test about PeerRecord

This commit is contained in:
fluo10 2025-09-17 09:20:06 +09:00
parent 7842e4eb3e
commit 88d87bd25d
5 changed files with 147 additions and 55 deletions

View file

@ -2,7 +2,7 @@ use caretta_id::{DoubleId, SingleId};
use chrono::{DateTime, Local, NaiveDateTime}; use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey}; use iroh::{NodeId, PublicKey};
use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION};
/// Response of node authentication. /// Response of node authentication.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -28,7 +28,7 @@ impl ReceivedAuthorizationRequest {
} }
} }
impl LocalModel for ReceivedAuthorizationRequest { impl LocalRecord for ReceivedAuthorizationRequest {
const TABLE_NAME: &str = "received_authorization_request"; const TABLE_NAME: &str = "received_authorization_request";
const DEFAULT_COLUMNS: &[&str] = &[ const DEFAULT_COLUMNS: &[&str] = &[
"request_id", "request_id",
@ -37,6 +37,14 @@ impl LocalModel for ReceivedAuthorizationRequest {
"created_at", "created_at",
"responded_at", "responded_at",
]; ];
type DefaultParams<'a> = (&'a SingleId, &'a [u8;32], &'a str, NaiveDateTime, Option<NaiveDateTime>)
where
Self: 'a;
fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a> {
(&self.request_id,&self.public_key.as_bytes(), &self.node_info, self.created_at.naive_utc(), self.responded_at.map(|x| x.naive_utc()))
}
fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> { fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
let created_at: NaiveDateTime = row.get(3)?; let created_at: NaiveDateTime = row.get(3)?;
let responded_at: Option<NaiveDateTime> = row.get(4)?; let responded_at: Option<NaiveDateTime> = row.get(4)?;
@ -77,4 +85,6 @@ impl LocalModel for ReceivedAuthorizationRequest {
} }
Ok(result) Ok(result)
} }
} }

View file

@ -3,7 +3,7 @@ use chrono::{DateTime, Local, NaiveDateTime};
use iroh::{NodeId, PublicKey}; use iroh::{NodeId, PublicKey};
use rusqlite::types::FromSqlError; use rusqlite::types::FromSqlError;
use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; use crate::{data::local::LocalRecord, global::LOCAL_DATABASE_CONNECTION};
/// Request of node authentication. /// Request of node authentication.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -12,10 +12,10 @@ pub struct SentAuthorizationRequest {
public_key: PublicKey, public_key: PublicKey,
passcode: String, passcode: String,
created_at: DateTime<Local>, created_at: DateTime<Local>,
sent_at: Option<DateTime<Local>>, responded_at: Option<DateTime<Local>>,
} }
impl LocalModel for SentAuthorizationRequest { impl LocalRecord for SentAuthorizationRequest {
const TABLE_NAME: &str = "sent_authorization"; const TABLE_NAME: &str = "sent_authorization";
const DEFAULT_COLUMNS: &[&str] = &[ const DEFAULT_COLUMNS: &[&str] = &[
@ -23,17 +23,20 @@ impl LocalModel for SentAuthorizationRequest {
"public_key", "public_key",
"passcode", "passcode",
"created_at", "created_at",
"sent_at" "responded_at"
]; ];
type DefaultParams<'a> = (&'a SingleId, &'a [u8;32], &'a str, NaiveDateTime, Option<NaiveDateTime>)
where
Self: 'a;
fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> { fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
let created_at: NaiveDateTime = row.get(2)?; let created_at: NaiveDateTime = row.get(2)?;
let sent_at: Option<NaiveDateTime> = row.get(3)?; let responded_at: Option<NaiveDateTime> = row.get(3)?;
Ok(Self { Ok(Self {
request_id: row.get(0)?, request_id: row.get(0)?,
public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))?, public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))?,
passcode: row.get(2)?, passcode: row.get(2)?,
created_at: DateTime::from(created_at.and_utc()), created_at: DateTime::from(created_at.and_utc()),
sent_at: sent_at.map(|x| DateTime::from(x.and_utc())), responded_at: responded_at.map(|x| DateTime::from(x.and_utc())),
}) })
} }
fn insert(&self) -> Result<(), rusqlite::Error> { fn insert(&self) -> Result<(), rusqlite::Error> {
@ -45,7 +48,7 @@ impl LocalModel for SentAuthorizationRequest {
&self.public_key.as_bytes(), &self.public_key.as_bytes(),
&self.passcode, &self.passcode,
&self.created_at.naive_utc(), &self.created_at.naive_utc(),
&self.sent_at.map(|x| x.naive_utc()) &self.responded_at.map(|x| x.naive_utc())
), ),
)?; )?;
Ok(()) Ok(())
@ -64,4 +67,8 @@ impl LocalModel for SentAuthorizationRequest {
} }
Ok(result) Ok(result)
} }
fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a> {
(&self.request_id, &self.public_key.as_bytes(), &self.passcode, self.created_at.naive_utc(), self.responded_at.map(|x| x.naive_utc()))
}
} }

View file

@ -3,11 +3,10 @@ use rusqlite::{Error, Connection};
pub fn migrate(con: &mut Connection) -> Result<(), Error>{ pub fn migrate(con: &mut Connection) -> Result<(), Error>{
let tx = con.transaction()?; let tx = con.transaction()?;
tx.execute_batch( tx.execute_batch(
"BEGIN; "CREATE TABLE peer (
CREATE TABLE peer (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
local_id INTEGER NOT NULL UNIQUE, local_peer_id INTEGER NOT NULL UNIQUE,
public_key BLOB UNIQUE NOT NULL, public_key BLOB UNIQUE NOT NULL
); );
CREATE TABLE received_authorization_request ( CREATE TABLE received_authorization_request (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@ -20,27 +19,26 @@ pub fn migrate(con: &mut Connection) -> Result<(), Error>{
CREATE TABLE sent_authorization_request ( CREATE TABLE sent_authorization_request (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
request_id INTEGER NOT NULL UNIQUE, request_id INTEGER NOT NULL UNIQUE,
public_key. BLOB NOT NULL UNIQUE, public_key BLOB NOT NULL UNIQUE,
passcode TEXT NOT NULL, passcode TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
sent_at TEXT responded_at TEXT
); );
CREATE TABLE authorized_peer ( CREATE TABLE authorized_peer (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
node_id BLOB NOT NULL UNIQUE, node_id BLOB NOT NULL UNIQUE,
last_synced_at TEXT, last_synced_at TEXT,
last_sent_version_vector BLOB last_sent_version_vector BLOB,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL
); );
CREATE TABLE authorization ( CREATE TABLE authorization (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
node_id BLOB UNIQUE NOT NULL, node_id BLOB UNIQUE NOT NULL,
passcode TEXT NOT NULL, passcode TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, updated_at TEXT NOT NULL
); );",
COMMIT;",
)?; )?;
tx.pragma_update(None, "user_version", 1)?; tx.pragma_update(None, "user_version", 1)?;
tx.commit()?; tx.commit()?;

View file

@ -5,7 +5,7 @@ pub mod migration;
use std::{cell::OnceCell, iter::Map, path::Path, sync::{LazyLock, OnceLock}}; use std::{cell::OnceCell, iter::Map, path::Path, sync::{LazyLock, OnceLock}};
use migration::migrate; use migration::migrate;
use rusqlite::{ffi::Error, Connection, MappedRows, Row}; use rusqlite::{ffi::Error, params, Connection, MappedRows, OptionalExtension, Params, Row, ToSql};
use crate::{config::StorageConfig, global::{CONFIG, LOCAL_DATABASE_CONNECTION}}; use crate::{config::StorageConfig, global::{CONFIG, LOCAL_DATABASE_CONNECTION}};
@ -13,10 +13,63 @@ pub use authorization_request::*;
/// Model trait for local database data. /// Model trait for local database data.
/// use LOCAL_DATABASE_CONNECTION for database connection. /// use LOCAL_DATABASE_CONNECTION for database connection.
pub trait LocalModel: Sized { pub trait LocalRecord: Sized {
const TABLE_NAME: &str; const TABLE_NAME: &str;
const DEFAULT_COLUMNS: &[&str]; const DEFAULT_COLUMNS: &[&str];
fn insert(&self) -> Result<(), rusqlite::Error>;
const DEFAULT_SELECT_STATEMENT: LazyLock<String> = LazyLock::new(|| {
String::from("SELECT ") + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME
});
const DEFAULT_PLACEHOLDER: LazyLock<String> = LazyLock::new(|| {
let mut result : Vec<String> = Vec::new();
for i in 0..Self::DEFAULT_COLUMNS.len() {
result.push(String::from("?") + &(i+1).to_string());
}
result.join(", ")
});
type DefaultParams<'a>: Params
where
Self: 'a;
fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a>;
fn insert(&self) -> Result<(), rusqlite::Error> {
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
connection.execute(
&("INSERT INTO ".to_owned() + Self::TABLE_NAME + " (" + &Self::DEFAULT_COLUMNS.join(", ") + ") VALUES (" + &*Self::DEFAULT_PLACEHOLDER + ")"),
self.as_default_params()
)?;
Ok(())
}
fn get_one_where<P>(where_statement: &str, params: P) -> Result<Option<Self>, rusqlite::Error>
where P: Params
{
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Ok(connection.query_row(
&(String::new() + &Self::DEFAULT_SELECT_STATEMENT + " " + where_statement),
params,
Self::from_default_row
).optional()?)
}
fn get_one_by<T>(field_name: &str, field_value: T) -> Result<Option<Self>, rusqlite::Error>
where
T: ToSql
{
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Ok(Some(connection.query_row(
&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE " + field_name + "=(?1)"),
params![field_value],
Self::from_default_row
)?))
}
fn get_one_by_id(id: u32) -> Result<Option<Self>, rusqlite::Error> {
Self::get_one_by("id", id )
}
fn from_default_row(row: &Row<'_>) -> Result<Self, rusqlite::Error>; fn from_default_row(row: &Row<'_>) -> Result<Self, rusqlite::Error>;
fn get_all() -> Result<Vec<Self>, rusqlite::Error>; fn get_all() -> Result<Vec<Self>, rusqlite::Error>;
} }

View file

@ -8,7 +8,7 @@ use iroh::{NodeId, PublicKey};
use rusqlite::{params, types::FromSqlError, Connection}; use rusqlite::{params, types::FromSqlError, Connection};
use uuid::Uuid; use uuid::Uuid;
use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION}; use crate::{data::local::{self, LocalRecord}, global::LOCAL_DATABASE_CONNECTION};
/// Peer information cached in local database. /// Peer information cached in local database.
/// ///
@ -17,53 +17,57 @@ use crate::{data::local::LocalModel, global::LOCAL_DATABASE_CONNECTION};
/// - Actual peer information is managed by iroh endpoint and not contained in this model. /// - Actual peer information is managed by iroh endpoint and not contained in this model.
/// - Once a peer is authorized, it is assigned a global (=synced) ID as authorized_peer so essentially this local id targets unauthorized peers. /// - Once a peer is authorized, it is assigned a global (=synced) ID as authorized_peer so essentially this local id targets unauthorized peers.
/// ///
pub struct Peer { #[derive(Clone, Debug, PartialEq)]
pub local_id: DoubleId, pub struct PeerRecord {
/// local id of peer.
/// this id is use only the node itself and not synced so another node has different local_peer_id even if its public_key is same.
pub local_peer_id: DoubleId,
pub public_key: PublicKey, pub public_key: PublicKey,
} }
impl Peer { impl PeerRecord {
pub fn get_by_local_id(local_id: DoubleId) -> Result<Option<Self>, rusqlite::Error> { pub fn get_or_insert_by_public_key(public_key: &PublicKey) -> Result<Self, rusqlite::Error> {
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); match Self::get_by_public_key(public_key)? {
Ok(Some(connection.query_row( Some(x) => Ok(x),
&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE local_id=(?1)"), None => {
params![local_id], let new = Self{
Self::from_default_row local_peer_id: rand::random(),
)?)) public_key: public_key.clone(),
};
new.insert()?;
Ok(new)
} }
pub fn get_by_public_key(public_key: PublicKey) -> Result<Option<Self>, rusqlite::Error> { }
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
Ok(Some(connection.query_row( }
&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME + " WHERE public_key=(?1)"), pub fn get_by_local_id(local_id: &DoubleId) -> Result<Option<Self>, rusqlite::Error> {
params![public_key.as_bytes()], Self::get_one_where("WHERE local_peer_id = ?1", (local_id,))
Self::from_default_row }
)?)) pub fn get_by_public_key(public_key: &PublicKey) -> Result<Option<Self>, rusqlite::Error> {
Self::get_one_where("WHERE public_Key = ?1", (public_key.as_bytes(),))
} }
} }
impl LocalModel for Peer { impl LocalRecord for PeerRecord {
const TABLE_NAME: &str = "peer"; const TABLE_NAME: &str = "peer";
const DEFAULT_COLUMNS: &[&str] = &[ const DEFAULT_COLUMNS: &[&str] = &[
"local_id", "local_peer_id",
"public_key" "public_key"
]; ];
type DefaultParams<'a> = (&'a DoubleId, &'a [u8;32]);
fn as_default_params<'a>(&'a self) -> Self::DefaultParams<'a>
{
(&self.local_peer_id, &self.public_key.as_bytes())
}
fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> { fn from_default_row(row: &rusqlite::Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self { Ok(Self {
local_id: row.get(0)?, local_peer_id: row.get(0)?,
public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))? public_key: PublicKey::from_bytes(&row.get(1)?).map_err(|e| FromSqlError::Other(Box::new(e)))?
}) })
} }
fn insert(&self) -> Result<(), rusqlite::Error> {
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
connection.execute(
&("INSERT INTO ".to_owned() + Self::TABLE_NAME + " (" + &Self::DEFAULT_COLUMNS.join(", ") + ") VALUES (?1, ?2, ?3, ?4)"),
(&self.local_id, &self.public_key.as_bytes()),
)?;
Ok(())
}
fn get_all() -> Result<Vec<Self>, rusqlite::Error> { fn get_all() -> Result<Vec<Self>, rusqlite::Error> {
let connection = LOCAL_DATABASE_CONNECTION.get_unchecked(); let connection = LOCAL_DATABASE_CONNECTION.get_unchecked();
let mut stmt = connection.prepare(&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME))?; let mut stmt = connection.prepare(&("SELECT ".to_string() + &Self::DEFAULT_COLUMNS.join(", ") + " FROM " + Self::TABLE_NAME))?;
@ -78,3 +82,23 @@ impl LocalModel for Peer {
Ok(result) Ok(result)
} }
} }
#[cfg(test)]
mod tests {
use iroh::SecretKey;
use crate::tests::TEST_CONFIG;
use super::*;
#[test]
fn insert_get_peer_record() {
LOCAL_DATABASE_CONNECTION.get_or_init(&TEST_CONFIG.storage.get_local_database_path());
let key = SecretKey::generate(&mut rand::rngs::OsRng);
let pubkey = key.public();
let record = PeerRecord::get_or_insert_by_public_key(&pubkey).unwrap();
assert_eq!(record, PeerRecord::get_by_local_id(&record.local_peer_id).unwrap().unwrap());
assert_eq!(record, PeerRecord::get_by_public_key(&record.public_key).unwrap().unwrap());
}
}