//! DB-layer contract tests for the account core (`db::users`). //! //! `users.rs` is the widest CRUD/lookup module and its contracts were asserted //! only indirectly through HTTP flows (auth, dashboard, admin), audit Run 18 //! Testing. These call the `db::users::` functions directly so the invariants //! the account surface leans on are pinned at the layer they live in: lookup //! round-trip (found vs not-found), exact-vs-normalized key matching for //! username/email, the creator-permission flag, the voluntary creator-pause //! toggle, and the Stripe-webhook status write keyed by connected account. use crate::harness::TestHarness; use makenotwork::db::{self, Email, Username}; // ── lookup round-trip: found vs not-found ── #[tokio::test] async fn lookups_round_trip_after_signup() { let mut h = TestHarness::new().await; let user_id = h .signup("lookup_rt", "lookup_rt@test.com", "password123") .await; // by id let by_id = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("signed-up user is found by id"); assert_eq!(by_id.id, user_id); assert_eq!(by_id.username.as_str(), "lookup_rt"); assert_eq!(by_id.email.as_str(), "lookup_rt@test.com"); // by username (exact match) let username = Username::new("lookup_rt").expect("valid username"); let by_username = db::users::get_user_by_username(&h.db, &username) .await .expect("get_user_by_username ok") .expect("signed-up user is found by username"); assert_eq!(by_username.id, user_id); // by email (normalized match) let email = Email::new("lookup_rt@test.com").expect("valid email"); let by_email = db::users::get_user_by_email(&h.db, &email) .await .expect("get_user_by_email ok") .expect("signed-up user is found by email"); assert_eq!(by_email.id, user_id); } #[tokio::test] async fn lookups_return_none_when_absent() { let h = TestHarness::new().await; // A random UUID that was never inserted. let missing_id = db::UserId::new(); assert!( db::users::get_user_by_id(&h.db, missing_id) .await .expect("get_user_by_id ok") .is_none(), "an unknown id resolves to None, not an error" ); let missing_username = Username::new("nobody_here").expect("valid username"); assert!( db::users::get_user_by_username(&h.db, &missing_username) .await .expect("get_user_by_username ok") .is_none(), "an unknown username resolves to None" ); let missing_email = Email::new("nobody@test.com").expect("valid email"); assert!( db::users::get_user_by_email(&h.db, &missing_email) .await .expect("get_user_by_email ok") .is_none(), "an unknown email resolves to None" ); } // ── key matching: username is exact, email is normalized ── #[tokio::test] async fn username_lookup_is_case_exact() { let mut h = TestHarness::new().await; // Signup stores the username verbatim (no lowercasing); the lookup SQL is // `WHERE username = $1`, so a differently-cased handle must NOT collide. let user_id = h .signup("CaseUser", "caseuser@test.com", "password123") .await; let exact = Username::new("CaseUser").expect("valid username"); let found = db::users::get_user_by_username(&h.db, &exact) .await .expect("get_user_by_username ok") .expect("exact-case username is found"); assert_eq!(found.id, user_id); let wrong_case = Username::new("caseuser").expect("valid username"); assert!( db::users::get_user_by_username(&h.db, &wrong_case) .await .expect("get_user_by_username ok") .is_none(), "username match is case-exact, so a lowercased variant does not resolve" ); } #[tokio::test] async fn email_lookup_is_case_insensitive_by_normalization() { let mut h = TestHarness::new().await; let user_id = h .signup("email_norm", "email_norm@test.com", "password123") .await; // `Email::new` trims + lowercases, so a mixed-case spelling normalizes to // the same stored value and still resolves to the same row. let mixed = Email::new("Email_Norm@Test.Com").expect("valid email"); let found = db::users::get_user_by_email(&h.db, &mixed) .await .expect("get_user_by_email ok") .expect("mixed-case email normalizes and is found"); assert_eq!(found.id, user_id); } // ── creator permission flag ── #[tokio::test] async fn grant_creator_flips_the_creator_flag() { let mut h = TestHarness::new().await; let user_id = h .signup("creator_flag", "creator_flag@test.com", "password123") .await; let before = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!( !before.can_create_projects, "a fresh signup cannot create projects" ); h.grant_creator(user_id).await; let after = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!( after.can_create_projects, "granting creator access sets can_create_projects" ); } // ── voluntary creator pause toggle ── #[tokio::test] async fn pause_and_unpause_creator_toggle_the_flag() { let mut h = TestHarness::new().await; let user_id = h.create_creator("pause_toggle").await; let fresh = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!(!fresh.is_creator_paused(), "a new creator is not paused"); db::users::pause_creator(&h.db, user_id) .await .expect("pause_creator ok"); let paused = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!( paused.is_creator_paused(), "pause_creator stamps creator_paused_at" ); db::users::unpause_creator(&h.db, user_id) .await .expect("unpause_creator ok"); let resumed = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!( !resumed.is_creator_paused(), "unpause_creator clears creator_paused_at" ); } // ── Stripe webhook status write (keyed by connected account) ── #[tokio::test] async fn update_user_stripe_status_persists_flags_by_account() { let mut h = TestHarness::new().await; let user_id = h.create_creator("stripe_status").await; // connect_stripe sets stripe_account_id and flips all three Stripe flags on. h.connect_stripe(user_id, "acct_dbul_status").await; // The webhook write finds the row by stripe_account_id and returns it. let updated = db::users::update_user_stripe_status( &h.db, "acct_dbul_status", true, // onboarding_complete false, // payouts_enabled true, // charges_enabled ) .await .expect("update_user_stripe_status ok") .expect("a matching connected account returns the updated row"); assert_eq!(updated.id, user_id); assert!(updated.stripe_onboarding_complete); assert!(!updated.stripe_payouts_enabled); assert!(updated.stripe_charges_enabled); // The flags are durable, not just echoed by the RETURNING clause. let reread = db::users::get_user_by_id(&h.db, user_id) .await .expect("get_user_by_id ok") .expect("user found"); assert!(reread.stripe_onboarding_complete); assert!(!reread.stripe_payouts_enabled); assert!(reread.stripe_charges_enabled); } #[tokio::test] async fn update_user_stripe_status_is_noop_for_unknown_account() { let h = TestHarness::new().await; // No user carries this account id, so the UPDATE matches nothing and the // webhook handler gets None (rather than an error) to ignore the event. let result = db::users::update_user_stripe_status(&h.db, "acct_dbul_nonexistent", true, true, true) .await .expect("update_user_stripe_status ok"); assert!( result.is_none(), "an unmatched stripe_account_id is a no-op returning None" ); }