Skip to main content

max / makenotwork

26.4 KB · 902 lines History Blame Raw
1 //! Integration tests for project and item creation wizards.
2
3 use crate::harness::TestHarness;
4
5 // =============================================================================
6 // Project Wizard
7 // =============================================================================
8
9 #[tokio::test]
10 async fn project_wizard_full_flow() {
11 let mut h = TestHarness::new().await;
12 let _user_id = h.create_creator("pwiz").await;
13
14 // Load wizard page
15 let resp = h.client.get("/dashboard/new-project").await;
16 assert_eq!(resp.status, 200);
17 assert!(resp.text.contains("Basics"), "Should show step 1");
18
19 // Step 1: Basics — creates project
20 let resp = h
21 .client
22 .post_form(
23 "/dashboard/new-project/step/basics",
24 "title=Wizard+Test&slug=wizard-test&project_type=blog&description=A+test+project",
25 )
26 .await;
27 assert!(resp.status.is_success(), "Step 1 failed: {}", resp.text);
28 assert!(
29 resp.text.contains("Appearance"),
30 "Should advance to step 2"
31 );
32
33 // Verify project created in DB
34 let exists: bool = sqlx::query_scalar(
35 "SELECT EXISTS(SELECT 1 FROM projects WHERE slug = 'wizard-test')",
36 )
37 .fetch_one(&h.db)
38 .await
39 .unwrap();
40 assert!(exists, "Project should exist");
41
42 // Step 2: Appearance — skip (no cover image)
43 let resp = h
44 .client
45 .post_form("/dashboard/new-project/wizard-test/step/appearance", "")
46 .await;
47 assert!(resp.status.is_success(), "Step 2 failed: {}", resp.text);
48 assert!(
49 resp.text.contains("Monetization"),
50 "Should advance to step 3"
51 );
52
53 // Step 3: Monetization — skip (no tiers)
54 let resp = h
55 .client
56 .post_form(
57 "/dashboard/new-project/wizard-test/step/monetization",
58 "",
59 )
60 .await;
61 assert!(resp.status.is_success(), "Step 3 failed: {}", resp.text);
62
63 // Step 4: First content — skip
64 let resp = h
65 .client
66 .post_form(
67 "/dashboard/new-project/wizard-test/step/first-content",
68 "",
69 )
70 .await;
71 assert!(resp.status.is_success(), "Step 4 failed: {}", resp.text);
72
73 // Step 5: Preview — publish
74 let resp = h
75 .client
76 .post_form(
77 "/dashboard/new-project/wizard-test/step/preview",
78 "action=publish",
79 )
80 .await;
81 // Preview step returns HX-Redirect
82 assert!(
83 resp.headers
84 .get("hx-redirect")
85 .is_some_and(|v| v.to_str().unwrap().contains("wizard-test")),
86 "Should redirect to project dashboard"
87 );
88
89 // Verify project is public (publish action confirms is_public = true)
90 let is_public: bool =
91 sqlx::query_scalar("SELECT is_public FROM projects WHERE slug = 'wizard-test'")
92 .fetch_one(&h.db)
93 .await
94 .unwrap();
95 assert!(is_public, "Project should be published");
96 }
97
98 #[tokio::test]
99 async fn project_wizard_save_as_draft() {
100 let mut h = TestHarness::new().await;
101 let _user_id = h.create_creator("pdraft").await;
102
103 // Step 1: Create project
104 h.client
105 .post_form(
106 "/dashboard/new-project/step/basics",
107 "title=Draft+Project&slug=draft-proj&project_type=music",
108 )
109 .await;
110
111 // Skip through steps 2-4
112 h.client
113 .post_form("/dashboard/new-project/draft-proj/step/appearance", "")
114 .await;
115 h.client
116 .post_form(
117 "/dashboard/new-project/draft-proj/step/monetization",
118 "",
119 )
120 .await;
121 h.client
122 .post_form(
123 "/dashboard/new-project/draft-proj/step/first-content",
124 "",
125 )
126 .await;
127
128 // Step 5: Save as draft (redirects to project dashboard without changing state)
129 let resp = h
130 .client
131 .post_form(
132 "/dashboard/new-project/draft-proj/step/preview",
133 "action=draft",
134 )
135 .await;
136 assert!(
137 resp.headers
138 .get("hx-redirect")
139 .is_some_and(|v| v.to_str().unwrap().contains("draft-proj")),
140 "Should redirect to project dashboard"
141 );
142
143 // Verify project exists
144 let exists: bool = sqlx::query_scalar(
145 "SELECT EXISTS(SELECT 1 FROM projects WHERE slug = 'draft-proj')",
146 )
147 .fetch_one(&h.db)
148 .await
149 .unwrap();
150 assert!(exists, "Draft project should exist");
151 }
152
153 // =============================================================================
154 // Item Wizard
155 // =============================================================================
156
157 #[tokio::test]
158 async fn item_wizard_full_flow() {
159 let mut h = TestHarness::new().await;
160 let _user_id = h.create_creator("iwiz").await;
161
162 // Create a project first via API
163 let resp = h
164 .client
165 .post_form("/api/projects", "slug=iwiz-proj&title=Item+Wizard+Test")
166 .await;
167 assert!(resp.status.is_success());
168
169 // Load item wizard page
170 let resp = h.client.get("/dashboard/project/iwiz-proj/new-item").await;
171 assert_eq!(resp.status, 200);
172 assert!(resp.text.contains("Type"), "Should show step 1 (type)");
173
174 // Step 1: Type — creates item
175 let resp = h
176 .client
177 .post_form(
178 "/dashboard/project/iwiz-proj/new-item/step/type",
179 "item_type=text",
180 )
181 .await;
182 assert!(resp.status.is_success(), "Step 1 failed: {}", resp.text);
183 assert!(resp.text.contains("Basics"), "Should advance to step 2 (basics)");
184
185 // Extract item ID from DB
186 let item_id: String = sqlx::query_scalar(
187 "SELECT i.id::text FROM items i JOIN projects p ON i.project_id = p.id WHERE p.slug = 'iwiz-proj' ORDER BY i.created_at DESC LIMIT 1",
188 )
189 .fetch_one(&h.db)
190 .await
191 .unwrap();
192
193 // Step 2: Basics
194 let resp = h
195 .client
196 .post_form(
197 &format!(
198 "/dashboard/project/iwiz-proj/new-item/{}/step/basics",
199 item_id
200 ),
201 "title=My+First+Article&description=A+great+article",
202 )
203 .await;
204 assert!(resp.status.is_success(), "Step 2 failed: {}", resp.text);
205 assert!(resp.text.contains("Content"), "Should advance to step 3 (content)");
206
207 // Step 3: Content (text body)
208 let resp = h
209 .client
210 .post_form(
211 &format!(
212 "/dashboard/project/iwiz-proj/new-item/{}/step/content",
213 item_id
214 ),
215 "body=Hello+world",
216 )
217 .await;
218 assert!(resp.status.is_success(), "Step 3 failed: {}", resp.text);
219 assert!(resp.text.contains("Pricing"), "Should advance to step 4 (pricing)");
220
221 // Step 4: Pricing (free)
222 let resp = h
223 .client
224 .post_form(
225 &format!(
226 "/dashboard/project/iwiz-proj/new-item/{}/step/pricing",
227 item_id
228 ),
229 "pricing_model=free",
230 )
231 .await;
232 assert!(resp.status.is_success(), "Step 4 failed: {}", resp.text);
233
234 // Step 5: Preview — publish
235 let resp = h
236 .client
237 .post_form(
238 &format!(
239 "/dashboard/project/iwiz-proj/new-item/{}/step/preview",
240 item_id
241 ),
242 "action=publish",
243 )
244 .await;
245 assert!(
246 resp.headers.get("hx-redirect").is_some(),
247 "Should redirect after publish"
248 );
249
250 // Verify item is published
251 let is_public: bool = sqlx::query_scalar(&format!(
252 "SELECT is_public FROM items WHERE id = '{}'",
253 item_id
254 ))
255 .fetch_one(&h.db)
256 .await
257 .unwrap();
258 assert!(is_public, "Item should be published");
259 }
260
261 #[tokio::test]
262 async fn item_wizard_pricing_models() {
263 let mut h = TestHarness::new().await;
264 let _user_id = h.create_creator("ipricing").await;
265
266 let resp = h
267 .client
268 .post_form(
269 "/api/projects",
270 "slug=ipricing-proj&title=Pricing+Test",
271 )
272 .await;
273 assert!(resp.status.is_success());
274
275 // Create item via wizard step 1
276 h.client
277 .post_form(
278 "/dashboard/project/ipricing-proj/new-item/step/type",
279 "item_type=digital",
280 )
281 .await;
282
283 let item_id: String = sqlx::query_scalar(
284 "SELECT i.id::text FROM items i JOIN projects p ON i.project_id = p.id WHERE p.slug = 'ipricing-proj' ORDER BY i.created_at DESC LIMIT 1",
285 )
286 .fetch_one(&h.db)
287 .await
288 .unwrap();
289
290 // Test fixed pricing
291 let resp = h
292 .client
293 .post_form(
294 &format!(
295 "/dashboard/project/ipricing-proj/new-item/{}/step/pricing",
296 item_id
297 ),
298 "pricing_model=fixed&price=9.99",
299 )
300 .await;
301 assert!(resp.status.is_success(), "Fixed pricing failed");
302
303 let price: i32 = sqlx::query_scalar(&format!(
304 "SELECT price_cents FROM items WHERE id = '{}'",
305 item_id
306 ))
307 .fetch_one(&h.db)
308 .await
309 .unwrap();
310 assert_eq!(price, 999, "Price should be 999 cents");
311
312 // Navigate back to pricing and test PWYW
313 let resp = h
314 .client
315 .post_form(
316 &format!(
317 "/dashboard/project/ipricing-proj/new-item/{}/step/pricing",
318 item_id
319 ),
320 "pricing_model=pwyw&suggested_price=5.00&min_price=1.00",
321 )
322 .await;
323 assert!(resp.status.is_success(), "PWYW pricing failed");
324
325 let (pwyw_enabled, price_cents, min_cents): (bool, i32, Option<i32>) = sqlx::query_as(&format!(
326 "SELECT pwyw_enabled, price_cents, pwyw_min_cents FROM items WHERE id = '{}'",
327 item_id
328 ))
329 .fetch_one(&h.db)
330 .await
331 .unwrap();
332 assert!(pwyw_enabled, "PWYW should be enabled");
333 assert_eq!(price_cents, 500, "Suggested price should be 500 cents");
334 assert_eq!(min_cents, Some(100), "Min price should be 100 cents");
335
336 // Test free pricing
337 let resp = h
338 .client
339 .post_form(
340 &format!(
341 "/dashboard/project/ipricing-proj/new-item/{}/step/pricing",
342 item_id
343 ),
344 "pricing_model=free",
345 )
346 .await;
347 assert!(resp.status.is_success(), "Free pricing failed");
348
349 let (price, pwyw): (i32, bool) = sqlx::query_as(&format!(
350 "SELECT price_cents, pwyw_enabled FROM items WHERE id = '{}'",
351 item_id
352 ))
353 .fetch_one(&h.db)
354 .await
355 .unwrap();
356 assert_eq!(price, 0, "Price should be 0 for free");
357 assert!(!pwyw, "PWYW should be disabled");
358 }
359
360 #[tokio::test]
361 async fn item_wizard_schedule_publish() {
362 let mut h = TestHarness::new().await;
363 let _user_id = h.create_creator("isched").await;
364
365 let resp = h
366 .client
367 .post_form(
368 "/api/projects",
369 "slug=isched-proj&title=Schedule+Test",
370 )
371 .await;
372 assert!(resp.status.is_success());
373
374 // Create item
375 h.client
376 .post_form(
377 "/dashboard/project/isched-proj/new-item/step/type",
378 "item_type=audio",
379 )
380 .await;
381
382 let item_id: String = sqlx::query_scalar(
383 "SELECT i.id::text FROM items i JOIN projects p ON i.project_id = p.id WHERE p.slug = 'isched-proj' LIMIT 1",
384 )
385 .fetch_one(&h.db)
386 .await
387 .unwrap();
388
389 // Schedule publish via preview step
390 let resp = h
391 .client
392 .post_form(
393 &format!(
394 "/dashboard/project/isched-proj/new-item/{}/step/preview",
395 item_id
396 ),
397 "action=schedule&publish_at=2030-01-15T10%3A00",
398 )
399 .await;
400 assert!(
401 resp.headers.get("hx-redirect").is_some(),
402 "Should redirect after schedule"
403 );
404
405 // Verify publish_at is set
406 let has_publish_at: bool = sqlx::query_scalar(&format!(
407 "SELECT publish_at IS NOT NULL FROM items WHERE id = '{}'",
408 item_id
409 ))
410 .fetch_one(&h.db)
411 .await
412 .unwrap();
413 assert!(has_publish_at, "publish_at should be set for scheduled item");
414 }
415
416 // =============================================================================
417 // Auth & Access Control
418 // =============================================================================
419
420 #[tokio::test]
421 async fn wizard_auth_required() {
422 let mut h = TestHarness::new().await;
423
424 // Unauthenticated access should redirect to login
425 let resp = h.client.get("/dashboard/new-project").await;
426 assert!(
427 resp.status.is_redirection() || resp.status == 401 || resp.status == 403,
428 "Unauthenticated user should not access wizard, got {}",
429 resp.status
430 );
431
432 let resp = h
433 .client
434 .get("/dashboard/project/anything/new-item")
435 .await;
436 assert!(
437 resp.status.is_redirection() || resp.status == 401 || resp.status == 403,
438 "Unauthenticated user should not access item wizard, got {}",
439 resp.status
440 );
441 }
442
443 #[tokio::test]
444 async fn wizard_ownership_check() {
445 let mut h = TestHarness::new().await;
446
447 // Creator A creates a project
448 let _user_a = h.create_creator("wizown_a").await;
449 let resp = h
450 .client
451 .post_form("/api/projects", "slug=owned-proj&title=Owned")
452 .await;
453 assert!(resp.status.is_success());
454
455 // Create item via wizard
456 h.client
457 .post_form(
458 "/dashboard/project/owned-proj/new-item/step/type",
459 "item_type=text",
460 )
461 .await;
462 let item_id: String = sqlx::query_scalar(
463 "SELECT i.id::text FROM items i JOIN projects p ON i.project_id = p.id WHERE p.slug = 'owned-proj' LIMIT 1",
464 )
465 .fetch_one(&h.db)
466 .await
467 .unwrap();
468
469 // Creator B logs in
470 h.client.post_form("/logout", "").await;
471 let _user_b = h.create_creator("wizown_b").await;
472
473 // Creator B should not access A's item wizard step
474 let resp = h
475 .client
476 .get(&format!(
477 "/dashboard/project/owned-proj/new-item/{}/step/details",
478 item_id
479 ))
480 .await;
481 assert!(
482 resp.status == 404 || resp.status == 403,
483 "Non-owner should get 404 or 403, got {}",
484 resp.status
485 );
486
487 // Creator B should not access A's project wizard step
488 let resp = h
489 .client
490 .get("/dashboard/new-project/owned-proj/step/appearance")
491 .await;
492 assert!(
493 resp.status == 404 || resp.status == 403,
494 "Non-owner should not access other's project wizard, got {}",
495 resp.status
496 );
497 }
498
499 #[tokio::test]
500 async fn wizard_back_navigation() {
501 let mut h = TestHarness::new().await;
502 let _user_id = h.create_creator("wback").await;
503
504 // Create project via wizard
505 let resp = h
506 .client
507 .post_form(
508 "/dashboard/new-project/step/basics",
509 "title=Back+Nav&slug=back-nav&project_type=general",
510 )
511 .await;
512 assert!(resp.status.is_success());
513
514 // Go forward to monetization
515 h.client
516 .post_form("/dashboard/new-project/back-nav/step/appearance", "")
517 .await;
518
519 // Navigate back to basics step via GET
520 let resp = h
521 .client
522 .get("/dashboard/new-project/back-nav/step/basics")
523 .await;
524 assert!(resp.status.is_success(), "GET basics step failed: {} {}", resp.status, resp.text);
525 assert!(
526 resp.text.contains("Back Nav"),
527 "Should show saved title when navigating back"
528 );
529
530 // Navigate back to appearance
531 let resp = h
532 .client
533 .get("/dashboard/new-project/back-nav/step/appearance")
534 .await;
535 assert!(resp.status.is_success());
536 }
537
538 // =============================================================================
539 // Wizard Features
540 // =============================================================================
541
542 #[tokio::test]
543 async fn item_wizard_license_keys() {
544 let mut h = TestHarness::new().await;
545 let _user_id = h.create_creator("ilickey").await;
546
547 let resp = h
548 .client
549 .post_form("/api/projects", "slug=lickey-proj&title=License+Test")
550 .await;
551 assert!(resp.status.is_success());
552
553 // Create plugin item (license keys relevant for this type)
554 h.client
555 .post_form(
556 "/dashboard/project/lickey-proj/new-item/step/type",
557 "item_type=plugin",
558 )
559 .await;
560
561 let item_id: String = sqlx::query_scalar(
562 "SELECT i.id::text FROM items i JOIN projects p ON i.project_id = p.id WHERE p.slug = 'lickey-proj' LIMIT 1",
563 )
564 .fetch_one(&h.db)
565 .await
566 .unwrap();
567
568 // Enable license keys via the API (no longer a wizard step)
569 let resp = h
570 .client
571 .put_form(
572 &format!("/api/items/{}/license-settings", item_id),
573 "enable_license_keys=on&default_max_activations=5",
574 )
575 .await;
576 assert!(
577 resp.status.is_success(),
578 "License settings update failed: {}",
579 resp.text
580 );
581
582 // Verify license settings saved
583 let (enabled, max_act): (bool, Option<i32>) = sqlx::query_as(&format!(
584 "SELECT enable_license_keys, default_max_activations FROM items WHERE id = '{}'",
585 item_id
586 ))
587 .fetch_one(&h.db)
588 .await
589 .unwrap();
590 assert!(enabled, "License keys should be enabled");
591 assert_eq!(max_act, Some(5), "Max activations should be 5");
592 }
593
594 #[tokio::test]
595 async fn project_wizard_subscription_tiers() {
596 let mut h = TestHarness::new().await;
597 let _user_id = h.create_creator("ptiers").await;
598
599 // Create project via wizard
600 h.client
601 .post_form(
602 "/dashboard/new-project/step/basics",
603 "title=Tier+Test&slug=tier-test&project_type=podcast",
604 )
605 .await;
606
607 // Skip appearance
608 h.client
609 .post_form("/dashboard/new-project/tier-test/step/appearance", "")
610 .await;
611
612 // Create tiers in monetization step
613 let resp = h
614 .client
615 .post_form(
616 "/dashboard/new-project/tier-test/step/monetization",
617 "tier_name_0=Basic&tier_price_0=5.00&tier_desc_0=Basic+access&tier_name_1=Pro&tier_price_1=15.00&tier_desc_1=Full+access",
618 )
619 .await;
620 assert!(
621 resp.status.is_success(),
622 "Monetization step failed: {}",
623 resp.text
624 );
625
626 // Verify tiers created
627 let tier_count: i64 = sqlx::query_scalar(
628 "SELECT COUNT(*) FROM subscription_tiers t JOIN projects p ON t.project_id = p.id WHERE p.slug = 'tier-test'",
629 )
630 .fetch_one(&h.db)
631 .await
632 .unwrap();
633 assert_eq!(tier_count, 2, "Should have created 2 tiers");
634 }
635
636 // =============================================================================
637 // Join Wizard
638 // =============================================================================
639
640 #[tokio::test]
641 async fn join_wizard_full_flow() {
642 let mut h = TestHarness::new().await;
643 h.client.fetch_csrf_token().await;
644
645 // Load wizard page
646 let resp = h.client.get("/join").await;
647 assert_eq!(resp.status, 200);
648 assert!(resp.text.contains("Create account"), "Should show step 1");
649 assert!(resp.text.contains("wizard-steps"), "Should have step nav");
650
651 // Step 1: Account — creates user and logs in
652 let resp = h
653 .client
654 .post_form(
655 "/join/step/account",
656 "username=jwiz&email=jwiz@test.com&password=testpassword123",
657 )
658 .await;
659 assert!(resp.status.is_success(), "Step 1 failed: {}", resp.text);
660 assert!(resp.text.contains("Profile"), "Should advance to step 2");
661
662 // Verify user created in DB
663 let _user_id: makenotwork::db::UserId =
664 sqlx::query_scalar("SELECT id FROM users WHERE username = 'jwiz'")
665 .fetch_one(&h.db)
666 .await
667 .unwrap();
668
669 // Step 2: Profile — update display_name and bio
670 let resp = h
671 .client
672 .post_form(
673 "/join/step/profile",
674 "display_name=Join+Wizard&bio=Testing+the+wizard",
675 )
676 .await;
677 assert!(resp.status.is_success(), "Step 2 failed: {}", resp.text);
678 // Profile now goes directly to welcome
679 assert!(
680 resp.text.contains("Welcome"),
681 "Should advance to welcome step"
682 );
683
684 // Verify profile saved
685 let display_name: Option<String> =
686 sqlx::query_scalar("SELECT display_name FROM users WHERE username = 'jwiz'")
687 .fetch_one(&h.db)
688 .await
689 .unwrap();
690 assert_eq!(display_name.as_deref(), Some("Join Wizard"));
691
692 // Welcome page should have intent branching
693 assert!(
694 resp.text.contains("Browse and buy") || resp.text.contains("I want to sell"),
695 "Should show intent options"
696 );
697 }
698
699 #[tokio::test]
700 async fn join_wizard_skip_all_optional() {
701 let mut h = TestHarness::new().await;
702 h.client.fetch_csrf_token().await;
703
704 // Step 1: Create account
705 let resp = h
706 .client
707 .post_form(
708 "/join/step/account",
709 "username=jskip&email=jskip@test.com&password=testpassword123",
710 )
711 .await;
712 assert!(resp.status.is_success(), "Step 1 failed: {}", resp.text);
713
714 // Skip directly to complete
715 let resp = h.client.get("/join/step/complete").await;
716 assert!(resp.status.is_success());
717 assert!(resp.text.contains("Welcome"));
718
719 // Verify user exists, no profile update, no waitlist entry
720 let display_name: Option<String> =
721 sqlx::query_scalar("SELECT display_name FROM users WHERE username = 'jskip'")
722 .fetch_one(&h.db)
723 .await
724 .unwrap();
725 assert!(display_name.is_none(), "Display name should not be set");
726
727 let has_waitlist: bool = sqlx::query_scalar(
728 "SELECT EXISTS(SELECT 1 FROM creator_waitlist WHERE user_id = (SELECT id FROM users WHERE username = 'jskip'))",
729 )
730 .fetch_one(&h.db)
731 .await
732 .unwrap();
733 assert!(!has_waitlist, "No waitlist entry should exist");
734 }
735
736 #[tokio::test]
737 async fn join_wizard_with_invite_code() {
738 let mut h = TestHarness::new().await;
739
740 // Create a creator who can issue invites
741 let creator_id = h.create_creator("jinviter").await;
742
743 // Create an invite code directly in DB
744 sqlx::query(
745 "INSERT INTO invite_codes (creator_id, code) VALUES ($1, 'TESTCODE')",
746 )
747 .bind(creator_id)
748 .execute(&h.db)
749 .await
750 .unwrap();
751
752 // Log out the creator
753 h.client.post_form("/logout", "").await;
754 h.client.fetch_csrf_token().await;
755
756 // Load join page with invite code
757 let resp = h.client.get("/join?invite=TESTCODE").await;
758 assert_eq!(resp.status, 200);
759 assert!(resp.text.contains("invited"), "Should show invite notice");
760
761 // Sign up with invite code
762 let resp = h
763 .client
764 .post_form(
765 "/join/step/account",
766 "username=jinvitee&email=jinvitee@test.com&password=testpassword123&invite_code=TESTCODE",
767 )
768 .await;
769 assert!(resp.status.is_success(), "Signup with invite failed: {}", resp.text);
770
771 // Verify invite redeemed
772 let redeemed: bool = sqlx::query_scalar(
773 "SELECT redeemed_by_id IS NOT NULL FROM invite_codes WHERE code = 'TESTCODE'",
774 )
775 .fetch_one(&h.db)
776 .await
777 .unwrap();
778 assert!(redeemed, "Invite should be redeemed");
779
780 // Verify waitlist entry created (invited type)
781 let has_waitlist: bool = sqlx::query_scalar(
782 "SELECT EXISTS(SELECT 1 FROM creator_waitlist WHERE user_id = (SELECT id FROM users WHERE username = 'jinvitee') AND selection_method = 'invited')",
783 )
784 .fetch_one(&h.db)
785 .await
786 .unwrap();
787 assert!(has_waitlist, "Invited waitlist entry should exist");
788 }
789
790 #[tokio::test]
791 async fn join_wizard_validation_errors() {
792 let mut h = TestHarness::new().await;
793 h.client.fetch_csrf_token().await;
794
795 // Short password (HTMX: returns error fragment with 200; non-HTMX: returns 422)
796 let resp = h
797 .client
798 .htmx_post_form(
799 "/join/step/account",
800 "username=jval&email=jval@test.com&password=short",
801 )
802 .await;
803 assert!(resp.status.is_success());
804 assert!(
805 resp.text.contains("8 characters"),
806 "Should show password error"
807 );
808
809 // Bad email
810 let resp = h
811 .client
812 .htmx_post_form(
813 "/join/step/account",
814 "username=jval&email=bademail&password=testpassword123",
815 )
816 .await;
817 assert!(resp.status.is_success());
818 assert!(
819 resp.text.contains("valid email"),
820 "Should show email error"
821 );
822
823 // Create a user, then try duplicate username
824 h.signup("jexisting", "jexisting@test.com", "testpassword123").await;
825 h.client.post_form("/logout", "").await;
826 h.client.fetch_csrf_token().await;
827
828 let resp = h
829 .client
830 .htmx_post_form(
831 "/join/step/account",
832 "username=jexisting&email=other@test.com&password=testpassword123",
833 )
834 .await;
835 assert!(resp.status.is_success());
836 assert!(
837 resp.text.contains("already taken"),
838 "Should show duplicate username error"
839 );
840 }
841
842 #[tokio::test]
843 async fn join_wizard_auth_required_after_step1() {
844 let mut h = TestHarness::new().await;
845
846 // Unauthenticated GET to step 2 should redirect/fail
847 let resp = h.client.get("/join/step/profile").await;
848 assert!(
849 resp.status.is_redirection() || resp.status == 401 || resp.status == 403,
850 "Unauthenticated step access should fail, got {}",
851 resp.status
852 );
853
854 // Unauthenticated POST to step 2 should redirect/fail
855 let resp = h
856 .client
857 .post_form("/join/step/profile", "display_name=test")
858 .await;
859 assert!(
860 resp.status.is_redirection() || resp.status == 401 || resp.status == 403,
861 "Unauthenticated step POST should fail, got {}",
862 resp.status
863 );
864 }
865
866 #[tokio::test]
867 async fn join_wizard_redirect_if_logged_in() {
868 let mut h = TestHarness::new().await;
869 h.signup("jloggedin", "jloggedin@test.com", "testpassword123").await;
870
871 // GET /join while logged in should redirect to /dashboard
872 let resp = h.client.get("/join").await;
873 assert!(
874 resp.status.is_redirection(),
875 "Logged-in user should be redirected, got {}",
876 resp.status
877 );
878 }
879
880 #[tokio::test]
881 async fn join_wizard_removed_steps_return_404() {
882 let mut h = TestHarness::new().await;
883 h.client.fetch_csrf_token().await;
884
885 // Create account
886 let resp = h
887 .client
888 .post_form(
889 "/join/step/account",
890 "username=jpitch&email=jpitch@test.com&password=testpassword123",
891 )
892 .await;
893 assert!(resp.status.is_success());
894
895 // Pitch and stripe steps no longer exist (removed in onboarding overhaul)
896 let resp = h.client.get("/join/step/pitch").await;
897 assert_eq!(resp.status, 404, "Pitch step should be removed");
898
899 let resp = h.client.get("/join/step/stripe").await;
900 assert_eq!(resp.status, 404, "Stripe step should be removed");
901 }
902