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)]
38pub struct Account {
39 pub id: Uuid,
40 pub profile: String,
46 pub pubkey: Vec<u8>,
47 pub contact: Vec<String>,
48 pub status: String,
49 pub created_at: i64,
50 pub eab_kid: Option<Uuid>,
53 pub terms_of_service_agreed: Option<bool>,
59 pub created_ip: Option<String>,
64 pub created_ptr: Option<String>,
65 pub last_seen_at: Option<i64>,
68 pub last_seen_ip: Option<String>,
69 pub last_seen_ptr: Option<String>,
70}
71
72pub const ACCOUNT_TOUCH_INTERVAL: i64 = 60;
83
84pub(crate) fn pubkey_fingerprint(pubkey: &[u8]) -> String {
91 let digest = ring::digest::digest(&ring::digest::SHA256, pubkey);
92 hex::encode(&digest.as_ref()[..8])
93}
94
95macro_rules! columns {
105 () => {
106 "id, profile, pubkey, contact, status, created_at, eab_kid, \
107 terms_of_service_agreed, created_ip, created_ptr, last_seen_at, \
108 last_seen_ip, last_seen_ptr"
109 };
110}
111
112impl Account {
113 fn from_row(row: SqliteRow) -> Result<Self, sqlx::Error> {
114 let contact_json: String = row.try_get("contact")?;
115 let contact: Vec<String> =
116 serde_json::from_str(&contact_json).map_err(|e| sqlx::Error::Decode(Box::new(e)))?;
117
118 Ok(Account {
119 id: row.try_get("id")?,
120 profile: row.try_get("profile")?,
121 pubkey: row.try_get("pubkey")?,
122 contact,
123 status: row.try_get("status")?,
124 created_at: row.try_get("created_at")?,
125 eab_kid: row.try_get("eab_kid")?,
126 terms_of_service_agreed: row.try_get("terms_of_service_agreed")?,
127 created_ip: row.try_get("created_ip")?,
128 created_ptr: row.try_get("created_ptr")?,
129 last_seen_at: row.try_get("last_seen_at")?,
130 last_seen_ip: row.try_get("last_seen_ip")?,
131 last_seen_ptr: row.try_get("last_seen_ptr")?,
132 })
133 }
134
135 #[tracing::instrument(name = "Account::find_by_pubkey", skip(pubkey, database))]
136 pub async fn find_by_pubkey(
137 profile: &str,
138 pubkey: &[u8],
139 database: &Database,
140 ) -> Result<Option<Account>, sqlx::Error> {
141 debug!(event = "db_account_find_by_pubkey_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
142 let row = sqlx::query(concat!(
143 "SELECT ",
144 columns!(),
145 " FROM accounts WHERE profile = ? AND pubkey = ?;"
146 ))
147 .bind(profile)
148 .bind(pubkey)
149 .fetch_optional(&database.pool)
150 .await?;
151
152 let result = row.map(Account::from_row).transpose()?;
153 if let Some(ref account) = result {
154 debug!(event = "db_account_found_by_pubkey", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
155 } else {
156 debug!(event = "db_account_not_found_by_pubkey", outcome = "failure", pubkey_fp = %pubkey_fingerprint(pubkey));
157 }
158 Ok(result)
159 }
160
161 #[tracing::instrument(name = "Account::find_by_id", skip(database), fields(account_id = %id))]
166 pub async fn find_by_id(
167 profile: &str,
168 id: &str,
169 database: &Database,
170 ) -> Result<Option<Account>, sqlx::Error> {
171 debug!(event = "db_account_find_by_id_started", outcome = "progress", profile = %profile, account_id = %id);
172 let Some(id) = crate::sqlite::id::parse(id) else {
173 return Ok(None);
174 };
175 let row = sqlx::query(concat!(
176 "SELECT ",
177 columns!(),
178 " FROM accounts WHERE profile = ? AND id = ?;"
179 ))
180 .bind(profile)
181 .bind(id)
182 .fetch_optional(&database.pool)
183 .await?;
184
185 let result = row.map(Account::from_row).transpose()?;
186 if let Some(ref account) = result {
187 debug!(event = "db_account_found_by_id", outcome = "success", account_id = %account.id);
188 } else {
189 debug!(event = "db_account_not_found_by_id", outcome = "failure", account_id = %id);
190 }
191 Ok(result)
192 }
193
194 #[tracing::instrument(name = "Account::find_or_create", skip(pubkey, client, database))]
204 pub async fn find_or_create(
205 profile: &str,
206 pubkey: &[u8],
207 contact: Vec<String>,
208 client: &ClientContext,
209 database: &Database,
210 ) -> Result<(Account, bool), sqlx::Error> {
211 debug!(event = "db_account_find_or_create_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
212 if let Some(account) = Account::find_by_pubkey(profile, pubkey, database).await? {
213 debug!(event = "db_account_found_existing", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
214 return Ok((account, false));
215 }
216
217 let account = Account {
218 id: crate::sqlite::id::mint(),
219 profile: profile.to_string(),
220 pubkey: pubkey.to_vec(),
221 contact,
222 status: "valid".to_string(),
223 created_at: now_secs(),
224 eab_kid: None,
225 terms_of_service_agreed: None,
226 created_ip: client.ip.clone(),
227 created_ptr: client.ptr.clone(),
228 last_seen_at: Some(now_secs()),
234 last_seen_ip: client.ip.clone(),
235 last_seen_ptr: client.ptr.clone(),
236 };
237
238 let contact_json = Value::from(account.contact.clone()).to_string();
240
241 debug!(event = "db_account_create_started", outcome = "progress", account_id = %account.id);
242 let inserted = sqlx::query(
243 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at, created_ip, \
244 created_ptr, last_seen_at, last_seen_ip, last_seen_ptr) \
245 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
246 )
247 .bind(account.id)
248 .bind(&account.profile)
249 .bind(&account.pubkey)
250 .bind(contact_json)
251 .bind(&account.status)
252 .bind(account.created_at)
253 .bind(&account.created_ip)
254 .bind(&account.created_ptr)
255 .bind(account.last_seen_at)
256 .bind(&account.last_seen_ip)
257 .bind(&account.last_seen_ptr)
258 .execute(&database.pool)
259 .await;
260
261 if let Err(error) = inserted {
275 if is_pubkey_conflict(&error)
276 && let Some(existing) = Account::find_by_pubkey(profile, pubkey, database).await?
277 {
278 debug!(event = "db_account_create_lost_race", outcome = "advisory", account_id = %existing.id, pubkey_fp = %pubkey_fingerprint(pubkey));
279 return Ok((existing, false));
280 }
281 return Err(error);
282 }
283
284 debug!(event = "db_account_created", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
285 Ok((account, true))
286 }
287
288 #[must_use]
304 pub fn needs_touch(&self, now: i64, ip: Option<&str>) -> bool {
305 match self.last_seen_at {
306 None => true,
307 Some(last) => {
308 now.saturating_sub(last) >= ACCOUNT_TOUCH_INTERVAL
309 || self.last_seen_ip.as_deref() != ip
310 }
311 }
312 }
313
314 #[tracing::instrument(name = "Account::touch", skip(self, client, database), fields(account_id = %self.id))]
322 pub async fn touch(
323 &mut self,
324 client: &ClientContext,
325 database: &Database,
326 ) -> Result<(), sqlx::Error> {
327 let now = now_secs();
328 sqlx::query(
329 "UPDATE accounts SET last_seen_at = ?, last_seen_ip = ?, last_seen_ptr = ? \
330 WHERE id = ?;",
331 )
332 .bind(now)
333 .bind(&client.ip)
334 .bind(&client.ptr)
335 .bind(self.id)
336 .execute(&database.pool)
337 .await?;
338
339 self.last_seen_at = Some(now);
340 self.last_seen_ip = client.ip.clone();
341 self.last_seen_ptr = client.ptr.clone();
342 debug!(event = "db_account_touched", outcome = "success", account_id = %self.id);
343 Ok(())
344 }
345
346 #[tracing::instrument(name = "Account::update_contact", skip(self, database), fields(account_id = %self.id))]
350 pub async fn update_contact(
351 &mut self,
352 contact: Vec<String>,
353 database: &Database,
354 ) -> Result<(), sqlx::Error> {
355 debug!(event = "db_account_contact_update_started", outcome = "progress", account_id = %self.id);
356 let contact_json = Value::from(contact.clone()).to_string();
358
359 sqlx::query("UPDATE accounts SET contact = ? WHERE id = ?;")
360 .bind(contact_json)
361 .bind(self.id)
362 .execute(&database.pool)
363 .await?;
364
365 self.contact = contact;
366 debug!(event = "db_account_contact_updated", outcome = "success", account_id = %self.id);
367 Ok(())
368 }
369
370 #[tracing::instrument(name = "Account::deactivate", skip(self, database), fields(account_id = %self.id))]
373 pub async fn deactivate(&mut self, database: &Database) -> Result<(), sqlx::Error> {
374 debug!(event = "db_account_deactivation_started", outcome = "progress", account_id = %self.id);
375 sqlx::query("UPDATE accounts SET status = 'deactivated' WHERE id = ?;")
376 .bind(self.id)
377 .execute(&database.pool)
378 .await?;
379
380 self.status = "deactivated".to_string();
381 debug!(event = "db_account_deactivated", outcome = "success", account_id = %self.id);
382 Ok(())
383 }
384
385 pub async fn update_pubkey(
391 &mut self,
392 pubkey: &[u8],
393 database: &Database,
394 ) -> Result<(), sqlx::Error> {
395 debug!(event = "db_account_pubkey_update_started", outcome = "progress", account_id = ?self.id);
396 sqlx::query("UPDATE accounts SET pubkey = ? WHERE id = ?;")
397 .bind(pubkey)
398 .bind(self.id)
399 .execute(&database.pool)
400 .await?;
401
402 self.pubkey = pubkey.to_vec();
403 info!(event = "db_account_pubkey_updated", outcome = "success", account_id = ?self.id);
404 Ok(())
405 }
406
407 pub async fn set_eab_kid(
414 &mut self,
415 eab_kid: Uuid,
416 database: &Database,
417 ) -> Result<(), sqlx::Error> {
418 debug!(event = "db_account_eab_kid_set_started", outcome = "progress", account_id = ?self.id, eab_kid = ?eab_kid);
419 sqlx::query("UPDATE accounts SET eab_kid = ? WHERE id = ?;")
420 .bind(eab_kid)
421 .bind(self.id)
422 .execute(&database.pool)
423 .await?;
424
425 self.eab_kid = Some(eab_kid);
426 info!(event = "db_account_eab_kid_set", outcome = "success", account_id = ?self.id, eab_kid = ?eab_kid);
427 Ok(())
428 }
429
430 pub async fn set_terms_agreed(&mut self, database: &Database) -> Result<(), sqlx::Error> {
439 debug!(event = "db_account_terms_agreed_started", outcome = "progress", account_id = ?self.id);
440 sqlx::query("UPDATE accounts SET terms_of_service_agreed = 1 WHERE id = ?;")
441 .bind(self.id)
442 .execute(&database.pool)
443 .await?;
444
445 self.terms_of_service_agreed = Some(true);
446 info!(event = "db_account_terms_agreed", outcome = "success", account_id = ?self.id);
447 Ok(())
448 }
449
450 pub async fn find_any_by_id(
457 id: &str,
458 database: &Database,
459 ) -> Result<Option<Account>, sqlx::Error> {
460 debug!(event = "db_account_find_any_by_id_started", outcome = "progress", account_id = %id);
461 let Some(id) = crate::sqlite::id::parse(id) else {
462 return Ok(None);
463 };
464 let row = sqlx::query(concat!(
465 "SELECT ",
466 columns!(),
467 " FROM accounts WHERE id = ?;"
468 ))
469 .bind(id)
470 .fetch_optional(&database.pool)
471 .await?;
472
473 row.map(Account::from_row).transpose()
474 }
475
476 pub async fn search(
491 profile: Option<&str>,
492 limit: i64,
493 offset: i64,
494 database: &Database,
495 ) -> Result<(Vec<Account>, i64), sqlx::Error> {
496 debug!(event = "db_account_search_started", outcome = "progress", profile = ?profile, limit = limit, offset = offset);
497
498 let (rows, total) = match profile {
502 Some(profile) => {
503 let rows = sqlx::query(concat!(
504 "SELECT ",
505 columns!(),
506 " FROM accounts WHERE profile = ? \
507 ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
508 ))
509 .bind(profile)
510 .bind(limit)
511 .bind(offset)
512 .fetch_all(&database.pool)
513 .await?;
514 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts WHERE profile = ?;")
515 .bind(profile)
516 .fetch_one(&database.pool)
517 .await?
518 .try_get(0)?;
519 (rows, total)
520 }
521 None => {
522 let rows = sqlx::query(concat!(
523 "SELECT ",
524 columns!(),
525 " FROM accounts ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
526 ))
527 .bind(limit)
528 .bind(offset)
529 .fetch_all(&database.pool)
530 .await?;
531 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts;")
532 .fetch_one(&database.pool)
533 .await?
534 .try_get(0)?;
535 (rows, total)
536 }
537 };
538
539 let accounts = rows
540 .into_iter()
541 .map(Account::from_row)
542 .collect::<Result<_, _>>()?;
543 Ok((accounts, total))
544 }
545
546 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
551 debug!(event = "db_account_delete_started", outcome = "progress", account_id = ?id);
552 let Some(id) = crate::sqlite::id::parse(id) else {
553 return Ok(false);
554 };
555 let result = sqlx::query("DELETE FROM accounts WHERE id = ?;")
556 .bind(id)
557 .execute(&database.pool)
558 .await?;
559
560 let deleted = result.rows_affected() > 0;
561 if deleted {
562 info!(event = "db_account_deleted", outcome = "success", account_id = ?id);
563 } else {
564 debug!(event = "db_account_delete_missing", outcome = "success", account_id = ?id);
565 }
566 Ok(deleted)
567 }
568
569 #[must_use]
572 pub fn to_json(&self, base_url: &str) -> Value {
573 let mut object = serde_json::Map::new();
574 object.insert("status".to_string(), Value::String(self.status.clone()));
575 if !self.contact.is_empty() {
576 object.insert("contact".to_string(), Value::from(self.contact.clone()));
577 }
578 object.insert(
579 "orders".to_string(),
580 Value::String(format!("{base_url}/acct/{}/orders", self.id)),
581 );
582 if let Some(agreed) = self.terms_of_service_agreed {
586 object.insert("termsOfServiceAgreed".to_string(), Value::Bool(agreed));
587 }
588 Value::Object(object)
589 }
590}
591
592pub(crate) fn is_pubkey_conflict(error: &sqlx::Error) -> bool {
607 matches!(error, sqlx::Error::Database(db) if db.is_unique_violation()
608 && db.message().contains("accounts.pubkey"))
609}
610
611#[cfg(test)]
612mod tests {
613
614 #[test]
619 fn needs_touch_yields_to_the_interval_but_never_to_a_changed_address() {
620 let mut account = Account {
621 id: crate::sqlite::id::mint(),
622 profile: "default".to_string(),
623 pubkey: vec![1],
624 contact: vec![],
625 status: "valid".to_string(),
626 created_at: 0,
627 eab_kid: None,
628 terms_of_service_agreed: None,
629 created_ip: None,
630 created_ptr: None,
631 last_seen_at: None,
632 last_seen_ip: None,
633 last_seen_ptr: None,
634 };
635
636 assert!(account.needs_touch(1_000, Some("203.0.113.7")));
638
639 account.last_seen_at = Some(1_000);
640 account.last_seen_ip = Some("203.0.113.7".to_string());
641
642 assert!(!account.needs_touch(1_000, Some("203.0.113.7")));
644 assert!(!account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL - 1, Some("203.0.113.7")));
645 assert!(account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL, Some("203.0.113.7")));
647 assert!(account.needs_touch(1_000, Some("198.51.100.4")));
649 assert!(account.needs_touch(1_000, None));
652
653 assert!(!account.needs_touch(0, Some("203.0.113.7")));
656 }
657
658 #[tokio::test]
662 async fn creation_stamps_the_address_once_and_touch_moves_only_the_last_seen_columns() {
663 let db = Database::connect_in_memory().await.unwrap();
664 let first = ClientContext {
665 ip: Some("203.0.113.7".to_string()),
666 ptr: Some("first.example.com".to_string()),
667 user_agent: Some("certbot".to_string()),
668 request_id: Some("req-1".to_string()),
669 };
670 let (created, is_new) = Account::find_or_create("default", &[42u8], vec![], &first, &db)
671 .await
672 .unwrap();
673 assert!(is_new);
674 assert_eq!(created.created_ip.as_deref(), Some("203.0.113.7"));
675 assert_eq!(created.created_ptr.as_deref(), Some("first.example.com"));
676 assert_eq!(created.last_seen_ip.as_deref(), Some("203.0.113.7"));
679 assert!(created.last_seen_at.is_some());
680
681 let second = ClientContext {
684 ip: Some("198.51.100.4".to_string()),
685 ptr: Some("second.example.com".to_string()),
686 ..ClientContext::default()
687 };
688 let (mut found, is_new) = Account::find_or_create("default", &[42u8], vec![], &second, &db)
689 .await
690 .unwrap();
691 assert!(!is_new);
692 assert_eq!(found.created_ip.as_deref(), Some("203.0.113.7"));
693 assert_eq!(found.created_ptr.as_deref(), Some("first.example.com"));
694
695 found.touch(&second, &db).await.unwrap();
696 assert_eq!(found.last_seen_ip.as_deref(), Some("198.51.100.4"));
698 assert_eq!(found.last_seen_ptr.as_deref(), Some("second.example.com"));
699 let reloaded = Account::find_by_id("default", found.id.to_string().as_str(), &db)
701 .await
702 .unwrap()
703 .unwrap();
704 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
705 assert_eq!(reloaded.last_seen_ip.as_deref(), Some("198.51.100.4"));
706 assert_eq!(
707 reloaded.last_seen_ptr.as_deref(),
708 Some("second.example.com")
709 );
710 assert!(reloaded.last_seen_at >= reloaded.created_at.into());
711
712 let nameless = ClientContext {
715 ip: Some("198.51.100.4".to_string()),
716 ..ClientContext::default()
717 };
718 found.touch(&nameless, &db).await.unwrap();
719 let reloaded = Account::find_by_id("default", found.id.to_string().as_str(), &db)
720 .await
721 .unwrap()
722 .unwrap();
723 assert_eq!(reloaded.last_seen_ptr, None);
724 }
725
726 #[tokio::test]
730 async fn to_json_exposes_none_of_the_traceability_columns() {
731 let db = Database::connect_in_memory().await.unwrap();
732 let client = ClientContext {
733 ip: Some("203.0.113.7".to_string()),
734 ptr: Some("host.example.com".to_string()),
735 ..ClientContext::default()
736 };
737 let (account, _) = Account::find_or_create("default", &[7u8], vec![], &client, &db)
738 .await
739 .unwrap();
740 let json = account.to_json("http://localhost:3000");
741 let object = json.as_object().unwrap();
742 for absent in [
743 "createdIp",
744 "created_ip",
745 "createdPtr",
746 "lastSeenAt",
747 "lastSeenIp",
748 "lastSeenPtr",
749 ] {
750 assert!(!object.contains_key(absent), "{absent} leaked into to_json");
751 }
752 assert!(!json.to_string().contains("203.0.113.7"));
753 }
754
755 use super::*;
756 use std::sync::Arc;
757
758 #[tokio::test]
759 async fn find_or_create_creates_then_returns_existing() {
760 let db = Arc::new(Database::connect_in_memory().await.unwrap());
761 let pubkey = vec![1u8, 2, 3, 4];
762 let contact = vec!["mailto:a@example.com".to_string()];
763
764 let (created, is_new) = Account::find_or_create(
765 "default",
766 &pubkey,
767 contact.clone(),
768 &ClientContext::default(),
769 &db,
770 )
771 .await
772 .unwrap();
773 assert!(is_new);
774 assert_eq!(created.status, "valid");
775 assert_eq!(created.contact, contact);
776
777 let (existing, is_new) =
780 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
781 .await
782 .unwrap();
783 assert!(!is_new);
784 assert_eq!(existing.id, created.id);
785 assert_eq!(existing.contact, contact);
786 }
787
788 #[tokio::test]
789 async fn find_by_id_and_pubkey_round_trip() {
790 let db = Arc::new(Database::connect_in_memory().await.unwrap());
791 let pubkey = vec![9u8; 16];
792
793 let (account, _) =
794 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
795 .await
796 .unwrap();
797
798 let by_id = Account::find_by_id("default", account.id.to_string().as_str(), &db)
799 .await
800 .unwrap()
801 .unwrap();
802 assert_eq!(by_id.pubkey, pubkey);
803
804 let by_key = Account::find_by_pubkey("default", &pubkey, &db)
805 .await
806 .unwrap()
807 .unwrap();
808 assert_eq!(by_key.id, account.id);
809 }
810
811 #[tokio::test]
812 async fn absent_lookups_return_none() {
813 let db = Arc::new(Database::connect_in_memory().await.unwrap());
814
815 assert!(
816 Account::find_by_id("default", "nope", &db)
817 .await
818 .unwrap()
819 .is_none()
820 );
821 assert!(
822 Account::find_by_pubkey("default", &[0u8; 4], &db)
823 .await
824 .unwrap()
825 .is_none()
826 );
827 }
828
829 #[tokio::test]
830 async fn update_contact_persists_and_syncs() {
831 let db = Arc::new(Database::connect_in_memory().await.unwrap());
832 let pubkey = vec![7u8; 8];
833
834 let (mut account, _) = Account::find_or_create(
835 "default",
836 &pubkey,
837 vec!["mailto:old@example.com".to_string()],
838 &ClientContext::default(),
839 &db,
840 )
841 .await
842 .unwrap();
843
844 let new_contact = vec!["mailto:new@example.com".to_string()];
845 account
846 .update_contact(new_contact.clone(), &db)
847 .await
848 .unwrap();
849
850 assert_eq!(account.contact, new_contact);
852 let reloaded = Account::find_by_id("default", account.id.to_string().as_str(), &db)
854 .await
855 .unwrap()
856 .unwrap();
857 assert_eq!(reloaded.contact, new_contact);
858 }
859
860 #[tokio::test]
861 async fn deactivate_persists_and_syncs() {
862 let db = Arc::new(Database::connect_in_memory().await.unwrap());
863 let pubkey = vec![8u8; 8];
864
865 let (mut account, _) =
866 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
867 .await
868 .unwrap();
869 assert_eq!(account.status, "valid");
870
871 account.deactivate(&db).await.unwrap();
872
873 assert_eq!(account.status, "deactivated");
874 let reloaded = Account::find_by_id("default", account.id.to_string().as_str(), &db)
875 .await
876 .unwrap()
877 .unwrap();
878 assert_eq!(reloaded.status, "deactivated");
879 }
880
881 #[tokio::test]
882 async fn update_pubkey_persists_and_syncs() {
883 let db = Arc::new(Database::connect_in_memory().await.unwrap());
884 let (mut account, _) =
885 Account::find_or_create("default", &[9u8; 8], vec![], &ClientContext::default(), &db)
886 .await
887 .unwrap();
888
889 let new_pubkey = vec![10u8; 8];
890 account.update_pubkey(&new_pubkey, &db).await.unwrap();
891
892 assert_eq!(account.pubkey, new_pubkey);
894 let reloaded = Account::find_by_id("default", account.id.to_string().as_str(), &db)
896 .await
897 .unwrap()
898 .unwrap();
899 assert_eq!(reloaded.pubkey, new_pubkey);
900 assert!(
901 Account::find_by_pubkey("default", &new_pubkey, &db)
902 .await
903 .unwrap()
904 .is_some()
905 );
906 }
907
908 #[tokio::test]
913 async fn update_pubkey_to_a_key_owned_by_another_account_is_rejected() {
914 let db = Arc::new(Database::connect_in_memory().await.unwrap());
915 let (_first, _) = Account::find_or_create(
916 "default",
917 &[11u8; 8],
918 vec![],
919 &ClientContext::default(),
920 &db,
921 )
922 .await
923 .unwrap();
924 let (mut second, _) = Account::find_or_create(
925 "default",
926 &[12u8; 8],
927 vec![],
928 &ClientContext::default(),
929 &db,
930 )
931 .await
932 .unwrap();
933
934 let error = second
935 .update_pubkey(&[11u8; 8], &db)
936 .await
937 .expect_err("taking another account's key must not succeed");
938 assert!(
944 is_pubkey_conflict(&error),
945 "the unique violation must be recognisable as a pubkey conflict: {error}"
946 );
947 }
948
949 #[tokio::test]
950 async fn set_eab_kid_persists_and_syncs() {
951 let db = Arc::new(Database::connect_in_memory().await.unwrap());
952 let kid = crate::sqlite::id::mint();
953 let (mut account, _) =
954 Account::find_or_create("default", &[5u8], vec![], &ClientContext::default(), &db)
955 .await
956 .unwrap();
957 assert!(account.eab_kid.is_none());
958
959 account.set_eab_kid(kid, &db).await.unwrap();
960 assert_eq!(account.eab_kid, Some(kid));
961
962 let reloaded = Account::find_by_id("default", account.id.to_string().as_str(), &db)
963 .await
964 .unwrap()
965 .unwrap();
966 assert_eq!(reloaded.eab_kid, Some(kid));
967 }
968
969 #[tokio::test]
970 async fn delete_removes_the_row_and_reports_true() {
971 let db = Arc::new(Database::connect_in_memory().await.unwrap());
972 let (account, _) =
973 Account::find_or_create("default", &[3u8], vec![], &ClientContext::default(), &db)
974 .await
975 .unwrap();
976
977 assert!(
978 Account::delete(account.id.to_string().as_str(), &db)
979 .await
980 .unwrap()
981 );
982 assert!(
983 Account::find_by_id("default", account.id.to_string().as_str(), &db)
984 .await
985 .unwrap()
986 .is_none()
987 );
988 }
989
990 #[tokio::test]
991 async fn delete_of_unknown_id_reports_false() {
992 let db = Arc::new(Database::connect_in_memory().await.unwrap());
993 assert!(!Account::delete("nope", &db).await.unwrap());
994 }
995
996 #[tokio::test]
997 async fn delete_cascades_to_the_accounts_orders() {
998 let db = Arc::new(Database::connect_in_memory().await.unwrap());
999 let (account, _) =
1000 Account::find_or_create("default", &[4u8], vec![], &ClientContext::default(), &db)
1001 .await
1002 .unwrap();
1003
1004 crate::sqlite::order::Order::create(
1005 "default",
1006 account.id,
1007 vec![],
1008 now_secs() + 3600,
1009 None,
1010 None,
1011 &db,
1012 )
1013 .await
1014 .unwrap();
1015
1016 Account::delete(account.id.to_string().as_str(), &db)
1017 .await
1018 .unwrap();
1019
1020 let remaining = crate::sqlite::order::Order::find_by_account(account.id, &db)
1021 .await
1022 .unwrap();
1023 assert!(remaining.is_empty());
1024 }
1025
1026 async fn seed_accounts(db: &Arc<Database>, profile: &str, count: usize) -> Vec<String> {
1029 let base = now_secs();
1030 let mut ids = Vec::new();
1031 for index in 0..count {
1032 let (account, _) = Account::find_or_create(
1033 profile,
1034 &[profile.len() as u8, index as u8],
1035 vec![],
1036 &ClientContext::default(),
1037 db,
1038 )
1039 .await
1040 .unwrap();
1041 sqlx::query("UPDATE accounts SET created_at = ? WHERE id = ?;")
1042 .bind(base - index as i64)
1043 .bind(account.id)
1044 .execute(&db.pool)
1045 .await
1046 .unwrap();
1047 ids.push(account.id);
1048 }
1049 ids.into_iter().map(|v| v.to_string()).collect()
1050 }
1051
1052 #[tokio::test]
1053 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1054 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1055 let ids = seed_accounts(&db, "default", 5).await;
1056
1057 let (page, total) = Account::search(None, 2, 0, &db).await.unwrap();
1058 assert_eq!(total, 5, "the total must ignore the page window");
1059 assert_eq!(
1060 page.iter().map(|a| a.id.to_string()).collect::<Vec<_>>(),
1061 ids[..2]
1062 );
1063
1064 let (second, _) = Account::search(None, 2, 2, &db).await.unwrap();
1065 assert_eq!(
1066 second.iter().map(|a| a.id.to_string()).collect::<Vec<_>>(),
1067 ids[2..4]
1068 );
1069
1070 let (beyond, total) = Account::search(None, 2, 99, &db).await.unwrap();
1072 assert!(beyond.is_empty());
1073 assert_eq!(total, 5);
1074 }
1075
1076 #[tokio::test]
1077 async fn search_scopes_by_profile_and_counts_only_that_profile() {
1078 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1079 seed_accounts(&db, "default", 2).await;
1080 seed_accounts(&db, "other", 3).await;
1081
1082 let (rows, total) = Account::search(Some("other"), 50, 0, &db).await.unwrap();
1083 assert_eq!(total, 3);
1084 assert_eq!(rows.len(), 3);
1085 assert!(rows.iter().all(|a| a.profile == "other"));
1086
1087 let (_, total) = Account::search(None, 50, 0, &db).await.unwrap();
1088 assert_eq!(total, 5, "no profile means every endpoint");
1089
1090 let (rows, total) = Account::search(Some("nope"), 50, 0, &db).await.unwrap();
1091 assert!(rows.is_empty());
1092 assert_eq!(total, 0);
1093 }
1094
1095 #[tokio::test]
1096 async fn search_on_an_empty_table_is_empty_rather_than_an_error() {
1097 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1098 let (rows, total) = Account::search(None, 50, 0, &db).await.unwrap();
1099 assert!(rows.is_empty());
1100 assert_eq!(total, 0);
1101 }
1102
1103 #[tokio::test]
1120 async fn concurrent_find_or_create_for_one_key_yields_one_account() {
1121 let file =
1122 std::env::temp_dir().join(format!("acme-proxy-test-{}.db", uuid::Uuid::now_v7()));
1123 let url = format!("sqlite://{}", file.display());
1124 let db = Arc::new(Database::connect(&url).await.unwrap());
1125
1126 const RACERS: usize = 8;
1127 let barrier = Arc::new(tokio::sync::Barrier::new(RACERS));
1128 let mut tasks = Vec::with_capacity(RACERS);
1129 for _ in 0..RACERS {
1130 let db = db.clone();
1131 let barrier = barrier.clone();
1132 tasks.push(tokio::spawn(async move {
1133 barrier.wait().await;
1134 Account::find_or_create(
1135 "default",
1136 &[7u8; 32],
1137 vec![],
1138 &ClientContext::default(),
1139 &db,
1140 )
1141 .await
1142 }));
1143 }
1144
1145 let mut ids = Vec::with_capacity(RACERS);
1146 let mut created = 0;
1147 for task in tasks {
1148 let (account, is_new) = task
1149 .await
1150 .unwrap()
1151 .expect("losing the insert race is not an error");
1152 if is_new {
1153 created += 1;
1154 }
1155 ids.push(account.id);
1156 }
1157
1158 assert_eq!(created, 1, "exactly one caller may create the account");
1159 assert!(
1160 ids.windows(2).all(|pair| pair[0] == pair[1]),
1161 "every caller must be handed the same account: {ids:?}"
1162 );
1163
1164 db.pool.close().await;
1165 for suffix in ["", "-wal", "-shm"] {
1166 let _ = std::fs::remove_file(format!("{}{suffix}", file.display()));
1167 }
1168 }
1169}