Skip to main content

max / makenotwork

Split the demo-buyer tests out of seed_examples The buyer phase added 409 lines to seed_examples.rs, taking it from 451 to 860 and the over-800 module count from 10 to 11. test_hygiene ratchets that count and says to split by domain rather than raise the bar, so this does. The buyer tests are a clean domain: they seed one account and assert its library, its idempotency across reseeds, and that a visitor sees nothing new. The catalog fixtures they need stay in seed_examples and widen to pub(super), since the catalog is what the buyer buys. Like migration 194, this landed after the v0.11.8 build and so met a gate for the first time in Sando run 44.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-07 20:19 UTC
Signed with PGP, not checked
Commit: 1b9a3735cac5a11873d1b66092071a8bf29bbb50
Parent: 7e1cab2
3 files changed, +237 insertions, -223 deletions
@@ -100,6 +100,7 @@
100 100 mod sandbox;
101 101 mod scanning;
102 102 mod security_idor_moderation;
103 + mod seed_buyer;
103 104 mod seed_examples;
104 105 mod session_revocation;
105 106 mod ssh_keys;
@@ -25,7 +25,7 @@
25 25 use makenotwork::storage::StorageBackend;
26 26
27 27 /// The Phase-1 roster size (see `seed::creators::ROSTER`).
28 - const SEEDED_CREATORS: i64 = 5;
28 + pub(super) const SEEDED_CREATORS: i64 = 5;
29 29 /// Phase-2 item count: five per project, spanning every `ItemType`. Widened
30 30 /// from eleven on 2026-08-07 so the storefront grids read as grids.
31 31 const SEEDED_ITEMS: i64 = 25;
@@ -35,7 +35,7 @@
35 35 const ITEM_TYPES: i64 = 11;
36 36
37 37 /// Guard-passing options for a test run: opt-in on, approved example host.
38 - fn testnot_opts() -> SeedOptions {
38 + pub(super) fn testnot_opts() -> SeedOptions {
39 39 SeedOptions {
40 40 allow_example_seed: true,
41 41 host_url: "https://testnot.work".to_string(),
@@ -44,7 +44,7 @@
44 44 }
45 45 }
46 46
47 - async fn count_example_creators(pool: &sqlx::PgPool) -> i64 {
47 + pub(super) async fn count_example_creators(pool: &sqlx::PgPool) -> i64 {
48 48 sqlx::query_scalar(
49 49 "SELECT COUNT(*) FROM users \
50 50 WHERE lower(split_part(email, '@', 2)) = 'example.test' AND is_sandbox = FALSE",
@@ -69,7 +69,7 @@
69 69 /// (`p.is_public AND u.is_sandbox = FALSE`, per `db::discover::discover_projects`).
70 70 /// Mirrors that predicate in SQL because the `discover` module is `pub(crate)` and
71 71 /// so unreachable from this external test crate.
72 - async fn discover_visible_example_slugs(pool: &sqlx::PgPool) -> Vec<String> {
72 + pub(super) async fn discover_visible_example_slugs(pool: &sqlx::PgPool) -> Vec<String> {
73 73 sqlx::query_scalar::<_, String>(
74 74 "SELECT p.slug::text FROM projects p \
75 75 JOIN users u ON u.id = p.user_id \
@@ -83,7 +83,7 @@
83 83 }
84 84
85 85 /// Total items under the seeded example projects.
86 - async fn count_example_items(pool: &sqlx::PgPool) -> i64 {
86 + pub(super) async fn count_example_items(pool: &sqlx::PgPool) -> i64 {
87 87 sqlx::query_scalar(
88 88 "SELECT COUNT(*) FROM items i \
89 89 JOIN projects p ON p.id = i.project_id \
@@ -382,7 +382,7 @@
382 382
383 383 /// Guard-passing options bundled with an in-memory storage backend, so the media
384 384 /// phase runs and flips items visible.
385 - fn media_ctx() -> SeedMedia {
385 + pub(super) fn media_ctx() -> SeedMedia {
386 386 let s3: Arc<dyn StorageBackend> = Arc::new(InMemoryStorage::new());
387 387 let public: Arc<dyn StorageBackend> = Arc::new(InMemoryStorage::new());
388 388 SeedMedia {
@@ -495,7 +495,7 @@
495 495 }
496 496 }
497 497
498 - async fn password_hash_of(pool: &sqlx::PgPool, id: uuid::Uuid) -> String {
498 + pub(super) async fn password_hash_of(pool: &sqlx::PgPool, id: uuid::Uuid) -> String {
499 499 sqlx::query_scalar("SELECT password_hash FROM users WHERE id = $1")
500 500 .bind(id)
501 501 .fetch_one(pool)
@@ -642,219 +642,3 @@
642 642 assert_eq!(harness_accounts, 0, "harness phase must be opt-in");
643 643 assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS);
644 644 }
645 -
646 - // ── Demo buyer (GoingsOn 839a8e5a, option B) ────────────────────────────────
647 - //
648 - // One login-capable account with a purchase history, so the landing carousel's
649 - // third frame has a library to photograph. Opt-in per box, like the harness.
650 -
651 - /// The demo buyer's password for a test run. Not a secret here; on testnot it
652 - /// comes from the box's EnvironmentFile.
653 - const BUYER_PASSWORD: &str = "demo-buyer-test-password";
654 -
655 - /// Purchases seeded for the demo buyer (see `seed::buyer::PURCHASES`).
656 - const BUYER_PURCHASES: i64 = 9;
657 -
658 - fn buyer_opts() -> buyer::BuyerOptions {
659 - buyer::BuyerOptions {
660 - password: BUYER_PASSWORD.to_string(),
661 - }
662 - }
663 -
664 - fn testnot_opts_with_buyer() -> SeedOptions {
665 - SeedOptions {
666 - buyer: Some(buyer_opts()),
667 - ..testnot_opts()
668 - }
669 - }
670 -
671 - async fn buyer_purchase_count(pool: &sqlx::PgPool) -> i64 {
672 - sqlx::query_scalar(
673 - "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND status = 'completed'",
674 - )
675 - .bind(BUYER_ACCOUNT_ID)
676 - .fetch_one(pool)
677 - .await
678 - .expect("count buyer purchases")
679 - }
680 -
681 - #[tokio::test]
682 - async fn demo_buyer_can_log_in_and_owns_a_library() {
683 - let db = TestDb::new().await;
684 - seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
685 - .await
686 - .expect("seed with buyer phase");
687 -
688 - // The login the capture run performs.
689 - let hash = password_hash_of(&db.pool, BUYER_ACCOUNT_ID).await;
690 - assert!(
691 - makenotwork::auth::verify_password_async(BUYER_PASSWORD.to_string(), hash)
692 - .await
693 - .expect("verify"),
694 - "the demo buyer's password should verify"
695 - );
696 -
697 - // Not a sandbox account: `SessionUser::check_not_sandbox` would refuse the
698 - // session, and not a creator, because the point is a buyer.
699 - let (is_sandbox, can_create): (bool, bool) =
700 - sqlx::query_as("SELECT is_sandbox, can_create_projects FROM users WHERE id = $1")
701 - .bind(BUYER_ACCOUNT_ID)
702 - .fetch_one(&db.pool)
703 - .await
704 - .expect("buyer account");
705 - assert!(!is_sandbox, "a sandbox account cannot hold a session");
706 - assert!(!can_create, "the demo buyer is a buyer");
707 -
708 - // Every purchase landed, and the library reads them through the `purchases`
709 - // view, which filters on status = 'completed'.
710 - assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
711 - let in_view: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM purchases WHERE buyer_id = $1")
712 - .bind(BUYER_ACCOUNT_ID)
713 - .fetch_one(&db.pool)
714 - .await
715 - .expect("purchases view");
716 - assert_eq!(in_view, BUYER_PURCHASES, "every purchase should be visible");
717 -
718 - // But not the whole catalog: a library holding all eleven reads as a
719 - // fixture rather than as somebody's shelf. (The constant-only half of this
720 - // is `seed::buyer`'s own unit test; here it is checked against the items
721 - // actually in the database.)
722 - let unbought = count_example_items(&db.pool).await - BUYER_PURCHASES;
723 - assert!(
724 - unbought > 0,
725 - "the buyer should leave some of the catalog unbought"
726 - );
727 -
728 - // Platform fee is zero on every row. MNW charges 0%, so a demo receipt
729 - // showing anything else would misrepresent the product.
730 - let nonzero_fees: i64 = sqlx::query_scalar(
731 - "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND platform_fee_cents <> 0",
732 - )
733 - .bind(BUYER_ACCOUNT_ID)
734 - .fetch_one(&db.pool)
735 - .await
736 - .expect("fee check");
737 - assert_eq!(nonzero_fees, 0);
738 -
739 - // Paid rows recorded what was paid. `get_user_purchases` derives its Free
740 - // badge from `amount_cents = 0`, so a paid item at zero would badge wrong.
741 - let paid: i64 = sqlx::query_scalar(
742 - "SELECT COUNT(*) FROM transactions t JOIN items i ON i.id = t.item_id \
743 - WHERE t.buyer_id = $1 AND t.amount_cents > 0",
744 - )
745 - .bind(BUYER_ACCOUNT_ID)
746 - .fetch_one(&db.pool)
747 - .await
748 - .expect("paid count");
749 - assert!(
750 - paid >= 4,
751 - "the history should include real payments, got {paid}"
752 - );
753 -
754 - // An active subscription, so the library's subscription block is not empty.
755 - let (tier, status): (String, String) = sqlx::query_as(
756 - "SELECT t.name, s.status FROM subscriptions s \
757 - JOIN subscription_tiers t ON t.id = s.tier_id WHERE s.subscriber_id = $1",
758 - )
759 - .bind(BUYER_ACCOUNT_ID)
760 - .fetch_one(&db.pool)
761 - .await
762 - .expect("buyer subscription");
763 - assert_eq!((tier.as_str(), status.as_str()), ("Patron", "active"));
764 -
765 - // License keys for the project that sells them: one of the things the
766 - // library page shows, and one of the things MNW sells.
767 - let keys: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM license_keys WHERE owner_id = $1")
768 - .bind(BUYER_ACCOUNT_ID)
769 - .fetch_one(&db.pool)
770 - .await
771 - .expect("license keys");
772 - assert!(keys >= 1, "a license-keyed purchase should carry a key");
773 -
774 - // Most rows are marked downloaded, so the "new version" badge is a signal
775 - // rather than the default state of every row.
776 - let downloaded_items: i64 =
777 - sqlx::query_scalar("SELECT COUNT(DISTINCT item_id) FROM user_downloads WHERE user_id = $1")
778 - .bind(BUYER_ACCOUNT_ID)
779 - .fetch_one(&db.pool)
780 - .await
781 - .expect("downloads");
782 - assert!(
783 - downloaded_items >= 1,
784 - "the buyer should have downloaded something"
785 - );
786 - }
787 -
788 - #[tokio::test]
789 - async fn demo_buyer_history_does_not_duplicate_across_reseeds() {
790 - let db = TestDb::new().await;
791 - seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
792 - .await
793 - .expect("first seed");
794 - seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
795 - .await
796 - .expect("reseed");
797 -
798 - // The catalog phases get idempotency from the example-data wipe, but this
799 - // account is keyed by a fixed id and is not in that set, so its history has
800 - // to be cleared explicitly. Without that, every reseed doubles the library.
801 - assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
802 -
803 - let subs: i64 =
804 - sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE subscriber_id = $1")
805 - .bind(BUYER_ACCOUNT_ID)
806 - .fetch_one(&db.pool)
807 - .await
808 - .expect("subscription count");
809 - assert_eq!(subs, 1, "a reseed must not stack subscriptions");
810 -
811 - // And the id is stable, which is what lets a stored session cookie or a
812 - // scripted login survive a reset.
813 - let id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE username = $1")
814 - .bind("demo_collector")
815 - .fetch_one(&db.pool)
816 - .await
817 - .expect("buyer id");
818 - assert_eq!(id, BUYER_ACCOUNT_ID);
819 - }
820 -
821 - #[tokio::test]
822 - async fn without_buyer_options_the_phase_does_not_run() {
823 - let db = TestDb::new().await;
824 - seed::run(&db.pool, &testnot_opts(), &media_ctx())
825 - .await
826 - .expect("seed without buyer");
827 -
828 - let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE id = $1")
829 - .bind(BUYER_ACCOUNT_ID)
830 - .fetch_one(&db.pool)
831 - .await
832 - .expect("count");
833 - assert_eq!(accounts, 0, "the demo-buyer phase must be opt-in");
834 - assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS);
835 - }
836 -
837 - #[tokio::test]
838 - async fn the_demo_buyer_changes_nothing_a_visitor_can_see() {
839 - let db = TestDb::new().await;
840 - seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
841 - .await
842 - .expect("seed with buyer phase");
843 -
844 - // Option B's boundary, asserted rather than merely documented: the buyer is
845 - // a capture credential, not a demo surface. It owns no project, so it never
846 - // appears on /discover or /creators, and it publishes nothing.
847 - let owned_projects: i64 =
848 - sqlx::query_scalar("SELECT COUNT(*) FROM projects WHERE user_id = $1")
849 - .bind(BUYER_ACCOUNT_ID)
850 - .fetch_one(&db.pool)
851 - .await
852 - .expect("owned projects");
853 - assert_eq!(owned_projects, 0);
854 -
855 - // The catalog a visitor sees is exactly what it was without the phase.
856 - assert_eq!(
857 - discover_visible_example_slugs(&db.pool).await.len() as i64,
858 - SEEDED_CREATORS
859 - );
860 - }
@@ -1,0 +1,229 @@
1 + //! DB-layer contract tests for the demo-buyer seed phase (`seed::buyer`).
2 + //!
3 + //! One login-capable account with a purchase history, so the landing carousel's
4 + //! third frame has a library to photograph. Opt-in per box, like the harness
5 + //! phase. GoingsOn 839a8e5a, option B.
6 + //!
7 + //! Split out of `seed_examples` on 2026-08-07: the buyer phase added 409 lines
8 + //! and pushed that module past the 800-line ratchet in `test_hygiene`. The
9 + //! fixtures the two share stay there, since the catalog is what the buyer buys.
10 +
11 + use crate::harness::db::TestDb;
12 + use makenotwork::seed::buyer::{self, BUYER_ACCOUNT_ID};
13 + use makenotwork::seed::{self, SeedOptions};
14 +
15 + use super::seed_examples::{
16 + SEEDED_CREATORS, count_example_creators, count_example_items, discover_visible_example_slugs,
17 + media_ctx, password_hash_of, testnot_opts,
18 + };
19 +
20 + /// The demo buyer's password for a test run. Not a secret here; on testnot it
21 + /// comes from the box's EnvironmentFile.
22 + const BUYER_PASSWORD: &str = "demo-buyer-test-password";
23 +
24 + /// Purchases seeded for the demo buyer (see `seed::buyer::PURCHASES`).
25 + const BUYER_PURCHASES: i64 = 9;
26 +
27 + fn buyer_opts() -> buyer::BuyerOptions {
28 + buyer::BuyerOptions {
29 + password: BUYER_PASSWORD.to_string(),
30 + }
31 + }
32 +
33 + fn testnot_opts_with_buyer() -> SeedOptions {
34 + SeedOptions {
35 + buyer: Some(buyer_opts()),
36 + ..testnot_opts()
37 + }
38 + }
39 +
40 + async fn buyer_purchase_count(pool: &sqlx::PgPool) -> i64 {
41 + sqlx::query_scalar(
42 + "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND status = 'completed'",
43 + )
44 + .bind(BUYER_ACCOUNT_ID)
45 + .fetch_one(pool)
46 + .await
47 + .expect("count buyer purchases")
48 + }
49 +
50 + #[tokio::test]
51 + async fn demo_buyer_can_log_in_and_owns_a_library() {
52 + let db = TestDb::new().await;
53 + seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
54 + .await
55 + .expect("seed with buyer phase");
56 +
57 + // The login the capture run performs.
58 + let hash = password_hash_of(&db.pool, BUYER_ACCOUNT_ID).await;
59 + assert!(
60 + makenotwork::auth::verify_password_async(BUYER_PASSWORD.to_string(), hash)
61 + .await
62 + .expect("verify"),
63 + "the demo buyer's password should verify"
64 + );
65 +
66 + // Not a sandbox account: `SessionUser::check_not_sandbox` would refuse the
67 + // session, and not a creator, because the point is a buyer.
68 + let (is_sandbox, can_create): (bool, bool) =
69 + sqlx::query_as("SELECT is_sandbox, can_create_projects FROM users WHERE id = $1")
70 + .bind(BUYER_ACCOUNT_ID)
71 + .fetch_one(&db.pool)
72 + .await
73 + .expect("buyer account");
74 + assert!(!is_sandbox, "a sandbox account cannot hold a session");
75 + assert!(!can_create, "the demo buyer is a buyer");
76 +
77 + // Every purchase landed, and the library reads them through the `purchases`
78 + // view, which filters on status = 'completed'.
79 + assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
80 + let in_view: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM purchases WHERE buyer_id = $1")
81 + .bind(BUYER_ACCOUNT_ID)
82 + .fetch_one(&db.pool)
83 + .await
84 + .expect("purchases view");
85 + assert_eq!(in_view, BUYER_PURCHASES, "every purchase should be visible");
86 +
87 + // But not the whole catalog: a library holding all eleven reads as a
88 + // fixture rather than as somebody's shelf. (The constant-only half of this
89 + // is `seed::buyer`'s own unit test; here it is checked against the items
90 + // actually in the database.)
91 + let unbought = count_example_items(&db.pool).await - BUYER_PURCHASES;
92 + assert!(
93 + unbought > 0,
94 + "the buyer should leave some of the catalog unbought"
95 + );
96 +
97 + // Platform fee is zero on every row. MNW charges 0%, so a demo receipt
98 + // showing anything else would misrepresent the product.
99 + let nonzero_fees: i64 = sqlx::query_scalar(
100 + "SELECT COUNT(*) FROM transactions WHERE buyer_id = $1 AND platform_fee_cents <> 0",
101 + )
102 + .bind(BUYER_ACCOUNT_ID)
103 + .fetch_one(&db.pool)
104 + .await
105 + .expect("fee check");
106 + assert_eq!(nonzero_fees, 0);
107 +
108 + // Paid rows recorded what was paid. `get_user_purchases` derives its Free
109 + // badge from `amount_cents = 0`, so a paid item at zero would badge wrong.
110 + let paid: i64 = sqlx::query_scalar(
111 + "SELECT COUNT(*) FROM transactions t JOIN items i ON i.id = t.item_id \
112 + WHERE t.buyer_id = $1 AND t.amount_cents > 0",
113 + )
114 + .bind(BUYER_ACCOUNT_ID)
115 + .fetch_one(&db.pool)
116 + .await
117 + .expect("paid count");
118 + assert!(
119 + paid >= 4,
120 + "the history should include real payments, got {paid}"
121 + );
122 +
123 + // An active subscription, so the library's subscription block is not empty.
124 + let (tier, status): (String, String) = sqlx::query_as(
125 + "SELECT t.name, s.status FROM subscriptions s \
126 + JOIN subscription_tiers t ON t.id = s.tier_id WHERE s.subscriber_id = $1",
127 + )
128 + .bind(BUYER_ACCOUNT_ID)
129 + .fetch_one(&db.pool)
130 + .await
131 + .expect("buyer subscription");
132 + assert_eq!((tier.as_str(), status.as_str()), ("Patron", "active"));
133 +
134 + // License keys for the project that sells them: one of the things the
135 + // library page shows, and one of the things MNW sells.
136 + let keys: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM license_keys WHERE owner_id = $1")
137 + .bind(BUYER_ACCOUNT_ID)
138 + .fetch_one(&db.pool)
139 + .await
140 + .expect("license keys");
141 + assert!(keys >= 1, "a license-keyed purchase should carry a key");
142 +
143 + // Most rows are marked downloaded, so the "new version" badge is a signal
144 + // rather than the default state of every row.
145 + let downloaded_items: i64 =
146 + sqlx::query_scalar("SELECT COUNT(DISTINCT item_id) FROM user_downloads WHERE user_id = $1")
147 + .bind(BUYER_ACCOUNT_ID)
148 + .fetch_one(&db.pool)
149 + .await
150 + .expect("downloads");
151 + assert!(
152 + downloaded_items >= 1,
153 + "the buyer should have downloaded something"
154 + );
155 + }
156 +
157 + #[tokio::test]
158 + async fn demo_buyer_history_does_not_duplicate_across_reseeds() {
159 + let db = TestDb::new().await;
160 + seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
161 + .await
162 + .expect("first seed");
163 + seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
164 + .await
165 + .expect("reseed");
166 +
167 + // The catalog phases get idempotency from the example-data wipe, but this
168 + // account is keyed by a fixed id and is not in that set, so its history has
169 + // to be cleared explicitly. Without that, every reseed doubles the library.
170 + assert_eq!(buyer_purchase_count(&db.pool).await, BUYER_PURCHASES);
171 +
172 + let subs: i64 =
173 + sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE subscriber_id = $1")
174 + .bind(BUYER_ACCOUNT_ID)
175 + .fetch_one(&db.pool)
176 + .await
177 + .expect("subscription count");
178 + assert_eq!(subs, 1, "a reseed must not stack subscriptions");
179 +
180 + // And the id is stable, which is what lets a stored session cookie or a
181 + // scripted login survive a reset.
182 + let id: uuid::Uuid = sqlx::query_scalar("SELECT id FROM users WHERE username = $1")
183 + .bind("demo_collector")
184 + .fetch_one(&db.pool)
185 + .await
186 + .expect("buyer id");
187 + assert_eq!(id, BUYER_ACCOUNT_ID);
188 + }
189 +
190 + #[tokio::test]
191 + async fn without_buyer_options_the_phase_does_not_run() {
192 + let db = TestDb::new().await;
193 + seed::run(&db.pool, &testnot_opts(), &media_ctx())
194 + .await
195 + .expect("seed without buyer");
196 +
197 + let accounts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM users WHERE id = $1")
198 + .bind(BUYER_ACCOUNT_ID)
199 + .fetch_one(&db.pool)
200 + .await
201 + .expect("count");
202 + assert_eq!(accounts, 0, "the demo-buyer phase must be opt-in");
203 + assert_eq!(count_example_creators(&db.pool).await, SEEDED_CREATORS);
204 + }
205 +
206 + #[tokio::test]
207 + async fn the_demo_buyer_changes_nothing_a_visitor_can_see() {
208 + let db = TestDb::new().await;
209 + seed::run(&db.pool, &testnot_opts_with_buyer(), &media_ctx())
210 + .await
211 + .expect("seed with buyer phase");
212 +
213 + // Option B's boundary, asserted rather than merely documented: the buyer is
214 + // a capture credential, not a demo surface. It owns no project, so it never
215 + // appears on /discover or /creators, and it publishes nothing.
216 + let owned_projects: i64 =
217 + sqlx::query_scalar("SELECT COUNT(*) FROM projects WHERE user_id = $1")
218 + .bind(BUYER_ACCOUNT_ID)
219 + .fetch_one(&db.pool)
220 + .await
221 + .expect("owned projects");
222 + assert_eq!(owned_projects, 0);
223 +
224 + // The catalog a visitor sees is exactly what it was without the phase.
225 + assert_eq!(
226 + discover_visible_example_slugs(&db.pool).await.len() as i64,
227 + SEEDED_CREATORS
228 + );
229 + }