feat(staging): switch to local postgres
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
|
||||
import { createLocalPostgresDataClient } from "../src/lib/db/local-postgres-client-core.ts";
|
||||
import { startPostgresFixture } from "./helpers/postgres-fixture.ts";
|
||||
|
||||
const runnerPath = fileURLToPath(
|
||||
new URL("../scripts/db-migrate.mjs", import.meta.url),
|
||||
);
|
||||
|
||||
test("local PostgreSQL applies the reviewed business schema and serves authenticated business calls", async () => {
|
||||
const fixture = startPostgresFixture();
|
||||
const schemaUrl = fixture.connectionUrl(
|
||||
"schema_owner",
|
||||
"schema-owner-test-password",
|
||||
);
|
||||
|
||||
try {
|
||||
const migration = spawnSync(process.execPath, [runnerPath], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
SCHEMA_DATABASE_URL: schemaUrl,
|
||||
},
|
||||
});
|
||||
assert.equal(migration.status, 0, migration.stderr);
|
||||
assert.match(migration.stdout, /applied 20260715000000_account_credits\.sql/);
|
||||
assert.match(migration.stdout, /applied 20260721150000_align_conversational_finance_domain\.sql/);
|
||||
|
||||
assert.equal(
|
||||
fixture.psql(`
|
||||
select string_agg(tablename, ',' order by tablename)
|
||||
from pg_tables
|
||||
where schemaname = 'public'
|
||||
`),
|
||||
[
|
||||
"birth_time_rectification_action_receipts",
|
||||
"birth_time_rectification_billing",
|
||||
"birth_time_rectification_cases",
|
||||
"birth_time_rectification_dynamic_state",
|
||||
"birth_time_rectification_event_evidence",
|
||||
"birth_time_rectification_handoff_attach_receipts",
|
||||
"birth_time_rectification_handoff_settlements",
|
||||
"birth_time_rectification_question_handoffs",
|
||||
"birth_time_rectification_scoring_jobs",
|
||||
"birth_time_rectification_turns",
|
||||
"chart_profiles",
|
||||
"chat_sessions",
|
||||
"consultation_requests",
|
||||
"credit_request_cancellations",
|
||||
"credit_transactions",
|
||||
"profiles",
|
||||
"redemption_codes",
|
||||
"synastry_reports",
|
||||
].join(","),
|
||||
);
|
||||
|
||||
fixture.psqlAs(
|
||||
"identity_runtime",
|
||||
"identity-runtime-test-password",
|
||||
`
|
||||
insert into identity.users (name, email, email_verified, email_verified_at)
|
||||
values ('Local User', 'local-user@example.com', true, now())
|
||||
`,
|
||||
);
|
||||
const userId = fixture.psql(
|
||||
"select id from identity.users where email = 'local-user@example.com'",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select email from auth.users where id = '${userId}'`),
|
||||
"local-user@example.com",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`select email || ':' || credits from public.profiles where id = '${userId}'`),
|
||||
"local-user@example.com:0",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psqlAs(
|
||||
"app_runtime",
|
||||
"app-runtime-test-password",
|
||||
`set role authenticated;
|
||||
select set_config('request.jwt.claim.sub', '${userId}', true);
|
||||
select email from public.profiles where id = '${userId}'`,
|
||||
),
|
||||
`SET\n${userId}\nlocal-user@example.com`,
|
||||
);
|
||||
|
||||
const local = createLocalPostgresDataClient(
|
||||
fixture.connectionUrl("app_runtime", "app-runtime-test-password"),
|
||||
{ id: userId, email: "local-user@example.com" },
|
||||
);
|
||||
const profile = await local.from("profiles")
|
||||
.select("id,email,credits")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
assert.equal(profile.error, null);
|
||||
assert.deepEqual(profile.data, {
|
||||
id: userId,
|
||||
email: "local-user@example.com",
|
||||
credits: 0,
|
||||
});
|
||||
const admin = createLocalPostgresDataClient(
|
||||
fixture.connectionUrl("admin_runtime", "admin-runtime-test-password"),
|
||||
null,
|
||||
"service_role",
|
||||
);
|
||||
const adminProfile = await admin.from("profiles")
|
||||
.select("id")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
assert.equal(adminProfile.error, null);
|
||||
assert.deepEqual(adminProfile.data, { id: userId });
|
||||
|
||||
const sessionId = "11111111-1111-4111-8111-111111111111";
|
||||
const inserted = await local.from("chat_sessions").insert({
|
||||
id: sessionId,
|
||||
user_id: userId,
|
||||
title: "Local conversation",
|
||||
theme: "general",
|
||||
model_id: "test-model",
|
||||
messages: [],
|
||||
session_type: "consultation",
|
||||
rectification_case_id: null,
|
||||
updated_at: new Date().toISOString(),
|
||||
}).select("id").single();
|
||||
assert.equal(inserted.error, null);
|
||||
assert.deepEqual(inserted.data, { id: sessionId });
|
||||
|
||||
fixture.psql(`
|
||||
insert into public.redemption_codes (code_hash, code_mask, credits)
|
||||
values ('${"a".repeat(64)}', 'JYOTISH-****-TEST', 3)
|
||||
`);
|
||||
const redeemed = await local.rpc("redeem_code", {
|
||||
p_code_hash: "a".repeat(64),
|
||||
});
|
||||
assert.equal(redeemed.error, null);
|
||||
assert.deepEqual(redeemed.data, [{ success: true, credits: 3, error_code: null }]);
|
||||
} finally {
|
||||
fixture.stop();
|
||||
}
|
||||
});
|
||||
@@ -33,6 +33,14 @@ test("database roles have no cluster privileges", () => {
|
||||
"schema_owner:f:f:f:f:f:f",
|
||||
].join("\n"),
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(`
|
||||
select rolcanlogin || ':' || rolbypassrls
|
||||
from pg_roles
|
||||
where rolname = 'service_role'
|
||||
`),
|
||||
"f:true",
|
||||
);
|
||||
assert.equal(
|
||||
fixture.psql(
|
||||
"select nspowner::regrole::text from pg_namespace where nspname = 'public'",
|
||||
|
||||
@@ -156,7 +156,7 @@ test("server compose defaults to local images without removing either build", ()
|
||||
}
|
||||
});
|
||||
|
||||
test("staging Caddy isolates the public and identity-only admin hosts", () => {
|
||||
test("staging Caddy isolates the business admin surface from the public host", () => {
|
||||
const caddy = readFileSync(
|
||||
new URL("../../deploy/Caddyfile.staging", import.meta.url),
|
||||
"utf8",
|
||||
@@ -168,7 +168,9 @@ test("staging Caddy isolates the public and identity-only admin hosts", () => {
|
||||
/\{\$ADMIN_SITE_ADDRESS:https:\/\/admin\.staging\.jyotisha\.chat\}/,
|
||||
);
|
||||
assert.match(caddy, /reverse_proxy web:3000/);
|
||||
assert.match(caddy, /@identity path \/login \/api\/auth\/\*/);
|
||||
assert.match(caddy, /@adminPaths path \/admin \/admin\/\* \/api\/admin\/\*/);
|
||||
assert.match(caddy, /redir @adminRoot \/admin\/codes 302/);
|
||||
assert.match(caddy, /@adminSurface path \/login \/admin \/admin\/\* \/api\/admin\/\* \/api\/auth\/\*/);
|
||||
assert.match(caddy, /respond "Not found" 404/);
|
||||
assert.doesNotMatch(caddy, /www\.jyotisha\.chat/);
|
||||
});
|
||||
@@ -234,15 +236,19 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi
|
||||
"CADDYFILE_PATH=./Caddyfile.staging",
|
||||
"SITE_ADDRESS=https://staging.jyotisha.chat",
|
||||
"ADMIN_SITE_ADDRESS=https://admin.staging.jyotisha.chat",
|
||||
"AUTH_PROVIDER=supabase",
|
||||
"AUTH_PROVIDER=self-hosted",
|
||||
"SELF_HOSTED_IDENTITY_ENABLED=true",
|
||||
"AUTH_USER_ORIGIN=https://staging.jyotisha.chat",
|
||||
"AUTH_ADMIN_ORIGIN=https://admin.staging.jyotisha.chat",
|
||||
"IDENTITY_DATABASE_URL=postgresql://identity_runtime:identity-runtime-test-password@postgres:5432/jyotisha",
|
||||
"APP_DATABASE_URL=postgresql://app_runtime:app-runtime-test-password@postgres:5432/jyotisha",
|
||||
"ADMIN_DATABASE_URL=postgresql://admin_runtime:admin-runtime-test-password@postgres:5432/jyotisha",
|
||||
"BETTER_AUTH_USER_SECRET=user-secret-that-is-at-least-32-bytes-long",
|
||||
"BETTER_AUTH_ADMIN_SECRET=admin-secret-that-is-at-least-32-bytes-long",
|
||||
"RESEND_API_KEY=re_test_key_that_must_not_be_printed",
|
||||
"RESEND_FROM_EMAIL=Jyotisha Staging <login@staging.jyotisha.chat>",
|
||||
"ADMIN_EMAILS=admin@example.com",
|
||||
"JYOTISH_DYNAMIC_RECTIFICATION_TOKEN=dynamic-token-that-is-at-least-32-bytes",
|
||||
];
|
||||
const run = () =>
|
||||
spawnSync("bash", [validator, envFile], { encoding: "utf8" });
|
||||
@@ -325,7 +331,7 @@ test("staging env validator rejects selector drift, duplicates, and unsafe permi
|
||||
writeEnv(
|
||||
validSelectors.map((line) =>
|
||||
line.startsWith("AUTH_PROVIDER=")
|
||||
? "AUTH_PROVIDER=self-hosted"
|
||||
? "AUTH_PROVIDER=supabase"
|
||||
: line,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ test("quality gate validates relevant changes once and publishes a digest manife
|
||||
assert.match(workflow, /node frontend\/scripts\/staging-image-manifest\.mjs/);
|
||||
assert.match(workflow, /name: staging-image-manifest-\$\{\{ github\.sha \}\}/);
|
||||
assert.match(workflow, /uses: actions\/upload-artifact@v4/);
|
||||
assert.doesNotMatch(workflow, /STAGING_SUPABASE|NEXT_PUBLIC_SUPABASE/);
|
||||
assert.doesNotMatch(workflow, /(?:^|:)latest$/m);
|
||||
});
|
||||
|
||||
@@ -337,6 +338,11 @@ test("manual migration uses only PostgreSQL and the digest-pinned migrator", ()
|
||||
assert.match(runner, /docker pull "\$WEB_IMAGE"/);
|
||||
assert.match(runner, /up -d --no-build --pull never --wait postgres/);
|
||||
assert.match(runner, /-f deploy\/docker-compose\.postgres\.yml/);
|
||||
assertOrder(runner, [
|
||||
"up -d --no-build --pull never --wait postgres",
|
||||
"002-ensure-business-compatibility-roles.sql",
|
||||
"--profile migration run --rm migrator",
|
||||
]);
|
||||
assert.match(runner, /--profile migration run --rm migrator/);
|
||||
assert.match(runner, /select filename from migration\.schema_migrations order by filename/);
|
||||
assert.doesNotMatch(runner, /docker-compose\.server\.yml/);
|
||||
|
||||
Reference in New Issue
Block a user