Skip to main content

max / makenotwork

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