Skip to content

test: Initial attempts at database testing #2

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

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .env.testing.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
APP_SECRET="appsecret123"
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
environment: build
steps:
- uses: actions/checkout@v3
- name: Set up Node
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ yarn-error.log

.env*
!.env.example
!.env.testing.example

coverage
46 changes: 46 additions & 0 deletions __tests__/__utils__/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { exec } from 'child_process';
import { Nullable } from '../../src/types';
import { connectToDB, getPrismaClient } from '../../src/boot/db';

type CommandOutput = {
error: Nullable<Error>;
stdout: string | Buffer;
stderr: string | Buffer;
};

let migrationsRun = false;

export const setupDatabase = async (
forceRun = false
): Promise<CommandOutput | void> => {
await connectToDB();

return new Promise<CommandOutput | void>((resolve, reject) => {
if (migrationsRun && !forceRun) {
resolve();
}

const rootPath = `${__dirname}/../../`;

const command = `cd ${rootPath} && DB_URL=${process.env.DB_URL} yarn prisma migrate reset --force`;
exec(command, (error, stdout, stderr) => {
if (error) {
reject({
error,
stdout,
stderr,
});
}

migrationsRun = true;
resolve();
});
});
};

export const resetDatabase = async (): Promise<void> => {
await connectToDB();

const client = getPrismaClient();
await client.user.deleteMany();
};
18 changes: 18 additions & 0 deletions __tests__/__utils__/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { User } from '@prisma/client';

export const generateStubFactory =
<E>(defaultValues: E) =>
(values: Partial<E> = {}): E => ({
...defaultValues,
values,
});

export const stubUser = generateStubFactory<User>({
id: 12345,
email: '[email protected]',
first_name: 'Test',
last_name: 'User',
password: 'passwordHashHere',
created_at: new Date(),
updated_at: new Date(),
});
109 changes: 109 additions & 0 deletions __tests__/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
describe('db test', () => {
test('that the stub works', () => {
expect(true).toBeTruthy();
});
});

/*
* I was attempting to get database tests working in a way where an individual test could request a fresh database.
* This would run the migrations if needed, and otherwise reset the data, allowing a clean slate before (and after) the test.
* I believe this might need to wait until after prisma adds support for programmatic migrations.
*/

// beforeAll(async () => {
// await connectToDB();
// });
//
// afterEach(async () => {
// await resetDatabase();
// });
//
// describe('db test', () => {
// test('that the database connection works', async () => {
// const client = getPrismaClient();
//
// await client.user.create({
// data: {
// first_name: 'Foo',
// last_name: 'Bar',
// email: '[email protected]',
// password: 'password123',
// },
// });
//
// const user = (await client.user.findFirstOrThrow({
// where: {
// email: '[email protected]',
// },
// })) as User;
//
// expect(user).not.toBeNull();
// expect(user.email).toEqual('[email protected]');
// });
//
// test('that the database connection works2', async () => {
// const client = getPrismaClient();
//
// await client.user.create({
// data: {
// first_name: 'Foo',
// last_name: 'Bar',
// email: '[email protected]',
// password: 'password123',
// },
// });
//
// const user = (await client.user.findFirstOrThrow({
// where: {
// email: '[email protected]',
// },
// })) as User;
//
// expect(user).not.toBeNull();
// expect(user.email).toEqual('[email protected]');
// });
//
// test('that the database connection works3', async () => {
// const client = getPrismaClient();
//
// await client.user.create({
// data: {
// first_name: 'Foo',
// last_name: 'Bar',
// email: '[email protected]',
// password: 'password123',
// },
// });
//
// const user = (await client.user.findFirstOrThrow({
// where: {
// email: '[email protected]',
// },
// })) as User;
//
// expect(user).not.toBeNull();
// expect(user.email).toEqual('[email protected]');
// });
//
// test('that the database connection works4', async () => {
// const client = getPrismaClient();
//
// await client.user.create({
// data: {
// first_name: 'Foo',
// last_name: 'Bar',
// email: '[email protected]',
// password: 'password123',
// },
// });
//
// const user = (await client.user.findFirstOrThrow({
// where: {
// email: '[email protected]',
// },
// })) as User;
//
// expect(user).not.toBeNull();
// expect(user.email).toEqual('[email protected]');
// });
// });
12 changes: 12 additions & 0 deletions __tests__/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { config } from 'dotenv';
import * as fs from 'fs';

