Files
insforge--insforge/backend/tests/unit/prefer-request-jwt-claims-migration.test.ts
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

182 lines
5.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(currentDir, '../../..');
const migrationDir = path.resolve(currentDir, '../../src/infra/database/migrations');
const migrationPath = path.resolve(migrationDir, '044_prefer-request-jwt-claims.sql');
const historicalDottedClaimMigrations = new Set([
'backend/src/infra/database/migrations/001_create-helper-functions.sql',
'backend/src/infra/database/migrations/013_create-auth-schema-functions.sql',
]);
function toPosixRelative(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join('/');
}
function collectFiles(dir: string, predicate: (filePath: string) => boolean): string[] {
const ignoredDirs = new Set(['.git', 'node_modules', 'dist', 'coverage']);
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!ignoredDirs.has(entry.name)) {
files.push(...collectFiles(entryPath, predicate));
}
continue;
}
if (entry.isFile() && predicate(entryPath)) {
files.push(entryPath);
}
}
return files;
}
function readRepoFiles(
paths: string[],
extensions: string[]
): Array<{ path: string; content: string }> {
return paths.flatMap((relativePath) => {
const absolutePath = path.resolve(repoRoot, relativePath);
if (!fs.existsSync(absolutePath)) {
return [];
}
return collectFiles(absolutePath, (filePath) =>
extensions.some((extension) => filePath.endsWith(extension))
).map((filePath) => ({
path: toPosixRelative(filePath),
content: fs.readFileSync(filePath, 'utf8'),
}));
});
}
function readSql(): string {
return fs.readFileSync(migrationPath, 'utf8');
}
describe('044_prefer-request-jwt-claims migration', () => {
it('migration file exists', () => {
expect(fs.existsSync(migrationPath)).toBe(true);
});
it.each(['uid', 'role', 'email'])('redefines auth.%s with CREATE OR REPLACE', (fnName) => {
const sql = readSql();
expect(sql).toMatch(new RegExp(`CREATE OR REPLACE FUNCTION auth\\.${fnName}\\(\\)`, 'i'));
});
it.each([
['uid', 'sub'],
['role', 'role'],
['email', 'email'],
])('auth.%s reads only canonical auth.jwt() claim %s', (_fnName, claimName) => {
const sql = readSql();
expect(sql).toMatch(
new RegExp(`SELECT nullif\\(auth\\.jwt\\(\\)\\s*->>\\s*'${claimName}',\\s*''\\)::`, 'i')
);
});
it('does not keep legacy dotted claim fallback', () => {
const sql = readSql();
expect(sql).not.toMatch(/request\.jwt\.claim\./i);
});
it('does not add or replace public helper functions', () => {
const sql = readSql();
expect(sql).not.toMatch(/CREATE OR REPLACE FUNCTION public\./i);
});
it('grants only missing storage runtime roles required before RLS can evaluate', () => {
const sql = readSql();
expect(sql).toMatch(/GRANT\s+USAGE\s+ON\s+SCHEMA\s+storage\s+TO\s+anon,\s*project_admin/i);
expect(sql).toMatch(
/GRANT\s+SELECT,\s*INSERT,\s*UPDATE,\s*DELETE\s+ON\s+storage\.objects\s+TO\s+anon,\s*project_admin/i
);
expect(sql).not.toMatch(/GRANT\s+SELECT\s+ON\s+storage\.buckets/i);
});
it('runs after migration 043', () => {
const migrations = fs
.readdirSync(migrationDir)
.filter((file) => file.endsWith('.sql'))
.sort();
expect(migrations.indexOf('044_prefer-request-jwt-claims.sql')).toBeGreaterThan(
migrations.indexOf('043_drop-deprecated-ai-configs-and-usage.sql')
);
});
it('keeps dotted jwt claims confined to immutable historical migrations', () => {
const scannedFiles = readRepoFiles(
[
'backend/src',
'backend/tests',
'docs',
'packages',
'openapi',
'.agents',
'.codex',
'.claude',
],
['.ts', '.tsx', '.js', '.jsx', '.sql', '.md', '.mdx', '.sh', '.json']
);
const offenders = scannedFiles
.filter(({ content }) => /request\.jwt\.claim\./i.test(content))
.map(({ path: filePath }) => filePath)
.filter((filePath) => !historicalDottedClaimMigrations.has(filePath));
expect(offenders).toEqual([]);
});
it('does not reference the removed db user-context service path', () => {
const scannedFiles = readRepoFiles(
['backend/src', 'backend/tests', '.agents', '.codex', '.claude'],
['.ts', '.tsx', '.js', '.jsx', '.md', '.mdx']
);
const offenders = scannedFiles
.filter(({ content }) => /services\/db\/user-context/i.test(content))
.map(({ path: filePath }) => filePath);
expect(offenders).toEqual([]);
});
it('does not reference the removed admin/database context abstraction', () => {
const scannedFiles = readRepoFiles(
[
'backend/src',
'backend/tests',
'docs',
'packages',
'openapi',
'.agents',
'.codex',
'.claude',
],
['.ts', '.tsx', '.js', '.jsx', '.sql', '.md', '.mdx', '.sh', '.json']
);
const removedContextPattern = new RegExp(
[
['ADMIN', 'DATABASE', 'CONTEXT'].join('_'),
['Database', 'Context'].join(''),
['with', 'Database', 'Context'].join(''),
].join('|'),
'i'
);
const offenders = scannedFiles
.filter(({ content }) => removedContextPattern.test(content))
.map(({ path: filePath }) => filePath);
expect(offenders).toEqual([]);
});
});