Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support users and logins #42

Merged
merged 31 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
abea675
test: switch server-e2e to use vitest instead of uvu
jmcdo29 Jun 30, 2023
d32425c
chore: upgrade ogma
jmcdo29 Jun 30, 2023
6f5c519
chore: run e2e tests in ci and general parallel speed ups
jmcdo29 Jun 30, 2023
dcfaba0
docs: add csrf flow diagram to the readme
jmcdo29 Jun 21, 2023
84dc312
chore: start working on authorization design pattern
jmcdo29 Jun 28, 2023
5a108e8
fix: move interceptor and filter to root module
jmcdo29 Jul 2, 2023
594c1da
feat: set up the database migrations and diagram for users
jmcdo29 Jul 2, 2023
66715a7
fix: update the logging module
jmcdo29 Jul 2, 2023
8fd8380
feat: create library for base32 values
jmcdo29 Jul 3, 2023
1d7f8ce
test: create test case for sign up and login
jmcdo29 Jul 3, 2023
ae895fa
feat(database): create new table types
jmcdo29 Jul 3, 2023
544ff36
test: add check for database rows in sign up test
jmcdo29 Jul 3, 2023
e2f4839
chore: remove unnecessary test
jmcdo29 Jul 3, 2023
9580633
feat: create security library
jmcdo29 Jul 3, 2023
2cbcb69
feat(zod): create zod validation pipe
jmcdo29 Jul 3, 2023
14ef0df
feat: implement the binding of the zod validation pipe
jmcdo29 Jul 3, 2023
cc15721
fix: better type for zod pipe
jmcdo29 Jul 3, 2023
6c22309
feat: add regex for ulid
jmcdo29 Jul 3, 2023
1aa4ed5
feat: create e2e specific migrate and env confg
jmcdo29 Jul 3, 2023
fc1ad2a
feat: run migrations before e2e tests and update signup test
jmcdo29 Jul 3, 2023
37daf5b
fix: resolve is verified typo
jmcdo29 Jul 3, 2023
bd8e35d
fix: add data parameter to pipe return
jmcdo29 Jul 3, 2023
b6c6228
feat: create hash module and service
jmcdo29 Jul 4, 2023
d8038f1
chore: use swc for the webpack compiler
jmcdo29 Jul 4, 2023
04e6c36
chore: update paths and deps
jmcdo29 Jul 4, 2023
736138d
feat: add expected post types for auth to shared types lib
jmcdo29 Jul 4, 2023
3169933
feat: set up sign up and login paths
jmcdo29 Jul 4, 2023
09e5a69
docs(zod): add documentation about the zod pipe
jmcdo29 Jul 4, 2023
5baf4cb
chore: clean up leftovers from testing purposes
jmcdo29 Jul 4, 2023
de2bda0
ci: set proper port for e2e tests in ci
jmcdo29 Jul 4, 2023
8170c68
chore: re-invent server-e2e task pipeline
jmcdo29 Jul 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,70 @@ jobs:
parallel-commands: |
pnpm exec nx-cloud record -- pnpm exec nx format:check
parallel-commands-on-agents: |
pnpm exec nx affected --target=lint --parallel=3
pnpm exec nx affected --target=build --parallel=3
pnpm exec nx affected -t lint build --parallel=3

e2e:
name: E2E Tests
runs-on: ubuntu-latest
env:
DATABASE_USER: postgres
DATABASE_PORT: 25432
DATABASE_NAME: test
DATABASE_PASSWORD: postgres
DATABASE_HOST: localhost
REDIS_URL: redis://localhost:26379
NODE_ENV: development
steps:
- name: Checkout Repo
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Nx SHAs
uses: nrwl/nx-set-shas@v3

- name: Install PNPM
shell: bash
run: npm i -g pnpm

- name: Print node/npm/yarn versions
id: versions
run: |
node_ver=$( node --version )
pnpm_ver=$( pnpm --version || true )