export default async function () {
const path = `${__dirname}/../.env.testing`;

if (fs.existsSync(path)) {
config({
path,
});
}
}
10 changes: 10 additions & 0 deletions __tests__/service/hash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { generateHash, validateHash } from '../../src/service/hash';

describe('hash service', () => {
test('hashes and validates properly', () => {
const hash = generateHash('foobar123');

expect(validateHash('foobar123', hash)).toBeTruthy();
expect(validateHash('incorrect', hash)).toBeFalsy();
});
});
33 changes: 33 additions & 0 deletions __tests__/service/jwt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { decodeJwt, generateJwt } from '../../src/service/jwt';
import { stubUser } from '../__utils__/model';

describe('jwt service', () => {
test('generates a jwt properly', () => {
const user = stubUser();
const jwt = generateJwt(user);

expect(jwt).not.toBeNull();
const decoded = decodeJwt(jwt);

expect(decoded).not.toBeNull();
});

test('will not decode a jwt with a different secret', () => {
const user = stubUser();
const jwt = generateJwt(user);

const appSecret = process.env.APP_SECRET;
process.env.APP_SECRET = `${appSecret}withNewCharacters`;

expect(() => {
decodeJwt(jwt);
}).toThrowError();

// Set this app secret back to the original
process.env.APP_SECRET = appSecret;

expect(() => {
decodeJwt(jwt);
}).not.toThrowError();
});
});
3 changes: 0 additions & 3 deletions __tests__/stub.ts

This file was deleted.

6 changes: 6 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
};
18 changes: 15 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
version: '3.9'
services:
postgres:
db:
image: postgres:14.6
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- soundbridge-db:/var/lib/postgresql/data
- dev-db:/var/lib/postgresql/data
ports:
- ${DB_PORT:-5432}:5432
testDB:
image: postgres:14.6
environment:
POSTGRES_DB: ${TEST_DB_NAME:-test}
POSTGRES_USER: ${TEST_DB_USER:-test}
POSTGRES_PASSWORD: ${TEST_DB_PASSWORD:-secret}
volumes:
- test-db:/var/lib/postgresql/data
ports:
- ${TEST_DB_PORT:-55432}:5432
volumes:
soundbridge-db:
dev-db:
driver: local
test-db:
driver: local
10 changes: 5 additions & 5 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ export default {
// collectCoverageFrom: undefined,

// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
coverageDirectory: 'coverage',

// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],

// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
coverageProvider: 'v8',

// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
Expand Down Expand Up @@ -59,7 +59,7 @@ export default {
// forceCoverageMatch: [],

// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
globalSetup: `${__dirname}/__tests__/init.ts`,

// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
Expand Down Expand Up @@ -87,11 +87,11 @@ export default {
// "node"
// ],

// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// A map from regular expressions to module names or to arrays of module names that allow to __utils__ out resources with a single module
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
modulePathIgnorePatterns: ['init.ts', '__fixtures__', '__utils__'],

// Activates notifications for test results
// notify: false,
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
"winston": "^3.8.2"
},
"devDependencies": {
"@babel/preset-env": "^7.20.2",
"@babel/preset-typescript": "^7.18.6",
"@jest/globals": "^29.4.1",
"@tsconfig/recommended": "^1.0.2",
"@types/bcryptjs": "^2.4.2",
"@types/jest": "^29.4.0",
Expand Down
10 changes: 9 additions & 1 deletion src/boot/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@ import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;

/* eslint-disable @typescript-eslint/ban-ts-comment */
export const connectToDB = async (): Promise<PrismaClient> => {
export const connectToDB = async (reset = false): Promise<PrismaClient> => {
if (prisma && !reset) {
// eslint-disable-next-line no-console
console.log('prisma already exists, pulling previous');
return getPrismaClient();
} else if (prisma) {
await prisma.$disconnect();
}

prisma = new PrismaClient();

return getPrismaClient();
Expand Down
8 changes: 3 additions & 5 deletions src/http/middleware/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppContext, AppMiddleware } from '../../types';
import { Next } from 'koa';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { getPrismaClient } from '../../boot/db';
import { decodeJwt } from '../../service/jwt';

type DecodedToken = JwtPayload & {
sub: number;
Expand Down Expand Up @@ -40,11 +41,8 @@ export const jwtAuth: AppMiddleware = async (

let decodedToken: DecodedToken;
try {
decodedToken = jwt.verify(
rawToken,
process.env.APP_SECRET as string
) as DecodedToken;
} catch (err) {
decodedToken = decodeJwt(rawToken);
} catch (error) {
ctx.throw(401);
return;
}
Expand Down
Loading