mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:25:29 +08:00
223 lines
6.4 KiB
JavaScript
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,
|
|
);
|
|
});
|