//! Integration tests for SqliteEmailAccountRepository. mod common; use chrono::{Duration, Utc}; use goingson_core::{EmailAccountRepository, EmailAuthType}; use goingson_db_sqlite::SqliteEmailAccountRepository; #[tokio::test] async fn test_create_password_account_and_get() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "Work Email", "work@example.com", "imap.example.com", 993, "smtp.example.com", 587, "work@example.com", "secret123", true, Some("Archive"), ) .await .expect("Failed to create account"); assert_eq!(created.account_name, "Work Email"); assert_eq!(created.email_address, "work@example.com"); assert_eq!(created.imap_server, "imap.example.com"); assert_eq!(created.imap_port, 993); assert_eq!(created.smtp_server, "smtp.example.com"); assert_eq!(created.smtp_port, 587); assert_eq!(created.username, "work@example.com"); assert_eq!(created.password, "secret123"); assert!(created.use_tls); assert_eq!(created.archive_folder_name, Some("Archive".to_string())); assert_eq!(created.auth_type, EmailAuthType::Password); assert!(created.last_sync_at.is_none()); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get account"); assert!(fetched.is_some()); let fetched = fetched.unwrap(); assert_eq!(fetched.id, created.id); assert_eq!(fetched.account_name, "Work Email"); assert_eq!(fetched.auth_type, EmailAuthType::Password); } #[tokio::test] async fn test_create_oauth_fastmail_account() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let expires = Utc::now() + Duration::hours(1); let created = repo .create_oauth( user_id, "Fastmail", "me@fastmail.com", "access-token-abc", "refresh-token-xyz", expires, "https://api.fastmail.com/jmap/session", "acct-123", ) .await .expect("Failed to create OAuth account"); assert_eq!(created.account_name, "Fastmail"); assert_eq!(created.email_address, "me@fastmail.com"); assert_eq!(created.auth_type, EmailAuthType::OAuth2Fastmail); assert_eq!(created.oauth2_access_token, Some("access-token-abc".to_string())); assert_eq!(created.oauth2_refresh_token, Some("refresh-token-xyz".to_string())); assert!(created.oauth2_token_expires_at.is_some()); assert_eq!(created.jmap_session_url, Some("https://api.fastmail.com/jmap/session".to_string())); assert_eq!(created.jmap_account_id, Some("acct-123".to_string())); } #[tokio::test] async fn test_create_oauth_imap_gmail() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let expires = Utc::now() + Duration::hours(1); let created = repo .create_oauth_imap( user_id, "Gmail", "me@gmail.com", EmailAuthType::OAuth2Google, "google-access-token", "google-refresh-token", expires, "imap.gmail.com", 993, "smtp.gmail.com", 587, ) .await .expect("Failed to create OAuth IMAP account"); assert_eq!(created.account_name, "Gmail"); assert_eq!(created.email_address, "me@gmail.com"); assert_eq!(created.auth_type, EmailAuthType::OAuth2Google); assert_eq!(created.imap_server, "imap.gmail.com"); assert_eq!(created.imap_port, 993); assert_eq!(created.smtp_server, "smtp.gmail.com"); assert_eq!(created.smtp_port, 587); assert_eq!(created.oauth2_access_token, Some("google-access-token".to_string())); assert_eq!(created.oauth2_refresh_token, Some("google-refresh-token".to_string())); } #[tokio::test] async fn test_list_by_user_ordered() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); repo.create( user_id, "Beta Account", "beta@example.com", "imap.beta.com", 993, "smtp.beta.com", 587, "beta", "pass", true, None, ) .await .expect("Failed to create beta account"); repo.create( user_id, "Alpha Account", "alpha@example.com", "imap.alpha.com", 993, "smtp.alpha.com", 587, "alpha", "pass", true, None, ) .await .expect("Failed to create alpha account"); let accounts = repo .list_by_user(user_id) .await .expect("Failed to list accounts"); assert_eq!(accounts.len(), 2); assert_eq!(accounts[0].account_name, "Alpha Account"); assert_eq!(accounts[1].account_name, "Beta Account"); } #[tokio::test] async fn test_update_account_with_password() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "Original", "old@example.com", "imap.old.com", 993, "smtp.old.com", 587, "olduser", "oldpass", true, None, ) .await .expect("Failed to create account"); let updated = repo .update( created.id, user_id, "Updated Name", "new@example.com", "imap.new.com", 995, "smtp.new.com", 465, "newuser", Some("newpass"), false, Some("All Mail"), ) .await .expect("Failed to update account"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.account_name, "Updated Name"); assert_eq!(updated.email_address, "new@example.com"); assert_eq!(updated.imap_server, "imap.new.com"); assert_eq!(updated.imap_port, 995); assert_eq!(updated.smtp_server, "smtp.new.com"); assert_eq!(updated.smtp_port, 465); assert_eq!(updated.username, "newuser"); assert_eq!(updated.password, "newpass"); assert!(!updated.use_tls); assert_eq!(updated.archive_folder_name, Some("All Mail".to_string())); } #[tokio::test] async fn test_update_account_without_password() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "My Account", "me@example.com", "imap.example.com", 993, "smtp.example.com", 587, "me", "originalpass", true, None, ) .await .expect("Failed to create account"); let updated = repo .update( created.id, user_id, "Renamed Account", "me@example.com", "imap.example.com", 993, "smtp.example.com", 587, "me", None, true, None, ) .await .expect("Failed to update account"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.account_name, "Renamed Account"); // Password should remain unchanged assert_eq!(updated.password, "originalpass"); } #[tokio::test] async fn test_update_oauth_tokens() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let expires = Utc::now() + Duration::hours(1); let created = repo .create_oauth( user_id, "Fastmail", "me@fastmail.com", "old-access", "old-refresh", expires, "https://api.fastmail.com/jmap/session", "acct-123", ) .await .expect("Failed to create account"); let new_expires = Utc::now() + Duration::hours(2); let updated = repo .update_oauth_tokens( created.id, user_id, "new-access-token", Some("new-refresh-token"), new_expires, ) .await .expect("Failed to update tokens"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.oauth2_access_token, Some("new-access-token".to_string())); assert_eq!(updated.oauth2_refresh_token, Some("new-refresh-token".to_string())); } #[tokio::test] async fn test_update_jmap_session() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let expires = Utc::now() + Duration::hours(1); let created = repo .create_oauth( user_id, "Fastmail", "me@fastmail.com", "access-tok", "refresh-tok", expires, "https://old.url/jmap/session", "old-acct-id", ) .await .expect("Failed to create account"); let updated = repo .update_jmap_session( created.id, user_id, "https://new.url/jmap/session", "new-acct-id", ) .await .expect("Failed to update JMAP session"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.jmap_session_url, Some("https://new.url/jmap/session".to_string())); assert_eq!(updated.jmap_account_id, Some("new-acct-id".to_string())); } #[tokio::test] async fn test_delete_account() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "Throwaway", "throw@example.com", "imap.example.com", 993, "smtp.example.com", 587, "throw", "pass", true, None, ) .await .expect("Failed to create account"); let deleted = repo .delete(created.id, user_id) .await .expect("Failed to delete account"); assert!(deleted); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get account"); assert!(fetched.is_none()); } #[tokio::test] async fn test_delete_wrong_user_returns_false() { let pool = common::setup_test_db().await; let user1 = common::create_test_user(&pool).await; let user2 = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user1, "User1 Email", "user1@example.com", "imap.example.com", 993, "smtp.example.com", 587, "user1", "pass", true, None, ) .await .expect("Failed to create account"); // User 2 cannot delete user 1's account let deleted = repo .delete(created.id, user2) .await .expect("Failed to attempt delete"); assert!(!deleted); // Account still exists for user 1 let fetched = repo .get_by_id(created.id, user1) .await .expect("Failed to get account"); assert!(fetched.is_some()); } #[tokio::test] async fn test_update_last_sync_sets_timestamp() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "Sync Test", "sync@example.com", "imap.example.com", 993, "smtp.example.com", 587, "sync", "pass", true, None, ) .await .expect("Failed to create account"); assert!(created.last_sync_at.is_none()); let synced = repo .update_last_sync(created.id, user_id) .await .expect("Failed to update last sync"); assert!(synced); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get account") .unwrap(); assert!(fetched.last_sync_at.is_some()); } #[tokio::test] async fn test_update_sync_interval() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let created = repo .create( user_id, "Interval Test", "interval@example.com", "imap.example.com", 993, "smtp.example.com", 587, "interval", "pass", true, None, ) .await .expect("Failed to create account"); // Default sync_interval_minutes is 15 (set by migration) assert_eq!(created.sync_interval_minutes, Some(15)); let updated = repo .update_sync_interval(created.id, user_id, Some(30)) .await .expect("Failed to update interval"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.sync_interval_minutes, Some(30)); // Disable sync interval let disabled = repo .update_sync_interval(created.id, user_id, None) .await .expect("Failed to disable interval"); assert!(disabled.is_some()); assert!(disabled.unwrap().sync_interval_minutes.is_none()); } #[tokio::test] async fn test_list_accounts_needing_sync_returns_due() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteEmailAccountRepository::new(pool.clone()); let account = repo .create( user_id, "Auto Sync", "autosync@example.com", "imap.example.com", 993, "smtp.example.com", 587, "autosync", "pass", true, None, ) .await .expect("Failed to create account"); // Enable sync with 1-minute interval repo.update_sync_interval(account.id, user_id, Some(1)) .await .expect("Failed to set interval"); // Set last_sync_at to 2 minutes ago so the account is due sqlx::query("UPDATE email_accounts SET last_sync_at = datetime('now', '-2 minutes') WHERE id = ?") .bind(account.id.to_string()) .execute(&pool) .await .unwrap(); let needing_sync = repo .list_accounts_needing_sync(user_id) .await .expect("Failed to list accounts needing sync"); assert_eq!(needing_sync.len(), 1); assert_eq!(needing_sync[0].id, account.id); }