Skip to main content

max / makenotwork

21.0 KB · 659 lines History Blame Raw
1 //! Health and API integration tests
2 //!
3 //! These tests verify that the server endpoints are functioning correctly.
4 //! Run with: cargo test --test health
5
6 use reqwest::StatusCode;
7 use sqlx::PgPool;
8 use std::time::Duration;
9
10 /// Base URL for the test server
11 const BASE_URL: &str = "http://localhost:3000";
12
13 /// Test helper to make HTTP requests
14 struct TestClient {
15 client: reqwest::Client,
16 }
17
18 impl TestClient {
19 fn new() -> Self {
20 TestClient {
21 client: reqwest::Client::builder()
22 .timeout(Duration::from_secs(10))
23 .cookie_store(true)
24 .build()
25 .expect("Failed to create HTTP client"),
26 }
27 }
28
29 async fn get(&self, path: &str) -> reqwest::Result<reqwest::Response> {
30 self.client.get(format!("{}{}", BASE_URL, path)).send().await
31 }
32
33 async fn post_form(
34 &self,
35 path: &str,
36 form: &[(&str, &str)],
37 ) -> reqwest::Result<reqwest::Response> {
38 self.client
39 .post(format!("{}{}", BASE_URL, path))
40 .form(form)
41 .send()
42 .await
43 }
44 }
45
46 // =============================================================================
47 // Public Endpoint Tests
48 // =============================================================================
49
50 #[tokio::test]
51 async fn test_health_endpoint() {
52 let client = TestClient::new();
53
54 // Simple health check should return OK
55 let resp = client.get("/health").await;
56 match resp {
57 Ok(r) => {
58 assert_eq!(r.status(), StatusCode::OK, "Health endpoint should return 200");
59 let body = r.text().await.unwrap_or_default();
60 assert!(body.contains("System Health"), "Health page should contain title");
61 }
62 Err(e) => {
63 // Server might not be running - skip test
64 eprintln!("Server not available: {}. Skipping test.", e);
65 }
66 }
67 }
68
69 #[tokio::test]
70 async fn test_index_page() {
71 let client = TestClient::new();
72
73 let resp = client.get("/").await;
74 match resp {
75 Ok(r) => {
76 assert_eq!(r.status(), StatusCode::OK, "Index page should return 200");
77 let body = r.text().await.unwrap_or_default();
78 assert!(body.contains("Makenot"), "Index page should contain site name");
79 }
80 Err(e) => {
81 eprintln!("Server not available: {}. Skipping test.", e);
82 }
83 }
84 }
85
86 #[tokio::test]
87 async fn test_login_page() {
88 let client = TestClient::new();
89
90 let resp = client.get("/login").await;
91 match resp {
92 Ok(r) => {
93 assert_eq!(r.status(), StatusCode::OK, "Login page should return 200");
94 let body = r.text().await.unwrap_or_default();
95 assert!(body.contains("Log in"), "Login page should contain login form");
96 assert!(body.contains("csrf-token"), "Login page should have CSRF token");
97 }
98 Err(e) => {
99 eprintln!("Server not available: {}. Skipping test.", e);
100 }
101 }
102 }
103
104 #[tokio::test]
105 async fn test_join_page() {
106 let client = TestClient::new();
107
108 let resp = client.get("/join").await;
109 match resp {
110 Ok(r) => {
111 assert_eq!(r.status(), StatusCode::OK, "Join page should return 200");
112 let body = r.text().await.unwrap_or_default();
113 assert!(body.contains("Create"), "Join page should contain create form");
114 }
115 Err(e) => {
116 eprintln!("Server not available: {}. Skipping test.", e);
117 }
118 }
119 }
120
121 #[tokio::test]
122 async fn test_discover_page() {
123 let client = TestClient::new();
124
125 let resp = client.get("/discover").await;
126 match resp {
127 Ok(r) => {
128 assert_eq!(r.status(), StatusCode::OK, "Discover page should return 200");
129 let body = r.text().await.unwrap_or_default();
130 assert!(body.contains("Discover") || body.contains("discover"), "Discover page should contain discover content");
131 }
132 Err(e) => {
133 eprintln!("Server not available: {}. Skipping test.", e);
134 }
135 }
136 }
137
138 #[tokio::test]
139 async fn test_nonexistent_page_returns_404() {
140 let client = TestClient::new();
141
142 let resp = client.get("/this-page-does-not-exist-12345").await;
143 match resp {
144 Ok(r) => {
145 assert_eq!(r.status(), StatusCode::NOT_FOUND, "Nonexistent page should return 404");
146 }
147 Err(e) => {
148 eprintln!("Server not available: {}. Skipping test.", e);
149 }
150 }
151 }
152
153 // =============================================================================
154 // Auth Endpoint Tests
155 // =============================================================================
156
157 #[tokio::test]
158 async fn test_login_with_invalid_credentials() {
159 let client = TestClient::new();
160
161 let resp = client
162 .post_form("/login", &[
163 ("login", "nonexistent@example.com"),
164 ("password", "wrongpassword"),
165 ])
166 .await;
167
168 match resp {
169 Ok(r) => {
170 // Login is exempt from CSRF, so should get through
171 // Should return 200 with error message (HTMX), or 400 (API)
172 let status = r.status();
173 // Login now exempt from CSRF, so we should get actual response
174 assert!(
175 status == StatusCode::OK || status == StatusCode::BAD_REQUEST ||
176 status == StatusCode::UNAUTHORIZED,
177 "Invalid login should return error, got: {}", status
178 );
179 }
180 Err(e) => {
181 eprintln!("Server not available: {}. Skipping test.", e);
182 }
183 }
184 }
185
186 #[tokio::test]
187 async fn test_protected_route_requires_auth() {
188 let client = TestClient::new();
189
190 let resp = client.get("/dashboard").await;
191 match resp {
192 Ok(r) => {
193 // Should redirect to login or return unauthorized
194 let status = r.status();
195 assert!(
196 status == StatusCode::UNAUTHORIZED ||
197 status == StatusCode::SEE_OTHER ||
198 status == StatusCode::FOUND ||
199 status == StatusCode::TEMPORARY_REDIRECT,
200 "Dashboard should require authentication, got: {}", status
201 );
202 }
203 Err(e) => {
204 eprintln!("Server not available: {}. Skipping test.", e);
205 }
206 }
207 }
208
209 // =============================================================================
210 // API Endpoint Tests
211 // =============================================================================
212
213 #[tokio::test]
214 async fn test_username_validation_endpoint_exists() {
215 let client = TestClient::new();
216
217 // Username validation - may require CSRF for authenticated users
218 // Just verify endpoint responds (not 404 or 500)
219 let resp = client
220 .post_form("/api/validate/username", &[("username", "testuser")])
221 .await;
222
223 match resp {
224 Ok(r) => {
225 let status = r.status();
226 // Should not be 404 or 500 - may be 200 (valid), 400 (invalid), or 403 (CSRF)
227 assert!(
228 status != StatusCode::NOT_FOUND && status != StatusCode::INTERNAL_SERVER_ERROR,
229 "Username validation endpoint should exist and not error, got: {}", status
230 );
231 }
232 Err(e) => {
233 eprintln!("Server not available: {}. Skipping test.", e);
234 }
235 }
236 }
237
238 // =============================================================================
239 // Static File Tests
240 // =============================================================================
241
242 #[tokio::test]
243 async fn test_static_css_served() {
244 let client = TestClient::new();
245
246 let resp = client.get("/static/style.css").await;
247 match resp {
248 Ok(r) => {
249 assert_eq!(r.status(), StatusCode::OK, "Static CSS should be served");
250 let content_type = r.headers().get("content-type").map(|v| v.to_str().unwrap_or(""));
251 assert!(
252 content_type.map(|ct| ct.contains("css")).unwrap_or(false),
253 "CSS file should have CSS content type"
254 );
255 }
256 Err(e) => {
257 eprintln!("Server not available: {}. Skipping test.", e);
258 }
259 }
260 }
261
262 // =============================================================================
263 // Database Integration Tests (requires DATABASE_URL)
264 // =============================================================================
265
266 #[tokio::test]
267 async fn test_database_connection() {
268 // Only run if DATABASE_URL is set
269 let database_url = match std::env::var("DATABASE_URL") {
270 Ok(url) => url,
271 Err(_) => {
272 eprintln!("DATABASE_URL not set, skipping database test");
273 return;
274 }
275 };
276
277 let pool = PgPool::connect(&database_url).await;
278 match pool {
279 Ok(p) => {
280 // Test a simple query
281 let result: Result<(i64,), _> = sqlx::query_as("SELECT COUNT(*) FROM users")
282 .fetch_one(&p)
283 .await;
284
285 assert!(result.is_ok(), "Should be able to query users table");
286 }
287 Err(e) => {
288 panic!("Failed to connect to database: {}", e);
289 }
290 }
291 }
292
293 #[tokio::test]
294 async fn test_database_tables_exist() {
295 let database_url = match std::env::var("DATABASE_URL") {
296 Ok(url) => url,
297 Err(_) => {
298 eprintln!("DATABASE_URL not set, skipping database test");
299 return;
300 }
301 };
302
303 let pool = PgPool::connect(&database_url).await.expect("Failed to connect");
304
305 // Check that all expected tables exist (must match migrations 001-025).
306 // Note: the session table was renamed from `sessions` to `user_sessions`
307 // when tower-sessions-sqlx-store config was updated; the old name was left
308 // in this list and broke the assertion in fresh DBs.
309 let tables = vec![
310 "users", "projects", "items", "versions", "transactions",
311 "custom_links", "user_sessions", "blog_posts", "chapters",
312 "creator_waitlist", "creator_waves", "login_tokens",
313 "license_keys", "license_activations",
314 "sync_apps", "sync_devices", "sync_log", "sync_keys",
315 "oauth_authorization_codes",
316 ];
317
318 for table in tables {
319 let result: Result<(bool,), _> = sqlx::query_as(
320 "SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = $1)"
321 )
322 .bind(table)
323 .fetch_one(&pool)
324 .await;
325
326 match result {
327 Ok((exists,)) => {
328 assert!(exists, "Table '{}' should exist", table);
329 }
330 Err(e) => {
331 panic!("Failed to check table '{}': {}", table, e);
332 }
333 }
334 }
335 }
336
337 // =============================================================================
338 // Public Pages (additional)
339 // =============================================================================
340
341 #[tokio::test]
342 async fn test_policy_page() {
343 let client = TestClient::new();
344 let resp = client.get("/policy").await;
345 match resp {
346 Ok(r) => {
347 assert_eq!(r.status(), StatusCode::OK, "Policy page should return 200");
348 }
349 Err(e) => {
350 eprintln!("Server not available: {}. Skipping test.", e);
351 }
352 }
353 }
354
355 #[tokio::test]
356 async fn test_creators_page() {
357 let client = TestClient::new();
358 let resp = client.get("/creators").await;
359 match resp {
360 Ok(r) => {
361 assert_eq!(r.status(), StatusCode::OK, "Creators page should return 200");
362 }
363 Err(e) => {
364 eprintln!("Server not available: {}. Skipping test.", e);
365 }
366 }
367 }
368
369 // =============================================================================
370 // 404 for nonexistent content
371 // =============================================================================
372
373 #[tokio::test]
374 async fn test_nonexistent_user_returns_404() {
375 let client = TestClient::new();
376 let resp = client.get("/u/nonexistent_user_99999").await;
377 match resp {
378 Ok(r) => {
379 assert_eq!(r.status(), StatusCode::NOT_FOUND, "Nonexistent user should return 404");
380 }
381 Err(e) => {
382 eprintln!("Server not available: {}. Skipping test.", e);
383 }
384 }
385 }
386
387 #[tokio::test]
388 async fn test_nonexistent_project_returns_404() {
389 let client = TestClient::new();
390 let resp = client.get("/p/nonexistent-project-slug-99999").await;
391 match resp {
392 Ok(r) => {
393 assert_eq!(r.status(), StatusCode::NOT_FOUND, "Nonexistent project should return 404");
394 }
395 Err(e) => {
396 eprintln!("Server not available: {}. Skipping test.", e);
397 }
398 }
399 }
400
401 // =============================================================================
402 // Auth-required API endpoints reject unauthenticated
403 // =============================================================================
404
405 #[tokio::test]
406 async fn test_api_projects_requires_auth() {
407 let client = TestClient::new();
408 let resp = client.post_form("/api/projects", &[("title", "Test")]).await;
409 match resp {
410 Ok(r) => {
411 let status = r.status();
412 assert!(
413 status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN ||
414 status == StatusCode::SEE_OTHER || status == StatusCode::FOUND,
415 "POST /api/projects should require auth, got: {}", status
416 );
417 }
418 Err(e) => {
419 eprintln!("Server not available: {}. Skipping test.", e);
420 }
421 }
422 }
423
424 #[tokio::test]
425 async fn test_api_items_requires_auth() {
426 let client = TestClient::new();
427 let resp = client
428 .post_form(
429 "/api/projects/00000000-0000-0000-0000-000000000000/items",
430 &[("title", "Test")],
431 )
432 .await;
433 match resp {
434 Ok(r) => {
435 let status = r.status();
436 assert!(
437 status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN ||
438 status == StatusCode::SEE_OTHER || status == StatusCode::FOUND,
439 "POST /api/projects/:id/items should require auth, got: {}", status
440 );
441 }
442 Err(e) => {
443 eprintln!("Server not available: {}. Skipping test.", e);
444 }
445 }
446 }
447
448 #[tokio::test]
449 async fn test_api_export_projects_requires_auth() {
450 let client = TestClient::new();
451 let resp = client.post_form("/api/export/projects", &[]).await;
452 match resp {
453 Ok(r) => {
454 let status = r.status();
455 assert!(
456 status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN ||
457 status == StatusCode::SEE_OTHER || status == StatusCode::FOUND,
458 "POST /api/export/projects should require auth, got: {}", status
459 );
460 }
461 Err(e) => {
462 eprintln!("Server not available: {}. Skipping test.", e);
463 }
464 }
465 }
466
467 // =============================================================================
468 // Discover variants
469 // =============================================================================
470
471 #[tokio::test]
472 async fn test_discover_projects_mode() {
473 let client = TestClient::new();
474 let resp = client.get("/discover?mode=projects").await;
475 match resp {
476 Ok(r) => {
477 assert_eq!(r.status(), StatusCode::OK, "Discover projects mode should return 200");
478 }
479 Err(e) => {
480 eprintln!("Server not available: {}. Skipping test.", e);
481 }
482 }
483 }
484
485 #[tokio::test]
486 async fn test_discover_results_partial() {
487 let client = TestClient::new();
488 let resp = client.get("/discover/results").await;
489 match resp {
490 Ok(r) => {
491 assert_eq!(r.status(), StatusCode::OK, "Discover results partial should return 200");
492 }
493 Err(e) => {
494 eprintln!("Server not available: {}. Skipping test.", e);
495 }
496 }
497 }
498
499 // =============================================================================
500 // RSS 404s for nonexistent content
501 // =============================================================================
502
503 #[tokio::test]
504 async fn test_nonexistent_user_rss_returns_404() {
505 let client = TestClient::new();
506 let resp = client.get("/u/nonexistent_user_99999/rss").await;
507 match resp {
508 Ok(r) => {
509 assert_eq!(r.status(), StatusCode::NOT_FOUND, "Nonexistent user RSS should return 404");
510 }
511 Err(e) => {
512 eprintln!("Server not available: {}. Skipping test.", e);
513 }
514 }
515 }
516
517 #[tokio::test]
518 async fn test_nonexistent_project_rss_returns_404() {
519 let client = TestClient::new();
520 let resp = client.get("/p/nonexistent-project-slug-99999/rss").await;
521 match resp {
522 Ok(r) => {
523 assert_eq!(r.status(), StatusCode::NOT_FOUND, "Nonexistent project RSS should return 404");
524 }
525 Err(e) => {
526 eprintln!("Server not available: {}. Skipping test.", e);
527 }
528 }
529 }
530
531 // =============================================================================
532 // Username validation
533 // =============================================================================
534
535 #[tokio::test]
536 async fn test_username_validation_short_input() {
537 let client = TestClient::new();
538 let resp = client
539 .post_form("/api/validate/username", &[("username", "ab")])
540 .await;
541 match resp {
542 Ok(r) => {
543 let status = r.status();
544 assert!(
545 status != StatusCode::NOT_FOUND && status != StatusCode::INTERNAL_SERVER_ERROR,
546 "Short username validation should not be 404/500, got: {}", status
547 );
548 }
549 Err(e) => {
550 eprintln!("Server not available: {}. Skipping test.", e);
551 }
552 }
553 }
554
555 #[tokio::test]
556 async fn test_username_validation_invalid_chars() {
557 let client = TestClient::new();
558 let resp = client
559 .post_form("/api/validate/username", &[("username", "user@name!")])
560 .await;
561 match resp {
562 Ok(r) => {
563 let status = r.status();
564 assert!(
565 status != StatusCode::NOT_FOUND && status != StatusCode::INTERNAL_SERVER_ERROR,
566 "Invalid-chars username validation should not be 404/500, got: {}", status
567 );
568 }
569 Err(e) => {
570 eprintln!("Server not available: {}. Skipping test.", e);
571 }
572 }
573 }
574
575 // =============================================================================
576 // JSON Health Endpoint
577 // =============================================================================
578
579 #[tokio::test]
580 async fn test_api_health_json_endpoint() {
581 let client = TestClient::new();
582
583 let resp = client.get("/api/health").await;
584 match resp {
585 Ok(r) => {
586 let status = r.status();
587 assert!(
588 status == StatusCode::OK || status == StatusCode::SERVICE_UNAVAILABLE,
589 "JSON health endpoint should return 200 or 503, got: {}", status
590 );
591
592 let content_type = r.headers().get("content-type")
593 .and_then(|v| v.to_str().ok())
594 .unwrap_or("");
595 assert!(
596 content_type.contains("application/json"),
597 "JSON health endpoint should return application/json, got: {}", content_type
598 );
599
600 let body: serde_json::Value = r.json().await.expect("Should parse as JSON");
601 assert!(
602 body.get("status").is_some(),
603 "JSON response should have 'status' field"
604 );
605 assert!(
606 body.get("version").is_some(),
607 "JSON response should have 'version' field"
608 );
609 }
610 Err(e) => {
611 eprintln!("Server not available: {}. Skipping test.", e);
612 }
613 }
614 }
615
616 // =============================================================================
617 // Health self-check (verifies uptime field)
618 // =============================================================================
619
620 #[tokio::test]
621 async fn test_health_contains_uptime() {
622 let client = TestClient::new();
623 let resp = client.get("/health").await;
624 match resp {
625 Ok(r) => {
626 assert_eq!(r.status(), StatusCode::OK);
627 let body = r.text().await.unwrap_or_default();
628 assert!(
629 body.contains("Uptime:"),
630 "Health page should contain uptime field"
631 );
632 }
633 Err(e) => {
634 eprintln!("Server not available: {}. Skipping test.", e);
635 }
636 }
637 }
638
639 // =============================================================================
640 // Test Runner Summary
641 // =============================================================================
642
643 /// Run this to see a summary of all tests
644 /// cargo test --test health -- --nocapture
645 #[tokio::test]
646 async fn test_summary() {
647 println!("\n=== Makenotwork Integration Tests ===\n");
648 println!("Tests check:");
649 println!(" - Public pages load correctly");
650 println!(" - Auth endpoints respond appropriately");
651 println!(" - Protected routes require authentication");
652 println!(" - Static files are served");
653 println!(" - Database connection works (if DATABASE_URL set)");
654 println!("\nNote: Tests that require the server will be skipped if not running.");
655 println!("\nTo run all tests with server:");
656 println!(" 1. Start server: cargo run");
657 println!(" 2. Run tests: cargo test --test health");
658 }
659