feat(core): introduce JWT API keys for the public API (#11005)

This commit is contained in:
Ricardo Espinoza
2024-10-18 12:06:44 +02:00
committed by GitHub
parent 6a722c45ea
commit 679fa4a10a
5 changed files with 252 additions and 69 deletions

View File

@@ -1,66 +1,88 @@
import { mock } from 'jest-mock-extended';
import { randomString } from 'n8n-workflow';
import { Container } from 'typedi';
import type { ApiKey } from '@/databases/entities/api-key';
import type { User } from '@/databases/entities/user';
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
import { EventService } from '@/events/event.service';
import type { ApiKeysRequest, AuthenticatedRequest } from '@/requests';
import { API_KEY_PREFIX } from '@/services/public-api-key.service';
import { PublicApiKeyService } from '@/services/public-api-key.service';
import { mockInstance } from '@test/mocking';
import { ApiKeysController } from '../api-keys.controller';
describe('ApiKeysController', () => {
const apiKeysRepository = mockInstance(ApiKeyRepository);
const publicApiKeyService = mockInstance(PublicApiKeyService);
const eventService = mockInstance(EventService);
const controller = Container.get(ApiKeysController);
let req: AuthenticatedRequest;
beforeAll(() => {
req = mock<AuthenticatedRequest>({ user: mock<User>({ id: '123' }) });
req = { user: { id: '123' } } as AuthenticatedRequest;
});
describe('createAPIKey', () => {
it('should create and save an API key', async () => {
// Arrange
const apiKeyData = {
id: '123',
userId: '123',
label: 'My API Key',
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
apiKey: 'apiKey********',
createdAt: new Date(),
} as ApiKey;
apiKeysRepository.upsert.mockImplementation();
const req = mock<AuthenticatedRequest>({ user: mock<User>({ id: '123' }) });
apiKeysRepository.findOneByOrFail.mockResolvedValue(apiKeyData);
publicApiKeyService.createPublicApiKeyForUser.mockResolvedValue(apiKeyData);
// Act
const newApiKey = await controller.createAPIKey(req);
expect(apiKeysRepository.upsert).toHaveBeenCalled();
// Assert
expect(publicApiKeyService.createPublicApiKeyForUser).toHaveBeenCalled();
expect(apiKeyData).toEqual(newApiKey);
expect(eventService.emit).toHaveBeenCalledWith(
'public-api-key-created',
expect.objectContaining({ user: req.user, publicApi: false }),
);
});
});
describe('getAPIKeys', () => {
it('should return the users api keys redacted', async () => {
// Arrange
const apiKeyData = {
id: '123',
userId: '123',
label: 'My API Key',
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
apiKey: 'apiKey***',
createdAt: new Date(),
updatedAt: new Date(),
} as ApiKey;
apiKeysRepository.findBy.mockResolvedValue([apiKeyData]);
publicApiKeyService.getRedactedApiKeysForUser.mockResolvedValue([apiKeyData]);
// Act
const apiKeys = await controller.getAPIKeys(req);
expect(apiKeys[0].apiKey).not.toEqual(apiKeyData.apiKey);
expect(apiKeysRepository.findBy).toHaveBeenCalledWith({ userId: req.user.id });
// Assert
expect(apiKeys).toEqual([apiKeyData]);
expect(publicApiKeyService.getRedactedApiKeysForUser).toHaveBeenCalledWith(
expect.objectContaining({ id: req.user.id }),
);
});
});
describe('deleteAPIKey', () => {
it('should delete the API key', async () => {
// Arrange
const user = mock<User>({
id: '123',
password: 'password',
@@ -68,12 +90,22 @@ describe('ApiKeysController', () => {
role: 'global:member',
mfaEnabled: false,
});
const req = mock<ApiKeysRequest.DeleteAPIKey>({ user, params: { id: user.id } });
// Act
await controller.deleteAPIKey(req);
expect(apiKeysRepository.delete).toHaveBeenCalledWith({
userId: req.user.id,
id: req.params.id,
});
publicApiKeyService.deleteApiKeyForUser.mockResolvedValue();
// Assert
expect(publicApiKeyService.deleteApiKeyForUser).toHaveBeenCalledWith(user, user.id);
expect(eventService.emit).toHaveBeenCalledWith(
'public-api-key-deleted',
expect.objectContaining({ user, publicApi: false }),
);
});
});
});