Skip to content
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
6 changes: 6 additions & 0 deletions .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ jobs:
- name: 📥 Monorepo install
uses: ./.github/actions/pnpm-install

- name: Run tests
run: pnpm --filter @t4/api test
env:
NO_D1_WARNING: true
CI: true

- name: Migrate database
run: pnpm --filter @t4/api migrate
env:
Expand Down
3 changes: 2 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"formulahendry.auto-rename-tag",
"dbaeumer.vscode-eslint",
"expo.vscode-expo-tools",
"Postman.postman-for-vscode"
"Postman.postman-for-vscode",
"orta.vscode-jest"
]
}
21 changes: 21 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@
"args": ["--extensionDevelopmentPath=${workspaceFolder}/apps/vscode"],
"outFiles": ["${workspaceFolder}/apps/vscode/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
// This will automatically be used when debugging tests in the api package
"type": "node",
"name": "vscode-jest-tests.v2.@t4/api",
"request": "launch",
"runtimeArgs": ["--experimental-vm-modules"],
"args": [
"--runInBand",
"--watchAll=false",
"--testTimeout=600000",
"--testNamePattern",
"${jest.testNamePattern}",
"--runTestsByPath",
"${jest.testFile}"
],
"cwd": "${workspaceFolder}/packages/api",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"program": "${workspaceFolder}/node_modules/jest/bin/jest.js"
}
]
}
12 changes: 11 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@
"*.js": "${capture}.js.map, ${capture}.d.ts, ${capture}.d.ts.map",
"*.ts": "$(capture).test.ts, $(capture).test.tsx, $(capture).test.node.ts, $(capture).test.node.tsx, $(capture).test.native.ts, $(capture).test.native.tsx, $(capture).test.ios.ts, $(capture).test.ios.tsx, $(capture).test.web.ts, $(capture).test.web.tsx, $(capture).test.android.ts, $(capture).test.android.tsx, ${capture}.native.tsx, ${capture}.ios.tsx, ${capture}.android.tsx, ${capture}.web.tsx, ${capture}.native.ts, ${capture}.ios.ts, ${capture}.android.ts, ${capture}.web.ts, ${capture}.native.js, ${capture}.ios.js, ${capture}.android.js, ${capture}.web.js, ${capture}.native.jsx, ${capture}.ios.jsx, ${capture}.android.jsx, ${capture}.web.jsx",
"*.tsx": "$(capture).test.ts, $(capture).test.tsx, $(capture).test.node.ts, $(capture).test.node.tsx, $(capture).test.native.ts, $(capture).test.native.tsx, $(capture).test.ios.ts, $(capture).test.ios.tsx, $(capture).test.web.ts, $(capture).test.web.tsx, $(capture).test.android.ts, $(capture).test.android.tsx, ${capture}.native.tsx, ${capture}.ios.tsx, ${capture}.android.tsx, ${capture}.web.tsx, ${capture}.native.ts, ${capture}.ios.ts, ${capture}.android.ts, ${capture}.web.ts, ${capture}.native.js, ${capture}.ios.js, ${capture}.android.js, ${capture}.web.js, ${capture}.native.jsx, ${capture}.ios.jsx, ${capture}.android.jsx, ${capture}.web.jsx"
}
},
// This requires installing the pre-release 6.x version of the Jest extension
"jest.virtualFolders": [
{
"autoRun": "off",
"name": "@t4/api",
"rootPath": "packages/api",
"jestCommandLine": "../../node_modules/jest/bin/jest.js",
"nodeEnv": { "NODE_OPTIONS": "--experimental-vm-modules" }
}
]
// Turn this on if you want to debug the T4 App Tools Extension
// "terminal.integrated.defaultProfile.windows": null
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@
"check-dependency-version-consistency": "^4.1.0",
"cross-env": "^7.0.3",
"eslint": "^8.50.0",
"miniflare": "3.20230922.0",
"node-gyp": "^9.4.0",
"prettier": "^3.0.3",
"react-native-url-polyfill": "^2.0.0",
"turbo": "^1.10.14",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"wrangler": "^3.10.1"
},
"engines": {
"node": ">=18.16.1",
Expand Down
25 changes: 25 additions & 0 deletions packages/api/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { JestConfigWithTsJest } from 'ts-jest'

const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest/presets/default-esm',
verbose: true,
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: 'test/tsconfig.json',
useESM: true,
},
],
},
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
setupFiles: ['./test/setup.js'],
testEnvironment: 'node',
testEnvironmentOptions: {
modules: true,
},
}

