diff --git a/server/src/crate_server/readiness.gleam b/server/src/crate_server/readiness.gleam index 7f1e70a..0935ebf 100644 --- a/server/src/crate_server/readiness.gleam +++ b/server/src/crate_server/readiness.gleam @@ -45,11 +45,17 @@ pub fn required_tables() -> List(String) { /// The generated readiness query, exposed for regression tests. Checks that /// every required table exists and the role holds baseline CRUD on it, rather /// than auditing columns and indexes: that is what gates the app, and it is -/// cheap enough to finish inside `local_wait_ms`. The interpolated names are -/// internal constants, never user input. +/// cheap enough to finish inside `local_wait_ms`. pub fn schema_check_sql() -> String { + schema_check_sql_for(required_tables()) +} + +/// `schema_check_sql` over an arbitrary table list, so a test can execute the +/// real query against a name it knows is absent. The interpolated names are +/// internal constants, never user input. +pub fn schema_check_sql_for(tables: List(String)) -> String { let names = - required_tables() + tables |> list.map(fn(name) { "'" <> name <> "'" }) |> string.join(", ") "select 1 where not exists ( @@ -101,15 +107,15 @@ pub fn ready() -> Check { } pub fn postgres(conn: pog.Connection) -> Check { - cached(fn() { probe_schema(conn) }, clock.now_seconds) + cached(fn() { schema_readiness(conn, schema_check_sql()) }, clock.now_seconds) } -// Zero rows proves the schema unusable; an error is only absence of proof. -fn probe_schema(conn: pog.Connection) -> Readiness { +/// What a live connection reports for `sql`: zero rows proves the schema +/// unusable, while an error is only ever an absence of proof. A pool that is +/// gone exits rather than erroring, which the worker-crash path answers. +pub fn schema_readiness(conn: pog.Connection, sql: String) -> Readiness { let row = decode.success(Nil) - case - pog.query(schema_check_sql()) |> pog.returning(row) |> pog.execute(conn) - { + case pog.query(sql) |> pog.returning(row) |> pog.execute(conn) { Ok(pog.Returned(rows: [_, ..], ..)) -> Ready Ok(pog.Returned(rows: [], ..)) -> Down("a required table is missing, or the role lacks CRUD on it") diff --git a/server/test/readiness_postgres_test.gleam b/server/test/readiness_postgres_test.gleam new file mode 100644 index 0000000..8ae7aec --- /dev/null +++ b/server/test/readiness_postgres_test.gleam @@ -0,0 +1,78 @@ +//// The readiness schema probe against a live Postgres, gated on +//// `DATABASE_URL` (a no-op when unset). This is the only check that the +//// hand-maintained `required_tables` lists still name the tables the stores' +//// migrations actually create. + +import crate_server/catalog_index_postgres +import crate_server/follow_index_postgres +import crate_server/known_users_postgres +import crate_server/oauth/sessions_postgres +import crate_server/readiness +import crate_server/shelf_index_postgres +import envoy +import exception +import gleam/erlang/process +import gleam/option.{None, Some} +import pog + +fn with_migrated_conn(run: fn(pog.Connection) -> Nil) -> Nil { + case envoy.get("DATABASE_URL") { + Error(Nil) -> Nil + Ok(url) -> { + let assert Ok(#(conn, pid)) = sessions_postgres.connect_test_pool(url) + // `pog.start` links the pool to its caller and `kill`'s exit signal is + // untrappable, so unlink first or this test process dies with the pool. + use <- exception.defer(fn() { + process.unlink(pid) + process.kill(pid) + }) + migrate_every_store(conn) + run(conn) + } + } +} + +fn migrate_every_store(conn: pog.Connection) -> Nil { + let assert Ok(_) = + sessions_postgres.table_store( + conn, + sessions_postgres.oauth_sessions_table, + Some(3600), + ) + let assert Ok(_) = + sessions_postgres.table_store( + conn, + sessions_postgres.discogs_creds_table, + None, + ) + let assert Ok(_) = known_users_postgres.table_store(conn) + let assert Ok(_) = catalog_index_postgres.table_store(conn) + let assert Ok(_) = shelf_index_postgres.table_store(conn) + let assert Ok(_) = follow_index_postgres.table_store(conn) + Nil +} + +pub fn the_schema_probe_passes_against_the_migrated_schema_test() { + use conn <- with_migrated_conn() + + assert readiness.schema_readiness(conn, readiness.schema_check_sql()) + == readiness.Ready +} + +pub fn the_schema_probe_reports_down_for_a_table_migrate_never_created_test() { + use conn <- with_migrated_conn() + let sql = readiness.schema_check_sql_for(["crate_no_such_table"]) + + let assert readiness.Down(_) = readiness.schema_readiness(conn, sql) + Nil +} + +// A query that errors instead of answering: the same shape a checkout timeout +// against a busy pool has, and equally not proof of anything. +pub fn a_failed_query_is_unknown_rather_than_down_test() { + use conn <- with_migrated_conn() + + let assert readiness.Unknown(_) = + readiness.schema_readiness(conn, "select no_such_column") + Nil +}