@zingage/postgres-multi-tenant-ids
Version:
PostgreSQL IDs for secure multi-tenant applications
118 lines (117 loc) • 7.17 kB
JavaScript
import fc from "fast-check";
import assert from "node:assert/strict";
import { exec } from "node:child_process";
import { after, before, describe, it } from "node:test";
import { promisify } from "node:util";
import pg from "pg";
import { makeFastCheckArbitraries } from "./fast-check.js";
import { isUnscopedId, makeMakeUnscopedIdBound, } from "./id-kind-implementations/cross-tenant-entity-ids.js";
import { getTenantShortIdFromFullId, isTenantId, } from "./id-kind-implementations/tenant-ids.js";
import { isScopedId, makeMakeScopedIdBound, } from "./id-kind-implementations/tenant-scoped-entity-ids.js";
import { exampleConfig } from "./test-helpers/exampleConfigFixture.js";
const { TenantIdArbitrary, TenantScopedIdArbitrary, CrossTenantIdArbitrary, IdDateArbitrary, TenantScopedEntityTypeArbitrary, CrossTenantEntityTypeArbitrary, } = makeFastCheckArbitraries(exampleConfig);
const makeScopedId = makeMakeScopedIdBound(exampleConfig.tenantScoped);
const makeUnscopedId = makeMakeUnscopedIdBound(exampleConfig.crossTenant);
describe("Database generated Zingage ids", () => {
let client;
before(async () => {
// Start docker container if not running
const dockerStartCmd = "docker compose up --wait";
await promisify(exec)(dockerStartCmd);
// Wait for DB to be ready
client = new pg.Pool({
connectionString: "postgres://postgres:postgres@localhost:9000/pg_ids_test",
});
});
after(async () => {
await client.end();
await promisify(exec)("docker compose down");
});
describe("Tenant-scoped entity ids", () => {
it("should agree with the js implementation, modulo random bits", async () => {
await fc.assert(fc.asyncProperty(fc.array(fc.tuple(TenantIdArbitrary, IdDateArbitrary, TenantScopedEntityTypeArbitrary), { minLength: 50 }), async (generatedData) => {
const individualResultQueries = generatedData.map(([tenantId, date, entityType]) => {
const { entityTypeHint, insertRate } = exampleConfig.tenantScoped[entityType];
const jsGeneratedId = makeScopedId(tenantId, entityType, date);
const pgUuidBytesExpression = `uuid_send(make_tenant_scoped_entity_id(
'${tenantId}',
'${insertRate}',
B'${entityTypeHint}',
'${date.toISOString()}'
))`;
const jsValUuidBytesExpression = `uuid_send('${jsGeneratedId}')`;
// Compare only the first 93 (i.e., the non-random) bits.
return `SELECT
substring(${pgUuidBytesExpression} from 1 for 11) =
substring(${jsValUuidBytesExpression} from 1 for 11) AND
(get_byte(${pgUuidBytesExpression}, 11) & B'11111000'::integer) =
(get_byte(${jsValUuidBytesExpression}, 11) & B'11111000'::integer) as res`;
});
const allResultsQuery = individualResultQueries.join(" UNION ALL ");
const allResults = await client.query(allResultsQuery);
assert.ok(allResults.rows.every((row) => row.res));
}), { numRuns: 1 });
});
it("getting short tenant id from tenant-scoped id should agree with the js implementation", async () => {
await fc.assert(fc.asyncProperty(TenantIdArbitrary, async (tenantId) => {
const scopedId = makeScopedId(tenantId, "ACCOUNT", new Date());
const query = `SELECT get_tenant_short_id_from_scoped_id('${scopedId}') as id`;
const res = await client.query(query);
assert.equal(res.rows[0]["id"], getTenantShortIdFromFullId(tenantId));
}));
});
});
describe("Cross-tenant entity ids", () => {
it("should agree with the js implementation, modulo random bits", async () => {
await fc.assert(fc.asyncProperty(fc.array(fc.tuple(IdDateArbitrary, CrossTenantEntityTypeArbitrary), {
minLength: 50,
}), async (gens) => {
const individualResultQueries = gens.map(([date, entityType]) => {
const { entityTypeHint, insertRate } = exampleConfig.crossTenant[entityType];
const jsGeneratedId = makeUnscopedId(entityType, date);
const pgUuidBytesExpression = `uuid_send(make_cross_tenant_entity_id(
'${insertRate}',
B'${entityTypeHint}',
'${date.toISOString()}'
))`;
const jsValUuidBytesExpression = `uuid_send('${jsGeneratedId}')`;
// Compare only the first 62 (i.e., the non-random) bits.
return `SELECT
substring(${pgUuidBytesExpression} from 1 for 7) =
substring(${jsValUuidBytesExpression} from 1 for 7) AND
(get_byte(${pgUuidBytesExpression}, 7) & B'11111100'::integer) =
(get_byte(${jsValUuidBytesExpression}, 7) & B'11111100'::integer) as res`;
});
const allResultsQuery = individualResultQueries.join(" UNION ALL ");
// run these manually in the db.
const allResults = await client.query(allResultsQuery);
assert.ok(allResults.rows.every((row) => row.res));
}), { numRuns: 1 });
});
});
describe("is_xxx_id id classificationdb functions", () => {
it("should agree with the js implementation", async () => {
await fc.assert(fc.asyncProperty(TenantScopedIdArbitrary, CrossTenantIdArbitrary, TenantIdArbitrary, async (ownedId, unownedId, tenantId) => {
const query = `
SELECT
ARRAY[is_tenant_id('${ownedId}'), is_tenant_scoped_id('${ownedId}'), is_cross_tenant_id('${ownedId}')] as a,
ARRAY[is_tenant_id('${unownedId}'), is_tenant_scoped_id('${unownedId}'), is_cross_tenant_id('${unownedId}')] as b,
ARRAY[is_tenant_id('${tenantId}'), is_tenant_scoped_id('${tenantId}'), is_cross_tenant_id('${tenantId}')] as c;`;
const res = await client.query(query);
const { a: ownedIdResults, b: unownedIdResults, c: tenantIdResults, } = res.rows[0];
// Test tenant-scoped id
assert.equal(ownedIdResults[0], isTenantId(ownedId));
assert.equal(ownedIdResults[1], isScopedId(ownedId));
assert.equal(ownedIdResults[2], isUnscopedId(ownedId));
// Test unowned id
assert.equal(unownedIdResults[0], isTenantId(unownedId));
assert.equal(unownedIdResults[1], isScopedId(unownedId));
assert.equal(unownedIdResults[2], isUnscopedId(unownedId));
// Test tenant id
assert.equal(tenantIdResults[0], isTenantId(tenantId));
assert.equal(tenantIdResults[1], isScopedId(tenantId));
assert.equal(tenantIdResults[2], isUnscopedId(tenantId));
}), { numRuns: 50 });
});
});
});