export default jestConfig
10 changes: 8 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "2.0.0",
"main": "src/index.ts",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "cross-env NO_D1_WARNING=true wrangler dev src/index.ts",
"generate": "drizzle-kit generate:sqlite --schema=./src/db/schema.ts --out=./migrations",
Expand All @@ -13,6 +14,8 @@
"studio": "drizzle-kit studio",
"deploy": "cross-env NO_D1_WARNING=true wrangler deploy --minify src/index.ts",
"postinstall": "pnpm generate",
"test": "wrangler deploy --dry-run --outdir=dist && cross-env NODE_OPTIONS=--experimental-vm-modules jest",
"types": "cross-env NO_D1_WARNING=true wrangler types",
"with-env": "dotenv -e ../../.env.local --",
"clean": "rm -rf .turbo node_modules"
},
Expand All @@ -28,11 +31,14 @@
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20230922.0",
"@types/jest": "^29.5.5",
"better-sqlite3": "^8.6.0",
"dotenv-cli": "^7.3.0",
"drizzle-kit": "^0.19.13",
"eslint": "^8.50.0",
"typescript": "^5.2.2",
"wrangler": "3.6.0"
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
4 changes: 2 additions & 2 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { cors } from 'hono/cors'
import { createContext } from './context'
import { trpcServer } from '@hono/trpc-server'

type Bindings = {
DB: D1Database
export type Bindings = Env & {
JWT_VERIFICATION_KEY: string
APP_URL: string
[k: string]: unknown
}

const app = new Hono<{ Bindings: Bindings }>()
Expand Down
116 changes: 116 additions & 0 deletions packages/api/test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as dotenv from 'dotenv'
dotenv.config({ path: './.dev.vars' })

import { Miniflare } from 'miniflare'
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import jwt from '@tsndr/cloudflare-worker-jwt'
import superjson from 'superjson'
import app, { Bindings } from '../src'
import { readMigrationFiles } from 'drizzle-orm/migrator'
import { createDb } from '../src/db/client'
import { AppRouter } from '../src/router'

export const JWT_VERIFICATION_KEY = process.env.JWT_VERIFICATION_KEY || '12345'

export const createMiniflare = () => {
return new Miniflare({
kvPersist: false,
d1Persist: false,
r2Persist: false,
cachePersist: false,
durableObjectsPersist: false,
liveReload: false,
workers: [
{
name: 'api',
bindings: {
APP_URL: process.env.APP_URL || 'http://localhost:3000',
DATABASE_ID: process.env.DATABASE_ID || '12345',
JWT_VERIFICATION_KEY,
},
d1Databases: {
DB: process.env.DATABASE_ID || '12345',
},
modules: true,
scriptPath: './dist/index.js',
compatibilityDate: '2023-09-22',
},
],
})
}

export async function getBindings({ miniflare }: { miniflare: Miniflare }): Promise<Bindings> {
return await miniflare.getBindings()
}
export async function getDb({ miniflare }: { miniflare: Miniflare }) {
return await miniflare.getD1Database('DB')
}
export async function getDrizzleDb({ miniflare }: { miniflare: Miniflare }) {
return createDb(await getDb({ miniflare }))
}
export async function executeSql(sql: string, { miniflare }: { miniflare: Miniflare }) {
const normalized = sql
.replaceAll(/--.*/g, '')
.replaceAll(/[\n\t]/g, '')
.trim()
if (!normalized) {
return undefined
}
// console.log(sql)
const db = await getDb({ miniflare })
return await db.exec(normalized)
}
export async function migrateDb({ miniflare }: { miniflare: Miniflare }) {
const files = readMigrationFiles({ migrationsFolder: './migrations' })
for (let i = 0; i < files.length; i++) {
const migrationMeta = files[i]
for (let j = 0; j < migrationMeta.sql.length; j++) {
await executeSql(migrationMeta.sql[j], { miniflare })
}
}
}

/**
* Creates a session token for the provided user ID. Defaults to 'test-user'
*/
export async function createSessionToken({ userId = 'test-user' }: { userId?: string }) {
const payload = {
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7,
sub: userId,
}
const algorithm = 'HS256'
return await jwt.sign(payload, JWT_VERIFICATION_KEY, { algorithm })
}

/**
* Creates a test TRPC server and client using the bindings from optional
* provided miniflare instance.
*
* If the userId option is provided, it will creates a session token and passes
* it in the authentication header in the trpc request
*/
export function createTRPCClient({
miniflare,
userId,
url = 'http://localhost:3000/trpc',
}: {
miniflare: Miniflare
userId?: string
url?: string
}) {
return createTRPCProxyClient<AppRouter>({
transformer: superjson,
links: [
httpBatchLink({
url,
async headers() {
return {
authorization: userId ? 'Bearer ' + (await createSessionToken({ userId })) : undefined,
}
},
fetch: async (resource, options) =>
app.fetch(new Request(resource, options), await getBindings({ miniflare })),
}),
],
})
}
49 changes: 49 additions & 0 deletions packages/api/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Miniflare } from 'miniflare'
import { getDb, getBindings, migrateDb, createMiniflare, createTRPCClient } from './helpers'

