100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
import { identityApp, type IdentityApp } from './helpers/identity-app';
|
|
import { legacyOrganizationId } from './helpers/test-database';
|
|
import { RecoveryStore } from '../src/identity/recovery.store';
|
|
import { issueToken } from '../src/identity/tokens';
|
|
|
|
describe('migration integrity', () => {
|
|
let ctx: IdentityApp;
|
|
beforeAll(async () => {
|
|
ctx = await identityApp();
|
|
}, 60000);
|
|
afterAll(async () => {
|
|
await ctx?.close();
|
|
});
|
|
it('preserves organization rows from the foundation migration', async () => {
|
|
expect(
|
|
await ctx.db.organization.findUnique({
|
|
where: { id: legacyOrganizationId },
|
|
}),
|
|
).toMatchObject({ name: 'Existing organization' });
|
|
});
|
|
it('enforces normalized emails in the database', async () => {
|
|
await expect(
|
|
ctx.db.user.create({
|
|
data: {
|
|
organizationId: ctx.owner.organizationId,
|
|
email: 'UPPER@example.com',
|
|
name: 'Bad',
|
|
passwordHash: 'hash',
|
|
},
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
it('prevents a second owner even when the API is bypassed', async () => {
|
|
await expect(
|
|
ctx.db.user.create({
|
|
data: {
|
|
organizationId: ctx.owner.organizationId,
|
|
email: 'second@example.com',
|
|
name: 'Second',
|
|
passwordHash: 'hash',
|
|
isOwner: true,
|
|
},
|
|
}),
|
|
).rejects.toThrow();
|
|
});
|
|
it('rejects audit modification and deletion', async () => {
|
|
const audit = await ctx.db.auditEvent.findFirstOrThrow();
|
|
await expect(
|
|
ctx.db.auditEvent.update({
|
|
where: { id: audit.id },
|
|
data: { action: 'tampered' },
|
|
}),
|
|
).rejects.toThrow();
|
|
await expect(
|
|
ctx.db.auditEvent.delete({ where: { id: audit.id } }),
|
|
).rejects.toThrow();
|
|
expect(
|
|
await ctx.db.auditEvent.findUnique({ where: { id: audit.id } }),
|
|
).not.toBeNull();
|
|
});
|
|
it('discards failed-delivery recovery tokens', async () => {
|
|
const token = issueToken();
|
|
const store = ctx.app.get(RecoveryStore);
|
|
await store.create(
|
|
ctx.owner.userId,
|
|
token.tokenHash,
|
|
new Date(Date.now() + 60000),
|
|
);
|
|
await store.discard(token.tokenHash);
|
|
expect(
|
|
await ctx.db.recoveryToken.findUnique({
|
|
where: { tokenHash: token.tokenHash },
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
const nativeOnly = process.env.TEST_DATABASE_URL ? it : it.skip;
|
|
nativeOnly(
|
|
'allows exactly one winner for concurrent recovery token consumption on native PostgreSQL',
|
|
async () => {
|
|
const token = issueToken();
|
|
const store = ctx.app.get(RecoveryStore);
|
|
await store.create(
|
|
ctx.owner.userId,
|
|
token.tokenHash,
|
|
new Date(Date.now() + 60000),
|
|
);
|
|
const results = await Promise.allSettled([
|
|
store.reset(token.tokenHash, 'hash-one'),
|
|
store.reset(token.tokenHash, 'hash-two'),
|
|
]);
|
|
expect(
|
|
results.filter((result) => result.status === 'fulfilled'),
|
|
).toHaveLength(1);
|
|
expect(
|
|
results.filter((result) => result.status === 'rejected'),
|
|
).toHaveLength(1);
|
|
},
|
|
);
|
|
});
|