Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
68
apps/api/src/db/publishing-migration.integration.test.ts
Normal file
68
apps/api/src/db/publishing-migration.integration.test.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Pool } from "pg";
|
||||
import { runMigrations } from "./migrate.js";
|
||||
import { assertPublishingSchema } from "@tactical-media-broadcast-hub/publishing-execution";
|
||||
const ownerUrl = process.env.PUBLISHING_IT_SUPER_DATABASE_URL;
|
||||
const appUrl = process.env.PUBLISHING_IT_DATABASE_URL;
|
||||
describe.skipIf(!ownerUrl || !appUrl)("publishing migration upgrade and resumability (local DB)", () => {
|
||||
let admin: Pool; let owner: Pool; let app: Pool; let dir: string;
|
||||
let database: string; let previousOwner: string | undefined;
|
||||
beforeAll(async () => {
|
||||
const url = new URL(ownerUrl!); url.pathname = "/postgres";
|
||||
admin = new Pool({ connectionString: url.toString() });
|
||||
database = `wp3_migration_sol_${Date.now()}`;
|
||||
await admin.query(`CREATE DATABASE ${database}`);
|
||||
url.pathname = `/${database}`; previousOwner = process.env.SUPER_DATABASE_URL;
|
||||
process.env.SUPER_DATABASE_URL = url.toString(); owner = new Pool({ connectionString: url.toString() });
|
||||
const runtimeUrl = new URL(appUrl!); runtimeUrl.pathname = `/${database}`; app = new Pool({ connectionString: runtimeUrl.toString() });
|
||||
dir = await mkdtemp(join(tmpdir(), "wp3-migrations-sol-"));
|
||||
for (const file of (await readdir("migrations")).filter((name) => name.endsWith(".sql") && name < "018")) await writeFile(join(dir, file), await readFile(join("migrations", file)));
|
||||
await runMigrations(dir);
|
||||
}, 30_000);
|
||||
afterAll(async () => {
|
||||
if (previousOwner === undefined) delete process.env.SUPER_DATABASE_URL; else process.env.SUPER_DATABASE_URL = previousOwner;
|
||||
await owner?.end(); await app?.end();
|
||||
if (admin && database) await admin.query(`DROP DATABASE ${database}`);
|
||||
await admin?.end();
|
||||
});
|
||||
it("upgrades legacy schedules without inventing authorization and resumes 019 after failed cutover", async () => {
|
||||
const user = (await owner.query("INSERT INTO users(external_id, username, display_name) VALUES('migration-sol', 'migration-sol', 'Migration') RETURNING id")).rows[0].id as string;
|
||||
const workspace = (await owner.query("INSERT INTO workspaces(name,slug) VALUES('Migration','migration-sol') RETURNING id")).rows[0].id as string;
|
||||
const connection = (await owner.query("INSERT INTO connections(workspace_id,provider,channel_id) VALUES($1,'mastodon','channel') RETURNING id", [workspace])).rows[0].id as string;
|
||||
const keys = ["legacy-pending", "legacy-flight", "legacy-done"];
|
||||
for (const [index, key] of keys.entries()) {
|
||||
const draft = (await owner.query("INSERT INTO drafts(workspace_id,author_user_id,post,state) VALUES($1,$2,'{}','scheduled') RETURNING id", [workspace, user])).rows[0].id as string;
|
||||
await owner.query("INSERT INTO scheduled_publications(workspace_id,draft_id,connection_id,publish_at,idempotency_key,state) VALUES($1,$2,$3,now(),$4,$5)", [workspace, draft, connection, key, index === 1 ? "in_flight" : "pending"]);
|
||||
if (index === 2) await owner.query("INSERT INTO publications(workspace_id,draft_id,connection_id,idempotency_key,remote_id,published_at) VALUES($1,$2,$3,$4,'remote',now())", [workspace, draft, connection, key]);
|
||||
}
|
||||
await writeFile(join(dir, "018_publishing_execution.sql"), await readFile("migrations/018_publishing_execution.sql"));
|
||||
await runMigrations(dir);
|
||||
const upgraded = await owner.query("SELECT state, scheduled_by_user_id, execution_version FROM scheduled_publications ORDER BY idempotency_key");
|
||||
expect(upgraded.rows.map((row) => row.state)).toEqual(["done", "failed", "needs_reauthorization"]);
|
||||
expect(upgraded.rows.every((row) => row.scheduled_by_user_id === null && row.execution_version === 0)).toBe(true);
|
||||
const original = await readFile("migrations/019_publishing_integration.sql", "utf8");
|
||||
await writeFile(join(dir, "019_publishing_integration.sql"), original.replace("DROP FUNCTION publishing_due_schedules(integer);", "SELECT nonexistent_cutover_function();"));
|
||||
await expect(runMigrations(dir)).rejects.toThrow("cutover failed");
|
||||
expect((await owner.query("SELECT name FROM schema_migrations WHERE name = '019_publishing_integration.sql'")).rowCount).toBe(0);
|
||||
await expect(assertPublishingSchema(app)).rejects.toThrow();
|
||||
expect((await owner.query("SELECT name FROM schema_migrations WHERE name LIKE '019%:%'")).rowCount).toBe(3);
|
||||
await writeFile(join(dir, "019_publishing_integration.sql"), original);
|
||||
await expect(runMigrations(dir)).resolves.toEqual(["019_publishing_integration.sql"]);
|
||||
await expect(assertPublishingSchema(app)).resolves.toBeUndefined();
|
||||
await expect(runMigrations(dir)).resolves.toEqual([]);
|
||||
const role = (await app.query("SELECT current_user, rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user")).rows[0];
|
||||
expect(role).toEqual({ current_user: "sp_app", rolsuper: false, rolbypassrls: false });
|
||||
}, 30_000);
|
||||
it("repairs an invalid concurrent index only when its definition matches", async () => {
|
||||
const file = "020_test_concurrent.sql";
|
||||
await writeFile(join(dir, file), "-- wp3:phase expand transaction\nCREATE TABLE migration_index_test(v integer); INSERT INTO migration_index_test VALUES(1),(1);\n-- wp3:phase idx concurrent\nCREATE UNIQUE INDEX CONCURRENTLY migration_index_test_idx ON migration_index_test (v);");
|
||||
await expect(runMigrations(dir)).rejects.toThrow("idx failed");
|
||||
expect((await owner.query("SELECT indisvalid FROM pg_index WHERE indexrelid = 'migration_index_test_idx'::regclass")).rows[0]?.indisvalid).toBe(false);
|
||||
await owner.query("DELETE FROM migration_index_test WHERE ctid NOT IN (SELECT min(ctid) FROM migration_index_test GROUP BY v)");
|
||||
await expect(runMigrations(dir)).resolves.toEqual([file]);
|
||||
expect((await owner.query("SELECT indisvalid FROM pg_index WHERE indexrelid = 'migration_index_test_idx'::regclass")).rows[0]?.indisvalid).toBe(true);
|
||||
}, 30_000);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue