//! Shared fixtures for the DB-backed integration tests. //! //! Every helper here executes SQL that is **character-for-character identical** to what `src/` //! actually runs (see the `// SRC:` markers). That is the whole point: a paraphrased query is a //! query nobody runs, and a test that passes against a paraphrase proves nothing about production. #![allow(dead_code)] // each integration-test crate uses a different subset of these helpers use sqlx::PgPool; use uuid::Uuid; /// Insert a bare, unreleased event (epoch 0, uploads open). pub async fn seed_event(pool: &PgPool, slug: &str) -> Uuid { sqlx::query_scalar("INSERT INTO event (slug, name) VALUES ($1, $2) RETURNING id") .bind(slug) .bind("Hochzeit") .fetch_one(pool) .await .expect("seed event") } /// Insert a guest with a zeroed byte total. pub async fn seed_user(pool: &PgPool, event_id: Uuid, name: &str) -> Uuid { sqlx::query_scalar( "INSERT INTO \"user\" (event_id, display_name, recovery_pin_hash) VALUES ($1, $2, 'x') RETURNING id", ) .bind(event_id) .bind(name) .fetch_one(pool) .await .expect("seed user") } /// SRC: `handlers/host.rs::release_gallery` — the claim + epoch bump, verbatim. /// Returns the POST-increment epoch, exactly as the handler consumes it. pub async fn release_gallery(pool: &PgPool, slug: &str) -> Option { let claimed: Option<(Uuid, String, i64)> = sqlx::query_as( "UPDATE event SET export_released_at = NOW(), uploads_locked_at = COALESCE(uploads_locked_at, NOW()), export_epoch = export_epoch + 1 WHERE slug = $1 AND export_released_at IS NULL RETURNING id, name, export_epoch", ) .bind(slug) .fetch_optional(pool) .await .expect("release_gallery"); if let Some((event_id, _, epoch)) = claimed { // The handler arms both jobs in the SAME transaction; for a single-connection fixture the // sequencing is equivalent. let mut conn = pool.acquire().await.expect("acquire"); enqueue_types_at_epoch(&mut conn, event_id, epoch, &["zip", "html"]).await; Some(epoch) } else { None } } /// SRC: `handlers/host.rs::open_event` — the one statement that retires an entire generation. /// Returns rows affected. pub async fn open_event(pool: &PgPool, slug: &str) -> u64 { sqlx::query( "UPDATE event SET uploads_locked_at = NULL, export_released_at = NULL, export_epoch = export_epoch + 1 WHERE slug = $1 AND (uploads_locked_at IS NOT NULL OR export_released_at IS NOT NULL)", ) .bind(slug) .execute(pool) .await .expect("open_event") .rows_affected() } /// SRC: `services/export.rs::enqueue_types_at_epoch` — verbatim upsert. pub async fn enqueue_types_at_epoch( conn: &mut sqlx::PgConnection, event_id: Uuid, epoch: i64, types: &[&str], ) { for export_type in types { sqlx::query( "INSERT INTO export_job (event_id, type, status, progress_pct, epoch) VALUES ($1, $2::export_type, 'pending', 0, $3) ON CONFLICT (event_id, type) DO UPDATE SET status = 'pending', progress_pct = 0, file_path = NULL, error_message = NULL, completed_at = NULL, epoch = EXCLUDED.epoch WHERE export_job.status <> 'done' OR export_job.epoch <> EXCLUDED.epoch", ) .bind(event_id) .bind(export_type) .bind(epoch) .execute(&mut *conn) .await .expect("enqueue_types_at_epoch"); } } /// SRC: `services/export.rs::claim_job` — verbatim. `true` = we won the generation. pub async fn claim_job(pool: &PgPool, event_id: Uuid, export_type: &str, epoch: i64) -> bool { sqlx::query( "UPDATE export_job SET status = 'running' WHERE event_id = $1 AND type = $2::export_type AND epoch = $3 AND status = 'pending'", ) .bind(event_id) .bind(export_type) .bind(epoch) .execute(pool) .await .expect("claim_job") .rows_affected() > 0 } /// SRC: `services/export.rs::finalize_job` — verbatim. This IS the publish step. pub async fn finalize_job( pool: &PgPool, event_id: Uuid, export_type: &str, epoch: i64, file_path: &str, ) -> bool { sqlx::query( "UPDATE export_job SET status = 'done', progress_pct = 100, file_path = $3, completed_at = NOW() WHERE event_id = $1 AND type = $2::export_type AND epoch = $4 AND status = 'running'", ) .bind(event_id) .bind(export_type) .bind(file_path) .bind(epoch) .execute(pool) .await .expect("finalize_job") .rows_affected() > 0 } /// SRC: `services/export.rs::update_progress` — verbatim. Doubles as the worker's liveness check: /// `false` means "your generation was retired, stop working". pub async fn update_progress( pool: &PgPool, event_id: Uuid, export_type: &str, epoch: i64, pct: i16, ) -> bool { sqlx::query( "UPDATE export_job SET progress_pct = $3 WHERE event_id = $1 AND type = $2::export_type AND epoch = $4 AND status = 'running'", ) .bind(event_id) .bind(export_type) .bind(pct) .bind(epoch) .execute(pool) .await .expect("update_progress") .rows_affected() > 0 } /// SRC: `services/export.rs::invalidate_and_arm` — the ViewerOnly ZIP carry-forward, verbatim. /// Returns `rows_affected() == 1`, which is what the production code branches on. pub async fn carry_zip_forward(pool: &PgPool, event_id: Uuid, epoch: i64) -> bool { sqlx::query( "UPDATE export_job SET epoch = $2 WHERE event_id = $1 AND type = 'zip'::export_type AND status = 'done' AND epoch = $2 - 1", ) .bind(event_id) .bind(epoch) .execute(pool) .await .expect("carry_zip_forward") .rows_affected() == 1 } /// SRC: `services/export.rs::invalidate_and_arm` — the epoch bump, verbatim. pub async fn bump_epoch(pool: &PgPool, slug: &str) -> Option<(Uuid, String, i64)> { sqlx::query_as( "UPDATE event SET export_epoch = export_epoch + 1 WHERE slug = $1 AND export_released_at IS NOT NULL RETURNING id, name, export_epoch", ) .bind(slug) .fetch_optional(pool) .await .expect("bump_epoch") } /// The event's authoritative epoch. pub async fn event_epoch(pool: &PgPool, event_id: Uuid) -> i64 { sqlx::query_scalar("SELECT export_epoch FROM event WHERE id = $1") .bind(event_id) .fetch_one(pool) .await .expect("event_epoch") } /// The raw job row, bypassing `export_current` — what the WORKER sees. pub async fn job_row( pool: &PgPool, event_id: Uuid, export_type: &str, ) -> Option<(String, i64, Option)> { sqlx::query_as( "SELECT status::text, epoch, file_path FROM export_job WHERE event_id = $1 AND type = $2::export_type", ) .bind(event_id) .bind(export_type) .fetch_optional(pool) .await .expect("job_row") } /// Does `export_current` expose this job at all? (The view itself, without the `status` filter — /// it is what `handlers/admin.rs::export_status` reports to the host UI.) pub async fn in_export_current(pool: &PgPool, event_id: Uuid, export_type: &str) -> bool { sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM export_current WHERE event_id = $1 AND type = $2::export_type", ) .bind(event_id) .bind(export_type) .fetch_one(pool) .await .expect("in_export_current") > 0 } /// THE download predicate. SRC: `handlers/admin.rs::download_export` reads exactly this shape — /// `SELECT c.file_path FROM export_current c WHERE ... AND c.status = 'done'`. If this returns /// `Some`, a guest can download the keepsake; if `None`, they get a 404. pub async fn downloadable(pool: &PgPool, event_id: Uuid, export_type: &str) -> Option { sqlx::query_scalar( "SELECT c.file_path FROM export_current c WHERE c.event_id = $1 AND c.type = $2::export_type AND c.status = 'done'", ) .bind(event_id) .bind(export_type) .fetch_optional(pool) .await .expect("downloadable") .flatten() }