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: String,
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<String>,
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 row = sqlx::query(concat!(
173 "SELECT ",
174 columns!(),
175 " FROM accounts WHERE profile = ? AND id = ?;"
176 ))
177 .bind(profile)
178 .bind(id)
179 .fetch_optional(&database.pool)
180 .await?;
181
182 let result = row.map(Account::from_row).transpose()?;
183 if let Some(ref account) = result {
184 debug!(event = "db_account_found_by_id", outcome = "success", account_id = %account.id);
185 } else {
186 debug!(event = "db_account_not_found_by_id", outcome = "failure", account_id = %id);
187 }
188 Ok(result)
189 }
190
191 #[tracing::instrument(name = "Account::find_or_create", skip(pubkey, client, database))]
201 pub async fn find_or_create(
202 profile: &str,
203 pubkey: &[u8],
204 contact: Vec<String>,
205 client: &ClientContext,
206 database: &Database,
207 ) -> Result<(Account, bool), sqlx::Error> {
208 debug!(event = "db_account_find_or_create_started", outcome = "progress", profile = %profile, pubkey_fp = %pubkey_fingerprint(pubkey));
209 if let Some(account) = Account::find_by_pubkey(profile, pubkey, database).await? {
210 debug!(event = "db_account_found_existing", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
211 return Ok((account, false));
212 }
213
214 let account = Account {
215 id: Uuid::new_v4().to_string(),
216 profile: profile.to_string(),
217 pubkey: pubkey.to_vec(),
218 contact,
219 status: "valid".to_string(),
220 created_at: now_secs(),
221 eab_kid: None,
222 terms_of_service_agreed: None,
223 created_ip: client.ip.clone(),
224 created_ptr: client.ptr.clone(),
225 last_seen_at: Some(now_secs()),
231 last_seen_ip: client.ip.clone(),
232 last_seen_ptr: client.ptr.clone(),
233 };
234
235 let contact_json = Value::from(account.contact.clone()).to_string();
237
238 debug!(event = "db_account_create_started", outcome = "progress", account_id = %account.id);
239 let inserted = sqlx::query(
240 "INSERT INTO accounts (id, profile, pubkey, contact, status, created_at, created_ip, \
241 created_ptr, last_seen_at, last_seen_ip, last_seen_ptr) \
242 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
243 )
244 .bind(&account.id)
245 .bind(&account.profile)
246 .bind(&account.pubkey)
247 .bind(contact_json)
248 .bind(&account.status)
249 .bind(account.created_at)
250 .bind(&account.created_ip)
251 .bind(&account.created_ptr)
252 .bind(account.last_seen_at)
253 .bind(&account.last_seen_ip)
254 .bind(&account.last_seen_ptr)
255 .execute(&database.pool)
256 .await;
257
258 if let Err(error) = inserted {
272 if is_pubkey_conflict(&error)
273 && let Some(existing) = Account::find_by_pubkey(profile, pubkey, database).await?
274 {
275 debug!(event = "db_account_create_lost_race", outcome = "advisory", account_id = %existing.id, pubkey_fp = %pubkey_fingerprint(pubkey));
276 return Ok((existing, false));
277 }
278 return Err(error);
279 }
280
281 debug!(event = "db_account_created", outcome = "success", account_id = %account.id, pubkey_fp = %pubkey_fingerprint(pubkey));
282 Ok((account, true))
283 }
284
285 #[must_use]
301 pub fn needs_touch(&self, now: i64, ip: Option<&str>) -> bool {
302 match self.last_seen_at {
303 None => true,
304 Some(last) => {
305 now.saturating_sub(last) >= ACCOUNT_TOUCH_INTERVAL
306 || self.last_seen_ip.as_deref() != ip
307 }
308 }
309 }
310
311 #[tracing::instrument(name = "Account::touch", skip(self, client, database), fields(account_id = %self.id))]
319 pub async fn touch(
320 &mut self,
321 client: &ClientContext,
322 database: &Database,
323 ) -> Result<(), sqlx::Error> {
324 let now = now_secs();
325 sqlx::query(
326 "UPDATE accounts SET last_seen_at = ?, last_seen_ip = ?, last_seen_ptr = ? \
327 WHERE id = ?;",
328 )
329 .bind(now)
330 .bind(&client.ip)
331 .bind(&client.ptr)
332 .bind(&self.id)
333 .execute(&database.pool)
334 .await?;
335
336 self.last_seen_at = Some(now);
337 self.last_seen_ip = client.ip.clone();
338 self.last_seen_ptr = client.ptr.clone();
339 debug!(event = "db_account_touched", outcome = "success", account_id = %self.id);
340 Ok(())
341 }
342
343 #[tracing::instrument(name = "Account::update_contact", skip(self, database), fields(account_id = %self.id))]
347 pub async fn update_contact(
348 &mut self,
349 contact: Vec<String>,
350 database: &Database,
351 ) -> Result<(), sqlx::Error> {
352 debug!(event = "db_account_contact_update_started", outcome = "progress", account_id = %self.id);
353 let contact_json = Value::from(contact.clone()).to_string();
355
356 sqlx::query("UPDATE accounts SET contact = ? WHERE id = ?;")
357 .bind(contact_json)
358 .bind(&self.id)
359 .execute(&database.pool)
360 .await?;
361
362 self.contact = contact;
363 debug!(event = "db_account_contact_updated", outcome = "success", account_id = %self.id);
364 Ok(())
365 }
366
367 #[tracing::instrument(name = "Account::deactivate", skip(self, database), fields(account_id = %self.id))]
370 pub async fn deactivate(&mut self, database: &Database) -> Result<(), sqlx::Error> {
371 debug!(event = "db_account_deactivation_started", outcome = "progress", account_id = %self.id);
372 sqlx::query("UPDATE accounts SET status = 'deactivated' WHERE id = ?;")
373 .bind(&self.id)
374 .execute(&database.pool)
375 .await?;
376
377 self.status = "deactivated".to_string();
378 debug!(event = "db_account_deactivated", outcome = "success", account_id = %self.id);
379 Ok(())
380 }
381
382 pub async fn update_pubkey(
388 &mut self,
389 pubkey: &[u8],
390 database: &Database,
391 ) -> Result<(), sqlx::Error> {
392 debug!(event = "db_account_pubkey_update_started", outcome = "progress", account_id = ?self.id);
393 sqlx::query("UPDATE accounts SET pubkey = ? WHERE id = ?;")
394 .bind(pubkey)
395 .bind(&self.id)
396 .execute(&database.pool)
397 .await?;
398
399 self.pubkey = pubkey.to_vec();
400 info!(event = "db_account_pubkey_updated", outcome = "success", account_id = ?self.id);
401 Ok(())
402 }
403
404 pub async fn set_eab_kid(
411 &mut self,
412 eab_kid: &str,
413 database: &Database,
414 ) -> Result<(), sqlx::Error> {
415 debug!(event = "db_account_eab_kid_set_started", outcome = "progress", account_id = ?self.id, eab_kid = ?eab_kid);
416 sqlx::query("UPDATE accounts SET eab_kid = ? WHERE id = ?;")
417 .bind(eab_kid)
418 .bind(&self.id)
419 .execute(&database.pool)
420 .await?;
421
422 self.eab_kid = Some(eab_kid.to_string());
423 info!(event = "db_account_eab_kid_set", outcome = "success", account_id = ?self.id, eab_kid = ?eab_kid);
424 Ok(())
425 }
426
427 pub async fn set_terms_agreed(&mut self, database: &Database) -> Result<(), sqlx::Error> {
436 debug!(event = "db_account_terms_agreed_started", outcome = "progress", account_id = ?self.id);
437 sqlx::query("UPDATE accounts SET terms_of_service_agreed = 1 WHERE id = ?;")
438 .bind(&self.id)
439 .execute(&database.pool)
440 .await?;
441
442 self.terms_of_service_agreed = Some(true);
443 info!(event = "db_account_terms_agreed", outcome = "success", account_id = ?self.id);
444 Ok(())
445 }
446
447 pub async fn find_any_by_id(
454 id: &str,
455 database: &Database,
456 ) -> Result<Option<Account>, sqlx::Error> {
457 debug!(event = "db_account_find_any_by_id_started", outcome = "progress", account_id = %id);
458 let row = sqlx::query(concat!(
459 "SELECT ",
460 columns!(),
461 " FROM accounts WHERE id = ?;"
462 ))
463 .bind(id)
464 .fetch_optional(&database.pool)
465 .await?;
466
467 row.map(Account::from_row).transpose()
468 }
469
470 pub async fn search(
485 profile: Option<&str>,
486 limit: i64,
487 offset: i64,
488 database: &Database,
489 ) -> Result<(Vec<Account>, i64), sqlx::Error> {
490 debug!(event = "db_account_search_started", outcome = "progress", profile = ?profile, limit = limit, offset = offset);
491
492 let (rows, total) = match profile {
496 Some(profile) => {
497 let rows = sqlx::query(concat!(
498 "SELECT ",
499 columns!(),
500 " FROM accounts WHERE profile = ? \
501 ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
502 ))
503 .bind(profile)
504 .bind(limit)
505 .bind(offset)
506 .fetch_all(&database.pool)
507 .await?;
508 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts WHERE profile = ?;")
509 .bind(profile)
510 .fetch_one(&database.pool)
511 .await?
512 .try_get(0)?;
513 (rows, total)
514 }
515 None => {
516 let rows = sqlx::query(concat!(
517 "SELECT ",
518 columns!(),
519 " FROM accounts ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?;"
520 ))
521 .bind(limit)
522 .bind(offset)
523 .fetch_all(&database.pool)
524 .await?;
525 let total: i64 = sqlx::query("SELECT COUNT(*) FROM accounts;")
526 .fetch_one(&database.pool)
527 .await?
528 .try_get(0)?;
529 (rows, total)
530 }
531 };
532
533 let accounts = rows
534 .into_iter()
535 .map(Account::from_row)
536 .collect::<Result<_, _>>()?;
537 Ok((accounts, total))
538 }
539
540 pub async fn delete(id: &str, database: &Database) -> Result<bool, sqlx::Error> {
545 debug!(event = "db_account_delete_started", outcome = "progress", account_id = ?id);
546 let result = sqlx::query("DELETE FROM accounts WHERE id = ?;")
547 .bind(id)
548 .execute(&database.pool)
549 .await?;
550
551 let deleted = result.rows_affected() > 0;
552 if deleted {
553 info!(event = "db_account_deleted", outcome = "success", account_id = ?id);
554 } else {
555 debug!(event = "db_account_delete_missing", outcome = "success", account_id = ?id);
556 }
557 Ok(deleted)
558 }
559
560 #[must_use]
563 pub fn to_json(&self, base_url: &str) -> Value {
564 let mut object = serde_json::Map::new();
565 object.insert("status".to_string(), Value::String(self.status.clone()));
566 if !self.contact.is_empty() {
567 object.insert("contact".to_string(), Value::from(self.contact.clone()));
568 }
569 object.insert(
570 "orders".to_string(),
571 Value::String(format!("{base_url}/acct/{}/orders", self.id)),
572 );
573 if let Some(agreed) = self.terms_of_service_agreed {
577 object.insert("termsOfServiceAgreed".to_string(), Value::Bool(agreed));
578 }
579 Value::Object(object)
580 }
581}
582
583pub(crate) fn is_pubkey_conflict(error: &sqlx::Error) -> bool {
598 matches!(error, sqlx::Error::Database(db) if db.is_unique_violation()
599 && db.message().contains("accounts.pubkey"))
600}
601
602#[cfg(test)]
603mod tests {
604
605 #[test]
610 fn needs_touch_yields_to_the_interval_but_never_to_a_changed_address() {
611 let mut account = Account {
612 id: "a".to_string(),
613 profile: "default".to_string(),
614 pubkey: vec![1],
615 contact: vec![],
616 status: "valid".to_string(),
617 created_at: 0,
618 eab_kid: None,
619 terms_of_service_agreed: None,
620 created_ip: None,
621 created_ptr: None,
622 last_seen_at: None,
623 last_seen_ip: None,
624 last_seen_ptr: None,
625 };
626
627 assert!(account.needs_touch(1_000, Some("203.0.113.7")));
629
630 account.last_seen_at = Some(1_000);
631 account.last_seen_ip = Some("203.0.113.7".to_string());
632
633 assert!(!account.needs_touch(1_000, Some("203.0.113.7")));
635 assert!(!account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL - 1, Some("203.0.113.7")));
636 assert!(account.needs_touch(1_000 + ACCOUNT_TOUCH_INTERVAL, Some("203.0.113.7")));
638 assert!(account.needs_touch(1_000, Some("198.51.100.4")));
640 assert!(account.needs_touch(1_000, None));
643
644 assert!(!account.needs_touch(0, Some("203.0.113.7")));
647 }
648
649 #[tokio::test]
653 async fn creation_stamps_the_address_once_and_touch_moves_only_the_last_seen_columns() {
654 let db = Database::connect_in_memory().await.unwrap();
655 let first = ClientContext {
656 ip: Some("203.0.113.7".to_string()),
657 ptr: Some("first.example.com".to_string()),
658 user_agent: Some("certbot".to_string()),
659 request_id: Some("req-1".to_string()),
660 };
661 let (created, is_new) = Account::find_or_create("default", &[42u8], vec![], &first, &db)
662 .await
663 .unwrap();
664 assert!(is_new);
665 assert_eq!(created.created_ip.as_deref(), Some("203.0.113.7"));
666 assert_eq!(created.created_ptr.as_deref(), Some("first.example.com"));
667 assert_eq!(created.last_seen_ip.as_deref(), Some("203.0.113.7"));
670 assert!(created.last_seen_at.is_some());
671
672 let second = ClientContext {
675 ip: Some("198.51.100.4".to_string()),
676 ptr: Some("second.example.com".to_string()),
677 ..ClientContext::default()
678 };
679 let (mut found, is_new) = Account::find_or_create("default", &[42u8], vec![], &second, &db)
680 .await
681 .unwrap();
682 assert!(!is_new);
683 assert_eq!(found.created_ip.as_deref(), Some("203.0.113.7"));
684 assert_eq!(found.created_ptr.as_deref(), Some("first.example.com"));
685
686 found.touch(&second, &db).await.unwrap();
687 assert_eq!(found.last_seen_ip.as_deref(), Some("198.51.100.4"));
689 assert_eq!(found.last_seen_ptr.as_deref(), Some("second.example.com"));
690 let reloaded = Account::find_by_id("default", &found.id, &db)
692 .await
693 .unwrap()
694 .unwrap();
695 assert_eq!(reloaded.created_ip.as_deref(), Some("203.0.113.7"));
696 assert_eq!(reloaded.last_seen_ip.as_deref(), Some("198.51.100.4"));
697 assert_eq!(
698 reloaded.last_seen_ptr.as_deref(),
699 Some("second.example.com")
700 );
701 assert!(reloaded.last_seen_at >= reloaded.created_at.into());
702
703 let nameless = ClientContext {
706 ip: Some("198.51.100.4".to_string()),
707 ..ClientContext::default()
708 };
709 found.touch(&nameless, &db).await.unwrap();
710 let reloaded = Account::find_by_id("default", &found.id, &db)
711 .await
712 .unwrap()
713 .unwrap();
714 assert_eq!(reloaded.last_seen_ptr, None);
715 }
716
717 #[tokio::test]
721 async fn to_json_exposes_none_of_the_traceability_columns() {
722 let db = Database::connect_in_memory().await.unwrap();
723 let client = ClientContext {
724 ip: Some("203.0.113.7".to_string()),
725 ptr: Some("host.example.com".to_string()),
726 ..ClientContext::default()
727 };
728 let (account, _) = Account::find_or_create("default", &[7u8], vec![], &client, &db)
729 .await
730 .unwrap();
731 let json = account.to_json("http://localhost:3000");
732 let object = json.as_object().unwrap();
733 for absent in [
734 "createdIp",
735 "created_ip",
736 "createdPtr",
737 "lastSeenAt",
738 "lastSeenIp",
739 "lastSeenPtr",
740 ] {
741 assert!(!object.contains_key(absent), "{absent} leaked into to_json");
742 }
743 assert!(!json.to_string().contains("203.0.113.7"));
744 }
745
746 use super::*;
747 use std::sync::Arc;
748
749 #[tokio::test]
750 async fn find_or_create_creates_then_returns_existing() {
751 let db = Arc::new(Database::connect_in_memory().await.unwrap());
752 let pubkey = vec![1u8, 2, 3, 4];
753 let contact = vec!["mailto:a@example.com".to_string()];
754
755 let (created, is_new) = Account::find_or_create(
756 "default",
757 &pubkey,
758 contact.clone(),
759 &ClientContext::default(),
760 &db,
761 )
762 .await
763 .unwrap();
764 assert!(is_new);
765 assert_eq!(created.status, "valid");
766 assert_eq!(created.contact, contact);
767
768 let (existing, is_new) =
771 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
772 .await
773 .unwrap();
774 assert!(!is_new);
775 assert_eq!(existing.id, created.id);
776 assert_eq!(existing.contact, contact);
777 }
778
779 #[tokio::test]
780 async fn find_by_id_and_pubkey_round_trip() {
781 let db = Arc::new(Database::connect_in_memory().await.unwrap());
782 let pubkey = vec![9u8; 16];
783
784 let (account, _) =
785 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
786 .await
787 .unwrap();
788
789 let by_id = Account::find_by_id("default", &account.id, &db)
790 .await
791 .unwrap()
792 .unwrap();
793 assert_eq!(by_id.pubkey, pubkey);
794
795 let by_key = Account::find_by_pubkey("default", &pubkey, &db)
796 .await
797 .unwrap()
798 .unwrap();
799 assert_eq!(by_key.id, account.id);
800 }
801
802 #[tokio::test]
803 async fn absent_lookups_return_none() {
804 let db = Arc::new(Database::connect_in_memory().await.unwrap());
805
806 assert!(
807 Account::find_by_id("default", "nope", &db)
808 .await
809 .unwrap()
810 .is_none()
811 );
812 assert!(
813 Account::find_by_pubkey("default", &[0u8; 4], &db)
814 .await
815 .unwrap()
816 .is_none()
817 );
818 }
819
820 #[tokio::test]
821 async fn update_contact_persists_and_syncs() {
822 let db = Arc::new(Database::connect_in_memory().await.unwrap());
823 let pubkey = vec![7u8; 8];
824
825 let (mut account, _) = Account::find_or_create(
826 "default",
827 &pubkey,
828 vec!["mailto:old@example.com".to_string()],
829 &ClientContext::default(),
830 &db,
831 )
832 .await
833 .unwrap();
834
835 let new_contact = vec!["mailto:new@example.com".to_string()];
836 account
837 .update_contact(new_contact.clone(), &db)
838 .await
839 .unwrap();
840
841 assert_eq!(account.contact, new_contact);
843 let reloaded = Account::find_by_id("default", &account.id, &db)
845 .await
846 .unwrap()
847 .unwrap();
848 assert_eq!(reloaded.contact, new_contact);
849 }
850
851 #[tokio::test]
852 async fn deactivate_persists_and_syncs() {
853 let db = Arc::new(Database::connect_in_memory().await.unwrap());
854 let pubkey = vec![8u8; 8];
855
856 let (mut account, _) =
857 Account::find_or_create("default", &pubkey, vec![], &ClientContext::default(), &db)
858 .await
859 .unwrap();
860 assert_eq!(account.status, "valid");
861
862 account.deactivate(&db).await.unwrap();
863
864 assert_eq!(account.status, "deactivated");
865 let reloaded = Account::find_by_id("default", &account.id, &db)
866 .await
867 .unwrap()
868 .unwrap();
869 assert_eq!(reloaded.status, "deactivated");
870 }
871
872 #[tokio::test]
873 async fn update_pubkey_persists_and_syncs() {
874 let db = Arc::new(Database::connect_in_memory().await.unwrap());
875 let (mut account, _) =
876 Account::find_or_create("default", &[9u8; 8], vec![], &ClientContext::default(), &db)
877 .await
878 .unwrap();
879
880 let new_pubkey = vec![10u8; 8];
881 account.update_pubkey(&new_pubkey, &db).await.unwrap();
882
883 assert_eq!(account.pubkey, new_pubkey);
885 let reloaded = Account::find_by_id("default", &account.id, &db)
887 .await
888 .unwrap()
889 .unwrap();
890 assert_eq!(reloaded.pubkey, new_pubkey);
891 assert!(
892 Account::find_by_pubkey("default", &new_pubkey, &db)
893 .await
894 .unwrap()
895 .is_some()
896 );
897 }
898
899 #[tokio::test]
904 async fn update_pubkey_to_a_key_owned_by_another_account_is_rejected() {
905 let db = Arc::new(Database::connect_in_memory().await.unwrap());
906 let (_first, _) = Account::find_or_create(
907 "default",
908 &[11u8; 8],
909 vec![],
910 &ClientContext::default(),
911 &db,
912 )
913 .await
914 .unwrap();
915 let (mut second, _) = Account::find_or_create(
916 "default",
917 &[12u8; 8],
918 vec![],
919 &ClientContext::default(),
920 &db,
921 )
922 .await
923 .unwrap();
924
925 let error = second
926 .update_pubkey(&[11u8; 8], &db)
927 .await
928 .expect_err("taking another account's key must not succeed");
929 assert!(
935 is_pubkey_conflict(&error),
936 "the unique violation must be recognisable as a pubkey conflict: {error}"
937 );
938 }
939
940 #[tokio::test]
941 async fn set_eab_kid_persists_and_syncs() {
942 let db = Arc::new(Database::connect_in_memory().await.unwrap());
943 let (mut account, _) =
944 Account::find_or_create("default", &[5u8], vec![], &ClientContext::default(), &db)
945 .await
946 .unwrap();
947 assert!(account.eab_kid.is_none());
948
949 account.set_eab_kid("some-kid", &db).await.unwrap();
950 assert_eq!(account.eab_kid.as_deref(), Some("some-kid"));
951
952 let reloaded = Account::find_by_id("default", &account.id, &db)
953 .await
954 .unwrap()
955 .unwrap();
956 assert_eq!(reloaded.eab_kid.as_deref(), Some("some-kid"));
957 }
958
959 #[tokio::test]
960 async fn delete_removes_the_row_and_reports_true() {
961 let db = Arc::new(Database::connect_in_memory().await.unwrap());
962 let (account, _) =
963 Account::find_or_create("default", &[3u8], vec![], &ClientContext::default(), &db)
964 .await
965 .unwrap();
966
967 assert!(Account::delete(&account.id, &db).await.unwrap());
968 assert!(
969 Account::find_by_id("default", &account.id, &db)
970 .await
971 .unwrap()
972 .is_none()
973 );
974 }
975
976 #[tokio::test]
977 async fn delete_of_unknown_id_reports_false() {
978 let db = Arc::new(Database::connect_in_memory().await.unwrap());
979 assert!(!Account::delete("nope", &db).await.unwrap());
980 }
981
982 #[tokio::test]
983 async fn delete_cascades_to_the_accounts_orders() {
984 let db = Arc::new(Database::connect_in_memory().await.unwrap());
985 let (account, _) =
986 Account::find_or_create("default", &[4u8], vec![], &ClientContext::default(), &db)
987 .await
988 .unwrap();
989
990 crate::sqlite::order::Order::create(
991 "default",
992 &account.id,
993 vec![],
994 now_secs() + 3600,
995 None,
996 None,
997 &db,
998 )
999 .await
1000 .unwrap();
1001
1002 Account::delete(&account.id, &db).await.unwrap();
1003
1004 let remaining = crate::sqlite::order::Order::find_by_account(&account.id, &db)
1005 .await
1006 .unwrap();
1007 assert!(remaining.is_empty());
1008 }
1009
1010 async fn seed_accounts(db: &Arc<Database>, profile: &str, count: usize) -> Vec<String> {
1013 let base = now_secs();
1014 let mut ids = Vec::new();
1015 for index in 0..count {
1016 let (account, _) = Account::find_or_create(
1017 profile,
1018 &[profile.len() as u8, index as u8],
1019 vec![],
1020 &ClientContext::default(),
1021 db,
1022 )
1023 .await
1024 .unwrap();
1025 sqlx::query("UPDATE accounts SET created_at = ? WHERE id = ?;")
1026 .bind(base - index as i64)
1027 .bind(&account.id)
1028 .execute(&db.pool)
1029 .await
1030 .unwrap();
1031 ids.push(account.id);
1032 }
1033 ids
1034 }
1035
1036 #[tokio::test]
1037 async fn search_pages_newest_first_and_reports_the_unpaged_total() {
1038 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1039 let ids = seed_accounts(&db, "default", 5).await;
1040
1041 let (page, total) = Account::search(None, 2, 0, &db).await.unwrap();
1042 assert_eq!(total, 5, "the total must ignore the page window");
1043 assert_eq!(
1044 page.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1045 ids[..2]
1046 );
1047
1048 let (second, _) = Account::search(None, 2, 2, &db).await.unwrap();
1049 assert_eq!(
1050 second.iter().map(|a| a.id.clone()).collect::<Vec<_>>(),
1051 ids[2..4]
1052 );
1053
1054 let (beyond, total) = Account::search(None, 2, 99, &db).await.unwrap();
1056 assert!(beyond.is_empty());
1057 assert_eq!(total, 5);
1058 }
1059
1060 #[tokio::test]
1061 async fn search_scopes_by_profile_and_counts_only_that_profile() {
1062 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1063 seed_accounts(&db, "default", 2).await;
1064 seed_accounts(&db, "other", 3).await;
1065
1066 let (rows, total) = Account::search(Some("other"), 50, 0, &db).await.unwrap();
1067 assert_eq!(total, 3);
1068 assert_eq!(rows.len(), 3);
1069 assert!(rows.iter().all(|a| a.profile == "other"));
1070
1071 let (_, total) = Account::search(None, 50, 0, &db).await.unwrap();
1072 assert_eq!(total, 5, "no profile means every endpoint");
1073
1074 let (rows, total) = Account::search(Some("nope"), 50, 0, &db).await.unwrap();
1075 assert!(rows.is_empty());
1076 assert_eq!(total, 0);
1077 }
1078
1079 #[tokio::test]
1080 async fn search_on_an_empty_table_is_empty_rather_than_an_error() {
1081 let db = Arc::new(Database::connect_in_memory().await.unwrap());
1082 let (rows, total) = Account::search(None, 50, 0, &db).await.unwrap();
1083 assert!(rows.is_empty());
1084 assert_eq!(total, 0);
1085 }
1086
1087 #[tokio::test]
1104 async fn concurrent_find_or_create_for_one_key_yields_one_account() {
1105 let file = std::env::temp_dir().join(format!("acme-proxy-test-{}.db", Uuid::new_v4()));
1106 let url = format!("sqlite://{}", file.display());
1107 let db = Arc::new(Database::connect(&url).await.unwrap());
1108
1109 const RACERS: usize = 8;
1110 let barrier = Arc::new(tokio::sync::Barrier::new(RACERS));
1111 let mut tasks = Vec::with_capacity(RACERS);
1112 for _ in 0..RACERS {
1113 let db = db.clone();
1114 let barrier = barrier.clone();
1115 tasks.push(tokio::spawn(async move {
1116 barrier.wait().await;
1117 Account::find_or_create(
1118 "default",
1119 &[7u8; 32],
1120 vec![],
1121 &ClientContext::default(),
1122 &db,
1123 )
1124 .await
1125 }));
1126 }
1127
1128 let mut ids = Vec::with_capacity(RACERS);
1129 let mut created = 0;
1130 for task in tasks {
1131 let (account, is_new) = task
1132 .await
1133 .unwrap()
1134 .expect("losing the insert race is not an error");
1135 if is_new {
1136 created += 1;
1137 }
1138 ids.push(account.id);
1139 }
1140
1141 assert_eq!(created, 1, "exactly one caller may create the account");
1142 assert!(
1143 ids.windows(2).all(|pair| pair[0] == pair[1]),
1144 "every caller must be handed the same account: {ids:?}"
1145 );
1146
1147 db.pool.close().await;
1148 for suffix in ["", "-wal", "-shm"] {
1149 let _ = std::fs::remove_file(format!("{}{suffix}", file.display()));
1150 }
1151 }
1152}