Skip to main content

max / makenotwork

9.9 KB · 358 lines History Blame Raw
1 //! Custom domain CRUD, caddy-ask, fallback routing, and item slug tests.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 #[tokio::test]
7 async fn add_custom_domain() {
8 let mut h = TestHarness::new().await;
9 let _uid = h.create_creator("domuser").await;
10
11 let resp = h
12 .client
13 .post_form("/api/domains", "domain=mysite.example.com")
14 .await;
15 assert!(
16 resp.status.is_success(),
17 "Add domain failed: {} {}",
18 resp.status,
19 resp.text
20 );
21
22 // Handler returns HTML with DNS verification instructions
23 assert!(
24 resp.text.contains("_mnw-verify.mysite.example.com"),
25 "Response should contain DNS verification instructions: {}",
26 resp.text
27 );
28 }
29
30 #[tokio::test]
31 async fn add_domain_rejects_duplicate() {
32 let mut h = TestHarness::new().await;
33 let _uid = h.create_creator("domdup1").await;
34
35 let resp = h
36 .client
37 .post_form("/api/domains", "domain=dup.example.com")
38 .await;
39 assert!(resp.status.is_success());
40
41 // Second user tries the same domain
42 h.client.post_form("/logout", "").await;
43 let _uid2 = h.create_creator("domdup2").await;
44
45 let resp = h
46 .client
47 .post_form("/api/domains", "domain=dup.example.com")
48 .await;
49 assert!(
50 !resp.status.is_success(),
51 "Duplicate domain should fail: {} {}",
52 resp.status,
53 resp.text
54 );
55 }
56
57 #[tokio::test]
58 async fn add_domain_one_per_user_limit() {
59 let mut h = TestHarness::new().await;
60 let _uid = h.create_creator("domlimit").await;
61
62 let resp = h
63 .client
64 .post_form("/api/domains", "domain=first.example.com")
65 .await;
66 assert!(resp.status.is_success());
67
68 // Second domain should fail
69 let resp = h
70 .client
71 .post_form("/api/domains", "domain=second.example.com")
72 .await;
73 assert!(
74 !resp.status.is_success(),
75 "Second domain should be rejected: {} {}",
76 resp.status,
77 resp.text
78 );
79 }
80
81 #[tokio::test]
82 async fn get_domain_returns_null_when_none() {
83 let mut h = TestHarness::new().await;
84 let _uid = h.create_creator("domget").await;
85
86 let resp = h.client.get("/api/domains").await;
87 assert!(resp.status.is_success());
88 let body: Value = resp.json();
89 assert!(body.is_null(), "Expected null when no domain, got: {}", body);
90 }
91
92 #[tokio::test]
93 async fn get_domain_returns_domain() {
94 let mut h = TestHarness::new().await;
95 let _uid = h.create_creator("domgetok").await;
96
97 h.client
98 .post_form("/api/domains", "domain=getme.example.com")
99 .await;
100
101 let resp = h.client.get("/api/domains").await;
102 assert!(resp.status.is_success());
103 let body: Value = resp.json();
104 assert_eq!(body["domain"].as_str().unwrap(), "getme.example.com");
105 }
106
107 #[tokio::test]
108 async fn remove_domain() {
109 let mut h = TestHarness::new().await;
110 let _uid = h.create_creator("domrm").await;
111
112 let resp = h
113 .client
114 .post_form("/api/domains", "domain=remove.example.com")
115 .await;
116 assert!(resp.status.is_success());
117
118 // GET the domain to retrieve its id (GET returns JSON)
119 let resp = h.client.get("/api/domains").await;
120 let body: Value = resp.json();
121 let id = body["id"].as_str().unwrap();
122
123 let resp = h.client.delete(&format!("/api/domains/{}", id)).await;
124 assert_eq!(resp.status, 204);
125
126 // Verify it's gone
127 let resp = h.client.get("/api/domains").await;
128 let body: Value = resp.json();
129 assert!(body.is_null());
130 }
131
132 #[tokio::test]
133 async fn caddy_ask_unknown_domain_returns_404() {
134 let mut h = TestHarness::new().await;
135
136 let resp = h
137 .client
138 .get("/api/domains/caddy-ask?domain=unknown.example.com")
139 .await;
140 assert_eq!(resp.status, 404);
141 }
142
143 #[tokio::test]
144 async fn caddy_ask_verified_domain_returns_200() {
145 let mut h = TestHarness::new().await;
146 let uid = h.create_creator("domcaddy").await;
147
148 // Insert a verified domain directly via SQL
149 sqlx::query(
150 "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at)
151 VALUES ($1, 'caddy.example.com', true, 'tok', NOW())",
152 )
153 .bind(uid)
154 .execute(&h.db)
155 .await
156 .unwrap();
157
158 // Also insert into domain cache (simulating startup warm)
159 // We can't access the cache directly, but the caddy-ask endpoint has a DB fallback
160 let resp = h
161 .client
162 .get("/api/domains/caddy-ask?domain=caddy.example.com")
163 .await;
164 assert_eq!(resp.status, 200);
165 }
166
167 #[tokio::test]
168 async fn custom_domain_fallback_user_profile() {
169 let mut h = TestHarness::new().await;
170 let uid = h.create_creator("domprofile").await;
171
172 // Insert verified domain directly
173 sqlx::query(
174 "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at)
175 VALUES ($1, 'profile.example.com', true, 'tok', NOW())",
176 )
177 .bind(uid)
178 .execute(&h.db)
179 .await
180 .unwrap();
181
182 // Insert into domain cache via caddy-ask (triggers DB fallback + cache populate)
183 h.client
184 .get("/api/domains/caddy-ask?domain=profile.example.com")
185 .await;
186
187 // Request the root path with the custom Host header
188 let resp = h
189 .client
190 .request_with_headers("GET", "/", None, &[("Host", "profile.example.com")])
191 .await;
192 assert!(
193 resp.status.is_success(),
194 "Profile fallback failed: {} {}",
195 resp.status,
196 resp.text
197 );
198 assert!(
199 resp.text.contains("domprofile"),
200 "Profile page should contain username"
201 );
202 }
203
204 #[tokio::test]
205 async fn custom_domain_fallback_project() {
206 let mut h = TestHarness::new().await;
207 let uid = h.create_creator("domproj").await;
208
209 // Create a project
210 let resp = h
211 .client
212 .post_form("/api/projects", "slug=test-project&title=Test+Project")
213 .await;
214 assert!(resp.status.is_success(), "Create project: {} {}", resp.status, resp.text);
215 let proj: Value = resp.json();
216 let project_id = proj["id"].as_str().unwrap();
217
218 // Publish project
219 h.client
220 .put_json(
221 &format!("/api/projects/{}", project_id),
222 r#"{"is_public": true}"#,
223 )
224 .await;
225
226 // Insert verified domain + warm cache
227 sqlx::query(
228 "INSERT INTO custom_domains (user_id, domain, verified, verification_token, verified_at)
229 VALUES ($1, 'proj.example.com', true, 'tok', NOW())",
230 )
231 .bind(uid)
232 .execute(&h.db)
233 .await
234 .unwrap();
235
236 h.client
237 .get("/api/domains/caddy-ask?domain=proj.example.com")
238 .await;
239
240 let resp = h
241 .client
242 .request_with_headers("GET", "/test-project", None, &[("Host", "proj.example.com")])
243 .await;
244 assert!(
245 resp.status.is_success(),
246 "Project fallback failed: {} {}",
247 resp.status,
248 resp.text
249 );
250 assert!(
251 resp.text.contains("Test Project"),
252 "Project page should contain title"
253 );
254 }
255
256 #[tokio::test]
257 async fn custom_domain_fallback_mnw_domain_returns_404() {
258 let mut h = TestHarness::new().await;
259
260 // A request with makenot.work Host to an unknown path should 404 (not trigger fallback)
261 let resp = h
262 .client
263 .request_with_headers("GET", "/nonexistent-path-xyz", None, &[("Host", "makenot.work")])
264 .await;
265 assert_eq!(
266 resp.status, 404,
267 "MNW domain unmatched path should 404, got {}",
268 resp.status
269 );
270 }
271
272 #[tokio::test]
273 async fn item_slug_auto_generated_on_create() {
274 let mut h = TestHarness::new().await;
275 let setup = h
276 .create_creator_with_item("domslug", "digital", 500)
277 .await;
278
279 // Verify the item has a slug
280 let row: (String,) = sqlx::query_as("SELECT slug FROM items WHERE id = $1::uuid")
281 .bind(&setup.item_id)
282 .fetch_one(&h.db)
283 .await
284 .unwrap();
285 assert!(!row.0.is_empty(), "Item slug should be non-empty");
286 assert_eq!(row.0, "test-item", "Item slug should be derived from title 'Test Item'");
287 }
288
289 #[tokio::test]
290 async fn item_slug_collision_handling() {
291 let mut h = TestHarness::new().await;
292 let _uid = h.create_creator("domcoll").await;
293
294 let resp = h
295 .client
296 .post_form("/api/projects", "slug=coll-proj&title=Collision+Project")
297 .await;
298 assert!(resp.status.is_success());
299 let proj: Value = resp.json();
300 let pid = proj["id"].as_str().unwrap();
301
302 // Create two items with the same title
303 let resp = h
304 .client
305 .post_form(
306 &format!("/api/projects/{}/items", pid),
307 "title=Same+Title&item_type=digital&price_cents=0",
308 )
309 .await;
310 assert!(resp.status.is_success(), "First item: {} {}", resp.status, resp.text);
311
312 let resp = h
313 .client
314 .post_form(
315 &format!("/api/projects/{}/items", pid),
316 "title=Same+Title&item_type=digital&price_cents=0",
317 )
318 .await;
319 assert!(resp.status.is_success(), "Second item: {} {}", resp.status, resp.text);
320
321 // Verify both have unique slugs
322 let slugs: Vec<(String,)> = sqlx::query_as(
323 "SELECT slug FROM items WHERE project_id = $1::uuid ORDER BY created_at",
324 )
325 .bind(pid)
326 .fetch_all(&h.db)
327 .await
328 .unwrap();
329
330 assert_eq!(slugs.len(), 2);
331 assert_ne!(slugs[0].0, slugs[1].0, "Slugs should be unique: {:?}", slugs);
332 assert!(slugs[1].0.starts_with("same-title"), "Second slug should start with 'same-title'");
333 }
334
335 #[tokio::test]
336 async fn add_domain_rejects_mnw_domains() {
337 let mut h = TestHarness::new().await;
338 let _uid = h.create_creator("dommnw").await;
339
340 let resp = h
341 .client
342 .post_form("/api/domains", "domain=makenot.work")
343 .await;
344 assert!(
345 !resp.status.is_success(),
346 "makenot.work should be rejected"
347 );
348
349 let resp = h
350 .client
351 .post_form("/api/domains", "domain=sub.makenot.work")
352 .await;
353 assert!(
354 !resp.status.is_success(),
355 "sub.makenot.work should be rejected"
356 );
357 }
358