ci/woodpecker/push/publish Pipeline failed
Co-authored-by: code-infra-01 <[email protected]>
140 lines
5.6 KiB
TypeScript
140 lines
5.6 KiB
TypeScript
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { sql } from 'drizzle-orm';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { createPgliteDb } from './client-pglite.js';
|
|
import { runPgliteMigrations } from './migrate.js';
|
|
import type { DbHandle } from './client.js';
|
|
|
|
interface PgliteExec {
|
|
exec(query: string): Promise<unknown>;
|
|
}
|
|
|
|
describe('runPgliteMigrations', () => {
|
|
let dataDir: string;
|
|
let handle: DbHandle;
|
|
|
|
beforeEach(() => {
|
|
dataDir = mkdtempSync(join(tmpdir(), 'mosaic-db-migrate-test-'));
|
|
handle = createPgliteDb(dataDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await handle.close();
|
|
rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('creates the BetterAuth tables required by the gateway', async () => {
|
|
await runPgliteMigrations(handle);
|
|
|
|
const result = (await handle.db.execute(sql`
|
|
SELECT table_name FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name
|
|
`)) as unknown as { rows: Array<{ table_name: string }> };
|
|
|
|
const tables = result.rows.map((r) => r.table_name);
|
|
|
|
// Auth tables — required for sign-in / bootstrap to function.
|
|
expect(tables).toContain('users');
|
|
expect(tables).toContain('sessions');
|
|
expect(tables).toContain('accounts');
|
|
expect(tables).toContain('verifications');
|
|
|
|
// Schema sanity check — admin token table consumed by mosaic gateway config.
|
|
expect(tables).toContain('admin_tokens');
|
|
});
|
|
|
|
it('is idempotent — running twice does not error', async () => {
|
|
await runPgliteMigrations(handle);
|
|
await expect(runPgliteMigrations(handle)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('gives accounts an issuer column (#1395) — better-auth >=1.7 requires it', async () => {
|
|
await runPgliteMigrations(handle);
|
|
|
|
const result = (await handle.db.execute(sql`
|
|
SELECT column_name, is_nullable, data_type
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'accounts' AND column_name = 'issuer'
|
|
`)) as unknown as {
|
|
rows: Array<{ column_name: string; is_nullable: string; data_type: string }>;
|
|
};
|
|
|
|
// Nullable by design: the 1.5.x line this repo's lockfile resolves to does
|
|
// not write the field; 1.7+ populates it. One schema serves both.
|
|
expect(result.rows).toHaveLength(1);
|
|
expect(result.rows[0]?.is_nullable).toBe('YES');
|
|
expect(result.rows[0]?.data_type).toBe('text');
|
|
});
|
|
|
|
it('backfills ONLY credential rows with the synthetic issuer (#1395 upgrade path)', async () => {
|
|
// Simulate an upgraded install: migrate through 0016 only, seed pre-issuer
|
|
// rows (one credential, one oauth), then apply 0017 and discriminate.
|
|
const client = (handle.db as unknown as { $client: PgliteExec }).$client;
|
|
|
|
// Migrate to 0016 by replaying every ledger file except 0017 — the ledger
|
|
// table gates re-application, so a plain replay of 0000..0016 is enough.
|
|
const fs = await import('node:fs');
|
|
const path = await import('node:path');
|
|
const dir = path.join(import.meta.dirname, '..', 'drizzle');
|
|
const files = fs
|
|
.readdirSync(dir)
|
|
.filter((f) => /^\d{4}_.*\.sql$/.test(f) && f < '0017')
|
|
.sort();
|
|
for (const f of files) {
|
|
const raw = fs.readFileSync(path.join(dir, f), 'utf-8');
|
|
for (const stmt of raw.split('--> statement-breakpoint')) {
|
|
const trimmed = stmt.trim();
|
|
if (trimmed) await client.exec(trimmed);
|
|
}
|
|
}
|
|
|
|
await client.exec(`
|
|
INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
|
|
VALUES ('u1', 'Legacy User', '[email protected]', true, now(), now());
|
|
INSERT INTO accounts (id, account_id, provider_id, user_id, created_at, updated_at)
|
|
VALUES
|
|
('a1', '[email protected]', 'credential', 'u1', now(), now()),
|
|
('a2', 'oauth-provider-1', 'google', 'u1', now(), now());
|
|
`);
|
|
|
|
// Apply 0017 (column + backfill).
|
|
const sql0017 = fs.readFileSync(path.join(dir, '0017_accounts_issuer.sql'), 'utf-8');
|
|
for (const stmt of sql0017.split('--> statement-breakpoint')) {
|
|
const trimmed = stmt.trim();
|
|
if (trimmed) await client.exec(trimmed);
|
|
}
|
|
|
|
const rows = (await handle.db.execute(sql`
|
|
SELECT provider_id, issuer FROM accounts ORDER BY id
|
|
`)) as unknown as { rows: Array<{ provider_id: string; issuer: string | null }> };
|
|
|
|
const byProvider = new Map(rows.rows.map((r) => [r.provider_id, r.issuer]));
|
|
// Credential rows get better-auth's synthetic local issuer — the value
|
|
// sign-in filters on (better-auth dist createLocalAccountIssuer).
|
|
expect(byProvider.get('credential')).toBe('local:credential');
|
|
// OAuth rows are LEFT NULL: better-auth owns their issuer semantics going
|
|
// forward (each provider's real issuer on its next flow).
|
|
expect(byProvider.get('google')).toBeNull();
|
|
});
|
|
|
|
it('surfaces statement-level error context on failure and leaves no ledger row', async () => {
|
|
// Pre-create a `users` table that conflicts with migration 0000's CREATE TABLE,
|
|
// forcing it to fail without IF NOT EXISTS.
|
|
const client = (handle.db as unknown as { $client: PgliteExec }).$client;
|
|
await client.exec('CREATE TABLE users (sentinel text)');
|
|
|
|
await expect(runPgliteMigrations(handle)).rejects.toThrow(
|
|
/migration hash=[a-f0-9]+ statement #\d+ failed/,
|
|
);
|
|
|
|
// Ledger should be empty — partial application must not pretend to be complete.
|
|
const ledger = (await handle.db.execute(
|
|
sql`SELECT count(*)::int AS count FROM drizzle.__drizzle_migrations`,
|
|
)) as unknown as { rows: Array<{ count: number }> };
|
|
expect(ledger.rows[0]?.count).toBe(0);
|
|
});
|
|
});
|