manicanldes-backend/test/identity-failures.spec.ts

141 lines
4.8 KiB
TypeScript

import { identityApp, type IdentityApp } from './helpers/identity-app';
import { AuthStore } from '../src/identity/auth.store';
import { RecoveryStore } from '../src/identity/recovery.store';
import { AccessStore } from '../src/identity/access.store';
import { RateLimitService } from '../src/identity/rate-limit.service';
import { SessionStore } from '../src/identity/session.store';
import { issueToken, hashToken } from '../src/identity/tokens';
describe('identity transactional failure paths', () => {
let ctx: IdentityApp;
beforeAll(async () => {
ctx = await identityApp();
}, 60000);
afterAll(async () => {
await ctx?.close();
});
it('does not issue a session using an obsolete password hash', async () => {
const issued = issueToken();
await expect(
ctx.app
.get(AuthStore)
.createSession(
ctx.owner.userId,
'outdated',
issued.tokenHash,
new Date(Date.now() + 60000),
),
).rejects.toThrow();
expect(
await ctx.db.session.findUnique({
where: { tokenHash: issued.tokenHash },
}),
).toBeNull();
});
it('rejects a session whose account is no longer active even without explicit revocation', async () => {
await ctx.db.user.update({
where: { id: ctx.owner.userId },
data: { status: 'SUSPENDED' },
});
await expect(
ctx.app.get(SessionStore).authenticate(ctx.token),
).rejects.toThrow();
await ctx.db.user.update({
where: { id: ctx.owner.userId },
data: { status: 'ACTIVE' },
});
});
it('rejects expired recovery tokens without changing credentials', async () => {
const issued = issueToken();
await ctx.app
.get(RecoveryStore)
.create(ctx.owner.userId, issued.tokenHash, new Date(0));
await expect(
ctx.app.get(RecoveryStore).reset(issued.tokenHash, 'new-hash'),
).rejects.toThrow('Invalid or expired');
expect((await ctx.login()).status).toBe(200);
});
it('rechecks account eligibility when storing and consuming recovery tokens', async () => {
const issued = issueToken();
await ctx.app
.get(RecoveryStore)
.create(ctx.owner.userId, issued.tokenHash, new Date(Date.now() + 60000));
await ctx.db.user.update({
where: { id: ctx.owner.userId },
data: { status: 'SUSPENDED' },
});
expect(
await ctx.app
.get(RecoveryStore)
.create(ctx.owner.userId, issueToken().tokenHash, new Date()),
).toBe(false);
await expect(
ctx.app.get(RecoveryStore).reset(issued.tokenHash, 'new-hash'),
).rejects.toThrow();
await ctx.db.user.update({
where: { id: ctx.owner.userId },
data: { status: 'ACTIVE' },
});
});
it('resets expired rate-limit windows', async () => {
const limits = ctx.app.get(RateLimitService);
await limits.consume('test-bucket', 1, 60);
await expect(limits.consume('test-bucket', 1, 60)).rejects.toThrow(
'Too many',
);
await ctx.db.rateLimit.update({
where: { key: hashToken('test-bucket') },
data: { expiresAt: new Date(0) },
});
await expect(limits.consume('test-bucket', 1, 60)).resolves.toBeUndefined();
});
it('rechecks current permissions inside administration transactions', async () => {
const actor = await ctx.app.get(SessionStore).authenticate(ctx.token);
const ownerRole = await ctx.db.role.findFirstOrThrow({
where: { isSystem: true },
});
const original = ownerRole.permissions;
await ctx.db.role.update({
where: { id: ownerRole.id },
data: { permissions: [] },
});
const work = jest.fn();
await expect(
ctx.app.get(AccessStore).mutate(actor, 'users.create', work),
).rejects.toThrow();
expect(work).not.toHaveBeenCalled();
await ctx.db.role.update({
where: { id: ownerRole.id },
data: { permissions: original },
});
});
it('preserves state when an audited transaction fails', async () => {
const actor = await ctx.app.get(SessionStore).authenticate(ctx.token);
await expect(
ctx.app.get(AccessStore).mutate(actor, 'roles.manage', async (tx) => {
await tx.role.create({
data: {
organizationId: actor.organizationId,
name: 'Rolled back',
permissions: [],
},
});
throw new Error('audit unavailable');
}),
).rejects.toThrow('audit unavailable');
expect(await ctx.db.role.count({ where: { name: 'Rolled back' } })).toBe(0);
});
it('uses the same public recovery response for an unknown account', async () => {
const response = await ctx
.api()
.post('/api/v1/auth/recovery/request')
.send({
organizationId: ctx.owner.organizationId,
email: 'unknown@example.com',
})
.expect(202);
expect(response.body.message).toContain('If the account is eligible');
});
});