SSO implementiert
This commit is contained in:
@@ -2,8 +2,9 @@ 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 { MailService } from './../src/mail/mail.service';
|
||||
import { OidcProfile, OidcService } from '../src/auth/oidc.service';
|
||||
|
||||
interface AuthResponseBody {
|
||||
accessToken?: string;
|
||||
@@ -11,7 +12,6 @@ interface AuthResponseBody {
|
||||
user: {
|
||||
id?: string;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,16 +28,36 @@ interface ListTemplateResponseBody {
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
let mailService: MailService;
|
||||
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],
|
||||
}).compile();
|
||||
})
|
||||
.overrideProvider(OidcService)
|
||||
.useValue(oidcService)
|
||||
.compile();
|
||||
|
||||
mailService = moduleFixture.get<MailService>(MailService);
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
await ensureSsoSchema(app.get(DataSource));
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
@@ -47,50 +67,13 @@ describe('AppController (e2e)', () => {
|
||||
.expect('Hello World!');
|
||||
});
|
||||
|
||||
it('/auth register, verify and login', async () => {
|
||||
const registerResponse = await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
})
|
||||
.expect(201);
|
||||
it('/auth sso callback and refresh', async () => {
|
||||
const email = uniqueEmail('auth-user');
|
||||
const loginBody = await loginWithSso(email);
|
||||
|
||||
const registerBody = registerResponse.body as unknown as AuthResponseBody;
|
||||
expect(registerBody.user.email).toBe('user@example.com');
|
||||
expect(registerBody.user.verified).toBe(false);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
})
|
||||
.expect(401);
|
||||
|
||||
const verificationUrl = mailService.getSentEmails()[0].verificationUrl;
|
||||
const token = new URL(verificationUrl).searchParams.get('token');
|
||||
|
||||
const verifyResponse = await request(app.getHttpServer())
|
||||
.get('/auth/verify-email')
|
||||
.query({ token })
|
||||
.expect(200);
|
||||
|
||||
const verifyBody = verifyResponse.body as unknown as AuthResponseBody;
|
||||
expect(verifyBody.user.verified).toBe(true);
|
||||
|
||||
const loginResponse = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const loginBody = loginResponse.body as unknown as AuthResponseBody;
|
||||
expect(loginBody.accessToken).toBeDefined();
|
||||
expect(loginBody.refreshToken).toBeDefined();
|
||||
expect(loginBody.user.email).toBe('user@example.com');
|
||||
expect(loginBody.user.email).toBe(email);
|
||||
|
||||
const refreshResponse = await request(app.getHttpServer())
|
||||
.post('/auth/refresh')
|
||||
@@ -113,8 +96,8 @@ describe('AppController (e2e)', () => {
|
||||
});
|
||||
|
||||
it('/list-templates creates, updates and uses a template', async () => {
|
||||
const accessToken = await registerVerifiedUserAndGetAccessToken(
|
||||
'template-user@example.com',
|
||||
const accessToken = await loginWithSsoAndGetAccessToken(
|
||||
uniqueEmail('template-user'),
|
||||
);
|
||||
|
||||
const initialTemplatesResponse = await request(app.getHttpServer())
|
||||
@@ -172,8 +155,8 @@ describe('AppController (e2e)', () => {
|
||||
});
|
||||
|
||||
it('/lists creates, updates and reads a concrete list', async () => {
|
||||
const accessToken = await registerVerifiedUserAndGetAccessToken(
|
||||
'list-user@example.com',
|
||||
const accessToken = await loginWithSsoAndGetAccessToken(
|
||||
uniqueEmail('list-user'),
|
||||
);
|
||||
|
||||
const createListResponse = await request(app.getHttpServer())
|
||||
@@ -222,41 +205,75 @@ describe('AppController (e2e)', () => {
|
||||
expect(fetchedList.items[0].checked).toBe(true);
|
||||
});
|
||||
|
||||
async function registerVerifiedUserAndGetAccessToken(
|
||||
async function loginWithSsoAndGetAccessToken(
|
||||
email: string,
|
||||
): Promise<string> {
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
email,
|
||||
password: 'password123',
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const verificationUrl =
|
||||
mailService.getSentEmails()[mailService.getSentEmails().length - 1]
|
||||
.verificationUrl;
|
||||
const token = new URL(verificationUrl).searchParams.get('token');
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/auth/verify-email')
|
||||
.query({ token })
|
||||
.expect(200);
|
||||
|
||||
const loginResponse = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
email,
|
||||
password: 'password123',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const loginBody = loginResponse.body as unknown as AuthResponseBody;
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user