fix: restore security hardening compatibility and validation

Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
elky
2026-09-07 21:14:27 +08:00
parent a5c3699ae9
commit a90d564931
191 changed files with 6785 additions and 1643 deletions
File diff suppressed because it is too large Load Diff
@@ -1374,8 +1374,8 @@ WHERE u.request_id = ANY($1)
"#;
const UPSERT_USAGE_ROUTING_SNAPSHOT_SQL: &str =
include_str!("queries/upsert_usage_routing_snapshot_sql.sql");
#[cfg(test)]
const UPSERT_USAGE_HTTP_AUDIT_SQL: &str = include_str!("queries/upsert_usage_http_audit_sql.sql");
const UPSERT_USAGE_BODY_BLOB_SQL: &str = include_str!("queries/upsert_usage_body_blob_sql.sql");
const UPSERT_USAGE_SETTLEMENT_PRICING_SNAPSHOT_SQL: &str =
include_str!("queries/upsert_usage_settlement_pricing_snapshot_sql.sql");
@@ -13362,19 +13362,36 @@ async fn sync_usage_body_blob_storage<'e, E>(
executor: E,
request_id: &str,
field: UsageBodyField,
_value: Option<&Value>,
_storage: &UsageBodyStorage,
_clear_existing: bool,
value: Option<&Value>,
storage: &UsageBodyStorage,
clear_existing: bool,
) -> Result<(), DataLayerError>
where
E: sqlx::Executor<'e, Database = Postgres>,
{
let body_ref = usage_body_ref(request_id, field);
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
.bind(&body_ref)
.execute(executor)
.await
.map_postgres_err()?;
if clear_existing {
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
.bind(&body_ref)
.execute(executor)
.await
.map_postgres_err()?;
} else if let Some(payload_gzip) = storage.detached_blob_bytes.as_ref() {
sqlx::query(UPSERT_USAGE_BODY_BLOB_SQL)
.bind(&body_ref)
.bind(request_id)
.bind(field.as_storage_field())
.bind(payload_gzip)
.execute(executor)
.await
.map_postgres_err()?;
} else if value.is_some() {
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
.bind(&body_ref)
.execute(executor)
.await
.map_postgres_err()?;
}
Ok(())
}
@@ -13383,43 +13400,46 @@ async fn sync_usage_http_audit_storage<'e, E>(
request_id: &str,
headers: &UsageHttpAuditHeaders<'_>,
refs: &UsageHttpAuditRefs,
_states: &UsageHttpAuditStates,
states: &UsageHttpAuditStates,
body_capture_mode: &str,
) -> Result<(), DataLayerError>
where
E: sqlx::Executor<'e, Database = Postgres>,
{
if headers.any_present() || refs.any_present() || body_capture_mode != "none" {
return Err(DataLayerError::InvalidInput(
"usage HTTP capture persistence is disabled".to_string(),
));
if !headers.any_present()
&& !refs.any_present()
&& !states.any_present()
&& body_capture_mode == "none"
{
return Ok(());
}
sqlx::query(
r#"
WITH deleted_audit AS (
DELETE FROM usage_http_audits WHERE request_id = $1
)
UPDATE usage
SET request_headers = NULL,
request_body = NULL,
provider_request_headers = NULL,
provider_request_body = NULL,
response_headers = NULL,
response_body = NULL,
client_response_headers = NULL,
client_response_body = NULL,
request_body_compressed = NULL,
provider_request_body_compressed = NULL,
response_body_compressed = NULL,
client_response_body_compressed = NULL
WHERE request_id = $1
"#,
)
.bind(request_id)
.execute(executor)
.await
.map_postgres_err()?;
sqlx::query(UPSERT_USAGE_HTTP_AUDIT_SQL)
.bind(request_id)
.bind(headers.request_headers_json)
.bind(headers.provider_request_headers_json)
.bind(headers.response_headers_json)
.bind(headers.client_response_headers_json)
.bind(refs.request_body_ref.as_deref())
.bind(refs.provider_request_body_ref.as_deref())
.bind(refs.response_body_ref.as_deref())
.bind(refs.client_response_body_ref.as_deref())
.bind(usage_body_capture_state_bind_text(
states.request_body_state,
))
.bind(usage_body_capture_state_bind_text(
states.provider_request_body_state,
))
.bind(usage_body_capture_state_bind_text(
states.response_body_state,
))
.bind(usage_body_capture_state_bind_text(
states.client_response_body_state,
))
.bind(body_capture_mode)
.execute(executor)
.await
.map_postgres_err()?;
Ok(())
}
@@ -187,6 +187,152 @@ async fn pending_batch_is_opt_in_and_rejects_non_pending_before_connecting() {
.contains("pending usage batch requires pending status"));
}
#[tokio::test]
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
async fn live_full_http_capture_round_trips_for_direct_and_batch_writes() {
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
database_url: std::env::var("AETHER_TEST_DATABASE_URL").unwrap(),
min_connections: 1,
max_connections: 2,
acquire_timeout_ms: 10_000,
idle_timeout_ms: 30_000,
max_lifetime_ms: 60_000,
statement_cache_capacity: 64,
require_ssl: false,
})
.unwrap();
let repository = SqlxUsageReadRepository::new(factory.connect_lazy().unwrap());
crate::run_migrations(repository.pool()).await.unwrap();
for batch in [false, true] {
let request_id = format!("req-full-capture-{}", uuid::Uuid::new_v4().simple());
let now_unix_secs = Utc::now().timestamp() as u64;
let mut pending = fast_clear_usage_record(
&request_id,
"full-capture-test",
now_unix_secs,
false,
UsageBodyCaptureState::Inline,
None,
);
pending.request_headers =
Some(json!({"content-type": "application/json", "authorization": "Bearer private"}));
pending.request_body =
Some(json!({"messages": [{"role": "user", "content": "original request"}]}));
pending.request_body_state = Some(UsageBodyCaptureState::Inline);
pending.provider_request_body = Some(json!({"input": "provider request"}));
pending.response_body = Some(json!("pending response"));
pending.response_body_state = Some(UsageBodyCaptureState::Inline);
pending.client_response_body = Some(json!("pending client response"));
pending.client_response_body_state = Some(UsageBodyCaptureState::Inline);
if batch {
repository
.upsert_pending_many(vec![pending.clone()])
.await
.unwrap();
} else {
repository.upsert(pending.clone()).await.unwrap();
}
for (field, expected) in [
(UsageBodyField::RequestBody, pending.request_body.as_ref()),
(
UsageBodyField::ProviderRequestBody,
pending.provider_request_body.as_ref(),
),
(UsageBodyField::ResponseBody, pending.response_body.as_ref()),
(
UsageBodyField::ClientResponseBody,
pending.client_response_body.as_ref(),
),
] {
assert_eq!(
repository
.resolve_body_ref(&usage_body_ref(&request_id, field))
.await
.unwrap()
.as_ref(),
expected,
"batch={batch}, field={field:?}"
);
}
let mut terminal = fast_clear_usage_record(
&request_id,
"full-capture-test",
now_unix_secs,
true,
UsageBodyCaptureState::None,
None,
);
terminal.provider_request_body_state = None;
terminal.response_headers =
Some(json!({"content-type": "text/event-stream", "set-cookie": "private"}));
terminal.response_body = Some(json!(format!(
"data: {}\n\ndata: [DONE]\n\n",
"streamed text".repeat(8192)
)));
terminal.response_body_state = Some(UsageBodyCaptureState::Inline);
terminal.client_response_body = Some(json!({"output": "final response"}));
terminal.client_response_body_state = Some(UsageBodyCaptureState::Inline);
repository.upsert(terminal.clone()).await.unwrap();
let stored = repository
.find_by_request_id_shallow(&request_id)
.await
.unwrap()
.unwrap();
assert_eq!(
stored.request_headers,
Some(json!({"content-type": "application/json", "authorization": "[redacted]"}))
);
assert_eq!(
stored.response_headers,
Some(json!({"content-type": "text/event-stream", "set-cookie": "[redacted]"}))
);
for (field, expected) in [
(UsageBodyField::RequestBody, pending.request_body.as_ref()),
(
UsageBodyField::ProviderRequestBody,
pending.provider_request_body.as_ref(),
),
(
UsageBodyField::ResponseBody,
terminal.response_body.as_ref(),
),
(
UsageBodyField::ClientResponseBody,
terminal.client_response_body.as_ref(),
),
] {
assert_eq!(
stored.body_state(field),
Some(UsageBodyCaptureState::Reference)
);
assert_eq!(
stored.body_ref(field),
Some(usage_body_ref(&request_id, field).as_str())
);
assert_eq!(
repository
.resolve_body_ref(stored.body_ref(field).unwrap())
.await
.unwrap()
.as_ref(),
expected,
"batch={batch}, field={field:?}"
);
}
let legacy_content_present: bool = sqlx::query_scalar("SELECT request_body IS NOT NULL OR request_headers IS NOT NULL OR response_body IS NOT NULL FROM usage WHERE request_id = $1")
.bind(&request_id).fetch_one(repository.pool()).await.unwrap();
assert!(!legacy_content_present);
sqlx::query("DELETE FROM usage WHERE request_id = $1")
.bind(&request_id)
.execute(repository.pool())
.await
.unwrap();
}
}
#[tokio::test]
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
async fn live_stale_terminal_event_is_a_full_transaction_noop() {
@@ -253,7 +399,7 @@ async fn live_stale_terminal_event_is_a_full_transaction_noop() {
.unwrap(),
);
let settlement_before = sqlx::query(
"SELECT billing_status, billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
"SELECT billing_status, billing_total_cost_usd::DOUBLE PRECISION AS billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
)
.bind(&request_id)
.fetch_one(repository.pool())
@@ -318,7 +464,7 @@ async fn live_stale_terminal_event_is_a_full_transaction_noop() {
.unwrap(),
);
let settlement_after = sqlx::query(
"SELECT billing_status, billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
"SELECT billing_status, billing_total_cost_usd::DOUBLE PRECISION AS billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
)
.bind(&request_id)
.fetch_one(repository.pool())
@@ -542,7 +688,7 @@ async fn live_pending_batch_persists_auxiliary_state_and_preserves_terminal_conf
.fetch_one(repository.pool())
.await
.expect("HTTP audit count should be readable");
assert_eq!(http_count, 0);
assert_eq!(http_count, 1);
let blob_count = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*)::BIGINT FROM usage_body_blobs WHERE request_id = $1",
)
@@ -550,7 +696,23 @@ async fn live_pending_batch_persists_auxiliary_state_and_preserves_terminal_conf
.fetch_one(repository.pool())
.await
.expect("body blob count should be readable");
assert_eq!(blob_count, 0);
assert_eq!(blob_count, 4);
let captured = repository
.find_by_request_id_shallow(&rich_request_id)
.await
.unwrap()
.unwrap();
assert_eq!(
captured.request_headers,
Some(json!({"x-request": "[redacted]"}))
);
assert_eq!(
repository
.resolve_body_ref(captured.body_ref(UsageBodyField::RequestBody).unwrap())
.await
.unwrap(),
Some(json!({"messages": [{"role": "user", "content": "hello"}]}))
);
let routing = sqlx::query(
"SELECT candidate_id, candidate_index, selected_provider_api_key_id FROM usage_routing_snapshots WHERE request_id = $1",
@@ -667,7 +829,7 @@ async fn live_pending_batch_and_terminal_upserts_count_each_provider_request_onc
let suffix = uuid::Uuid::new_v4().simple().to_string();
let provider_name = format!("pending-terminal-race-provider-{suffix}");
let provider_key_id = format!("pending-terminal-race-key-{suffix}");
let provider_key_id = uuid::Uuid::new_v4().to_string();
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
let request_ids = (0..REQUESTS)
.map(|index| format!("req-pending-terminal-race-{index}-{suffix}"))
@@ -793,7 +955,7 @@ async fn live_first_byte_fast_path_is_atomic_and_preserves_terminal_state() {
let existing_request_id = format!("req-first-byte-existing-{suffix}");
let metadata_fill_request_id = format!("req-first-byte-metadata-fill-{suffix}");
let provider_name = format!("first-byte-fast-{suffix}");
let missing_provider_key_id = format!("key-first-byte-missing-{suffix}");
let missing_provider_key_id = uuid::Uuid::new_v4().to_string();
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
let mut missing_first_byte = first_byte_usage_record(
@@ -1064,7 +1226,7 @@ async fn live_first_byte_reads_provider_contribution_after_waiting_for_canonical
let suffix = uuid::Uuid::new_v4().simple().to_string();
let request_id = format!("req-first-byte-lock-snapshot-{suffix}");
let provider_name = format!("first-byte-lock-snapshot-{suffix}");
let provider_key_id = format!("key-first-byte-lock-snapshot-{suffix}");
let provider_key_id = uuid::Uuid::new_v4().to_string();
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
let mut pending = first_byte_usage_record(
&request_id,
@@ -1209,14 +1371,14 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
let request_b = format!("req-first-byte-batch-b-{suffix}");
let request_missing = format!("req-first-byte-batch-missing-{suffix}");
let request_terminal = format!("req-first-byte-batch-terminal-{suffix}");
let missing_provider_key_id = format!("key-first-byte-batch-missing-{suffix}");
let missing_provider_key_id = uuid::Uuid::new_v4().to_string();
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
let mut pending_a = first_byte_usage_record(
&request_a,
&provider_name,
now_unix_secs,
Some(json!({"seed": "a"})),
Some(json!({"trace_id": "seed-a"})),
);
pending_a.status = "pending".to_string();
pending_a.first_byte_time_ms = None;
@@ -1241,7 +1403,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
);
terminal.is_stream = Some(true);
terminal.first_byte_time_ms = Some(44);
terminal.request_metadata = Some(json!({"terminal": true}));
terminal.request_metadata = Some(json!({"trace_id": "terminal"}));
repository
.upsert(pending_a)
@@ -1260,7 +1422,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
&request_a,
&provider_name,
now_unix_secs + 1,
Some(json!({"incoming": "a"})),
Some(json!({"trace_id": "incoming-a"})),
);
first_a.first_byte_time_ms = Some(30);
first_a.has_format_conversion = None;
@@ -1272,7 +1434,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
&request_b,
&provider_name,
now_unix_secs + 1,
Some(json!({"incoming": "b"})),
Some(json!({"trace_id": "incoming-b"})),
);
first_b.has_format_conversion = Some(true);
@@ -1280,7 +1442,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
&request_terminal,
&provider_name,
now_unix_secs + 2,
Some(json!({"late": true})),
Some(json!({"trace_id": "late"})),
);
late_terminal.first_byte_time_ms = Some(3);
late_terminal.has_format_conversion = None;
@@ -1288,7 +1450,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
&request_missing,
&provider_name,
now_unix_secs + 1,
Some(json!({"incoming": "missing"})),
Some(json!({"trace_id": "incoming-missing"})),
);
first_missing.provider_api_key_id = Some(missing_provider_key_id.clone());
@@ -1345,7 +1507,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
row_a
.try_get::<Option<serde_json::Value>, _>("request_metadata")
.unwrap(),
Some(json!({"seed": "a"})),
Some(json!({"trace_id": "seed-a"})),
"existing metadata remains authoritative"
);
@@ -1364,7 +1526,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
row_b
.try_get::<Option<serde_json::Value>, _>("request_metadata")
.unwrap(),
Some(json!({"incoming": "b"}))
Some(json!({"trace_id": "incoming-b"}))
);
let row_terminal = rows
@@ -2044,7 +2206,7 @@ async fn live_provider_performance_grouping_sets_matches_separate_queries() {
}
#[tokio::test]
#[ignore = "requires AETHER_TEST_DATABASE_URL and a populated PostgreSQL database"]
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
let database_url = std::env::var("AETHER_TEST_DATABASE_URL")
.expect("AETHER_TEST_DATABASE_URL must point at the test database");
@@ -2062,6 +2224,20 @@ async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
let repository =
SqlxUsageReadRepository::new(factory.connect_lazy().expect("lazy pool should build"));
let until = Utc::now().timestamp().max(0) as u64;
crate::run_migrations(repository.pool()).await.unwrap();
let request_id = format!("daily-breakdown-{}", uuid::Uuid::new_v4().simple());
let provider_name = format!("daily-provider-{}", uuid::Uuid::new_v4().simple());
repository
.upsert(fast_clear_usage_record(
&request_id,
&provider_name,
until.saturating_sub(60),
true,
UsageBodyCaptureState::None,
None,
))
.await
.unwrap();
let started = std::time::Instant::now();
let rows = repository
.list_dashboard_daily_breakdown(&UsageDashboardDailyBreakdownQuery {
@@ -2077,7 +2253,17 @@ async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
started.elapsed(),
rows.len()
);
assert!(!rows.is_empty());
let seeded = rows
.iter()
.find(|row| row.provider == provider_name)
.unwrap();
assert_eq!(seeded.requests, 1);
assert_eq!(seeded.total_tokens, 2);
sqlx::query("DELETE FROM \"usage\" WHERE request_id = $1")
.bind(&request_id)
.execute(repository.pool())
.await
.unwrap();
}
#[tokio::test]
@@ -81,12 +81,12 @@ fn select_video_task_full_columns() -> String {
fn select_video_task_claim_columns() -> String {
select_video_task_columns(
"NULL::TEXT",
"prompt",
"NULL::jsonb",
"NULL::INTEGER",
"NULL::TEXT",
"NULL::TEXT",
"NULL::TEXT",
"duration_seconds",
"resolution",
"aspect_ratio",
"size",
)
}
@@ -1176,7 +1176,7 @@ fn map_video_task_row(row: &PgRow) -> Result<StoredVideoTask, DataLayerError> {
#[cfg(test)]
mod tests {
use super::{update_if_active_sql, upsert_sql, SqlxVideoTaskRepository};
use super::{claim_due_sql, update_if_active_sql, upsert_sql, SqlxVideoTaskRepository};
use crate::{PostgresPoolConfig, PostgresPoolFactory};
use aether_data_contracts::repository::video_tasks::{
UpsertVideoTask, VideoTaskLookupKey, VideoTaskQueryFilter, VideoTaskReadRepository,
@@ -1240,6 +1240,142 @@ mod tests {
assert!(update.contains("created_at = COALESCE(created_at, TO_TIMESTAMP($34))"));
}
#[test]
fn poll_claim_returns_business_fields_required_by_identity_guards() {
let sql = claim_due_sql();
for field in [
"prompt",
"duration_seconds",
"resolution",
"aspect_ratio",
"size",
] {
assert!(
sql.contains(&format!("{field} AS {field}")),
"claim must retain {field}"
);
}
assert!(sql.contains("NULL::jsonb AS original_request_body"));
assert!(sql.contains("FOR UPDATE SKIP LOCKED"));
}
#[tokio::test]
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
async fn live_video_task_capture_claim_and_completion_preserve_business_fields() {
let database_url = std::env::var("AETHER_TEST_DATABASE_URL")
.expect("AETHER_TEST_DATABASE_URL must point at the test database");
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect(&database_url)
.await
.expect("test database should connect");
crate::run_migrations(&pool)
.await
.expect("test database should migrate");
sqlx::query("CREATE TEMP TABLE video_tasks (LIKE public.video_tasks INCLUDING ALL)")
.execute(&pool)
.await
.expect("isolated task table should be created");
let repository = SqlxVideoTaskRepository::new(pool);
for api_format in ["openai:video", "gemini:video"] {
let task_id = uuid::Uuid::new_v4().to_string();
let original = UpsertVideoTask {
id: task_id.clone(),
short_id: Some(uuid::Uuid::new_v4().simple().to_string()[..16].to_string()),
request_id: format!("request-{task_id}"),
user_id: None,
api_key_id: None,
username: Some("alice".to_string()),
api_key_name: Some("video-client".to_string()),
external_task_id: Some("upstream-task-1".to_string()),
provider_id: None,
endpoint_id: None,
key_id: None,
client_api_format: Some(api_format.to_string()),
provider_api_format: Some(api_format.to_string()),
format_converted: false,
model: Some("video-model".to_string()),
prompt: Some("business prompt".to_string()),
original_request_body: Some(serde_json::json!({"token": "private"})),
duration_seconds: Some(8),
resolution: Some("1080p".to_string()),
aspect_ratio: Some("16:9".to_string()),
size: Some("1920x1080".to_string()),
status: VideoTaskStatus::Submitted,
progress_percent: 0,
progress_message: None,
retry_count: 0,
poll_interval_seconds: 10,
next_poll_at_unix_secs: Some(10),
poll_count: 0,
max_poll_count: 360,
created_at_unix_ms: 1,
submitted_at_unix_secs: Some(1),
completed_at_unix_secs: None,
updated_at_unix_secs: 1,
error_code: None,
error_message: None,
video_url: None,
request_metadata: Some(serde_json::json!({"authorization": "private"})),
};
let stored = repository
.upsert(original.clone())
.await
.expect("task should persist");
assert_eq!(stored.prompt, original.prompt);
assert_eq!(stored.username, original.username);
assert_eq!(stored.api_key_name, original.api_key_name);
assert!(stored.original_request_body.is_none());
assert!(stored.request_metadata.is_none());
let mut claimed = repository
.claim_due(20, 50, 10)
.await
.expect("task should be claimed");
assert_eq!(claimed.len(), 1);
let mut completion: UpsertVideoTask = claimed.pop().expect("claimed task").into();
stored
.ensure_immutable_identity_matches(&completion)
.expect("claim must preserve task identity");
assert_eq!(completion.prompt, original.prompt);
let mut mismatched = completion.clone();
mismatched.duration_seconds = Some(99);
assert!(repository
.update_if_active(mismatched)
.await
.expect("guarded update should execute")
.is_none());
completion.status = VideoTaskStatus::Completed;
completion.progress_percent = 100;
completion.next_poll_at_unix_secs = None;
completion.completed_at_unix_secs = Some(21);
completion.updated_at_unix_secs = 21;
completion.video_url = Some(
"https://cdn.example.test/video.mp4?alt=media&signature=a%2Fb%2Bc%3D&part=2&part=1"
.to_string(),
);
let completed = repository
.update_if_active(completion.clone())
.await
.expect("completion should execute")
.expect("matching active task should complete");
assert_eq!(completed.video_url, completion.video_url);
let reloaded = repository
.find(VideoTaskLookupKey::Id(&task_id))
.await
.expect("task should reload")
.expect("task should exist");
assert_eq!(reloaded.status, VideoTaskStatus::Completed);
assert_eq!(reloaded.prompt, original.prompt);
assert_eq!(reloaded.video_url, completion.video_url);
assert_eq!(reloaded.duration_seconds, original.duration_seconds);
assert_eq!(reloaded.size, original.size);
assert_eq!(reloaded.username, original.username);
assert!(reloaded.request_metadata.is_none());
}
repository.pool().close().await;
}
#[tokio::test]
async fn repository_constructs_from_lazy_pool() {
let repository = SqlxVideoTaskRepository::new(build_pool());