Files
qinglong/packages/ql3-cluster-postgres/test/projectPolicyRepository.test.cjs
T

223 lines
6.4 KiB
JavaScript

const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
ProjectPolicyUnavailableError,
ProjectRoleBindingMutationConflictError,
ProjectRoleBindingVersionConflictError,
} = require('@qinglong/runtime-core/project-policy');
const {
PostgresProjectPolicyRepository,
} = require('../dist/security/projectPolicyRepository');
const BINDING = Object.freeze({
projectId: 'default',
subject: Object.freeze({ type: 'user', id: 'usr_primary' }),
version: 1,
state: 'active',
role: 'owner',
mutationId: 'grant-owner-1',
changedBy: Object.freeze({ type: 'system', id: 'bootstrap' }),
createdAtMs: 1,
});
function bindingRow(overrides = {}) {
return {
projectId: 'default',
subjectType: 'user',
subjectId: 'usr_primary',
version: 1,
state: 'active',
role: 'owner',
mutationId: 'grant-owner-1',
changedByType: 'system',
changedById: 'bootstrap',
createdAtMs: '1',
...overrides,
};
}
function snapshotRow(overrides = {}) {
return {
projectId: 'default',
projectName: 'Default',
projectSlug: 'default',
projectStatus: 'active',
projectVersion: 1,
projectCreatedAtMs: '0',
projectUpdatedAtMs: '0',
bindingProjectId: 'default',
bindingSubjectType: 'user',
bindingSubjectId: 'usr_primary',
bindingVersion: 1,
bindingState: 'active',
bindingRole: 'owner',
bindingMutationId: 'grant-owner-1',
bindingChangedByType: 'system',
bindingChangedById: 'bootstrap',
bindingCreatedAtMs: '1',
...overrides,
};
}
test('resolves one normalized Project and latest RoleBinding snapshot', async () => {
const queries = [];
const repository = new PostgresProjectPolicyRepository({
async query(text, values) {
queries.push({ text, values });
return { rows: [snapshotRow()] };
},
async connect() {
throw new Error('not used');
},
});
assert.deepEqual(
await repository.resolve('default', { type: 'user', id: 'usr_primary' }),
{
project: {
id: 'default',
name: 'Default',
slug: 'default',
status: 'active',
version: 1,
createdAtMs: 0,
updatedAtMs: 0,
},
binding: BINDING,
},
);
assert.deepEqual(queries[0].values, ['default', 'user', 'usr_primary']);
assert.match(queries[0].text, /LEFT JOIN LATERAL/);
assert.match(queries[0].text, /ORDER BY candidate\.version DESC/);
await assert.rejects(
repository.resolve('x'.repeat(129), {
type: 'user',
id: 'usr_primary',
}),
);
assert.equal(queries.length, 1);
});
function appendPool(options = {}) {
const events = [];
let connection = 0;
return {
events,
pool: {
async query() {
throw new Error('not used');
},
async connect() {
connection += 1;
let inserted = false;
return {
async query(text) {
events.push(text.split('\n', 1)[0]);
if (text.startsWith('SELECT id FROM'))
return { rows: [{ id: 'default' }] };
if (text.includes('mutation_id = $2')) {
return {
rows: options.replay ? [bindingRow(options.replay)] : [],
};
}
if (text.startsWith('SELECT version FROM')) {
return {
rows:
options.currentVersion === undefined
? []
: [{ version: options.currentVersion }],
};
}
if (text.startsWith('INSERT INTO')) {
if (options.retryOnce && connection === 1 && !inserted) {
inserted = true;
throw Object.assign(new Error('serialization'), {
code: '40001',
});
}
return { rows: [], rowCount: 1 };
}
return { rows: [] };
},
release() {
events.push(`release:${connection}`);
},
};
},
},
};
}
test('appends under a serializable Project lock and replays exactly', async () => {
const insertedFixture = appendPool();
const inserted = await new PostgresProjectPolicyRepository(
insertedFixture.pool,
).append({ expectedCurrentVersion: 0, binding: BINDING });
assert.deepEqual(inserted, { status: 'inserted', binding: BINDING });
assert.equal(
insertedFixture.events.some((event) =>
event.startsWith('BEGIN ISOLATION LEVEL SERIALIZABLE'),
),
true,
);
assert.ok(
insertedFixture.events.findIndex((event) => event.startsWith('SELECT id')) <
insertedFixture.events.findIndex((event) => event.startsWith('INSERT')),
);
assert.ok(insertedFixture.events.includes('COMMIT'));
const replayFixture = appendPool({ replay: {} });
const replay = await new PostgresProjectPolicyRepository(
replayFixture.pool,
).append({ expectedCurrentVersion: 0, binding: BINDING });
assert.deepEqual(replay, { status: 'existing', binding: BINDING });
assert.equal(
replayFixture.events.some((event) => event.startsWith('INSERT')),
false,
);
});
test('rejects stale versions and conflicting mutation replay', async () => {
await assert.rejects(
new PostgresProjectPolicyRepository(
appendPool({ currentVersion: 1 }).pool,
).append({ expectedCurrentVersion: 0, binding: BINDING }),
ProjectRoleBindingVersionConflictError,
);
await assert.rejects(
new PostgresProjectPolicyRepository(
appendPool({ replay: { role: 'viewer' } }).pool,
).append({ expectedCurrentVersion: 0, binding: BINDING }),
ProjectRoleBindingMutationConflictError,
);
});
test('retries serialization failures but fails closed on corrupt rows', async () => {
const retryFixture = appendPool({ retryOnce: true });
assert.equal(
(
await new PostgresProjectPolicyRepository(retryFixture.pool).append({
expectedCurrentVersion: 0,
binding: BINDING,
})
).status,
'inserted',
);
assert.equal(
retryFixture.events.filter((event) => event.startsWith('release:')).length,
2,
);
const repository = new PostgresProjectPolicyRepository({
async query() {
return { rows: [snapshotRow({ projectVersion: 'not-a-number' })] };
},
async connect() {
throw new Error('not used');
},
});
await assert.rejects(
repository.resolve('default', { type: 'user', id: 'usr_primary' }),
ProjectPolicyUnavailableError,
);
});