|
|
|
|
@ -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<Body> {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|