feat: add preferred sources — prioritized during synthesis generation
Users can mark sources as preferred via star buttons on the theme page. Preferred sources are processed first in the pipeline (ordered before non-preferred in waves, shuffled separately then merged). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>master
parent
48b5e77e7e
commit
e43a4d2180
@ -0,0 +1 @@
|
||||
ALTER TABLE sources ADD COLUMN is_preferred BOOLEAN NOT NULL DEFAULT false;
|
||||
@ -0,0 +1,184 @@
|
||||
//! Integration tests for the `PUT /api/v1/sources/preferred` endpoint.
|
||||
//!
|
||||
//! Tests:
|
||||
//! - Setting preferred sources
|
||||
//! - Clearing all preferred sources
|
||||
//! - Authentication requirement
|
||||
//!
|
||||
//! Requires a running Postgres instance. Set `TEST_DATABASE_URL` to run.
|
||||
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
|
||||
fn require_test_db() -> bool {
|
||||
std::env::var("TEST_DATABASE_URL").is_ok()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_preferred_sets_sources() {
|
||||
if !require_test_db() {
|
||||
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
|
||||
return;
|
||||
}
|
||||
|
||||
let app = common::TestApp::new().await;
|
||||
let (_user_id, session) = app
|
||||
.create_authenticated_user("pref-set@example.com")
|
||||
.await;
|
||||
|
||||
// Create a theme first
|
||||
let theme_body = serde_json::json!({
|
||||
"name": "Pref Theme",
|
||||
"theme": "Test",
|
||||
"categories": ["Cat"]
|
||||
});
|
||||
let (theme_status, theme_resp) = app
|
||||
.post_with_session("/api/v1/themes", &theme_body, &session)
|
||||
.await;
|
||||
assert_eq!(theme_status, StatusCode::CREATED);
|
||||
let theme_id = theme_resp["id"].as_str().unwrap();
|
||||
|
||||
// Create 3 sources
|
||||
let mut source_ids = Vec::new();
|
||||
for i in 1..=3 {
|
||||
let body = serde_json::json!({
|
||||
"title": format!("Source {}", i),
|
||||
"url": format!("https://source{}.example.com", i),
|
||||
"theme_id": theme_id
|
||||
});
|
||||
let (status, resp) = app
|
||||
.post_with_session("/api/v1/sources", &body, &session)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::CREATED);
|
||||
source_ids.push(resp["id"].as_str().unwrap().to_string());
|
||||
}
|
||||
|
||||
// PUT /sources/preferred with [id1, id3]
|
||||
let pref_body = serde_json::json!({
|
||||
"source_ids": [source_ids[0], source_ids[2]]
|
||||
});
|
||||
let (pref_status, _) = app
|
||||
.put_with_session("/api/v1/sources/preferred", &pref_body, &session)
|
||||
.await;
|
||||
assert_eq!(pref_status, StatusCode::OK);
|
||||
|
||||
// GET /sources → verify preferred flags
|
||||
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");
|
||||
assert_eq!(sources.len(), 3);
|
||||
|
||||
for source in sources {
|
||||
let id = source["id"].as_str().unwrap();
|
||||
let is_preferred = source["is_preferred"].as_bool().unwrap();
|
||||
if id == source_ids[0] || id == source_ids[2] {
|
||||
assert!(is_preferred, "Source {} should be preferred", id);
|
||||
} else {
|
||||
assert!(!is_preferred, "Source {} should NOT be preferred", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_preferred_clears_all_when_empty() {
|
||||
if !require_test_db() {
|
||||
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
|
||||
return;
|
||||
}
|
||||
|
||||
let app = common::TestApp::new().await;
|
||||
let (_user_id, session) = app
|
||||
.create_authenticated_user("pref-clear@example.com")
|
||||
.await;
|
||||
|
||||
// Create a theme
|
||||
let theme_body = serde_json::json!({
|
||||
"name": "Pref Clear Theme",
|
||||
"theme": "Test",
|
||||
"categories": ["Cat"]
|
||||
});
|
||||
let (_, theme_resp) = app
|
||||
.post_with_session("/api/v1/themes", &theme_body, &session)
|
||||
.await;
|
||||
let theme_id = theme_resp["id"].as_str().unwrap();
|
||||
|
||||
// Create 2 sources
|
||||
let mut source_ids = Vec::new();
|
||||
for i in 1..=2 {
|
||||
let body = serde_json::json!({
|
||||
"title": format!("Source {}", i),
|
||||
"url": format!("https://clear-source{}.example.com", i),
|
||||
"theme_id": theme_id
|
||||
});
|
||||
let (_, resp) = app
|
||||
.post_with_session("/api/v1/sources", &body, &session)
|
||||
.await;
|
||||
source_ids.push(resp["id"].as_str().unwrap().to_string());
|
||||
}
|
||||
|
||||
// Set some as preferred
|
||||
let pref_body = serde_json::json!({
|
||||
"source_ids": [source_ids[0]]
|
||||
});
|
||||
let (pref_status, _) = app
|
||||
.put_with_session("/api/v1/sources/preferred", &pref_body, &session)
|
||||
.await;
|
||||
assert_eq!(pref_status, StatusCode::OK);
|
||||
|
||||
// Clear all preferred
|
||||
let clear_body = serde_json::json!({
|
||||
"source_ids": []
|
||||
});
|
||||
let (clear_status, _) = app
|
||||
.put_with_session("/api/v1/sources/preferred", &clear_body, &session)
|
||||
.await;
|
||||
assert_eq!(clear_status, StatusCode::OK);
|
||||
|
||||
// GET /sources → all is_preferred=false
|
||||
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");
|
||||
for source in sources {
|
||||
assert_eq!(
|
||||
source["is_preferred"].as_bool().unwrap(),
|
||||
false,
|
||||
"All sources should be non-preferred after clearing"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_preferred_without_auth_returns_401() {
|
||||
if !require_test_db() {
|
||||
eprintln!("SKIPPED: TEST_DATABASE_URL not set");
|
||||
return;
|
||||
}
|
||||
|
||||
let app = common::TestApp::new().await;
|
||||
let body = serde_json::json!({
|
||||
"source_ids": []
|
||||
});
|
||||
let (status, resp) = app
|
||||
.put_with_session("/api/v1/sources/preferred", &body, "invalid-session-token")
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"PUT /sources/preferred without auth should return 401"
|
||||
);
|
||||
assert_eq!(resp["error"], "unauthorized");
|
||||
}
|
||||
Loading…
Reference in New Issue