Skip to content

Commit

Permalink
Set up integration testing with Testcontainers (#648)
Browse files Browse the repository at this point in the history
  • Loading branch information
junlarsen authored Nov 9, 2023
1 parent a8be832 commit d0b5874
Show file tree
Hide file tree
Showing 41 changed files with 542 additions and 152 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"test": "turbo run test",
"test:it": "turbo run test:it",
"dev": "dotenv -e .env -- turbo run dev",
"migrate": "dotenv -e .env -- turbo run migrate && cd packages/db && pnpm generate-types",
"migrate-down": "dotenv -e .env -- turbo run migrate -- down && cd packages/db && pnpm generate-types",
Expand Down
3 changes: 3 additions & 0 deletions packages/auth/src/auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export const getAuthOptions = ({
async session({ session, token }) {
if (token.sub) {
const user = await core.userService.getUserBySubject(token.sub)
if (user === undefined) {
throw new NotFoundError(`Found no matching user for ${token.sub}`)
}
session.user.id = user.id
session.sub = token.sub
}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"lint:fix": "eslint --fix .",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:it": "vitest run -c ./vitest-integration.config.ts",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
},
Expand All @@ -19,16 +20,19 @@
"date-fns": "^2.30.0",
"kysely": "^0.26.3",
"stripe": "^13.11.0",
"ulid": "^2.3.0",
"zod": "^3.22.4"
},
"peerDependencies": {
"next": "^13.5.6"
},
"devDependencies": {
"@dotkomonline/types": "workspace:*",
"@testcontainers/postgresql": "^10.2.2",
"@types/node": "^18.18.3",
"@vitest/ui": "^0.34.6",
"eslint": "^8.52.0",
"testcontainers": "^10.2.2",
"typescript": "^5.2.2",
"vitest": "^0.34.6"
}
Expand Down
72 changes: 72 additions & 0 deletions packages/core/src/modules/user/__test__/user-service.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import crypto from "crypto"
import { beforeEach, describe, expect, it } from "vitest"
import { ulid } from "ulid"
import { createEnvironment } from "@dotkomonline/env"
import { createKysely } from "@dotkomonline/db"
import { createServiceLayer, type ServiceLayer } from "../../core"

describe("users", () => {
let core: ServiceLayer

beforeEach(async () => {
const env = createEnvironment()
const db = createKysely(env)
core = await createServiceLayer({ db })
})

it("can create new users", async () => {
const none = await core.userService.getAllUsers(100)
expect(none).toHaveLength(0)

const user = await core.userService.createUser({
cognitoSub: crypto.randomUUID(),
studyYear: 0,
})

const users = await core.userService.getAllUsers(100)
expect(users).toContainEqual(user)
})

it("will not allow two users the same subject", async () => {
const subject = crypto.randomUUID()
const first = await core.userService.createUser({
cognitoSub: subject,
studyYear: 0,
})
expect(first).toBeDefined()
await expect(
core.userService.createUser({
cognitoSub: subject,
studyYear: 0,
})
).rejects.toThrow()
})

it("will find users by their user id", async () => {
const user = await core.userService.createUser({
cognitoSub: crypto.randomUUID(),
studyYear: 0,
})

const match = await core.userService.getUserById(user.id)
expect(match).toEqual(user)
const fail = await core.userService.getUserById(ulid())
expect(fail).toBeUndefined()
})

it("can update users given their id", async () => {
await expect(
core.userService.updateUser(ulid(), {
cognitoSub: crypto.randomUUID(),
})
).rejects.toThrow()
const user = await core.userService.createUser({
cognitoSub: crypto.randomUUID(),
studyYear: 0,
})
const updated = await core.userService.updateUser(user.id, {
cognitoSub: crypto.randomUUID(),
})
expect(updated.cognitoSub).not.toEqual(user.cognitoSub)
})
})
4 changes: 2 additions & 2 deletions packages/core/src/modules/user/user-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface UserRepository {
getBySubject(cognitoSubject: string): Promise<User | undefined>
getAll(limit: number): Promise<User[]>
create(userWrite: UserWrite): Promise<User>
update(id: UserId, data: UserWrite): Promise<User>
update(id: UserId, data: Partial<UserWrite>): Promise<User>
search(searchQuery: string, take: number, cursor?: Cursor): Promise<User[]>
}

Expand All @@ -36,7 +36,7 @@ export class UserRepositoryImpl implements UserRepository {
const user = await this.db.insertInto("owUser").values(userWrite).returningAll().executeTakeFirstOrThrow()
return mapToUser(user)
}
async update(id: UserId, data: UserWrite) {
async update(id: UserId, data: Partial<UserWrite>) {
const user = await this.db
.updateTable("owUser")
.set(data)
Expand Down
11 changes: 2 additions & 9 deletions packages/core/src/modules/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
import { type NotificationPermissionsRepository } from "./notification-permissions-repository"
import { type PrivacyPermissionsRepository } from "./privacy-permissions-repository"
import { type UserRepository } from "./user-repository"
import { NotFoundError } from "../../errors/errors"
import { type Cursor } from "../../utils/db-utils"

export interface UserService {
Expand All @@ -19,7 +18,7 @@ export interface UserService {
getAllUsers(limit: number): Promise<User[]>
searchUsers(searchQuery: string, take: number): Promise<User[]>
createUser(input: UserWrite): Promise<User>
updateUser(id: UserId, payload: UserWrite): Promise<User>
updateUser(id: UserId, payload: Partial<UserWrite>): Promise<User>
getPrivacyPermissionsByUserId(id: string): Promise<PrivacyPermissions>
updatePrivacyPermissionsForUserId(
id: UserId,
Expand All @@ -40,9 +39,6 @@ export class UserServiceImpl implements UserService {

async getUserById(id: UserId) {
const user = await this.userRepository.getById(id)
if (user === undefined) {
throw new NotFoundError(`User with ID:${id} not found`)
}
return user
}

Expand All @@ -53,9 +49,6 @@ export class UserServiceImpl implements UserService {

async getUserBySubject(id: User["cognitoSub"]) {
const user = await this.userRepository.getBySubject(id)
if (!user) {
throw new NotFoundError(`User with subject:${id} not found`)
}
return user
}

Expand All @@ -64,7 +57,7 @@ export class UserServiceImpl implements UserService {
return res
}

async updateUser(id: UserId, data: UserWrite) {
async updateUser(id: UserId, data: Partial<UserWrite>) {
const res = await this.userRepository.update(id, data)
return res
}
Expand Down
19 changes: 19 additions & 0 deletions packages/core/vitest-integration.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineConfig } from "vitest/config"

const defaultExclude = [
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
]

export default defineConfig({
test: {
exclude: defaultExclude.concat("**/*.spec.ts"),
include: ["**/*.e2e-spec.ts"],
threads: false,
mockReset: true,
setupFiles: ["./vitest-integration.setup.ts"],
},
})
25 changes: 25 additions & 0 deletions packages/core/vitest-integration.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { beforeEach } from "vitest"
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql"
import { createMigrator, createKysely } from "@dotkomonline/db"
import { createEnvironment } from "@dotkomonline/env"

const containers: StartedPostgreSqlContainer[] = []

beforeEach(async () => {
const container = await new PostgreSqlContainer("public.ecr.aws/z5h0l8j6/dotkom/pgx-ulid:0.1.3")
.withExposedPorts(5432)
.withUsername("local")
.withPassword("local")
.withDatabase("main")
.start()
process.env.DATABASE_URL = container.getConnectionUri()
const env = createEnvironment()
const kysely = createKysely(env)
const migrator = createMigrator(kysely, new URL("node_modules/@dotkomonline/db/src/migrations", import.meta.url))
await migrator.migrateToLatest()
containers.push(container)
})

process.on("beforeExit", async () => {
await Promise.all(containers.map(async (container) => container.stop()))
})
9 changes: 9 additions & 0 deletions packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { defineConfig } from "vitest/config"

const defaultExclude = [
"**/node_modules/**",
"**/dist/**",
"**/cypress/**",
"**/.{idea,git,cache,output,temp}/**",
"**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
]

export default defineConfig({
test: {
exclude: defaultExclude.concat("**/.e2e-spec.ts"),
globals: true,
environment: "node",
coverage: {
Expand Down
3 changes: 3 additions & 0 deletions packages/db/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
# db

Migrations have to be written in JS with JSDoc annotations in order for Vite to be able to dynamically import them
properly during Vitest runs
13 changes: 0 additions & 13 deletions packages/db/src/cockroach.ts

This file was deleted.

9 changes: 5 additions & 4 deletions packages/db/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { CamelCasePlugin, Kysely, PostgresDialect } from "kysely"
import { env } from "@dotkomonline/env"
import { env, type Environment } from "@dotkomonline/env"
import pg from "pg"
import { type DB } from "./db.generated"

export { CockroachDialect } from "./cockroach"
export { createMigrator } from "./migrator"

export type Database = DB

Expand All @@ -13,8 +13,7 @@ declare global {
var kysely: Kysely<Database> | undefined
}

export const kysely =
global.kysely ||
export const createKysely = (env: Environment) =>
new Kysely<Database>({
dialect: new PostgresDialect({
pool: new pg.Pool({
Expand All @@ -24,6 +23,8 @@ export const kysely =
plugins: [new CamelCasePlugin()],
})

export const kysely = global.kysely || createKysely(env)

if (env.NODE_ENV !== "production") {
global.kysely = kysely
}
14 changes: 14 additions & 0 deletions packages/db/src/migrations/0001_create_user_and_auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { sql } from "kysely"
import { createTableWithDefaults } from "../utils.js"

/** @param db {import('kysely').Kysely} */
export async function up(db) {
const query = sql`CREATE EXTENSION IF NOT EXISTS ulid;`.compile(db)
await db.executeQuery(query)
await createTableWithDefaults("ow_user", { id: true, createdAt: true }, db.schema).execute()
}

/** @param db {import('kysely').Kysely} */
export async function down(db) {
await db.schema.dropTable("ow_user").execute()
}
14 changes: 0 additions & 14 deletions packages/db/src/migrations/0001_create_user_and_auth.ts

This file was deleted.

1 change: 1 addition & 0 deletions packages/db/src/migrations/0002_create_company.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/db/src/migrations/0002_create_company.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type Kysely, sql } from "kysely"
import { createTableWithDefaults } from "../utils"
import { sql } from "kysely"
import { createTableWithDefaults } from "../utils.js"

export async function up(db: Kysely<any>) {
/** @param db {import('kysely').Kysely} */
export async function up(db) {
await db.schema.createType("event_status").asEnum(["TBA", "PUBLIC", "NO_LIMIT", "ATTENDANCE"]).execute()

await createTableWithDefaults("event", { id: true, createdAt: true, updatedAt: true }, db.schema)
Expand Down Expand Up @@ -44,7 +45,8 @@ export async function up(db: Kysely<any>) {
.execute()
}

export async function down(db: Kysely<any>): Promise<void> {
/** @param db {import('kysely').Kysely */
export async function down(db) {
await db.schema.dropTable("attendee").execute()
await db.schema.dropTable("attendance").execute()
await db.schema.dropTable("event_company").execute()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type Kysely, sql } from "kysely"
import { createTableWithDefaults } from "../utils"
import { sql } from "kysely"
import { createTableWithDefaults } from "../utils.js"

export async function up(db: Kysely<any>) {
/** @param db {import('kysely').Kysely} */
export async function up(db) {
await createTableWithDefaults("mark", { id: true, createdAt: false, updatedAt: true }, db.schema)
.addColumn("title", "varchar(255)", (col) => col.notNull())
.addColumn("given_at", "timestamptz", (col) => col.notNull())
Expand All @@ -18,7 +19,8 @@ export async function up(db: Kysely<any>) {
.execute()
}

export async function down(db: Kysely<any>): Promise<void> {
/** @param db {import('kysely').Kysely */
export async function down(db) {
await db.schema.dropTable("personal_mark").execute()
await db.schema.dropTable("mark").execute()
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { type Kysely, sql } from "kysely"
import { sql } from "kysely"

export async function up(db: Kysely<any>) {
/** @param db {import('kysely').Kysely} */
export async function up(db) {
await db.schema.createType("event_type").asEnum(["SOCIAL", "ACADEMIC", "COMPANY", "BEDPRES"]).execute()
await db.schema
.alterTable("event")
.addColumn("type", sql`event_type`)
.execute()
}

export async function down(db: Kysely<any>) {
/** @param db {import('kysely').Kysely} */
export async function down(db) {
await db.schema.alterTable("event").dropColumn("type").execute()
await db.schema.dropType("event_type").ifExists().execute()
}
Loading

1 comment on commit d0b5874

@vercel
Copy link

@vercel vercel bot commented on d0b5874 Nov 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

rif – ./apps/rif

rif-two.vercel.app
rif-dotkom.vercel.app
dev.interesse.online.ntnu.no
rif-git-main-dotkom.vercel.app

Please sign in to comment.