281 lines
8.4 KiB
TypeScript
281 lines
8.4 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication } from '@nestjs/common';
|
|
import request from 'supertest';
|
|
import { App } from 'supertest/types';
|
|
import { DataSource } from 'typeorm';
|
|
import { AppModule } from './../src/app.module';
|
|
import { OidcProfile, OidcService } from '../src/auth/oidc.service';
|
|
|
|
interface AuthResponseBody {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
user: {
|
|
id?: string;
|
|
email: string;
|
|
};
|
|
}
|
|
|
|
interface ListTemplateResponseBody {
|
|
id: string;
|
|
name: string;
|
|
kind: string;
|
|
items: {
|
|
id: string;
|
|
title: string;
|
|
checked?: boolean;
|
|
}[];
|
|
}
|
|
|
|
describe('AppController (e2e)', () => {
|
|
let app: INestApplication<App>;
|
|
let oidcService: {
|
|
createAuthorizationUrl: jest.Mock<Promise<string>, []>;
|
|
exchangeCallback: jest.Mock<
|
|
Promise<OidcProfile>,
|
|
[string | undefined, string | undefined]
|
|
>;
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
oidcService = {
|
|
createAuthorizationUrl: jest.fn(
|
|
async () => 'https://sso.example.test/authorize',
|
|
),
|
|
exchangeCallback: jest.fn(async () => ({
|
|
subject: 'oidc-default',
|
|
email: 'default@example.com',
|
|
name: 'Default User',
|
|
})),
|
|
};
|
|
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
})
|
|
.overrideProvider(OidcService)
|
|
.useValue(oidcService)
|
|
.compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
await app.init();
|
|
await ensureSsoSchema(app.get(DataSource));
|
|
});
|
|
|
|
it('/ (GET)', () => {
|
|
return request(app.getHttpServer())
|
|
.get('/')
|
|
.expect(200)
|
|
.expect('Hello World!');
|
|
});
|
|
|
|
it('/auth sso callback and refresh', async () => {
|
|
const email = uniqueEmail('auth-user');
|
|
const loginBody = await loginWithSso(email);
|
|
|
|
expect(loginBody.accessToken).toBeDefined();
|
|
expect(loginBody.refreshToken).toBeDefined();
|
|
expect(loginBody.user.email).toBe(email);
|
|
|
|
const refreshResponse = await request(app.getHttpServer())
|
|
.post('/auth/refresh')
|
|
.send({
|
|
refreshToken: loginBody.refreshToken,
|
|
})
|
|
.expect(201);
|
|
|
|
const refreshBody = refreshResponse.body as unknown as AuthResponseBody;
|
|
expect(refreshBody.accessToken).toBeDefined();
|
|
expect(refreshBody.refreshToken).toBeDefined();
|
|
expect(refreshBody.refreshToken).not.toBe(loginBody.refreshToken);
|
|
|
|
await request(app.getHttpServer())
|
|
.post('/auth/refresh')
|
|
.send({
|
|
refreshToken: loginBody.refreshToken,
|
|
})
|
|
.expect(401);
|
|
});
|
|
|
|
it('/list-templates creates, updates and uses a template', async () => {
|
|
const accessToken = await loginWithSsoAndGetAccessToken(
|
|
uniqueEmail('template-user'),
|
|
);
|
|
|
|
const initialTemplatesResponse = await request(app.getHttpServer())
|
|
.get('/list-templates')
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.expect(200);
|
|
const initialTemplates =
|
|
initialTemplatesResponse.body as unknown as ListTemplateResponseBody[];
|
|
|
|
expect(initialTemplates).toHaveLength(3);
|
|
expect(initialTemplates.map((template) => template.name)).toContain(
|
|
'Urlaub',
|
|
);
|
|
|
|
const createTemplateResponse = await request(app.getHttpServer())
|
|
.post('/list-templates')
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
name: 'Urlaub',
|
|
kind: 'packing',
|
|
items: [{ title: 'Pass' }, { title: 'Tickets' }],
|
|
})
|
|
.expect(201);
|
|
|
|
const createdTemplate =
|
|
createTemplateResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(createdTemplate.name).toBe('Urlaub');
|
|
expect(createdTemplate.items).toHaveLength(2);
|
|
|
|
const updateTemplateResponse = await request(app.getHttpServer())
|
|
.patch(`/list-templates/${createdTemplate.id}`)
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
name: 'Sommerurlaub',
|
|
})
|
|
.expect(200);
|
|
|
|
const updatedTemplate =
|
|
updateTemplateResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(updatedTemplate.name).toBe('Sommerurlaub');
|
|
|
|
const createListResponse = await request(app.getHttpServer())
|
|
.post(`/list-templates/${createdTemplate.id}/lists`)
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
name: 'Sommerurlaub 2026',
|
|
})
|
|
.expect(201);
|
|
|
|
const createdList =
|
|
createListResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(createdList.name).toBe('Sommerurlaub 2026');
|
|
expect(createdList.items[0].title).toBe('Pass');
|
|
expect(createdList.items[0].checked).toBe(false);
|
|
});
|
|
|
|
it('/lists creates, updates and reads a concrete list', async () => {
|
|
const accessToken = await loginWithSsoAndGetAccessToken(
|
|
uniqueEmail('list-user'),
|
|
);
|
|
|
|
const createListResponse = await request(app.getHttpServer())
|
|
.post('/lists')
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
name: 'Wocheneinkauf',
|
|
kind: 'shopping',
|
|
})
|
|
.expect(201);
|
|
|
|
const createdList =
|
|
createListResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(createdList.name).toBe('Wocheneinkauf');
|
|
expect(createdList.items).toHaveLength(0);
|
|
|
|
const addItemResponse = await request(app.getHttpServer())
|
|
.post(`/lists/${createdList.id}/items`)
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
title: 'Milch',
|
|
quantity: 2,
|
|
})
|
|
.expect(201);
|
|
|
|
const listWithItem =
|
|
addItemResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(listWithItem.items).toHaveLength(1);
|
|
expect(listWithItem.items[0].title).toBe('Milch');
|
|
|
|
await request(app.getHttpServer())
|
|
.patch(`/lists/${createdList.id}/items/${listWithItem.items[0].id}`)
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.send({
|
|
checked: true,
|
|
})
|
|
.expect(200);
|
|
|
|
const getListResponse = await request(app.getHttpServer())
|
|
.get(`/lists/${createdList.id}`)
|
|
.set('Authorization', `Bearer ${accessToken}`)
|
|
.expect(200);
|
|
|
|
const fetchedList =
|
|
getListResponse.body as unknown as ListTemplateResponseBody;
|
|
expect(fetchedList.items[0].checked).toBe(true);
|
|
});
|
|
|
|
async function loginWithSsoAndGetAccessToken(
|
|
email: string,
|
|
): Promise<string> {
|
|
const loginBody = await loginWithSso(email);
|
|
expect(loginBody.accessToken).toBeDefined();
|
|
|
|
return loginBody.accessToken ?? '';
|
|
}
|
|
|
|
async function loginWithSso(email: string): Promise<AuthResponseBody> {
|
|
oidcService.exchangeCallback.mockResolvedValueOnce({
|
|
subject: `sub-${email}`,
|
|
email,
|
|
name: 'Test User',
|
|
});
|
|
|
|
const exchangeResponse = await request(app.getHttpServer())
|
|
.post('/auth/sso/exchange')
|
|
.send({ code: 'code', state: 'state' })
|
|
.expect(200);
|
|
|
|
return exchangeResponse.body as unknown as AuthResponseBody;
|
|
}
|
|
|
|
function uniqueEmail(prefix: string): string {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}@example.com`;
|
|
}
|
|
|
|
async function ensureSsoSchema(dataSource: DataSource): Promise<void> {
|
|
const usersTables = (await dataSource.query(
|
|
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users'",
|
|
)) as unknown[];
|
|
|
|
if (!usersTables.length) {
|
|
return;
|
|
}
|
|
|
|
const oidcSubjectColumns = (await dataSource.query(
|
|
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'oidcSubject'",
|
|
)) as unknown[];
|
|
|
|
if (!oidcSubjectColumns.length) {
|
|
await dataSource.query(
|
|
'ALTER TABLE `users` ADD `oidcSubject` varchar(255) NULL',
|
|
);
|
|
await dataSource.query(
|
|
'CREATE UNIQUE INDEX `IDX_users_oidc_subject` ON `users` (`oidcSubject`)',
|
|
);
|
|
}
|
|
|
|
await dropColumnIfExists(dataSource, 'passwordHash');
|
|
await dropColumnIfExists(dataSource, 'verificationToken');
|
|
await dropColumnIfExists(dataSource, 'verified');
|
|
}
|
|
|
|
async function dropColumnIfExists(
|
|
dataSource: DataSource,
|
|
columnName: string,
|
|
): Promise<void> {
|
|
const columns = (await dataSource.query(
|
|
'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?',
|
|
['users', columnName],
|
|
)) as unknown[];
|
|
|
|
if (columns.length) {
|
|
await dataSource.query(`ALTER TABLE \`users\` DROP COLUMN \`${columnName}\``);
|
|
}
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await app.close();
|
|
});
|
|
});
|