describe('T4 App API Server', () => {
// in case we have parallelized tests...
let serverId = 0
let servers: Miniflare[] = []
beforeEach(async () => {
serverId = serverId + 1
servers[serverId] = createMiniflare()
await migrateDb({ miniflare: servers[serverId] })
})
afterEach(async () => {
await servers[serverId]?.dispose()
})
test('has cloudflare bindings', async () => {
const miniflare = servers[serverId]
const db = await getDb({ miniflare })
expect(db).toBeTruthy()
})
test('has env vars', async () => {
const miniflare = servers[serverId]
const bindings = await getBindings({ miniflare })
expect(bindings.JWT_VERIFICATION_KEY).toBeTruthy()
})
test('can make api requests to the mocked api server', async () => {
const miniflare = servers[serverId]
const res = await miniflare.dispatchFetch('http://localhost:8787/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `
query {
hello
}
`,
}),
})
expect(res.status).toBe(404)
})
test('can call hono and trpc routes directly', async () => {
const miniflare = servers[serverId]
const client = createTRPCClient({ miniflare })
const res = await client.hello.world.query('world')
expect(res).toBe('Hello world!')
})
})
2 changes: 2 additions & 0 deletions packages/api/test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { webcrypto } from 'node:crypto'
global.crypto = webcrypto
15 changes: 15 additions & 0 deletions packages/api/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"types": [
"@cloudflare/workers-types/2023-07-01",
"node",
"jest",
"../worker-configuration.d.ts"
]
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}
42 changes: 42 additions & 0 deletions packages/api/test/user.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { UserTable } from '../src/db/schema'
import { createMiniflare, createTRPCClient, getDrizzleDb, migrateDb } from './helpers'
import type { Miniflare } from 'miniflare'
import { TRPCClientError } from '@trpc/client'
import { AppRouter } from '../src/router'

describe('User router', () => {
// in case we have parallelized tests...
let serverId = 0
let servers: Miniflare[] = []
beforeEach(async () => {
serverId = serverId + 1
servers[serverId] = createMiniflare()
await migrateDb({ miniflare: servers[serverId] })
})
afterEach(async () => {
await servers[serverId]?.dispose()
})
test('current - error when not authenticated', async () => {
const miniflare = servers[serverId]
const client = createTRPCClient({ miniflare })
let err: TRPCClientError<AppRouter> | null = null
try {
const res = await client.user.current.query()
} catch (e) {
if (e instanceof TRPCClientError) {
err = e
}
}
expect(err?.message).toBe('Not authenticated')
})
test('current - returns details for authenticated user', async () => {
const miniflare = servers[serverId]
const userId = 'test-user'
const db = await getDrizzleDb({ miniflare })
await db.insert(UserTable).values({ id: userId, email: 'test@example.com' })

const client = createTRPCClient({ userId, miniflare })
const res = await client.user.current.query()
expect(res?.id).toBe(userId)
})
})
2 changes: 1 addition & 1 deletion packages/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"skipLibCheck": true,
"strict": true,
"lib": ["esnext"],
"types": ["@cloudflare/workers-types", "node"],
"types": ["@cloudflare/workers-types/2023-07-01", "node", "./worker-configuration.d.ts"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
Expand Down
4 changes: 4 additions & 0 deletions packages/api/worker-configuration.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Generated by Wrangler on Wed Oct 04 2023 00:34:48 GMT-0400 (Eastern Daylight Time)
interface Env {
DB: D1Database
}
Loading