Skip to main content

max / makenotwork

7.3 KB · 260 lines History Blame Raw
1 //! Sandbox workflow: ephemeral account creation, feature restrictions, visibility rules.
2
3 use crate::harness::TestHarness;
4 use makenotwork::constants::SANDBOX_MAX_PER_IP;
5
6 /// Helper: create a sandbox account via POST /sandbox.
7 /// The client must have a CSRF token (fetched from GET /sandbox).
8 /// Returns the response from POST /sandbox (should be a 302 redirect).
9 async fn create_sandbox(h: &mut TestHarness) -> crate::harness::client::TestResponse {
10 // GET /sandbox to establish session + extract CSRF token
11 let resp = h.client.get("/sandbox").await;
12 assert_eq!(
13 resp.status, 200,
14 "GET /sandbox failed: {} {}",
15 resp.status, resp.text
16 );
17
18 // POST /sandbox to create the account (new session, CSRF regenerated)
19 let resp = h.client.post_form("/sandbox", "").await;
20
21 // Fetch a page to pick up the new CSRF token for the fresh session
22 let _ = h.client.get("/library").await;
23
24 resp
25 }
26
27 /// Look up the sandbox username from the DB (most recently created sandbox_ user).
28 async fn sandbox_username(h: &TestHarness) -> String {
29 sqlx::query_scalar::<_, String>(
30 "SELECT username FROM users WHERE username LIKE 'sandbox_%' ORDER BY created_at DESC LIMIT 1",
31 )
32 .fetch_one(&h.db)
33 .await
34 .expect("No sandbox user found")
35 }
36
37 #[tokio::test]
38 async fn create_sandbox_account() {
39 let mut h = TestHarness::new().await;
40
41 let resp = create_sandbox(&mut h).await;
42 assert!(
43 resp.status.is_redirection(),
44 "POST /sandbox should redirect, got {}",
45 resp.status
46 );
47
48 // Should be able to access dashboard as the sandbox user
49 let resp = h.client.get("/dashboard").await;
50 assert_eq!(
51 resp.status, 200,
52 "Dashboard should be accessible after sandbox creation"
53 );
54 }
55
56 #[tokio::test]
57 async fn sandbox_blocks_restricted_endpoints() {
58 let mut h = TestHarness::new().await;
59 create_sandbox(&mut h).await;
60
61 // Custom domains
62 let resp = h
63 .client
64 .post_form("/api/domains", "domain=sandbox.example.com")
65 .await;
66 assert_eq!(
67 resp.status, 403,
68 "Sandbox: POST /api/domains should be 403, got {}",
69 resp.status
70 );
71
72 // Git repos
73 let resp = h
74 .client
75 .post_json("/api/repos", r#"{"name": "test-repo"}"#)
76 .await;
77 assert_eq!(
78 resp.status, 403,
79 "Sandbox: POST /api/repos should be 403, got {}",
80 resp.status
81 );
82
83 // Imports
84 let resp = h.client.post_json(
85 "/api/users/me/import",
86 r#"{"project_id": "00000000-0000-0000-0000-000000000000", "source": "generic_csv", "csv_data": "ZW1haWwKdGVzdEB0ZXN0LmNvbQo=", "column_mapping": {"email": 0}}"#,
87 ).await;
88 assert_eq!(
89 resp.status, 403,
90 "Sandbox: POST /api/users/me/import should be 403, got {}",
91 resp.status
92 );
93
94 // Guest purchase claim
95 let resp = h
96 .client
97 .post_json(
98 "/api/purchases/claim",
99 r#"{"claim_token": "00000000-0000-0000-0000-000000000000"}"#,
100 )
101 .await;
102 assert_eq!(
103 resp.status, 403,
104 "Sandbox: POST /api/purchases/claim should be 403, got {}",
105 resp.status
106 );
107 }
108
109 #[tokio::test]
110 async fn sandbox_content_not_visible_on_item_page() {
111 let mut h = TestHarness::new().await;
112 create_sandbox(&mut h).await;
113
114 // Create a project
115 let resp = h
116 .client
117 .post_form("/api/projects", "slug=sandbox-proj&title=Sandbox+Project")
118 .await;
119 assert_eq!(
120 resp.status, 200,
121 "Create project failed: {} {}",
122 resp.status, resp.text
123 );
124 let project: serde_json::Value = resp.json();
125 let project_id = project["id"].as_str().unwrap();
126
127 // Publish project
128 h.client
129 .put_json(
130 &format!("/api/projects/{project_id}"),
131 r#"{"is_public": true}"#,
132 )
133 .await;
134
135 // Create an item
136 let resp = h
137 .client
138 .post_form(
139 &format!("/api/projects/{project_id}/items"),
140 "title=Sandbox+Item&item_type=digital&price_cents=0",
141 )
142 .await;
143 assert_eq!(
144 resp.status, 200,
145 "Create item failed: {} {}",
146 resp.status, resp.text
147 );
148 let item: serde_json::Value = resp.json();
149 let item_id = item["id"].as_str().unwrap();
150
151 // Publish the item
152 h.client
153 .put_form(&format!("/api/items/{item_id}"), "is_public=true")
154 .await;
155
156 // Use a second harness (unauthenticated client) to visit the item page
157 let mut h2 = TestHarness::new().await;
158 let resp = h2.client.get(&format!("/i/{item_id}")).await;
159 assert_eq!(
160 resp.status, 404,
161 "Sandbox item should return 404 to unauthenticated visitor, got {}",
162 resp.status
163 );
164 }
165
166 #[tokio::test]
167 async fn sandbox_rss_returns_404() {
168 let mut h = TestHarness::new().await;
169 create_sandbox(&mut h).await;
170
171 let username = sandbox_username(&h).await;
172
173 let resp = h.client.get(&format!("/u/{username}/rss")).await;
174 assert_eq!(
175 resp.status, 404,
176 "Sandbox user RSS feed should return 404, got {}",
177 resp.status
178 );
179 }
180
181 #[tokio::test]
182 async fn sandbox_per_ip_cap() {
183 let mut h = TestHarness::new().await;
184
185 // Create SANDBOX_MAX_PER_IP sandboxes without logging out.
186 // The cap counts concurrent active sessions per IP, logout deletes
187 // the session row, which would defeat the count.
188 for i in 0..SANDBOX_MAX_PER_IP {
189 let resp = create_sandbox(&mut h).await;
190 assert!(
191 resp.status.is_redirection(),
192 "Sandbox {} should succeed, got {}",
193 i + 1,
194 resp.status
195 );
196 }
197
198 // Try one more, should be rejected (cap reached)
199 let resp = h.client.get("/sandbox").await;
200 assert_eq!(resp.status, 200, "{}", resp.text);
201
202 let resp = h.client.post_form("/sandbox", "").await;
203 assert_eq!(
204 resp.status, 400,
205 "Sandbox beyond per-IP cap should return 400, got {}",
206 resp.status
207 );
208 }
209
210 #[tokio::test]
211 async fn sandbox_blog_no_email() {
212 let mut h = TestHarness::with_mocks().await;
213 create_sandbox(&mut h).await;
214
215 // Clear any emails from sandbox creation
216 h.mock_email.as_ref().unwrap().clear();
217
218 // Create a project
219 let resp = h
220 .client
221 .post_form("/api/projects", "slug=sb-blog&title=Sandbox+Blog")
222 .await;
223 assert_eq!(
224 resp.status, 200,
225 "Create project failed: {} {}",
226 resp.status, resp.text
227 );
228 let project: serde_json::Value = resp.json();
229 let project_id = project["id"].as_str().unwrap();
230
231 // Publish project
232 h.client
233 .put_json(
234 &format!("/api/projects/{project_id}"),
235 r#"{"is_public": true}"#,
236 )
237 .await;
238
239 // Create and immediately publish a blog post
240 let resp = h
241 .client
242 .post_json(
243 &format!("/api/projects/{project_id}/blog"),
244 r#"{"title": "Sandbox Post", "body_markdown": "Hello from sandbox!", "is_published": true}"#,
245 )
246 .await;
247 assert_eq!(
248 resp.status, 200,
249 "Create blog post failed: {} {}",
250 resp.status, resp.text
251 );
252
253 // No emails should have been sent (sandbox skips announcements)
254 let count = h.mock_email.as_ref().unwrap().count();
255 assert_eq!(
256 count, 0,
257 "Sandbox blog publish should send 0 emails, got {count}"
258 );
259 }
260