diff --git a/backend/tests/api_sources_test.rs b/backend/tests/api_sources_test.rs index 5c85db9..bb8e070 100644 --- a/backend/tests/api_sources_test.rs +++ b/backend/tests/api_sources_test.rs @@ -22,6 +22,20 @@ fn require_test_db() -> bool { std::env::var("TEST_DATABASE_URL").is_ok() } +/// Helper: create a theme and return its id. +async fn create_theme(app: &common::TestApp, session: &str) -> String { + let body = serde_json::json!({ + "name": "Test Theme", + "theme": "Test", + "categories": ["Cat"] + }); + let (status, resp) = app + .post_with_session("/api/v1/themes", &body, session) + .await; + assert_eq!(status.as_u16(), 201, "Theme creation should succeed"); + resp["id"].as_str().unwrap().to_string() +} + // ═══════════════════════════════════════════════════════════════════════════ // Authentication // ═══════════════════════════════════════════════════════════════════════════ @@ -107,8 +121,11 @@ async fn get_sources_empty_list() { let (_user_id, session) = app .create_authenticated_user("sources-empty@example.com") .await; + let theme_id = create_theme(&app, &session).await; - let (status, body) = app.get_with_session("/api/v1/sources", &session).await; + let (status, body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; assert_eq!(status, StatusCode::OK, "GET /sources should return 200"); let sources = body.as_array().expect("Response should be an array"); @@ -126,10 +143,12 @@ async fn create_source_with_valid_data_returns_201() { let (_user_id, session) = app .create_authenticated_user("sources-create@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "My Blog", - "url": "https://blog.example.com" + "url": "https://blog.example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -163,11 +182,13 @@ async fn create_source_then_list_shows_it() { let (_user_id, session) = app .create_authenticated_user("sources-create-list@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create a source let body = serde_json::json!({ "title": "Tech News", - "url": "https://technews.example.com" + "url": "https://technews.example.com", + "theme_id": theme_id }); let (create_status, create_resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -176,7 +197,9 @@ async fn create_source_then_list_shows_it() { let created_id = create_resp["id"].as_str().unwrap(); // List sources - let (list_status, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (list_status, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; assert_eq!(list_status, StatusCode::OK); let sources = list_body.as_array().expect("Should be an array"); @@ -197,12 +220,14 @@ async fn create_multiple_sources_list_returns_all() { let (_user_id, session) = app .create_authenticated_user("sources-multi@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create three sources for i in 1..=3 { let body = serde_json::json!({ "title": format!("Source {}", i), - "url": format!("https://source{}.example.com", i) + "url": format!("https://source{}.example.com", i), + "theme_id": theme_id }); let (status, _) = app .post_with_session("/api/v1/sources", &body, &session) @@ -211,7 +236,9 @@ async fn create_multiple_sources_list_returns_all() { } // List should have 3 sources - let (status, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (status, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; assert_eq!(status, StatusCode::OK); let sources = list_body.as_array().unwrap(); @@ -233,10 +260,12 @@ async fn create_source_invalid_url_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-invalid-url@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "My Blog", - "url": "not-a-valid-url" + "url": "not-a-valid-url", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -261,10 +290,12 @@ async fn create_source_ftp_url_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-ftp-url@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "FTP Source", - "url": "ftp://files.example.com" + "url": "ftp://files.example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -289,10 +320,12 @@ async fn create_source_empty_title_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-empty-title@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": " ", - "url": "https://example.com" + "url": "https://example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -317,11 +350,13 @@ async fn create_source_title_too_long_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-title-long@example.com") .await; + let theme_id = create_theme(&app, &session).await; let long_title = "a".repeat(201); let body = serde_json::json!({ "title": long_title, - "url": "https://example.com" + "url": "https://example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -346,12 +381,14 @@ async fn create_source_url_too_long_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-url-long@example.com") .await; + let theme_id = create_theme(&app, &session).await; let long_url = format!("https://example.com/{}", "a".repeat(990)); assert!(long_url.len() > 1000); let body = serde_json::json!({ "title": "My Blog", - "url": long_url + "url": long_url, + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -376,10 +413,12 @@ async fn create_source_empty_url_returns_422() { let (_user_id, session) = app .create_authenticated_user("sources-empty-url@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "My Blog", - "url": "" + "url": "", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -408,10 +447,12 @@ async fn create_source_duplicate_url_returns_error() { let (_user_id, session) = app .create_authenticated_user("sources-dup@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "My Blog", - "url": "https://duplicate.example.com" + "url": "https://duplicate.example.com", + "theme_id": theme_id }); // First creation should succeed @@ -423,7 +464,8 @@ async fn create_source_duplicate_url_returns_error() { // Second creation with same URL should fail (DB unique constraint) let body2 = serde_json::json!({ "title": "Different Title", - "url": "https://duplicate.example.com" + "url": "https://duplicate.example.com", + "theme_id": theme_id }); let (status2, _) = app .post_with_session("/api/v1/sources", &body2, &session) @@ -457,11 +499,13 @@ async fn delete_source_valid_id_returns_204() { let (_user_id, session) = app .create_authenticated_user("sources-delete@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create a source let body = serde_json::json!({ "title": "To Delete", - "url": "https://delete.example.com" + "url": "https://delete.example.com", + "theme_id": theme_id }); let (create_status, create_resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -491,11 +535,13 @@ async fn delete_source_then_list_no_longer_shows_it() { let (_user_id, session) = app .create_authenticated_user("sources-delete-list@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create two sources let body1 = serde_json::json!({ "title": "Keep This", - "url": "https://keep.example.com" + "url": "https://keep.example.com", + "theme_id": theme_id }); let (_, resp1) = app .post_with_session("/api/v1/sources", &body1, &session) @@ -504,7 +550,8 @@ async fn delete_source_then_list_no_longer_shows_it() { let body2 = serde_json::json!({ "title": "Delete This", - "url": "https://delete-me.example.com" + "url": "https://delete-me.example.com", + "theme_id": theme_id }); let (_, resp2) = app .post_with_session("/api/v1/sources", &body2, &session) @@ -518,7 +565,9 @@ async fn delete_source_then_list_no_longer_shows_it() { assert_eq!(del_status, StatusCode::NO_CONTENT); // List should only show the first source - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 1, "Should have exactly 1 source after delete"); assert_eq!(sources[0]["id"], keep_id); @@ -568,11 +617,14 @@ async fn user_a_sources_not_visible_to_user_b() { let (_user_b_id, session_b) = app .create_authenticated_user("user-b-sources@example.com") .await; + let theme_id_a = create_theme(&app, &session_a).await; + let theme_id_b = create_theme(&app, &session_b).await; // User A creates a source let body = serde_json::json!({ "title": "User A Blog", - "url": "https://usera.example.com" + "url": "https://usera.example.com", + "theme_id": theme_id_a }); let (status, _) = app .post_with_session("/api/v1/sources", &body, &session_a) @@ -581,7 +633,7 @@ async fn user_a_sources_not_visible_to_user_b() { // User B lists sources -> should be empty let (list_status, list_body) = app - .get_with_session("/api/v1/sources", &session_b) + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id_b), &session_b) .await; assert_eq!(list_status, StatusCode::OK); let sources = list_body.as_array().unwrap(); @@ -592,7 +644,7 @@ async fn user_a_sources_not_visible_to_user_b() { // User A lists sources -> should see their source let (_, list_body_a) = app - .get_with_session("/api/v1/sources", &session_a) + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id_a), &session_a) .await; let sources_a = list_body_a.as_array().unwrap(); assert_eq!(sources_a.len(), 1, "User A should see their own source"); @@ -613,11 +665,13 @@ async fn user_b_cannot_delete_user_a_source_returns_404() { let (_user_b_id, session_b) = app .create_authenticated_user("attacker-b@example.com") .await; + let theme_id_a = create_theme(&app, &session_a).await; // User A creates a source let body = serde_json::json!({ "title": "User A Private", - "url": "https://private-a.example.com" + "url": "https://private-a.example.com", + "theme_id": theme_id_a }); let (_, create_resp) = app .post_with_session("/api/v1/sources", &body, &session_a) @@ -638,7 +692,7 @@ async fn user_b_cannot_delete_user_a_source_returns_404() { // Verify User A's source is still there let (_, list_body) = app - .get_with_session("/api/v1/sources", &session_a) + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id_a), &session_a) .await; let sources = list_body.as_array().unwrap(); assert_eq!( @@ -663,13 +717,15 @@ async fn bulk_import_valid_sources_succeeds() { let (_user_id, session) = app .create_authenticated_user("bulk-import@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "sources": [ { "title": "Blog 1", "url": "https://blog1.example.com" }, { "title": "Blog 2", "url": "https://blog2.example.com" }, { "title": "Blog 3", "url": "https://blog3.example.com" } - ] + ], + "theme_id": theme_id }); let (status, resp) = app @@ -685,7 +741,9 @@ async fn bulk_import_valid_sources_succeeds() { assert_eq!(resp["skipped"], 0, "Should have skipped 0 sources"); // Verify they appear in the list - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 3); } @@ -701,11 +759,13 @@ async fn bulk_import_with_duplicates_skips_them() { let (_user_id, session) = app .create_authenticated_user("bulk-dup@example.com") .await; + let theme_id = create_theme(&app, &session).await; // First, create one source normally let body = serde_json::json!({ "title": "Existing", - "url": "https://existing.example.com" + "url": "https://existing.example.com", + "theme_id": theme_id }); let (status, _) = app .post_with_session("/api/v1/sources", &body, &session) @@ -717,7 +777,8 @@ async fn bulk_import_with_duplicates_skips_them() { "sources": [ { "title": "Existing Dup", "url": "https://existing.example.com" }, { "title": "New One", "url": "https://new.example.com" } - ] + ], + "theme_id": theme_id }); let (bulk_status, resp) = app @@ -735,7 +796,9 @@ async fn bulk_import_with_duplicates_skips_them() { ); // Total should be 2 (original + new) - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 2); } @@ -751,9 +814,11 @@ async fn bulk_import_empty_array_returns_error() { let (_user_id, session) = app .create_authenticated_user("bulk-empty@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ - "sources": [] + "sources": [], + "theme_id": theme_id }); let (status, resp) = app @@ -779,6 +844,7 @@ async fn bulk_import_with_invalid_entries_reports_errors() { let (_user_id, session) = app .create_authenticated_user("bulk-invalid@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "sources": [ @@ -786,7 +852,8 @@ async fn bulk_import_with_invalid_entries_reports_errors() { { "title": "", "url": "https://empty-title.example.com" }, { "title": "Bad URL", "url": "not-a-url" }, { "title": "Also Valid", "url": "https://alsovalid.example.com" } - ] + ], + "theme_id": theme_id }); let (status, resp) = app @@ -806,7 +873,9 @@ async fn bulk_import_with_invalid_entries_reports_errors() { ); // Verify only 2 sources exist - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 2); } @@ -850,12 +919,14 @@ async fn export_csv_with_sources_returns_csv() { let (_user_id, session) = app .create_authenticated_user("csv-export@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create some sources for i in 1..=2 { let body = serde_json::json!({ "title": format!("Source {}", i), - "url": format!("https://source{}.example.com", i) + "url": format!("https://source{}.example.com", i), + "theme_id": theme_id }); let (s, _) = app .post_with_session("/api/v1/sources", &body, &session) @@ -866,7 +937,7 @@ async fn export_csv_with_sources_returns_csv() { // Export CSV let req = Request::builder() .method(Method::GET) - .uri("/api/v1/sources/export-csv") + .uri(&format!("/api/v1/sources/export-csv?theme_id={}", theme_id)) .header( "Cookie", format!("ai_synth_session={}", session), @@ -927,10 +998,11 @@ async fn export_csv_with_no_sources_returns_header_only() { let (_user_id, session) = app .create_authenticated_user("csv-export-empty@example.com") .await; + let theme_id = create_theme(&app, &session).await; let req = Request::builder() .method(Method::GET) - .uri("/api/v1/sources/export-csv") + .uri(&format!("/api/v1/sources/export-csv?theme_id={}", theme_id)) .header( "Cookie", format!("ai_synth_session={}", session), @@ -977,6 +1049,7 @@ async fn export_csv_without_auth_returns_401() { fn build_csv_multipart_request( csv_content: &str, session_cookie: &str, + theme_id: &str, ) -> Request { let boundary = "----TestBoundary12345"; let body = format!( @@ -992,7 +1065,7 @@ fn build_csv_multipart_request( Request::builder() .method(Method::POST) - .uri("/api/v1/sources/import-csv") + .uri(&format!("/api/v1/sources/import-csv?theme_id={}", theme_id)) .header( "Content-Type", format!("multipart/form-data; boundary={}", boundary), @@ -1017,9 +1090,10 @@ async fn import_csv_with_valid_data_succeeds() { let (_user_id, session) = app .create_authenticated_user("csv-import@example.com") .await; + let theme_id = create_theme(&app, &session).await; let csv_content = "title,url\nBlog One,https://blog1.example.com\nBlog Two,https://blog2.example.com"; - let req = build_csv_multipart_request(csv_content, &session); + let req = build_csv_multipart_request(csv_content, &session, &theme_id); let response = app.raw_request(req).await; let status = response.status(); @@ -1037,7 +1111,9 @@ async fn import_csv_with_valid_data_succeeds() { assert_eq!(resp["skipped"], 0); // Verify via list - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 2); } @@ -1053,9 +1129,10 @@ async fn import_csv_semicolon_separated_succeeds() { let (_user_id, session) = app .create_authenticated_user("csv-semicolon@example.com") .await; + let theme_id = create_theme(&app, &session).await; let csv_content = "titre;lien\nMon Blog;https://monblog.example.com\nActus;https://actus.example.com"; - let req = build_csv_multipart_request(csv_content, &session); + let req = build_csv_multipart_request(csv_content, &session, &theme_id); let response = app.raw_request(req).await; let status = response.status(); @@ -1078,7 +1155,9 @@ async fn import_csv_semicolon_separated_succeeds() { ); // Verify the actual titles - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 2); let titles: Vec<&str> = sources @@ -1143,6 +1222,7 @@ async fn csv_export_roundtrip() { let (_user_id, session) = app .create_authenticated_user("csv-roundtrip@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Create sources let source_data = vec![ @@ -1152,7 +1232,7 @@ async fn csv_export_roundtrip() { ]; for (title, url) in &source_data { - let body = serde_json::json!({ "title": title, "url": url }); + let body = serde_json::json!({ "title": title, "url": url, "theme_id": theme_id }); let (s, _) = app .post_with_session("/api/v1/sources", &body, &session) .await; @@ -1162,7 +1242,7 @@ async fn csv_export_roundtrip() { // Export CSV let req = Request::builder() .method(Method::GET) - .uri("/api/v1/sources/export-csv") + .uri(&format!("/api/v1/sources/export-csv?theme_id={}", theme_id)) .header("Cookie", format!("ai_synth_session={}", session)) .body(Body::empty()) .unwrap(); @@ -1210,29 +1290,35 @@ async fn max_sources_limit_enforced() { let (user_id, session) = app .create_authenticated_user("max-sources@example.com") .await; + let theme_id = create_theme(&app, &session).await; + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); // Insert 100 sources directly into the database (faster than 100 API calls) for i in 0..100 { sqlx::query( - "INSERT INTO sources (user_id, title, url) VALUES ($1, $2, $3)", + "INSERT INTO sources (user_id, title, url, theme_id) VALUES ($1, $2, $3, $4)", ) .bind(user_id) .bind(format!("Source {}", i)) .bind(format!("https://source{}.example.com", i)) + .bind(theme_uuid) .execute(&app.pool) .await .expect("Failed to insert source"); } // Verify we have 100 - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 100, "Should have 100 sources"); // Attempt to create the 101st source via API let body = serde_json::json!({ "title": "One Too Many", - "url": "https://toomany.example.com" + "url": "https://toomany.example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -1264,15 +1350,18 @@ async fn bulk_import_respects_max_sources_limit() { let (user_id, session) = app .create_authenticated_user("bulk-max@example.com") .await; + let theme_id = create_theme(&app, &session).await; + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); // Insert 98 sources directly for i in 0..98 { sqlx::query( - "INSERT INTO sources (user_id, title, url) VALUES ($1, $2, $3)", + "INSERT INTO sources (user_id, title, url, theme_id) VALUES ($1, $2, $3, $4)", ) .bind(user_id) .bind(format!("Source {}", i)) .bind(format!("https://source{}.example.com", i)) + .bind(theme_uuid) .execute(&app.pool) .await .expect("Failed to insert source"); @@ -1288,7 +1377,7 @@ async fn bulk_import_respects_max_sources_limit() { }) .collect(); - let body = serde_json::json!({ "sources": sources }); + let body = serde_json::json!({ "sources": sources, "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources/bulk", &body, &session) .await; @@ -1306,7 +1395,9 @@ async fn bulk_import_respects_max_sources_limit() { ); // Verify total is exactly 100 - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let all_sources = list_body.as_array().unwrap(); assert_eq!(all_sources.len(), 100, "Should have exactly 100 sources"); } @@ -1326,12 +1417,14 @@ async fn create_source_with_boundary_values_succeeds() { let (_user_id, session) = app .create_authenticated_user("sources-boundary@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Title exactly 200 chars let title_200 = "a".repeat(200); let body = serde_json::json!({ "title": title_200, - "url": "https://boundary-title.example.com" + "url": "https://boundary-title.example.com", + "theme_id": theme_id }); let (status, _) = app .post_with_session("/api/v1/sources", &body, &session) @@ -1347,7 +1440,8 @@ async fn create_source_with_boundary_values_succeeds() { assert_eq!(url_1000.len(), 1000); let body2 = serde_json::json!({ "title": "Boundary URL", - "url": url_1000 + "url": url_1000, + "theme_id": theme_id }); let (status2, _) = app .post_with_session("/api/v1/sources", &body2, &session) @@ -1361,7 +1455,8 @@ async fn create_source_with_boundary_values_succeeds() { // Minimal valid source let body3 = serde_json::json!({ "title": "A", - "url": "http://x.co" + "url": "http://x.co", + "theme_id": theme_id }); let (status3, _) = app .post_with_session("/api/v1/sources", &body3, &session) @@ -1384,10 +1479,12 @@ async fn create_source_with_http_url_succeeds() { let (_user_id, session) = app .create_authenticated_user("sources-http@example.com") .await; + let theme_id = create_theme(&app, &session).await; let body = serde_json::json!({ "title": "HTTP Source", - "url": "http://insecure.example.com" + "url": "http://insecure.example.com", + "theme_id": theme_id }); let (status, resp) = app .post_with_session("/api/v1/sources", &body, &session) @@ -1412,13 +1509,15 @@ async fn bulk_import_all_duplicates_within_batch() { let (_user_id, session) = app .create_authenticated_user("bulk-inner-dup@example.com") .await; + let theme_id = create_theme(&app, &session).await; // Same URL appearing twice in one batch let body = serde_json::json!({ "sources": [ { "title": "First", "url": "https://same.example.com" }, { "title": "Second", "url": "https://same.example.com" } - ] + ], + "theme_id": theme_id }); let (status, resp) = app @@ -1431,7 +1530,9 @@ async fn bulk_import_all_duplicates_within_batch() { assert_eq!(resp["skipped"], 1, "The duplicate should be counted as skipped"); // Verify only 1 source exists - let (_, list_body) = app.get_with_session("/api/v1/sources", &session).await; + let (_, list_body) = app + .get_with_session(&format!("/api/v1/sources?theme_id={}", theme_id), &session) + .await; let sources = list_body.as_array().unwrap(); assert_eq!(sources.len(), 1); }