-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support users and logins (#42)
There's honestly a lot of features in this PR. A `ZodValidationPipe`, a `ServerHashModule` and service, a `base32` library that holds the valid values for a base32 string and a regex for a ulid (should be renamed), e2e tests has been updated so that migrations run for the test database before the tests are ran, the e2e tests are now ran through vitest (uvu should be fully removed soon), the binding of interceptors and filters has been moved to the `RootModule` to better manage when they get used, **and finally**, user signup and login is now functional as well. I should still verify that the cookies are sent back properly with the necessary information, if any, but the session that should be created is indeed updated in the Redis database after signup, so it should all be functional as is
- Loading branch information
Showing
74 changed files
with
5,060 additions
and
2,463 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
DATABASE_NAME=test | ||
DATABASE_PORT=25432 | ||
DATABASE_USER=postgres | ||
DATABASE_HOST=localhost | ||
DATABASE_PASSWORD=postgres | ||
REDIS_URL=redis://localhost:26379 | ||
NODE_ENV=development |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
DATABASE_NAME=test | ||
DATABASE_PORT=5432 | ||
DATABASE_PORT=25432 | ||
DATABASE_USER=postgres | ||
DATABASE_HOST=localhost | ||
DATABASE_PASSWORD=postgres | ||
REDIS_URL=redis://localhost:6379 | ||
REDIS_URL=redis://localhost:26379 | ||
NODE_ENV=development |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { Database } from '@unteris/server/kysely'; | ||
import { Kysely } from 'kysely'; | ||
|
||
export interface DbContext { | ||
db: Kysely<Database>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,28 +1,29 @@ | ||
import { INestApplication } from '@nestjs/common'; | ||
import { NestFactory } from '@nestjs/core'; | ||
import { OgmaService } from '@ogma/nestjs-module'; | ||
import { getKyselyInstanceToken } from '@unteris/server/kysely'; | ||
import { RootModule } from '@unteris/server/root'; | ||
import { request, spec } from 'pactum'; | ||
import { suite } from 'uvu'; | ||
import { csrfTests } from './tests/csrf'; | ||
import { request } from 'pactum'; | ||
import { describe, beforeAll, beforeEach } from 'vitest'; | ||
import { DbContext } from './interfaces/test-context.interface'; | ||
import { csrfTest } from './tests/csrf'; | ||
import { signUpAndLoginTests } from './tests/signup-and-login'; | ||
|
||
type UvuContext = { app: INestApplication }; | ||
|
||
const ApplicationE2E = suite<UvuContext>('Unteris E2E test suite'); | ||
|
||
ApplicationE2E.before(async (context) => { | ||
const app = await NestFactory.create(RootModule, { bufferLogs: true }); | ||
app.useLogger(app.get(OgmaService)); | ||
await app.listen(0); | ||
const reqURL = await app.getUrl(); | ||
request.setBaseUrl(reqURL.replace('[::1]', 'localhost')); | ||
context.app = app; | ||
}); | ||
|
||
ApplicationE2E.after(async ({ app }) => { | ||
await app.close(); | ||
describe('Unteris E2E test suite', () => { | ||
let app: INestApplication; | ||
beforeAll(async () => { | ||
app = await NestFactory.create(RootModule, { bufferLogs: true }); | ||
app.useLogger(app.get(OgmaService)); | ||
await app.listen(0); | ||
const reqURL = await app.getUrl(); | ||
request.setBaseUrl(reqURL.replace('[::1]', 'localhost')); | ||
return async () => { | ||
await app.close(); | ||
}; | ||
}); | ||
beforeEach<DbContext>((context) => { | ||
context.db = app.get(getKyselyInstanceToken(), { strict: false }); | ||
}); | ||
csrfTest(); | ||
signUpAndLoginTests(); | ||
}); | ||
|
||
ApplicationE2E('CSRF Testing', csrfTests); | ||
|
||
ApplicationE2E.run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,54 @@ | ||
import { parse } from 'lightcookie'; | ||
import { spec } from 'pactum'; | ||
import { Callback } from 'uvu'; | ||
import { ok, unreachable } from 'uvu/assert'; | ||
import { describe, expect, test } from 'vitest'; | ||
|
||
export const csrfTests: Callback<any> = async (_context: any) => { | ||
await spec() | ||
.get('/csrf') | ||
.expectStatus(200) | ||
.expectJsonLike({ csrfToken: /\w+/ }) | ||
.expect(({ res }) => { | ||
const cookies = res.headers['set-cookie']; | ||
if (!cookies || cookies.length === 0) { | ||
unreachable('Received no cookies from the server'); | ||
return; | ||
} | ||
cookies.forEach((cookie) => { | ||
const parsed = parse(cookie); | ||
ok( | ||
['sessionId', 'refreshId'].some((cookieName) => { | ||
return cookieName in parsed; | ||
}) | ||
); | ||
}); | ||
}) | ||
.stores((_req, res) => { | ||
const { csrfToken } = res.body; | ||
const cookies = res.headers['set-cookie']; | ||
if (!cookies || cookies.length === 0) { | ||
unreachable('Received no cookies from the server'); | ||
return; | ||
} | ||
const sessionId = cookies | ||
.map((c) => parse(c)) | ||
.find((cookie) => 'sessionId' in cookie)?.sessionId; | ||
const refreshId = cookies | ||
.map((c) => parse(c)) | ||
.find((cookie) => 'refreshId' in cookie)?.refreshId; | ||
return { | ||
csrfToken, | ||
sessionId, | ||
refreshId, | ||
}; | ||
}) | ||
.toss(); | ||
await spec() | ||
.post('/csrf/verify') | ||
.withHeaders('X-UNTERIS-CSRF-PROTECTION', '$S{csrfToken}') | ||
.withCookies('sessionId', '$S{sessionId}') | ||
.expectStatus(201) | ||
.expectJson({ success: true }) | ||
.toss(); | ||
export const csrfTest = () => { | ||
return describe('CSRF Tests', () => { | ||
test('CSRF Testing', async () => { | ||
await spec() | ||
.get('/csrf') | ||
.expectStatus(200) | ||
.expectJsonLike({ csrfToken: /\w+/ }) | ||
.expect(({ res }) => { | ||
const cookies = res.headers['set-cookie']; | ||
if (!cookies || cookies.length === 0) { | ||
throw new Error('Received no cookies from the server'); | ||
} | ||
cookies.forEach((cookie) => { | ||
const parsed = parse(cookie); | ||
expect( | ||
['sessionId', 'refreshId'].some((cookieName) => { | ||
return cookieName in parsed; | ||
}) | ||
); | ||
}); | ||
}) | ||
.stores((_req, res) => { | ||
const { csrfToken } = res.body; | ||
const cookies = res.headers['set-cookie']; | ||
if (!cookies || cookies.length === 0) { | ||
throw new Error('Received no cookies from the server'); | ||
} | ||
const sessionId = cookies | ||
.map((c) => parse(c)) | ||
.find((cookie) => 'sessionId' in cookie)?.sessionId; | ||
const refreshId = cookies | ||
.map((c) => parse(c)) | ||
.find((cookie) => 'refreshId' in cookie)?.refreshId; | ||
return { | ||
csrfToken, | ||
sessionId, | ||
refreshId, | ||
}; | ||
}) | ||
.toss(); | ||
await spec() | ||
.post('/csrf/verify') | ||
.withHeaders('X-UNTERIS-CSRF-PROTECTION', '$S{csrfToken}') | ||
.withCookies('sessionId', '$S{sessionId}') | ||
.expectStatus(201) | ||
.expectJson({ success: true }) | ||
.toss(); | ||
}); | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { base32Regex } from '@unteris/shared/base32'; | ||
import { randomUUID } from 'crypto'; | ||
import { spec } from 'pactum'; | ||
import { regex } from 'pactum-matchers'; | ||
import { describe, expect, test } from 'vitest'; | ||
import { DbContext } from '../interfaces/test-context.interface'; | ||
|
||
export const signUpAndLoginTests = () => { | ||
return describe('SignUp and Login', () => { | ||
test<DbContext>('A new user should be able to sign up', async (context) => { | ||
const email = `${randomUUID()}@testing.com`; | ||
const res = await spec() | ||
.post('/auth/signup') | ||
.withBody({ | ||
email, | ||
password: 'ALongEnoughP4ssw0rdToBeFin3', | ||
confirmationPassword: 'ALongEnoughP4ssw0rdToBeFin3', | ||
name: 'Test User' + randomUUID(), | ||
}) | ||
.expectStatus(201) | ||
.expectJsonMatch({ | ||
id: regex(base32Regex), | ||
success: true, | ||
}) | ||
.returns('.id'); | ||
// assert we properly made the user, login method, and related local login | ||
const userAccount = await context.db | ||
.selectFrom('userAccount as ua') | ||
.innerJoin('loginMethod as lm', 'lm.userId', 'ua.id') | ||
.innerJoin('localLogin as ll', 'll.loginMethodId', 'lm.id') | ||
.select(({ fn }) => [fn.count('ua.id').as('count')]) | ||
.where('ua.id', '=', res) | ||
.execute(); | ||
expect(userAccount[0].count).toBe('1'); | ||
await spec() | ||
.post('/auth/login') | ||
.withBody({ | ||
email, | ||
password: 'ALongEnoughP4ssw0rdToBeFin3', | ||
}) | ||
.expectJson({ | ||
success: true, | ||
}) | ||
.expectStatus(201) | ||
.expect(({ res: _res }) => { | ||
// assert session token here | ||
}); | ||
}); | ||
}); | ||
}; |
Oops, something went wrong.