//! Custom domain CRUD, caddy-ask, fallback routing, and item slug tests. use crate::harness::TestHarness; use serde_json::Value; #[tokio::test] async fn add_custom_domain() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domuser").await; let resp = h .client .post_form("/api/domains", "domain=mysite.example.com") .await; assert!( resp.status.is_success(), "Add domain failed: {} {}", resp.status, resp.text ); // Handler returns HTML with DNS verification instructions assert!( resp.text.contains("_mnw-verify.mysite.example.com"), "Response should contain DNS verification instructions: {}", resp.text ); } #[tokio::test] async fn add_domain_rejects_duplicate() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domdup1").await; let resp = h .client .post_form("/api/domains", "domain=dup.example.com") .await; assert!(resp.status.is_success()); // Second user tries the same domain h.client.post_form("/logout", "").await; let _uid2 = h.create_creator("domdup2").await; let resp = h .client .post_form("/api/domains", "domain=dup.example.com") .await; assert!( !resp.status.is_success(), "Duplicate domain should fail: {} {}", resp.status, resp.text ); } #[tokio::test] async fn add_domain_one_per_user_limit() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domlimit").await; let resp = h .client .post_form("/api/domains", "domain=first.example.com") .await; assert!(resp.status.is_success()); // Second domain should fail let resp = h .client .post_form("/api/domains", "domain=second.example.com") .await; assert!( !resp.status.is_success(), "Second domain should be rejected: {} {}", resp.status, resp.text ); } #[tokio::test] async fn get_domain_returns_null_when_none() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domget").await; let resp = h.client.get("/api/domains").await; assert!(resp.status.is_success()); let body: Value = resp.json(); assert!(body.is_null(), "Expected null when no domain, got: {}", body); } #[tokio::test] async fn get_domain_returns_domain() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domgetok").await; h.client .post_form("/api/domains", "domain=getme.example.com") .await; let resp = h.client.get("/api/domains").await; assert!(resp.status.is_success()); let body: Value = resp.json(); assert_eq!(body["domain"].as_str().unwrap(), "getme.example.com"); } #[tokio::test] async fn remove_domain() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domrm").await; let resp = h .client .post_form("/api/domains", "domain=remove.example.com") .await; assert!(resp.status.is_success()); // GET the domain to retrieve its id (GET returns JSON) let resp = h.client.get("/api/domains").await; let body: Value = resp.json(); let id = body["id"].as_str().unwrap(); let resp = h.client.delete(&format!("/api/domains/{}", id)).await; assert_eq!(resp.status, 204); // Verify it's gone let resp = h.client.get("/api/domains").await; let body: Value = resp.json(); assert!(body.is_null()); } #[tokio::test] async fn caddy_ask_unknown_domain_returns_404() { let mut h = TestHarness::new().await; let resp = h .client .get("/api/domains/caddy-ask?domain=unknown.example.com") .await; assert_eq!(resp.status, 404); } #[tokio::test] async fn caddy_ask_verified_domain_returns_200() { let mut h = TestHarness::new().await; let uid = h.create_creator("domcaddy").await; // Insert a verified domain directly via SQL sqlx::query( "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at) VALUES ($1, 'caddy.example.com', true, 'tok', NOW())", ) .bind(uid) .execute(&h.db) .await .unwrap(); // Also insert into domain cache (simulating startup warm) // We can't access the cache directly, but the caddy-ask endpoint has a DB fallback let resp = h .client .get("/api/domains/caddy-ask?domain=caddy.example.com") .await; assert_eq!(resp.status, 200); } #[tokio::test] async fn custom_domain_fallback_user_profile() { let mut h = TestHarness::new().await; let uid = h.create_creator("domprofile").await; // Insert verified domain directly sqlx::query( "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at) VALUES ($1, 'profile.example.com', true, 'tok', NOW())", ) .bind(uid) .execute(&h.db) .await .unwrap(); // Insert into domain cache via caddy-ask (triggers DB fallback + cache populate) h.client .get("/api/domains/caddy-ask?domain=profile.example.com") .await; // Request the root path with the custom Host header let resp = h .client .request_with_headers("GET", "/", None, &[("Host", "profile.example.com")]) .await; assert!( resp.status.is_success(), "Profile fallback failed: {} {}", resp.status, resp.text ); assert!( resp.text.contains("domprofile"), "Profile page should contain username" ); } #[tokio::test] async fn custom_domain_fallback_project() { let mut h = TestHarness::new().await; let uid = h.create_creator("domproj").await; // Create a project let resp = h .client .post_form("/api/projects", "slug=test-project&title=Test+Project") .await; assert!(resp.status.is_success(), "Create project: {} {}", resp.status, resp.text); let proj: Value = resp.json(); let project_id = proj["id"].as_str().unwrap(); // Publish project h.client .put_json( &format!("/api/projects/{}", project_id), r#"{"is_public": true}"#, ) .await; // Insert verified domain + warm cache sqlx::query( "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at) VALUES ($1, 'proj.example.com', true, 'tok', NOW())", ) .bind(uid) .execute(&h.db) .await .unwrap(); h.client .get("/api/domains/caddy-ask?domain=proj.example.com") .await; let resp = h .client .request_with_headers("GET", "/test-project", None, &[("Host", "proj.example.com")]) .await; assert!( resp.status.is_success(), "Project fallback failed: {} {}", resp.status, resp.text ); assert!( resp.text.contains("Test Project"), "Project page should contain title" ); } #[tokio::test] async fn custom_domain_fallback_mnw_domain_returns_404() { let mut h = TestHarness::new().await; // A request with makenot.work Host to an unknown path should 404 (not trigger fallback) let resp = h .client .request_with_headers("GET", "/nonexistent-path-xyz", None, &[("Host", "makenot.work")]) .await; assert_eq!( resp.status, 404, "MNW domain unmatched path should 404, got {}", resp.status ); } #[tokio::test] async fn item_slug_auto_generated_on_create() { let mut h = TestHarness::new().await; let setup = h .create_creator_with_item("domslug", "digital", 500) .await; // Verify the item has a slug let row: (String,) = sqlx::query_as("SELECT slug FROM items WHERE id = $1::uuid") .bind(&setup.item_id) .fetch_one(&h.db) .await .unwrap(); assert!(!row.0.is_empty(), "Item slug should be non-empty"); assert_eq!(row.0, "test-item", "Item slug should be derived from title 'Test Item'"); } #[tokio::test] async fn item_slug_collision_handling() { let mut h = TestHarness::new().await; let _uid = h.create_creator("domcoll").await; let resp = h .client .post_form("/api/projects", "slug=coll-proj&title=Collision+Project") .await; assert!(resp.status.is_success()); let proj: Value = resp.json(); let pid = proj["id"].as_str().unwrap(); // Create two items with the same title let resp = h .client .post_form( &format!("/api/projects/{}/items", pid), "title=Same+Title&item_type=digital&price_cents=0", ) .await; assert!(resp.status.is_success(), "First item: {} {}", resp.status, resp.text); let resp = h .client .post_form( &format!("/api/projects/{}/items", pid), "title=Same+Title&item_type=digital&price_cents=0", ) .await; assert!(resp.status.is_success(), "Second item: {} {}", resp.status, resp.text); // Verify both have unique slugs let slugs: Vec<(String,)> = sqlx::query_as( "SELECT slug FROM items WHERE project_id = $1::uuid ORDER BY created_at", ) .bind(pid) .fetch_all(&h.db) .await .unwrap(); assert_eq!(slugs.len(), 2); assert_ne!(slugs[0].0, slugs[1].0, "Slugs should be unique: {:?}", slugs); assert!(slugs[1].0.starts_with("same-title"), "Second slug should start with 'same-title'"); } #[tokio::test] async fn add_domain_rejects_mnw_domains() { let mut h = TestHarness::new().await; let _uid = h.create_creator("dommnw").await; let resp = h .client .post_form("/api/domains", "domain=makenot.work") .await; assert!( !resp.status.is_success(), "makenot.work should be rejected" ); let resp = h .client .post_form("/api/domains", "domain=sub.makenot.work") .await; assert!( !resp.status.is_success(), "sub.makenot.work should be rejected" ); }