1use serde_json::Value;
2use sqlx::Row;
3use sqlx::sqlite::SqliteRow;
4use tracing::{debug, info};
5use uuid::Uuid;
6
7use crate::audit::ClientContext;
8use crate::sqlite::db::Database;
9use crate::sqlite::nonce::now_secs;
10
11#[derive(Debug)]
39pub struct Account {
40 pub id: String,
41 pub profile: String,
47 pub pubkey: Vec<u8>,
48 pub contact: Vec<String>,
49 pub status: String,
50 pub created_at: i64,
51 pub eab_kid: Option<String>,
54 pub terms_of_service_agreed: Option<bool>,
60 pub created_ip: Option<String>,
65 pub created_ptr: Option<String>,
66 pub last_seen_at: Option<i64>,
69 pub last_seen_ip: Option<String>,
70 pub last_seen_ptr: Option<String>,
71}
72
73pub const ACCOUNT_TOUCH_INTERVAL: i64 = 60;
84
85pub(crate) fn pubkey_fingerprint(pubkey: &[u8]) -> String {
92 let digest = ring::digest::digest(&ring::digest::SHA256, pubkey);
93 hex::encode(&digest.as_ref()[..8])
94}
95
96macro_rules! columns {
106 () => {
107 "id, profile, pubkey, contact, status, created_at, eab_kid, \
108 terms_of_service_agreed, created_ip, created_ptr, last_seen_at, \
109 last_seen_ip, last_seen_ptr"
110 };
111}
112
113impl Account {
114 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
115 let contact_json: String = row.try_get("contact")?;
116 let contact: Vec<String> =
117 serde_json::from_str(&contact_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
118
119 Ok(Account {
120 id: row.try_get("id")?,
121 profile: row.try_get("profile")?,
122 pubkey: row.try_get("pubkey")?,
123 contact,
124 status: row.try_get("status")?,
125 created_at: row.try_get("created_at")?,
126 eab_kid: row.try_get("eab_kid")?,
127 terms_of_service_agreed: row.try_get("terms_of_service_agreed")?,
128 created_ip: row.try_get("created_ip")?,
129 created_ptr: row.try_get("created_ptr")?,
130 last_seen_at: row.try_get("last_seen_at")?,
131 last_seen_ip: row.try_get("last_seen_ip")?,
132 last_seen_ptr: row.try_get("last_seen_ptr")?,
133 })
134 }
135
136 #[tracing::instrument(name = "Account::find_by_pubkey", skip(pubkey, database))]
137 pub async fn find_by_pubkey(
138 profile: &str,
139 pubkey: &[u8],
140 database: &Database,
141 ) -> Result<Option<Account>, sqlx::Error> {
142 debug!(event = "db_account_find_by_pubkey_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
143 let row = sqlx::query(concat!(
144 "SELECT ",
145 columns!(),
146 " FROM accounts WHERE profile = ? AND pubkey = ?;"
147 ))
148 .bind(profile)
149 .bind(pubkey)
150 .fetch_optional(&database.pool)
151 .await?;
152
153 let result = row.map(Account::from_row).transpose()?;
154 if let Some(ref account) = result {
155 debug!(event = "db_account_found_by_pubkey", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
156 } else {
157 debug!(event = "db_account_not_found_by_pubkey", outcome = "failure", pubkey_fp = %pubkey_fingerprint(pubkey));
158 }
159 Ok(result)
160 }
161
162 #[tracing::instrument(name = "Account::find_by_id", skip(database), fields(account_id = %id))]
167 pub async fn find_by_id(
168 profile: &str,
169 id: &str,
170 database: &Database,
171 ) -> Result<Option<Account>, sqlx::Error> {
172 debug!(event = "db_account_find_by_id_started", outcome = "progress", profile = %profile, account_id = %id);
173 let row = sqlx::query(concat!(
174 "SELECT ",
175 columns!(),
176 " FROM accounts WHERE profile = ? AND id = ?;"
177 ))
178 .bind(profile)
179 .bind(id)
180 .fetch_optional(&database.pool)
181 .await?;
182
183 let result = row.map(Account::from_row).transpose()?;
184 if let Some(ref account) = result {
185 debug!(event = "db_account_found_by_id", outcome = "success", account_id = %account.id);
186 } else {
187 debug!(event = "db_account_not_found_by_id", outcome = "failure", account_id = %id);
188 }
189 Ok(result)
190 }
191
192 #[tracing::instrument(name = "Account::find_or_create", skip(pubkey, client, database))]
202 pub async fn find_or_create(
203 profile: &str,
204 pubkey: &[u8],
205 contact: Vec<String>,
206 client: &ClientContext,
207 database: &Database,
208 ) -> Result<(Account, bool), sqlx::Error> {
209 debug!(event = "db_account_find_or_create_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
210 if let Some(account) = Account::find_by_pubkey(profile, pubkey, database).await? {
211 debug!(event = "db_account_found_existing", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
212 return Ok((account, false));
213 }
214
215 let account = Account {
216 id: Uuid::new_v4().to_string(),
217 profile: profile.to_string(),
218 pubkey: pubkey.to_vec(),
219 contact,
220 status: "valid".to_string(),
221 created_at: now_secs(),
222 eab_kid: None,
223 terms_of_service_agreed: None,
224 created_ip: client.ip.clone(),
225 created_ptr: client.ptr.clone(),
226 last_seen_at: Some(now_secs()),
232 last_seen_ip: client.ip.clone(),
233 last_seen_ptr: client.ptr.clone(),
234 };
235
236 let contact_json = Value::from(account.contact.clone()).to_string();
238
239 debug!(event = "db_account_create_started", outcome = "progress", account_id = %account.id);
240 sqlx::query(
241 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at, created_ip, \
242 created_ptr, last_seen_at, last_seen_ip, last_seen_ptr) \
243 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
244 )
245 .bind(&account.id)
246 .bind(&account.profile)
247 .bind(&account.pubkey)
248 .bind(contact_json)
249 .bind(&account.status)
250 .bind(account.created_at)
251 .bind(&account.created_ip)
252 .bind(&account.created_ptr)
253 .bind(account.last_seen_at)
254 .bind(&account.last_seen_ip)
255 .bind(&account.last_seen_ptr)
256 .execute(&database.pool)
257 .await?;
258
259 debug!(event = "db_account_created", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
260 Ok((account, true))
261 }
262
263 #[must_use]
279 pub fn needs_touch(&self, now: i64, ip: Option<&str>) -> bool {
280 match self.last_seen_at {
281 None => true,
282 Some(last) => {
283 now.saturating_sub(last) >= ACCOUNT_TOUCH_INTERVAL
284 || self.last_seen_ip.as_deref() != ip
285 }
286 }
287 }
288
289 #[tracing::instrument(name = "Account::touch", skip(self, client, database), fields(account_id = %self.id))]
297 pub async fn touch(
298 &mut self,
299 client: &ClientContext,
300 database: &Database,
301 ) -> Result<(), sqlx::Error> {
302 let now = now_secs();
303 sqlx::query(
304 "UPDATE accounts SET last_seen_at = ?, last_seen_ip = ?, last_seen_ptr = ? \
305 WHERE id = ?;",
306 )
307 .bind(now)
308 .bind(&client.ip)
309 .bind(&client.ptr)
310 .bind(&self.id)
311 .execute(&database.pool)
312 .await?;
313
314 self.last_seen_at = Some(now);
315 self.last_seen_ip = client.ip.clone();
316 self.last_seen_ptr = client.ptr.clone();
317 debug!(event = "db_account_touched", outcome = "success", account_id = %self.id);
318 Ok(())
319 }
320
321 #[tracing::instrument(name = "Account::update_contact", skip(self, database), fields(account_id = %self.id))]
325 pub async fn update_contact(
326 &mut self,
327 contact: Vec<String>,
328 database: &Database,
329 ) -> Result<(), sqlx::Error> {
330 debug!(event = "db_account_contact_update_started", outcome = "progress", account_id = %self.id);
331 let contact_json = Value::from(contact.clone()).to_string();
333
334 sqlx::query("UPDATE accounts SET contact = ? WHERE id = ?;")
335 .bind(contact_json)
336 .bind(&self.id)
337 .execute(&database.pool)
338 .await?;
339
340 self.contact = contact;
341 debug!(event = "db_account_contact_updated", outcome = "success", account_id = %self.id);
342 Ok(())
343 }
344
345 #[tracing::instrument(name = "Account::deactivate", skip(self, database), fields(account_id = %self.id))]
348 pub async fn deactivate(&mut self, database: &Database) -> Result<(), sqlx::Error> {
349 debug!(event = "db_account_deactivation_started", outcome = "progress", account_id = %self.id);
350 sqlx::query("UPDATE accounts SET status = 'deactivated' WHERE id = ?;")
351 .bind(&self.id)
352 .execute(&database.pool)
353 .await?;
354
355 self.status = "deactivated".to_string();
356 debug!(event = "db_account_deactivated", outcome = "success", account_id = %self.id);
357 Ok(())
358 }
359
360 pub async fn update_pubkey(
366 &mut self,
367 pubkey: &[u8],
368 database: &Database,
369 ) -> Result<(), sqlx::Error> {
370 debug!(event = "db_account_pubkey_update_started", outcome = "progress", account_id = ?self.id);
371 sqlx::query("UPDATE accounts SET pubkey = ? WHERE id = ?;")
372 .bind(pubkey)
373 .bind(&self.id)
374 .execute(&database.pool)
375 .await?;
376
377 self.pubkey = pubkey.to_vec();
378 info!(event = "db_account_pubkey_updated", outcome = "success", account_id = ?self.id);
379 Ok(())
380 }
381
382 pub async fn set_eab_kid(
389 &mut self,
390 eab_kid: &str,
391 database: &Database,
392 ) -> Result<(), sqlx::Error> {
393 debug!(event = "db_account_eab_kid_set_started", outcome = "progress", account_id = ?self.id, eab_kid = ?eab_kid);
394 sqlx::query("UPDATE accounts SET eab_kid = ? WHERE id = ?;")
395 .bind(eab_kid)
396 .bind(&self.id)
397 .execute(&database.pool)
398 .await?;
399
400 self.eab_kid = Some(eab_kid.to_string());
401 info!(event = "db_account_eab_kid_set", outcome = "success", account_id = ?self.id, eab_kid = ?eab_kid);
402 Ok(())
403 }
404
405 pub async fn set_terms_agreed(&mut self, database: &Database) -> Result<(), sqlx::Error> {
414 debug!(event = "db_account_terms_agreed_started", outcome = "progress", account_id = ?self.id);
415 sqlx::query("UPDATE accounts SET terms_of_service_agreed = 1 WHERE id = ?;")
416 .bind(&self.id)
417 .execute(&database.pool)
418 .await?;
419
420 self.terms_of_service_agreed = Some(true);
421 info!(event = "db_account_terms_agreed", outcome = "success", account_id = ?self.id);
422 Ok(())
423 }
424
425 pub async fn find_any_by_id(
432 id: &str,
433 database: &Database,
434 ) -> Result<Option<Account>, sqlx::Error> {
435 debug!(event = "db_account_find_any_by_id_started", outcome = "progress", account_id = %id);
436 let row = sqlx::query(concat!(
437 "SELECT ",
438 columns!(),
439 " FROM accounts WHERE id = ?;"
440 ))
441 .bind(id)
442 .fetch_optional(&database.pool)
443 .await?;
444
445 row.map(Account::from_row).transpose()
446 }
447
448 pub async fn list_all(
452 profile: Option<&str>,
453 database: &Database,
454 ) -> Result<Vec<Account>, sqlx::Error> {
455 debug!(event = "db_account_list_all_started", outcome = "progress", profile = ?profile);
456 let rows = match profile {
457 Some(profile) => {
458 sqlx::query(concat!(
459 "SELECT ",
460 columns!(),
461 " FROM accounts WHERE profile = ? ORDER BY created_at ASC;"
462 ))
463 .bind(profile)
464 .fetch_all(&database.pool)
465 .await?
466 }
467 None => {
468 sqlx::query(concat!(
469 "SELECT ",
470 columns!(),
471 " FROM accounts ORDER BY created_at ASC;"
472 ))
473 .fetch_all(&database.pool)
474 .await?
475 }
476 };
477
478 rows.into_iter().map(Account::from_row).collect()
479 }
480
481 pub async fn search(
492 profile: Option<&str>,
493 limit: i64,
494 offset: i64,
495 database: &Database,
496 ) -> Result<(Vec<Account>, i64), sqlx::Error> {
497 debug!(event = "db_account_search_started", outcome = "progress", profile = ?profile, limit = limit, offset = offset);
498
499 let (rows, total) = match profile {
503 Some(profile) => {
504 let rows = sqlx::query(concat!(
505 "SELECT ",
506 columns!(),
507 " FROM accounts WHERE profile = ? \
508 ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
509 ))
510 .bind(profile)
511 .bind(limit)
512 .bind(offset)
513 .fetch_all(&database.pool)
514 .await?;
515 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts WHERE profile = ?;")
516 .bind(profile)
517 .fetch_one(&database.pool)
518 .await?
519 .try_get(0)?;
520 (rows, total)
521 }
522 None => {
523 let rows = sqlx::query(concat!(
524 "SELECT ",
525 columns!(),
526 " FROM accounts ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
527 ))
528 .bind(limit)
529 .bind(offset)
530 .fetch_all(&database.pool)
531 .await?;
532 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts;")
533 .fetch_one(&database.pool)
534 .await?
535 .try_get(0)?;
536 (rows, total)
537 }
538 };
539
540 let accounts = rows
541 .into_iter()
542 .map(Account::from_row)
543 .collect::<Result<_, _>>()?;
544 Ok((accounts, total))
545 }
546
547 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
552 debug!(event = "db_account_delete_started", outcome = "progress", account_id = ?id);
553 let result = sqlx::query("DELETE FROM accounts WHERE id = ?;")
554 .bind(id)
555 .execute(&database.pool)
556 .await?;
557
558 let deleted = result.rows_affected() > 0;
559 if deleted {
560 info!(event = "db_account_deleted", outcome = "success", account_id = ?id);
561 } else {
562 debug!(event = "db_account_delete_missing", outcome = "success", account_id = ?id);
563 }
564 Ok(deleted)
565 }
566
567 #[must_use]
570 pub fn to_json(&self, base_url: &str) -> Value {
571 let mut object = serde_json::Map::new();
572 object.insert("status".to_string(), Value::String(self.status.clone()));
573 if !self.contact.is_empty() {
574 object.insert("contact".to_string(), Value::from(self.contact.clone()));
575 }
576 object.insert(
577 "orders".to_string(),
578 Value::String(format!("{base_url}/acct/{}/orders", self.id)),
579 );
580 if let Some(agreed) = self.terms_of_service_agreed {
584 object.insert("termsOfServiceAgreed".to_string(), Value::Bool(agreed));
585 }
586 Value::Object(object)
587 }
588}
589
590#[cfg(test)]
591mod tests {
592
593 #[test]
598 fn needs_touch_yields_to_the_interval_but_never_to_a_changed_address() {
599 let mut account = Account {
600 id: "a".to_string(),
601 profile: "default".to_string(),
602 pubkey: vec![1],
603 contact: vec![],
604 status: "valid".to_string(),
605 created_at: 0,
606 eab_kid: None,
607 terms_of_service_agreed: None,
608 created_ip: None,
609 created_ptr: None,
610 last_seen_at: None,
611 last_seen_ip: None,
612 last_seen_ptr: None,
613 };
614
615 assert!(account.needs_touch(1_000, Some("203.0.113.7")));
617
618 account.last_seen_at = Some(1_000);
619 account.last_seen_ip = Some("203.0.113.7".to_string());
620
621 assert!(!account.needs_touch(1_000, Some("203.0.113.7")));
623 assert!(!account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL - 1, Some("203.0.113.7")));
624 assert!(account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL, Some("203.0.113.7")));
626 assert!(account.needs_touch(1_000, Some("198.51.100.4")));
628 assert!(account.needs_touch(1_000, None));
631
632 assert!(!account.needs_touch(0, Some("203.0.113.7")));
635 }
636
637 #[tokio::test]
641 async fn creation_stamps_the_address_once_and_touch_moves_only_the_last_seen_columns() {
642 let db = Database::connect_in_memory().await.unwrap();
643 let first = ClientContext {
644 ip: Some("203.0.113.7".to_string()),
645 ptr: Some("first.example.com".to_string()),
646 user_agent: Some("certbot".to_string()),
647 request_id: Some("req-1".to_string()),
648 };
649 let (created, is_new) = Account::find_or_create("default", &[42u8], vec![], &first, &db)
650 .await
651 .unwrap();
652 assert!(is_new);
653 assert_eq!(created.created_ip.as_deref(), Some("203.0.113.7"));
654 assert_eq!(created.created_ptr.as_deref(), Some("first.example.com"));
655 assert_eq!(created.last_seen_ip.as_deref(), Some("203.0.113.7"));
658 assert!(created.last_seen_at.is_some());
659
660 let second = ClientContext {
663 ip: Some("198.51.100.4".to_string()),
664 ptr: Some("second.example.com".to_string()),
665 ..ClientContext::default()
666 };
667 let (mut found, is_new) = Account::find_or_create("default", &[42u8], vec![], &second, &db)
668 .await
669 .unwrap();
670 assert!(!is_new);
671 assert_eq!(found.created_ip.as_deref(), Some("203.0.113.7"));
672 assert_eq!(found.created_ptr.as_deref(), Some("first.example.com"));
673
674 found.touch(&second, &db).await.unwrap();
675 assert_eq!(found.last_seen_ip.as_deref(), Some("198.51.100.4"));
677 assert_eq!(found.last_seen_ptr.as_deref(), Some("second.example.com"));
678 let reloaded = Account::find_by_id("default", &found.id, &db)
680 .await
681 .unwrap()
682 .unwrap();
683 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
684 assert_eq!(reloaded.last_seen_ip.as_deref(), Some("198.51.100.4"));
685 assert_eq!(
686 reloaded.last_seen_ptr.as_deref(),
687 Some("second.example.com")
688 );
689 assert!(reloaded.last_seen_at >= reloaded.created_at.into());
690
691 let nameless = ClientContext {
694 ip: Some("198.51.100.4".to_string()),
695 ..ClientContext::default()
696 };
697 found.touch(&nameless, &db).await.unwrap();
698 let reloaded = Account::find_by_id("default", &found.id, &db)
699 .await
700 .unwrap()
701 .unwrap();
702 assert_eq!(reloaded.last_seen_ptr, None);
703 }
704
705 #[tokio::test]
709 async fn to_json_exposes_none_of_the_traceability_columns() {
710 let db = Database::connect_in_memory().await.unwrap();
711 let client = ClientContext {
712 ip: Some("203.0.113.7".to_string()),
713 ptr: Some("host.example.com".to_string()),
714 ..ClientContext::default()
715 };
716 let (account, _) = Account::find_or_create("default", &[7u8], vec![], &client, &db)
717 .await
718 .unwrap();
719 let json = account.to_json("http://localhost:3000");
720 let object = json.as_object().unwrap();
721 for absent in [
722 "createdIp",
723 "created_ip",
724 "createdPtr",
725 "lastSeenAt",
726 "lastSeenIp",
727 "lastSeenPtr",
728 ] {
729 assert!(!object.contains_key(absent), "{absent} leaked into to_json");
730 }
731 assert!(!json.to_string().contains("203.0.113.7"));
732 }
733
734 use super::*;
735 use std::sync::Arc;
736
737 #[tokio::test]
738 async fn find_or_create_creates_then_returns_existing() {
739 let db = Arc::new(Database::connect_in_memory().await.unwrap());
740 let pubkey = vec![1u8, 2, 3, 4];
741 let contact = vec!["mailto:a@example.com".to_string()];
742
743 let (created, is_new) = Account::find_or_create(
744 "default",
745 &pubkey,
746 contact.clone(),
747 &ClientContext::default(),
748 &db,
749 )
750 .await
751 .unwrap();
752 assert!(is_new);
753 assert_eq!(created.status, "valid");
754 assert_eq!(created.contact, contact);
755
756 let (existing, is_new) =
759 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
760 .await
761 .unwrap();
762 assert!(!is_new);
763 assert_eq!(existing.id, created.id);
764 assert_eq!(existing.contact, contact);
765 }
766
767 #[tokio::test]
768 async fn find_by_id_and_pubkey_round_trip() {
769 let db = Arc::new(Database::connect_in_memory().await.unwrap());
770 let pubkey = vec![9u8; 16];
771
772 let (account, _) =
773 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
774 .await
775 .unwrap();
776
777 let by_id = Account::find_by_id("default", &account.id, &db)
778 .await
779 .unwrap()
780 .unwrap();
781 assert_eq!(by_id.pubkey, pubkey);
782
783 let by_key = Account::find_by_pubkey("default", &pubkey, &db)
784 .await
785 .unwrap()
786 .unwrap();
787 assert_eq!(by_key.id, account.id);
788 }
789
790 #[tokio::test]
791 async fn absent_lookups_return_none() {
792 let db = Arc::new(Database::connect_in_memory().await.unwrap());
793
794 assert!(
795 Account::find_by_id("default", "nope", &db)
796 .await
797 .unwrap()
798 .is_none()
799 );
800 assert!(
801 Account::find_by_pubkey("default", &[0u8; 4], &db)
802 .await
803 .unwrap()
804 .is_none()
805 );
806 }
807
808 #[tokio::test]
809 async fn update_contact_persists_and_syncs() {
810 let db = Arc::new(Database::connect_in_memory().await.unwrap());
811 let pubkey = vec![7u8; 8];
812
813 let (mut account, _) = Account::find_or_create(
814 "default",
815 &pubkey,
816 vec!["mailto:old@example.com".to_string()],
817 &ClientContext::default(),
818 &db,
819 )
820 .await
821 .unwrap();
822
823 let new_contact = vec!["mailto:new@example.com".to_string()];
824 account
825 .update_contact(new_contact.clone(), &db)
826 .await
827 .unwrap();
828
829 assert_eq!(account.contact, new_contact);
831 let reloaded = Account::find_by_id("default", &account.id, &db)
833 .await
834 .unwrap()
835 .unwrap();
836 assert_eq!(reloaded.contact, new_contact);
837 }
838
839 #[tokio::test]
840 async fn deactivate_persists_and_syncs() {
841 let db = Arc::new(Database::connect_in_memory().await.unwrap());
842 let pubkey = vec![8u8; 8];
843
844 let (mut account, _) =
845 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
846 .await
847 .unwrap();
848 assert_eq!(account.status, "valid");
849
850 account.deactivate(&db).await.unwrap();
851
852 assert_eq!(account.status, "deactivated");
853 let reloaded = Account::find_by_id("default", &account.id, &db)
854 .await
855 .unwrap()
856 .unwrap();
857 assert_eq!(reloaded.status, "deactivated");
858 }
859
860 #[tokio::test]
861 async fn update_pubkey_persists_and_syncs() {
862 let db = Arc::new(Database::connect_in_memory().await.unwrap());
863 let (mut account, _) =
864 Account::find_or_create("default", &[9u8; 8], vec![], &ClientContext::default(), &db)
865 .await
866 .unwrap();
867
868 let new_pubkey = vec![10u8; 8];
869 account.update_pubkey(&new_pubkey, &db).await.unwrap();
870
871 assert_eq!(account.pubkey, new_pubkey);
873 let reloaded = Account::find_by_id("default", &account.id, &db)
875 .await
876 .unwrap()
877 .unwrap();
878 assert_eq!(reloaded.pubkey, new_pubkey);
879 assert!(
880 Account::find_by_pubkey("default", &new_pubkey, &db)
881 .await
882 .unwrap()
883 .is_some()
884 );
885 }
886
887 #[tokio::test]
892 async fn update_pubkey_to_a_key_owned_by_another_account_is_rejected() {
893 let db = Arc::new(Database::connect_in_memory().await.unwrap());
894 let (_first, _) = Account::find_or_create(
895 "default",
896 &[11u8; 8],
897 vec![],
898 &ClientContext::default(),
899 &db,
900 )
901 .await
902 .unwrap();
903 let (mut second, _) = Account::find_or_create(
904 "default",
905 &[12u8; 8],
906 vec![],
907 &ClientContext::default(),
908 &db,
909 )
910 .await
911 .unwrap();
912
913 assert!(second.update_pubkey(&[11u8; 8], &db).await.is_err());
914 }
915
916 #[tokio::test]
917 async fn set_eab_kid_persists_and_syncs() {
918 let db = Arc::new(Database::connect_in_memory().await.unwrap());
919 let (mut account, _) =
920 Account::find_or_create("default", &[5u8], vec![], &ClientContext::default(), &db)
921 .await
922 .unwrap();
923 assert!(account.eab_kid.is_none());
924
925 account.set_eab_kid("some-kid", &db).await.unwrap();
926 assert_eq!(account.eab_kid.as_deref(), Some("some-kid"));
927
928 let reloaded = Account::find_by_id("default", &account.id, &db)
929 .await
930 .unwrap()
931 .unwrap();
932 assert_eq!(reloaded.eab_kid.as_deref(), Some("some-kid"));
933 }
934
935 #[tokio::test]
936 async fn list_all_orders_oldest_first() {
937 let db = Arc::new(Database::connect_in_memory().await.unwrap());
938
939 let (first, _) =
940 Account::find_or_create("default", &[1u8], vec![], &ClientContext::default(), &db)
941 .await
942 .unwrap();
943 let (second, _) =
944 Account::find_or_create("default", &[2u8], vec![], &ClientContext::default(), &db)
945 .await
946 .unwrap();
947
948 let all = Account::list_all(None, &db).await.unwrap();
949 assert_eq!(all.len(), 2);
950 assert_eq!(all[0].id, first.id);
951 assert_eq!(all[1].id, second.id);
952 }
953
954 #[tokio::test]
955 async fn list_all_when_empty_is_empty() {
956 let db = Arc::new(Database::connect_in_memory().await.unwrap());
957 assert!(Account::list_all(None, &db).await.unwrap().is_empty());
958 }
959
960 #[tokio::test]
961 async fn delete_removes_the_row_and_reports_true() {
962 let db = Arc::new(Database::connect_in_memory().await.unwrap());
963 let (account, _) =
964 Account::find_or_create("default", &[3u8], vec![], &ClientContext::default(), &db)
965 .await
966 .unwrap();
967
968 assert!(Account::delete(&account.id, &db).await.unwrap());
969 assert!(
970 Account::find_by_id("default", &account.id, &db)
971 .await
972 .unwrap()
973 .is_none()
974 );
975 }
976
977 #[tokio::test]
978 async fn delete_of_unknown_id_reports_false() {
979 let db = Arc::new(Database::connect_in_memory().await.unwrap());
980 assert!(!Account::delete("nope", &db).await.unwrap());
981 }
982
983 #[tokio::test]
984 async fn delete_cascades_to_the_accounts_orders() {
985 let db = Arc::new(Database::connect_in_memory().await.unwrap());
986 let (account, _) =
987 Account::find_or_create("default", &[4u8], vec![], &ClientContext::default(), &db)
988 .await
989 .unwrap();
990
991 crate::sqlite::order::Order::create(
992 "default",
993 &account.id,
994 vec![],
995 now_secs() + 3600,
996 None,
997 None,
998 &db,
999 )
1000 .await
1001 .unwrap();
1002
1003 Account::delete(&account.id, &db).await.unwrap();
1004
1005 let remaining = crate::sqlite::order::Order::find_by_account(&account.id, &db)
1006 .await
1007 .unwrap();
1008 assert!(remaining.is_empty());
1009 }
1010
1011 async fn seed_accounts(db: &Arc<Database>, profile: &str, count: usize) -> Vec<String> {
1014 let base = now_secs();
1015 let mut ids = Vec::new();
1016 for index in 0..count {
1017 let (account, _) = Account::find_or_create(
1018 profile,
1019 &[profile.len() as u8, index as u8],
1020 vec![],
1021 &ClientContext::default(),
1022 db,
1023 )
1024 .await
1025 .unwrap();
1026 sqlx::query("UPDATE accounts SET created_at = ? WHERE id = ?;")
1027 .bind(base - index as i64)
1028 .bind(&account.id)
1029 .execute(&db.pool)
1030 .await
1031 .unwrap();
1032 ids.push(account.id);
1033 }
1034 ids
1035 }
1036
1037 #[tokio::test]
1038 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1039 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1040 let ids = seed_accounts(&db, "default", 5).await;
1041
1042 let (page, total) = Account::search(None, 2, 0, &db).await.unwrap();
1043 assert_eq!(total, 5, "the total must ignore the page window");
1044 assert_eq!(
1045 page.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1046 ids[..2]
1047 );
1048
1049 let (second, _) = Account::search(None, 2, 2, &db).await.unwrap();
1050 assert_eq!(
1051 second.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1052 ids[2..4]
1053 );
1054
1055 let (beyond, total) = Account::search(None, 2, 99, &db).await.unwrap();
1057 assert!(beyond.is_empty());
1058 assert_eq!(total, 5);
1059 }
1060
1061 #[tokio::test]
1062 async fn search_scopes_by_profile_and_counts_only_that_profile() {
1063 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1064 seed_accounts(&db, "default", 2).await;
1065 seed_accounts(&db, "other", 3).await;
1066
1067 let (rows, total) = Account::search(Some("other"), 50, 0, &db).await.unwrap();
1068 assert_eq!(total, 3);
1069 assert_eq!(rows.len(), 3);
1070 assert!(rows.iter().all(|a| a.profile == "other"));
1071
1072 let (_, total) = Account::search(None, 50, 0, &db).await.unwrap();
1073 assert_eq!(total, 5, "no profile means every endpoint");
1074
1075 let (rows, total) = Account::search(Some("nope"), 50, 0, &db).await.unwrap();
1076 assert!(rows.is_empty());
1077 assert_eq!(total, 0);
1078 }
1079
1080 #[tokio::test]
1081 async fn search_on_an_empty_table_is_empty_rather_than_an_error() {
1082 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1083 let (rows, total) = Account::search(None, 50, 0, &db).await.unwrap();
1084 assert!(rows.is_empty());
1085 assert_eq!(total, 0);
1086 }
1087}