Skip to main content

max / makenotwork

25.8 KB · 858 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4
5 #[test]
6 fn discount_type_round_trip() {
7 assert_eq!(DiscountType::Percentage.to_string(), "percentage");
8 assert_eq!(
9 "fixed".parse::<DiscountType>().unwrap(),
10 DiscountType::Fixed
11 );
12 assert!("bogus".parse::<DiscountType>().is_err());
13 }
14
15 #[test]
16 fn waitlist_status_round_trip() {
17 assert_eq!(WaitlistStatus::Pending.to_string(), "pending");
18 assert_eq!(
19 "approved".parse::<WaitlistStatus>().unwrap(),
20 WaitlistStatus::Approved
21 );
22 }
23
24 #[test]
25 fn selection_method_round_trip() {
26 assert_eq!(SelectionMethod::HandPicked.to_string(), "hand_picked");
27 assert_eq!(
28 "lottery".parse::<SelectionMethod>().unwrap(),
29 SelectionMethod::Lottery
30 );
31 assert_eq!(SelectionMethod::Invited.to_string(), "invited");
32 assert_eq!(
33 "invited".parse::<SelectionMethod>().unwrap(),
34 SelectionMethod::Invited
35 );
36 }
37
38 #[test]
39 fn transaction_status_round_trip() {
40 assert_eq!(TransactionStatus::Completed.to_string(), "completed");
41 assert_eq!(
42 "refunded".parse::<TransactionStatus>().unwrap(),
43 TransactionStatus::Refunded
44 );
45 }
46
47 #[test]
48 fn follow_target_type_round_trip() {
49 assert_eq!(FollowTargetType::User.to_string(), "user");
50 assert_eq!(
51 "tag".parse::<FollowTargetType>().unwrap(),
52 FollowTargetType::Tag
53 );
54 }
55
56 #[test]
57 fn subscription_status_round_trip() {
58 assert_eq!(SubscriptionStatus::PastDue.to_string(), "past_due");
59 assert_eq!(
60 "canceled".parse::<SubscriptionStatus>().unwrap(),
61 SubscriptionStatus::Canceled
62 );
63 assert_eq!(SubscriptionStatus::Trialing.to_string(), "trialing");
64 assert_eq!(
65 "trialing".parse::<SubscriptionStatus>().unwrap(),
66 SubscriptionStatus::Trialing
67 );
68 assert_eq!(SubscriptionStatus::Incomplete.to_string(), "incomplete");
69 assert_eq!(
70 "incomplete".parse::<SubscriptionStatus>().unwrap(),
71 SubscriptionStatus::Incomplete
72 );
73 assert_eq!(
74 SubscriptionStatus::IncompleteExpired.to_string(),
75 "incomplete_expired"
76 );
77 assert_eq!(
78 "incomplete_expired".parse::<SubscriptionStatus>().unwrap(),
79 SubscriptionStatus::IncompleteExpired
80 );
81 }
82
83 #[test]
84 fn sync_operation_round_trip() {
85 assert_eq!(SyncOperation::Insert.to_string(), "INSERT");
86 assert_eq!(
87 "DELETE".parse::<SyncOperation>().unwrap(),
88 SyncOperation::Delete
89 );
90 }
91
92 #[test]
93 fn sync_platform_round_trip() {
94 assert_eq!(SyncPlatform::Macos.to_string(), "macos");
95 assert_eq!("web".parse::<SyncPlatform>().unwrap(), SyncPlatform::Web);
96 }
97
98 #[test]
99 fn item_type_round_trip() {
100 assert_eq!(ItemType::Audio.to_string(), "audio");
101 assert_eq!("plugin".parse::<ItemType>().unwrap(), ItemType::Plugin);
102 assert_eq!(ItemType::Bundle.to_string(), "bundle");
103 assert_eq!("bundle".parse::<ItemType>().unwrap(), ItemType::Bundle);
104 }
105
106 #[test]
107 fn insertion_position_round_trip() {
108 assert_eq!(InsertionPosition::PreRoll.to_string(), "pre_roll");
109 assert_eq!(
110 "mid_roll".parse::<InsertionPosition>().unwrap(),
111 InsertionPosition::MidRoll
112 );
113 assert_eq!(
114 "post_roll".parse::<InsertionPosition>().unwrap(),
115 InsertionPosition::PostRoll
116 );
117 assert!("invalid".parse::<InsertionPosition>().is_err());
118 }
119
120 #[test]
121 fn item_type_label() {
122 assert_eq!(ItemType::Audio.label(), "Audio");
123 assert_eq!(ItemType::Plugin.label(), "Plugin");
124 assert_eq!(ItemType::Template.label(), "Template");
125 }
126
127 #[test]
128 fn appeal_decision_round_trip() {
129 assert_eq!(AppealDecision::Approved.to_string(), "approved");
130 assert_eq!(
131 "denied".parse::<AppealDecision>().unwrap(),
132 AppealDecision::Denied
133 );
134 assert!("bogus".parse::<AppealDecision>().is_err());
135 }
136
137 #[test]
138 fn discover_sort_round_trip() {
139 assert_eq!(DiscoverSort::Newest.to_string(), "newest");
140 assert_eq!(
141 "most_sold".parse::<DiscoverSort>().unwrap(),
142 DiscoverSort::MostSold
143 );
144 assert_eq!(
145 "price_asc".parse::<DiscoverSort>().unwrap(),
146 DiscoverSort::PriceAsc
147 );
148 assert_eq!(
149 "price_desc".parse::<DiscoverSort>().unwrap(),
150 DiscoverSort::PriceDesc
151 );
152 assert!("invalid".parse::<DiscoverSort>().is_err());
153 }
154
155 #[test]
156 fn file_scan_status_round_trip() {
157 assert_eq!(FileScanStatus::Clean.to_string(), "clean");
158 assert_eq!(FileScanStatus::Pending.to_string(), "pending");
159 assert_eq!(FileScanStatus::Scanning.to_string(), "scanning");
160 assert_eq!(
161 "pending".parse::<FileScanStatus>().unwrap(),
162 FileScanStatus::Pending
163 );
164 assert_eq!(
165 "scanning".parse::<FileScanStatus>().unwrap(),
166 FileScanStatus::Scanning
167 );
168 assert_eq!(
169 "held_for_review".parse::<FileScanStatus>().unwrap(),
170 FileScanStatus::HeldForReview
171 );
172 assert_eq!(FileScanStatus::HeldForReview.to_string(), "held_for_review");
173 assert_eq!(
174 "quarantined".parse::<FileScanStatus>().unwrap(),
175 FileScanStatus::Quarantined
176 );
177 assert!("bogus".parse::<FileScanStatus>().is_err());
178 }
179
180 #[test]
181 fn code_purpose_round_trip() {
182 assert_eq!(CodePurpose::Discount.to_string(), "discount");
183 assert_eq!(
184 "free_access".parse::<CodePurpose>().unwrap(),
185 CodePurpose::FreeAccess
186 );
187 assert_eq!(
188 "free_trial".parse::<CodePurpose>().unwrap(),
189 CodePurpose::FreeTrial
190 );
191 assert!("bogus".parse::<CodePurpose>().is_err());
192 }
193
194 #[test]
195 fn issue_status_round_trip() {
196 assert_eq!(IssueStatus::Open.to_string(), "open");
197 assert_eq!(
198 "closed".parse::<IssueStatus>().unwrap(),
199 IssueStatus::Closed
200 );
201 assert!("bogus".parse::<IssueStatus>().is_err());
202 }
203
204 #[test]
205 fn report_target_type_round_trip() {
206 assert_eq!(ReportTargetType::Project.to_string(), "project");
207 assert_eq!(
208 "item".parse::<ReportTargetType>().unwrap(),
209 ReportTargetType::Item
210 );
211 assert!("bogus".parse::<ReportTargetType>().is_err());
212 }
213
214 #[test]
215 fn report_type_round_trip() {
216 assert_eq!(ReportType::Mislabeled.to_string(), "mislabeled");
217 assert_eq!("spam".parse::<ReportType>().unwrap(), ReportType::Spam);
218 assert_eq!("abuse".parse::<ReportType>().unwrap(), ReportType::Abuse);
219 assert_eq!(
220 "infringement".parse::<ReportType>().unwrap(),
221 ReportType::Infringement
222 );
223 assert_eq!("other".parse::<ReportType>().unwrap(), ReportType::Other);
224 assert!("bogus".parse::<ReportType>().is_err());
225 }
226
227 #[test]
228 fn report_status_round_trip() {
229 assert_eq!(ReportStatus::Open.to_string(), "open");
230 assert_eq!(
231 "resolved".parse::<ReportStatus>().unwrap(),
232 ReportStatus::Resolved
233 );
234 assert_eq!(
235 "dismissed".parse::<ReportStatus>().unwrap(),
236 ReportStatus::Dismissed
237 );
238 assert!("bogus".parse::<ReportStatus>().is_err());
239 }
240
241 #[test]
242 fn creator_tier_round_trip() {
243 assert_eq!(CreatorTier::Basic.to_string(), "basic");
244 assert_eq!(
245 "small_files".parse::<CreatorTier>().unwrap(),
246 CreatorTier::SmallFiles
247 );
248 assert_eq!(
249 "big_files".parse::<CreatorTier>().unwrap(),
250 CreatorTier::BigFiles
251 );
252 assert_eq!(
253 "everything".parse::<CreatorTier>().unwrap(),
254 CreatorTier::Everything
255 );
256 assert!("bogus".parse::<CreatorTier>().is_err());
257 }
258
259 #[test]
260 fn creator_tier_label_and_price() {
261 crate::tier_prices::TierPrices::install_test_default();
262 assert_eq!(CreatorTier::Basic.label(), "Basic");
263 assert_eq!(CreatorTier::SmallFiles.label(), "Small Files");
264 // Prices come from assumptions.toml. Assert invariants, founder = std/2,
265 // monotone across tiers, not literal cents, so a future toml edit doesn't
266 // break this test.
267 let tiers = [
268 CreatorTier::Basic,
269 CreatorTier::SmallFiles,
270 CreatorTier::BigFiles,
271 CreatorTier::Everything,
272 ];
273 for pair in tiers.windows(2) {
274 assert!(
275 pair[0].price_cents() < pair[1].price_cents(),
276 "{:?} price_cents ({}) should be < {:?} ({})",
277 pair[0],
278 pair[0].price_cents(),
279 pair[1],
280 pair[1].price_cents(),
281 );
282 }
283 assert!(CreatorTier::Basic.price_cents() > 0);
284 }
285
286 #[test]
287 fn creator_tier_file_limits() {
288 crate::tier_prices::TierPrices::install_test_default();
289 // File caps monotone across tiers; Basic ≤ SmallFiles ≤ BigFiles == Everything.
290 let tiers = [
291 CreatorTier::Basic,
292 CreatorTier::SmallFiles,
293 CreatorTier::BigFiles,
294 CreatorTier::Everything,
295 ];
296 for pair in tiers.windows(2) {
297 assert!(
298 pair[0].max_file_bytes() <= pair[1].max_file_bytes(),
299 "{:?} max_file_bytes ({}) should be <= {:?} ({})",
300 pair[0],
301 pair[0].max_file_bytes(),
302 pair[1],
303 pair[1].max_file_bytes(),
304 );
305 }
306 assert!(CreatorTier::Basic.max_file_bytes() > 0);
307 }
308
309 #[test]
310 fn creator_tier_storage_limits() {
311 crate::tier_prices::TierPrices::install_test_default();
312 let tiers = [
313 CreatorTier::Basic,
314 CreatorTier::SmallFiles,
315 CreatorTier::BigFiles,
316 CreatorTier::Everything,
317 ];
318 for pair in tiers.windows(2) {
319 assert!(
320 pair[0].max_storage_bytes() <= pair[1].max_storage_bytes(),
321 "{:?} max_storage_bytes ({}) should be <= {:?} ({})",
322 pair[0],
323 pair[0].max_storage_bytes(),
324 pair[1],
325 pair[1].max_storage_bytes(),
326 );
327 }
328 assert!(CreatorTier::Basic.max_storage_bytes() > 0);
329 // Every tier's per-file cap must fit in its total storage cap or uploads
330 // are impossible. Catches an accidental toml edit that shrinks a total
331 // below the per-file limit.
332 for &tier in &tiers {
333 assert!(
334 tier.max_file_bytes() <= tier.max_storage_bytes(),
335 "{tier:?}: file cap ({}) exceeds storage cap ({})",
336 tier.max_file_bytes(),
337 tier.max_storage_bytes(),
338 );
339 }
340 }
341
342 #[test]
343 fn creator_tier_allows_file_uploads() {
344 assert!(!CreatorTier::Basic.allows_file_uploads());
345 assert!(CreatorTier::SmallFiles.allows_file_uploads());
346 assert!(CreatorTier::BigFiles.allows_file_uploads());
347 assert!(CreatorTier::Everything.allows_file_uploads());
348 }
349
350 #[test]
351 fn creator_tier_features_track_live_capabilities() {
352 assert!(CreatorTier::Basic.features().is_empty());
353 assert_eq!(CreatorTier::SmallFiles.features(), &["file_uploads"]);
354 assert_eq!(
355 CreatorTier::BigFiles.features(),
356 &["file_uploads", "large_files"]
357 );
358 assert_eq!(
359 CreatorTier::Everything.features(),
360 &["file_uploads", "large_files"]
361 );
362 }
363
364 #[test]
365 fn project_feature_round_trip() {
366 assert_eq!(ProjectFeature::Audio.to_string(), "audio");
367 assert_eq!(
368 "downloads".parse::<ProjectFeature>().unwrap(),
369 ProjectFeature::Downloads
370 );
371 assert_eq!(
372 "license_keys".parse::<ProjectFeature>().unwrap(),
373 ProjectFeature::LicenseKeys
374 );
375 assert_eq!(
376 "source_code".parse::<ProjectFeature>().unwrap(),
377 ProjectFeature::SourceCode
378 );
379 assert!("bogus".parse::<ProjectFeature>().is_err());
380 }
381
382 #[test]
383 fn project_feature_label_and_description() {
384 assert_eq!(ProjectFeature::Audio.label(), "Audio");
385 assert_eq!(ProjectFeature::LicenseKeys.label(), "License Keys");
386 assert!(!ProjectFeature::Audio.description().is_empty());
387 }
388
389 #[test]
390 fn project_feature_all() {
391 let all = ProjectFeature::all();
392 assert_eq!(all.len(), 8);
393 assert_eq!(all[0].0, "audio");
394 assert_eq!(all[7].0, "cloud_sync");
395 }
396
397 #[test]
398 fn project_feature_allowed_item_types_audio() {
399 let types = ProjectFeature::Audio.allowed_item_types();
400 assert!(types.contains(&ItemType::Audio));
401 assert!(types.contains(&ItemType::Sample));
402 assert!(types.contains(&ItemType::Preset));
403 assert!(!types.contains(&ItemType::Text));
404 }
405
406 #[test]
407 fn project_feature_allowed_item_types_downloads() {
408 let types = ProjectFeature::Downloads.allowed_item_types();
409 assert!(types.contains(&ItemType::Digital));
410 assert!(types.contains(&ItemType::Plugin));
411 assert!(types.contains(&ItemType::Video));
412 assert!(!types.contains(&ItemType::Audio));
413 }
414
415 #[test]
416 fn project_feature_allowed_item_types_text() {
417 let types = ProjectFeature::Text.allowed_item_types();
418 assert!(types.contains(&ItemType::Text));
419 assert_eq!(types.len(), 1);
420 }
421
422 #[test]
423 fn project_feature_allowed_item_types_non_content() {
424 assert!(ProjectFeature::Blog.allowed_item_types().is_empty());
425 assert!(
426 ProjectFeature::Subscriptions
427 .allowed_item_types()
428 .is_empty()
429 );
430 assert!(ProjectFeature::LicenseKeys.allowed_item_types().is_empty());
431 assert!(ProjectFeature::SourceCode.allowed_item_types().is_empty());
432 assert!(ProjectFeature::CloudSync.allowed_item_types().is_empty());
433 }
434
435 #[test]
436 fn project_feature_allowed_cards_filtered() {
437 let cards = ProjectFeature::allowed_item_type_cards(&["audio".into()]);
438 let values: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect();
439 assert!(values.contains(&"audio"));
440 assert!(values.contains(&"sample"));
441 assert!(values.contains(&"preset"));
442 assert!(values.contains(&"bundle")); // Bundle always included
443 assert!(!values.contains(&"text"));
444 assert!(!values.contains(&"digital"));
445 }
446
447 #[test]
448 fn project_feature_allowed_cards_combined() {
449 let cards = ProjectFeature::allowed_item_type_cards(&["audio".into(), "text".into()]);
450 let values: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect();
451 assert!(values.contains(&"audio"));
452 assert!(values.contains(&"text"));
453 assert!(values.contains(&"bundle")); // Bundle always included
454 assert!(!values.contains(&"digital"));
455 }
456
457 #[test]
458 fn project_feature_allowed_cards_empty_features_shows_all() {
459 let cards = ProjectFeature::allowed_item_type_cards(&[]);
460 assert_eq!(cards.len(), 11); // 10 content types + bundle
461 }
462
463 #[test]
464 fn project_feature_allowed_cards_non_content_features_shows_all() {
465 let cards = ProjectFeature::allowed_item_type_cards(&["blog".into(), "subscriptions".into()]);
466 // Blog and subscriptions don't gate item types, so all should be shown
467 assert_eq!(cards.len(), 11); // 10 content types + bundle
468 }
469
470 #[test]
471 fn project_feature_derive_type() {
472 assert_eq!(
473 ProjectFeature::derive_project_type(&["audio".into(), "blog".into()]),
474 ProjectType::Music,
475 );
476 assert_eq!(
477 ProjectFeature::derive_project_type(&["text".into()]),
478 ProjectType::Blog,
479 );
480 assert_eq!(
481 ProjectFeature::derive_project_type(&["downloads".into(), "text".into()]),
482 ProjectType::Software,
483 );
484 assert_eq!(
485 ProjectFeature::derive_project_type(&["subscriptions".into()]),
486 ProjectType::General,
487 );
488 }
489
490 #[test]
491 fn project_type_round_trip() {
492 assert_eq!(ProjectType::Blog.to_string(), "blog");
493 assert_eq!(
494 "software".parse::<ProjectType>().unwrap(),
495 ProjectType::Software
496 );
497 assert_eq!(
498 "general".parse::<ProjectType>().unwrap(),
499 ProjectType::General
500 );
501 assert_eq!(ProjectType::default(), ProjectType::General);
502 assert!("bogus".parse::<ProjectType>().is_err());
503 }
504
505 #[test]
506 fn project_type_label() {
507 assert_eq!(ProjectType::Blog.label(), "Blog");
508 assert_eq!(ProjectType::Software.label(), "Software");
509 assert_eq!(ProjectType::General.label(), "General");
510 }
511
512 #[test]
513 fn project_type_all() {
514 let all = ProjectType::all();
515 assert_eq!(all.len(), 9);
516 assert_eq!(all[0], ("blog", "Blog"));
517 assert_eq!(all[8], ("general", "General"));
518 }
519
520 #[test]
521 fn build_status_round_trip() {
522 assert_eq!(BuildStatus::Pending.to_string(), "pending");
523 assert_eq!(
524 "running".parse::<BuildStatus>().unwrap(),
525 BuildStatus::Running
526 );
527 assert_eq!(
528 "succeeded".parse::<BuildStatus>().unwrap(),
529 BuildStatus::Succeeded
530 );
531 assert_eq!(
532 "failed".parse::<BuildStatus>().unwrap(),
533 BuildStatus::Failed
534 );
535 assert_eq!(
536 "cancelled".parse::<BuildStatus>().unwrap(),
537 BuildStatus::Cancelled
538 );
539 assert!("bogus".parse::<BuildStatus>().is_err());
540 }
541
542 #[test]
543 fn serde_json_round_trip() {
544 let dt = DiscountType::Percentage;
545 let json = serde_json::to_string(&dt).unwrap();
546 assert_eq!(json, "\"percentage\"");
547 let back: DiscountType = serde_json::from_str(&json).unwrap();
548 assert_eq!(back, dt);
549 }
550
551 #[test]
552 fn pricing_kind_round_trip() {
553 assert_eq!(PricingKind::Free.to_string(), "free");
554 assert_eq!(
555 "buy_once".parse::<PricingKind>().unwrap(),
556 PricingKind::BuyOnce
557 );
558 assert_eq!("pwyw".parse::<PricingKind>().unwrap(), PricingKind::Pwyw);
559 assert_eq!(
560 "subscription".parse::<PricingKind>().unwrap(),
561 PricingKind::Subscription
562 );
563 assert_eq!(PricingKind::default(), PricingKind::Free);
564 assert!("bogus".parse::<PricingKind>().is_err());
565 }
566
567 #[test]
568 fn mailing_list_type_round_trip() {
569 assert_eq!(MailingListType::Content.to_string(), "content");
570 assert_eq!(
571 "devlog".parse::<MailingListType>().unwrap(),
572 MailingListType::Devlog
573 );
574 assert_eq!(
575 "patches".parse::<MailingListType>().unwrap(),
576 MailingListType::Patches
577 );
578 assert!("bogus".parse::<MailingListType>().is_err());
579 }
580
581 #[test]
582 fn serde_json_subscription_status() {
583 let s = SubscriptionStatus::PastDue;
584 let json = serde_json::to_string(&s).unwrap();
585 assert_eq!(json, "\"past_due\"");
586 let back: SubscriptionStatus = serde_json::from_str(&json).unwrap();
587 assert_eq!(back, s);
588
589 let t = SubscriptionStatus::Trialing;
590 let json = serde_json::to_string(&t).unwrap();
591 assert_eq!(json, "\"trialing\"");
592 let back: SubscriptionStatus = serde_json::from_str(&json).unwrap();
593 assert_eq!(back, t);
594 }
595
596 // --- ItemType::wizard_group ---
597
598 #[test]
599 fn wizard_group_text() {
600 assert_eq!(ItemType::Text.wizard_group(), "text");
601 }
602
603 #[test]
604 fn wizard_group_audio() {
605 assert_eq!(ItemType::Audio.wizard_group(), "audio");
606 }
607
608 #[test]
609 fn wizard_group_video() {
610 assert_eq!(ItemType::Video.wizard_group(), "video");
611 }
612
613 #[test]
614 fn wizard_group_file_types() {
615 for t in [
616 ItemType::Digital,
617 ItemType::Course,
618 ItemType::Plugin,
619 ItemType::Sample,
620 ItemType::Preset,
621 ItemType::Template,
622 ItemType::Image,
623 ] {
624 assert_eq!(t.wizard_group(), "file", "{t:?} should be in file group");
625 }
626 }
627
628 #[test]
629 fn wizard_group_bundle() {
630 assert_eq!(ItemType::Bundle.wizard_group(), "bundle");
631 }
632
633 // --- ProjectFeature::wizard_type_cards ---
634
635 #[test]
636 fn wizard_cards_text_only_two_groups() {
637 // Text + bundle (bundle always included)
638 let cards = ProjectFeature::wizard_type_cards(&["text".into()]);
639 assert_eq!(cards.len(), 2);
640 let groups: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect();
641 assert!(groups.contains(&"text"));
642 assert!(groups.contains(&"bundle"));
643 }
644
645 #[test]
646 fn wizard_cards_downloads_three_groups() {
647 // Download types split into "file" + "video" groups + bundle → 3 cards
648 let cards = ProjectFeature::wizard_type_cards(&["downloads".into()]);
649 assert_eq!(cards.len(), 3);
650 let groups: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect();
651 assert!(groups.contains(&"digital")); // first type in file group
652 assert!(groups.contains(&"video")); // video group
653 assert!(groups.contains(&"bundle"));
654 }
655
656 #[test]
657 fn wizard_cards_audio_feature_three_groups() {
658 // Audio feature allows audio (audio group) + sample, preset (file group) + bundle
659 let cards = ProjectFeature::wizard_type_cards(&["audio".into()]);
660 assert_eq!(cards.len(), 3);
661 let groups: Vec<&str> = cards.iter().map(|(v, _, _)| *v).collect();
662 assert!(groups.contains(&"audio"));
663 assert!(groups.contains(&"sample")); // first file-group type
664 assert!(groups.contains(&"bundle"));
665 }
666
667 #[test]
668 fn wizard_cards_text_and_audio_four_groups() {
669 let cards = ProjectFeature::wizard_type_cards(&["text".into(), "audio".into()]);
670 assert_eq!(cards.len(), 4); // text, audio, file, bundle
671 }
672
673 #[test]
674 fn wizard_cards_empty_features_all_five_groups() {
675 // No content features → all types → 5 wizard groups (text, audio, video, file, bundle)
676 let cards = ProjectFeature::wizard_type_cards(&[]);
677 assert_eq!(cards.len(), 5);
678 }
679
680 #[test]
681 fn ai_tier_round_trip() {
682 assert_eq!(AiTier::Handmade.to_string(), "handmade");
683 assert_eq!("assisted".parse::<AiTier>().unwrap(), AiTier::Assisted);
684 assert_eq!("generated".parse::<AiTier>().unwrap(), AiTier::Generated);
685 assert!("bogus".parse::<AiTier>().is_err());
686 }
687
688 #[test]
689 fn ai_tier_label() {
690 assert_eq!(AiTier::Handmade.label(), "Handmade");
691 assert_eq!(AiTier::Assisted.label(), "Assisted");
692 assert_eq!(AiTier::Generated.label(), "Generated");
693 }
694
695 #[test]
696 fn import_source_round_trip() {
697 assert_eq!(ImportSource::GenericCsv.to_string(), "generic_csv");
698 assert_eq!(
699 "substack".parse::<ImportSource>().unwrap(),
700 ImportSource::Substack
701 );
702 assert_eq!(
703 "ghost".parse::<ImportSource>().unwrap(),
704 ImportSource::Ghost
705 );
706 assert_eq!(
707 "gumroad".parse::<ImportSource>().unwrap(),
708 ImportSource::Gumroad
709 );
710 assert_eq!(
711 "bandcamp".parse::<ImportSource>().unwrap(),
712 ImportSource::Bandcamp
713 );
714 assert_eq!(
715 "lemon_squeezy".parse::<ImportSource>().unwrap(),
716 ImportSource::LemonSqueezy
717 );
718 assert_eq!(
719 "patreon".parse::<ImportSource>().unwrap(),
720 ImportSource::Patreon
721 );
722 assert!("bogus".parse::<ImportSource>().is_err());
723 }
724
725 #[test]
726 fn import_job_status_round_trip() {
727 assert_eq!(ImportJobStatus::Pending.to_string(), "pending");
728 assert_eq!(
729 "processing".parse::<ImportJobStatus>().unwrap(),
730 ImportJobStatus::Processing
731 );
732 assert_eq!(
733 "completed".parse::<ImportJobStatus>().unwrap(),
734 ImportJobStatus::Completed
735 );
736 assert_eq!(
737 "failed".parse::<ImportJobStatus>().unwrap(),
738 ImportJobStatus::Failed
739 );
740 assert!("bogus".parse::<ImportJobStatus>().is_err());
741 }
742
743 #[test]
744 fn checkout_type_round_trip() {
745 assert_eq!(CheckoutType::Guest.to_string(), "guest");
746 assert_eq!(CheckoutType::Subscription.to_string(), "subscription");
747 assert_eq!(CheckoutType::Tip.to_string(), "tip");
748 assert_eq!(CheckoutType::FanPlus.to_string(), "fan_plus");
749 assert_eq!(CheckoutType::CreatorTier.to_string(), "creator_tier");
750 assert_eq!(
751 "guest".parse::<CheckoutType>().unwrap(),
752 CheckoutType::Guest
753 );
754 assert_eq!(
755 "fan_plus".parse::<CheckoutType>().unwrap(),
756 CheckoutType::FanPlus
757 );
758 assert!("bogus".parse::<CheckoutType>().is_err());
759 }
760
761 #[test]
762 fn moderation_action_type_round_trip() {
763 assert_eq!(ModerationActionType::Warning.to_string(), "warning");
764 assert_eq!(ModerationActionType::Suspension.to_string(), "suspension");
765 assert_eq!(ModerationActionType::Termination.to_string(), "termination");
766 assert_eq!(
767 ModerationActionType::ContentRemoval.to_string(),
768 "content_removal"
769 );
770 assert_eq!(
771 "warning".parse::<ModerationActionType>().unwrap(),
772 ModerationActionType::Warning
773 );
774 assert_eq!(
775 "content_removal".parse::<ModerationActionType>().unwrap(),
776 ModerationActionType::ContentRemoval
777 );
778 assert!("bogus".parse::<ModerationActionType>().is_err());
779 }
780
781 /// Every `impl_str_enum!` enum that also derives serde must agree on its wire
782 /// string across BOTH representations: the macro's Display/FromStr/sqlx form
783 /// (`VARIANTS`) and serde's JSON form. These are defined in two places (the
784 /// macro literal and the `#[serde(rename_all = ...)]` attribute), so they can
785 /// silently diverge, a variant travels via Display today and via serde
786 /// tomorrow, and the string disagrees with what is already in Postgres. This
787 /// test drives every serde-deriving enum through its `VARIANTS` (which round
788 /// through `FromStr`) and asserts serde emits the identical string, in both
789 /// directions. Add every serde-deriving enum here.
790 macro_rules! assert_serde_matches_wire {
791 ($($enum:ty),+ $(,)?) => {{
792 $(
793 for wire in <$enum>::VARIANTS {
794 let value: $enum = wire.parse().unwrap_or_else(|e| {
795 panic!("{}: VARIANTS string {wire:?} does not parse: {e}", stringify!($enum))
796 });
797 let json = serde_json::to_string(&value).expect("serialize");
798 assert_eq!(
799 json,
800 format!("\"{wire}\""),
801 "{}: serde emits {json} but the DB/Display wire string is {wire:?} \
802 (add or fix `#[serde(rename_all = ...)]`)",
803 stringify!($enum)
804 );
805 let back: $enum = serde_json::from_str(&json).unwrap_or_else(|e| {
806 panic!("{}: serde cannot round-trip {json}: {e}", stringify!($enum))
807 });
808 assert_eq!(
809 back.to_string(),
810 *wire,
811 "{}: serde deserialize of {json} disagrees with Display",
812 stringify!($enum)
813 );
814 }
815 )+
816 }};
817 }
818
819 #[test]
820 fn serde_repr_matches_db_wire_repr_for_every_enum() {
821 assert_serde_matches_wire!(
822 DiscountType,
823 CodePurpose,
824 WaitlistStatus,
825 SelectionMethod,
826 TransactionStatus,
827 FollowTargetType,
828 SubscriptionStatus,
829 SyncBillingStatus,
830 SyncEnforcementMode,
831 Visibility,
832 GitRepoKind,
833 ProjectRole,
834 SyncOperation,
835 SyncPlatform,
836 FileScanStatus,
837 InsertionPosition,
838 AppealDecision,
839 DiscoverSort,
840 ItemType,
841 IssueStatus,
842 ReportTargetType,
843 ReportType,
844 ReportStatus,
845 CreatorTier,
846 AiTier,
847 ProjectFeature,
848 ProjectType,
849 BuildStatus,
850 PricingKind,
851 MailingListType,
852 ImportSource,
853 ImportJobStatus,
854 ModerationActionType,
855 CheckoutType,
856 );
857 }
858