From 0897133e493422aec8f55d21e7f718362ee2d709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl=20J=2E=20Over=C3=A5?= Date: Sat, 3 Feb 2024 15:21:50 +0100 Subject: [PATCH] run tests in GHA --- .github/workflows/test.yml | 15 ++++++++++++ compose.yaml | 13 ++++++++--- src/db.ts | 48 +++++++++++++++++++++++++++----------- src/server.test.ts | 27 ++++++++++++++++++++- src/server.ts | 19 +++++++++++---- 5 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..974b916 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,15 @@ +name: Run Tests + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install --frozen-lockfile + - name: Start server + run: docker-compose up --build -d + - name: Run Tests + run: bun test diff --git a/compose.yaml b/compose.yaml index 79cf116..680070a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,13 +2,20 @@ services: api: build: . ports: - - "6969:6969" + - '6969:6969' environment: - DOCKER_COMPOSE=true + depends_on: + - db db: - image: "postgres:15" + image: 'postgres:15' ports: - - "5432:5432" + - '5432:5432' environment: - POSTGRES_PASSWORD=postgres - POSTGRES_USER=postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 30s + timeout: 10s + retries: 3 diff --git a/src/db.ts b/src/db.ts index a7755d6..fd29b1f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -4,19 +4,21 @@ import { cwd } from 'node:process' const host = Bun.env.DOCKER_COMPOSE ? 'db' : 'localhost' -export const client = new Client({ - host: host, - port: 5432, - database: 'postgres', - user: 'postgres', - password: 'postgres', -}) +let _client: Client | null = null -export async function initDb() { - console.info('Running migrations if needed') +export function getClient(): Client { + if (_client == null) { + throw new Error('Database not initialized') + } - await connectWithRetry() + return _client +} +export async function initDb() { + console.info('Connecting to database') + const client = await connectWithRetry() + + console.info('Running migrations if needed') const authorsExistQuery = await client.query(` SELECT EXISTS (SELECT FROM information_schema.tables @@ -43,18 +45,36 @@ export async function initDb() { } } +function createClient() { + console.info(`DB host is ${host}`) + + return new Client({ + host: host, + port: 5432, + database: 'postgres', + user: 'postgres', + password: 'postgres', + }) +} + async function connectWithRetry() { let retry = 0 while (true) { try { - await client.connect() - break + const freshClient = createClient() + await freshClient.connect() + + console.info('Connected to successfully to database') + + _client = freshClient + + return _client } catch (e) { retry++ if (retry > 3) throw e - console.error(`Unable to connect to database, retrying in 3 second (${retry}/3)`) - await new Promise((resolve) => setTimeout(resolve, 3000)) + console.error(`Unable to connect to database, retrying in 10 second (${retry}/3)`) + await new Promise((resolve) => setTimeout(resolve, 5000)) } } } diff --git a/src/server.test.ts b/src/server.test.ts index be5dbac..3f57eea 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -1,6 +1,10 @@ -import { test, describe, expect, beforeAll, beforeEach } from 'bun:test' +import { test, describe, expect, beforeEach, beforeAll } from 'bun:test' describe('API sanity checks', () => { + beforeAll(async () => { + await serverReady() + }) + beforeEach(async () => { await fetch('http://localhost:6969/dev/re-seed', { method: 'POST' }) }) @@ -115,3 +119,24 @@ describe('API sanity checks', () => { expect(authorBooksData).toHaveLength(1) }) }) + +async function serverReady(): Promise { + let waits = 0 + while (true) { + if (waits > 30) { + throw new Error('Server did not start in time') + } + + try { + const res = await fetch('http://localhost:6969/dev/ready') + if (res.status === 200 && ((await res.json()) as { status: string }).status === 'ok') { + break + } + } catch (e) { + // ignore + } finally { + waits++ + await new Promise((resolve) => setTimeout(resolve, 1000)) + } + } +} diff --git a/src/server.ts b/src/server.ts index d7c586a..0379360 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,5 @@ import { Elysia, t } from 'elysia' -import { client, initDb } from './db.ts' +import { getClient, initDb } from './db.ts' import swagger from '@elysiajs/swagger' import { DbAuthor, DbBook } from './db-types.ts' import path from 'path' @@ -22,7 +22,7 @@ const app = new Elysia() .get( 'authors', async () => { - const result = await client.query('SELECT * FROM authors') + const result = await getClient().query('SELECT * FROM authors') return result.rows.map((it) => ({ uuid: it.uuid, @@ -44,6 +44,7 @@ const app = new Elysia() .get( 'author/:uuid', async ({ params: { uuid } }) => { + const client = getClient() const result = await client.query('SELECT * FROM authors WHERE uuid = $1', [uuid]) if (result.rows.length === 0) { @@ -76,6 +77,7 @@ const app = new Elysia() .post( 'author', async ({ body, set }) => { + const client = getClient() if (await client.query('SELECT * FROM authors WHERE name = $1', [body.name]).then((it) => it.rows.length > 0)) { set.status = 'Conflict' return null @@ -106,6 +108,7 @@ const app = new Elysia() .delete( 'author/:uuid', async ({ params: { uuid }, set }) => { + const client = getClient() if (await client.query('SELECT * FROM authors WHERE uuid = $1', [uuid]).then((it) => it.rows.length === 0)) { set.status = 'Not Found' return null @@ -130,7 +133,7 @@ const app = new Elysia() .get( 'author/:uuid/books', async ({ params: { uuid } }) => { - const result = await client.query>( + const result = await getClient().query>( `SELECT title, published_date, isbn FROM books LEFT JOIN authors a on a.id = books.author_id WHERE a.uuid = $1`, @@ -157,7 +160,7 @@ const app = new Elysia() .get( 'books', async () => { - const result = await client.query('SELECT * FROM books') + const result = await getClient().query('SELECT * FROM books') return result.rows.map((it) => ({ title: it.title, @@ -179,6 +182,8 @@ const app = new Elysia() .post( 'book', async ({ body, set }) => { + const client = getClient() + if (await client.query('SELECT * FROM books WHERE isbn = $1', [body.isbn]).then((it) => it.rows.length > 0)) { set.status = 'Conflict' return null @@ -210,6 +215,7 @@ const app = new Elysia() }, ) .post('dev/re-seed', async () => { + const client = getClient() await client.query('BEGIN') await client.query('DELETE FROM books') await client.query('DELETE FROM authors') @@ -219,13 +225,16 @@ const app = new Elysia() return { success: true } }) + .get('dev/ready', async () => { + return { status: 'ok' } + }) .onError((err) => { console.error(err) }) app.onStop(async () => { console.log('Stopping server, closing database connection') - await client.end() + await getClient().end() }) app.listen(6969)