echo "Node: ${node_ver:1}"
if [[ $pnpm_ver != '' ]]; then echo "PNPM: $pnpm_ver"; fi

echo "node_version=${node_ver:1}" >> $GITHUB_OUTPUT

- name: Get pnpm cache directory path
id: pnpm-cache-dir-path
run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT

- name: Use the node_modules cache if available
uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-

- name: Use the Cypress Cache if available
uses: actions/cache@v3
with:
path: ./.cache/Cypress
key: ${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-

- name: Install Deps
shell: bash
run: pnpm i

- name: Run E2E Tests
shell: bash
run: pnpm exec nx affected -t e2e --parallel --configuration=ci
continue-on-error: true

agents:
name: Nx Cloud - Agents
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

node_modules/.bin/nx affected:lint
node_modules/.bin/nx affected:lint --parallel
node_modules/.bin/nx format:write
if git diff --cached --name-only | grep -Eq \(diagram\.mmd\|migrations\/README\); then
node_modules/.bin/embedme --stdout --strip-embed-comment libs/db/migrations/README.template.md > libs/db/migrations/README.md
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ you can just watch the project and see what becomes of it.

Any major questions I guess you can raise an issue or [email me about it][email]

[email]: mailto://me+unteris@jaymcdoniel.dev
[email]: mailto://dev@unteris.com
7 changes: 7 additions & 0 deletions apps/kysely-cli/.env.migrate-e2e
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
21 changes: 21 additions & 0 deletions apps/kysely-cli/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@
}
]
},
"migrate-e2e": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "kysely-cli:build",
"watch": false
},
"configurations": {
"development": {
"buildTarget": "kysely-cli:build:development"
},
"production": {
"buildTarget": "kysely-cli:build:production"
}
},
"dependsOn": [
{
"target": "build",
"dependencies": true
}
]
},
"seed": {
"executor": "@nx/js:node",
"options": {
Expand Down
5 changes: 3 additions & 2 deletions apps/server-e2e/.env.e2e
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
32 changes: 28 additions & 4 deletions apps/server-e2e/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,38 @@
"projectType": "application",
"targets": {
"e2e": {
"executor": "nx:run-commands",
"options": {
"commands": ["docker compose -f docker-compose.test.yml down"]
},
"dependsOn": ["run-tests"]
},
"run-tests": {
"executor": "@nx/vite:test",
"options": {
// "config": "./apps/server-e2e/vite.config.ts"
},
"dependsOn": [
{
"projects": "self",
"target": "run-migrations"
}
]
},
"run-migrations": {
"executor": "nx:run-commands",
"options": {
"command": "pnpm nx run kysely-cli:migrate-e2e"
},
"dependsOn": ["start-docker"]
},
"start-docker": {
"executor": "nx:run-commands",
"options": {
"commands": [
"docker compose -f docker-compose.test.yml up -d",
"while ! nc -q0 localhost 6379 < /dev/null > /dev/null 2>&1; do sleep 10; done",
"while ! nc -q0 localhost 5432 < /dev/null > /dev/null 2>&1; do sleep 10; done",
"NODE_ENV=development node -r @swc/register apps/server-e2e/src/main.ts",
"docker compose -f docker-compose.test.yml down"
"while ! nc -q0 localhost 26379 < /dev/null > /dev/null 2>&1; do sleep 10; done",
"while ! nc -q0 localhost 25432 < /dev/null > /dev/null 2>&1; do sleep 10; done"
]
}
},
Expand Down
6 changes: 6 additions & 0 deletions apps/server-e2e/src/interfaces/test-context.interface.ts
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>;
}
45 changes: 23 additions & 22 deletions apps/server-e2e/src/main.ts
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();
99 changes: 50 additions & 49 deletions apps/server-e2e/src/tests/csrf.ts
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();
});
});
};
50 changes: 50 additions & 0 deletions apps/server-e2e/src/tests/signup-and-login.ts
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
});
});
});
};
Loading
Loading