@accounter/server
Version:
Accounter GraphQL server
136 lines • 5.35 kB
JavaScript
import { GraphQLError } from 'graphql';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiKeysProvider } from '../providers/api-keys.provider.js';
import { apiKeysResolvers } from '../resolvers/api-keys.resolver.js';
const listApiKeysResolver = apiKeysResolvers.Query?.listApiKeys;
const revokeApiKeyResolver = apiKeysResolvers.Mutation?.revokeApiKey;
const mockInfo = {};
describe('API key management', () => {
let mockAuthProvider;
let mockDb;
let mockAuditProvider;
let apiKeysProvider;
beforeEach(() => {
mockAuthProvider = {
getAuthContext: vi.fn().mockResolvedValue({
authType: 'jwt',
token: 'owner-token',
user: {
userId: 'owner-user-id',
roleId: 'business_owner',
email: 'owner@example.com',
auth0UserId: 'auth0|owner-user-id',
permissions: [],
emailVerified: true,
permissionsVersion: 1,
},
tenant: {
businessId: 'business-123',
roleId: 'business_owner',
},
}),
};
mockDb = {
query: vi.fn(),
transaction: vi.fn().mockImplementation(async (callback) => {
return callback(mockDb);
}),
};
mockAuditProvider = {
log: vi.fn().mockResolvedValue(undefined),
};
apiKeysProvider = new ApiKeysProvider(mockDb, mockAuthProvider, mockAuditProvider);
});
afterEach(() => {
vi.clearAllMocks();
});
function createContext() {
return {
injector: {
get(token) {
if (token === ApiKeysProvider) {
return apiKeysProvider;
}
throw new Error(`Unexpected token requested: ${String(token)}`);
},
},
};
}
it('listApiKeys returns active keys sorted by creation time', async () => {
mockDb.query.mockResolvedValueOnce({
rows: [
{
id: 'key-2',
name: 'newer key',
role_id: 'scraper',
last_used_at: new Date('2030-02-01T00:00:00.000Z'),
created_at: new Date('2030-02-01T00:00:00.000Z'),
},
{
id: 'key-1',
name: 'older key',
role_id: 'employee',
last_used_at: null,
created_at: new Date('2030-01-01T00:00:00.000Z'),
},
],
rowCount: 2,
});
const result = await listApiKeysResolver({}, {}, createContext(), mockInfo);
expect(result).toEqual([
{
id: 'key-2',
name: 'newer key',
roleId: 'scraper',
lastUsedAt: new Date('2030-02-01T00:00:00.000Z'),
createdAt: new Date('2030-02-01T00:00:00.000Z'),
},
{
id: 'key-1',
name: 'older key',
roleId: 'employee',
lastUsedAt: null,
createdAt: new Date('2030-01-01T00:00:00.000Z'),
},
]);
});
it('revokeApiKey soft-deletes and writes API_KEY_REVOKED audit log', async () => {
mockDb.query.mockResolvedValueOnce({
rows: [{ id: 'key-1' }],
rowCount: 1,
});
const revoked = await revokeApiKeyResolver({}, { id: 'key-1' }, createContext(), mockInfo);
expect(revoked).toBe(true);
expect(mockAuditProvider.log).toHaveBeenCalledTimes(1);
const auditEvent = mockAuditProvider.log.mock.calls[0][0];
expect(JSON.stringify(auditEvent)).not.toContain({
action: 'API_KEY_REVOKED',
entity: 'ApiKey',
entityId: 'key-1',
ownerId: 'business-123',
userId: 'owner-user-id',
});
});
it('revokeApiKey returns false when key does not exist or already revoked', async () => {
mockDb.query.mockResolvedValueOnce({
rows: [],
rowCount: 0,
});
const revoked = await revokeApiKeyResolver({}, { id: 'missing-key' }, createContext(), mockInfo);
expect(revoked).toBe(false);
expect(mockAuditProvider.log).not.toHaveBeenCalled();
});
it('listApiKeys wraps unexpected failures', async () => {
mockDb.query.mockRejectedValueOnce(new Error('db down'));
await expect(listApiKeysResolver({}, {}, createContext(), mockInfo)).rejects.toSatisfy(error => {
return error instanceof GraphQLError && error.extensions?.code === 'API_KEY_LIST_FAILED';
});
});
it('revokeApiKey wraps unexpected failures', async () => {
mockDb.query.mockRejectedValueOnce(new Error('db down'));
await expect(revokeApiKeyResolver({}, { id: 'key-1' }, createContext(), mockInfo)).rejects.toSatisfy(error => {
return (error instanceof GraphQLError && error.extensions?.code === 'API_KEY_REVOCATION_FAILED');
});
});
});
//# sourceMappingURL=api-key-management.test.js.map