From 201d233f5ddd4b762341970a8c6b1d6e16db687e Mon Sep 17 00:00:00 2001 From: kevin olson Date: Wed, 21 Aug 2024 04:21:47 -0500 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=92=9A=20created=20a=20pen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/auth.ts | 9 ++++++++- test/config.ts | 10 ++++++++++ test/pen.test.ts | 22 ++++++++++++++++++++++ test/user.test.ts | 22 ++++++++++------------ 4 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 test/config.ts create mode 100644 test/pen.test.ts diff --git a/test/auth.ts b/test/auth.ts index 77a1b0b..7f4de34 100644 --- a/test/auth.ts +++ b/test/auth.ts @@ -35,13 +35,20 @@ async function setupUsers() { })) } +async function userFromEmail(email: string) { + const user = users.find(user => user.email === email) + if (!user) throw new Error('User not found') + return user +} + async function actingAs(email: string) { const user = users.find(user => user.email === email) if (!user) throw new Error('User not found') const { data } = await $fetch('/api/test/session', { method: 'POST', body: { id: user?.session?.id.toString(), hash: user?.session?.hash } }) user.cookie = data.cookie[1].split(';')[0] as string const get = (url: string) => $fetch(url, { headers: { cookie: user.cookie as string } }) - return { get } + const post = (url: string, params: object) => $fetch(url, { method: 'POST', body: params, headers: { cookie: user.cookie as string } }) + return { get, post } } export { diff --git a/test/config.ts b/test/config.ts new file mode 100644 index 0000000..768fa6d --- /dev/null +++ b/test/config.ts @@ -0,0 +1,10 @@ +function setupConfig() { + if (process.env.DEVRUN === 'true' && !process.env.CI) + return { host: 'http://localhost:3000' } + else + return {} +} + +export { + setupConfig, +} diff --git a/test/pen.test.ts b/test/pen.test.ts new file mode 100644 index 0000000..a34cb20 --- /dev/null +++ b/test/pen.test.ts @@ -0,0 +1,22 @@ +import { beforeAll, describe, expect, it } from 'vitest' +import { setup } from '@nuxt/test-utils' +import { actingAs, setupUsers, users } from './auth' +import { setupConfig } from './config' +import { penColors } from '~/utils/shared' +import type { MetapiResponse } from '~/types/metapi' +import type { Pen } from '~/types/models' + +beforeAll(setupUsers) +describe('/api/pen', async () => { + await setup(setupConfig()) + + const pens: Pen[] = [] + + it('post /api/pen - create a pen', async () => { + const { post } = await actingAs('test@test.com') + const { data: pen } = await post('/api/pen', { color: penColors[0] }) as MetapiResponse + expect(pen.color).toBe(penColors[0]) + expect(pen.userId).toBe(users[0]?.session.id.toString()) + pens.push(pen) + }) +}) diff --git a/test/user.test.ts b/test/user.test.ts index 0e09d23..b8080b1 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -1,19 +1,12 @@ import { beforeAll, describe, expect, it } from 'vitest' import { $fetch, setup } from '@nuxt/test-utils/e2e' import { actingAs, setupUsers, users } from './auth' +import { setupConfig } from './config' import type { MetapiResponse } from '~/types/metapi' import type { User } from '~/types/models' beforeAll(setupUsers) - -function setupConfig() { - if (process.env.DEVRUN === 'true' && !process.env.CI) - return { host: 'http://localhost:3000' } - else - return {} -} - -describe('/api/me', async () => { +describe('/api/me and /api/user', async () => { await setup(setupConfig()) it('/api/me should 401', async () => { try { await $fetch('/api/me') } @@ -22,13 +15,13 @@ describe('/api/me', async () => { } }) - it('/api/me returns the currently logged in user', async () => { + it('get /api/me - current user session', async () => { const { get } = await actingAs('test@test.com') const response = await get('/api/me') as MetapiResponse expect(response.data.email).toEqual(users[0]?.session.email) }) - it ('/api/user should 404 if a non-admin accesses it', async () => { + it ('get /api/user isAdmin: false - 404', async () => { try { await (await actingAs('test@test.com')).get('/api/user') } @@ -37,8 +30,13 @@ describe('/api/me', async () => { } }) - it ('/api/use should return a list of all users if an admin accesses it', async () => { + it ('get /api/user GET', async () => { const response = await (await actingAs('admin@test.com')).get('/api/user') as MetapiResponse expect(response.data.length).toBe(2) }) + + it ('get /api/user/:id', async () => { + const response = await (await actingAs('admin@test.com')).get(`/api/user/${users[0]?.session.id}`) as MetapiResponse + expect(response.data.id).toBe(users[0]?.session.id.toString()) + }) }) From 78c55289c83fac7c3e0dfc514407edd761686160 Mon Sep 17 00:00:00 2001 From: kevin olson Date: Wed, 21 Aug 2024 04:55:45 -0500 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=85=20lots=20of=20of=20user=20pen=20c?= =?UTF-8?q?ontroller=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/types/models.d.ts | 2 +- package.json | 1 + server/controllers/pen.ts | 41 +++++++++++++++++++++++++-------------- test/auth.ts | 9 ++++++--- test/pen.test.ts | 22 +++++++++++++++++++-- 5 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/types/models.d.ts b/app/types/models.d.ts index 81e3625..f4022e6 100644 --- a/app/types/models.d.ts +++ b/app/types/models.d.ts @@ -35,5 +35,5 @@ export interface Cartridge extends PrismaCartridge { } export interface Pen extends PrismaPen { - cartridge?: Cartridge + cartridge: Cartridge | null } diff --git a/package.json b/package.json index 2992e32..ed8fd15 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "vitest run", "test:dev": "DEVRUN=true vitest run", "test:reset": "pnpm run db:test:reset; vitest run", + "test:dev:reset": "pnpm run db:test:reset; DEVRUN=true vitest run", "db:test:reset": "dotenv -e .env.test -- npx prisma migrate reset --force", "test:coverage": "vitest run --coverage.enabled true", "lint": "eslint .", diff --git a/server/controllers/pen.ts b/server/controllers/pen.ts index a18b6e2..295d00c 100644 --- a/server/controllers/pen.ts +++ b/server/controllers/pen.ts @@ -1,6 +1,18 @@ import { z } from 'zod' import { penColors } from '~/utils/shared' +const inc = { + cartridge: { + include: { + shots: { + orderBy: { + date: 'asc', + }, + }, + }, + }, +} + const index = defineEventHandler(async (event) => { const { user } = await requireUserSession(event) return metapi().render( @@ -8,24 +20,18 @@ const index = defineEventHandler(async (event) => { where: { userId: BigInt(user.id), }, - include: { - cartridge: { - include: { - shots: { - orderBy: { - date: 'asc', - }, - }, - }, - }, + include: inc, + orderBy: { + updatedAt: 'desc', }, }), ) }) -const create = defineEventHandler(async (event) => { - const { user } = await requireUserSession(event) - const schema = z.object({ color: z.enum(penColors as [string, ...string[]]) }) +const create = authedHandler(async ({ user, event }) => { + const schema = z.object({ + color: z.enum(penColors as [string, ...string[]]), + }) const parsed = schema.safeParse(await readBody(event)) if (!parsed.success) return metapi().error(event, parsed.error.issues, 400) const pen = await prisma.pen.create({ @@ -34,20 +40,22 @@ const create = defineEventHandler(async (event) => { userId: user.id, cartridgeId: null, }, + include: inc, }) return metapi().success('pen created', pen) }) -const update = defineEventHandler(async (event) => { - const { user } = await requireUserSession(event) +const update = authedHandler(async ({ user, event }) => { const schema = z.object({ id: z.number(), + color: z.enum(penColors as [string, ...string[]]), cartridgeId: z.number().optional(), }) const parsed = schema.safeParse({ id: Number.parseInt(event.context.params?.id as string), cartridgeId: Number.parseInt((await readBody(event))?.cartridgeId) || undefined, + color: (await readBody(event))?.color || penColors[0], }) if (!parsed.success) return metapi().error(event, parsed.error.issues, 400) const pen = await prisma.pen.update({ @@ -57,7 +65,9 @@ const update = defineEventHandler(async (event) => { }, data: { cartridgeId: parsed.data.cartridgeId ? BigInt(parsed.data.cartridgeId) : null, + color: parsed.data.color, }, + include: inc, }) return metapi().success('pen updated', pen) @@ -74,6 +84,7 @@ const get = defineEventHandler(async (event) => { id: parsed.data.id, userId: user.id, }, + include: inc, })) }) diff --git a/test/auth.ts b/test/auth.ts index 7f4de34..f3e0e2e 100644 --- a/test/auth.ts +++ b/test/auth.ts @@ -31,28 +31,31 @@ const users = [ async function setupUsers() { await Promise.all(users.map(async (userData) => { + if (userData.session?.id) return userData.session = await createUser(userData, 'github', {}) })) } -async function userFromEmail(email: string) { +function userFromEmail(email: string) { const user = users.find(user => user.email === email) if (!user) throw new Error('User not found') return user } async function actingAs(email: string) { - const user = users.find(user => user.email === email) + const user = userFromEmail(email) if (!user) throw new Error('User not found') const { data } = await $fetch('/api/test/session', { method: 'POST', body: { id: user?.session?.id.toString(), hash: user?.session?.hash } }) user.cookie = data.cookie[1].split(';')[0] as string const get = (url: string) => $fetch(url, { headers: { cookie: user.cookie as string } }) const post = (url: string, params: object) => $fetch(url, { method: 'POST', body: params, headers: { cookie: user.cookie as string } }) - return { get, post } + const put = (url: string, params: object) => $fetch(url, { method: 'PUT', body: params, headers: { cookie: user.cookie as string } }) + return { get, post, put } } export { users, setupUsers, actingAs, + userFromEmail, } diff --git a/test/pen.test.ts b/test/pen.test.ts index a34cb20..324b108 100644 --- a/test/pen.test.ts +++ b/test/pen.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, expect, it } from 'vitest' import { setup } from '@nuxt/test-utils' -import { actingAs, setupUsers, users } from './auth' +import { actingAs, setupUsers, userFromEmail } from './auth' import { setupConfig } from './config' import { penColors } from '~/utils/shared' import type { MetapiResponse } from '~/types/metapi' @@ -16,7 +16,25 @@ describe('/api/pen', async () => { const { post } = await actingAs('test@test.com') const { data: pen } = await post('/api/pen', { color: penColors[0] }) as MetapiResponse expect(pen.color).toBe(penColors[0]) - expect(pen.userId).toBe(users[0]?.session.id.toString()) pens.push(pen) + expect(pen.userId).toBe(userFromEmail('test@test.com').session.id.toString()) + }) + + it(' get /api/pen - list all pens', async () => { + const { get } = await actingAs('test@test.com') + const response = await get('/api/pen') as MetapiResponse + expect(response.data[0]).toStrictEqual(pens[0]) + }) + + it ('get /api/pen/:id - get a pen', async () => { + const { get } = await actingAs('test@test.com') + const response = await get(`/api/pen/${pens[0]?.id}`) as MetapiResponse + expect(response.data).toStrictEqual(pens[0]) + }) + + it ('put /api/pen/:id - update a pen', async () => { + const { put } = await actingAs('test@test.com') + const { data: pen } = await put(`/api/pen/${pens[0]?.id}`, { color: penColors[1] }) as MetapiResponse + expect(pen.color).toBe(penColors[1]) }) }) From 1a0c33bc7a4543809070aed85ac8a07a7022b52e Mon Sep 17 00:00:00 2001 From: kevin olson Date: Wed, 21 Aug 2024 05:35:23 -0500 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=92=9A=20need=20to=20figure=20out=20h?= =?UTF-8?q?ow=20to=20run=20setupUsers=20only=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/types/models.d.ts | 5 +++++ server/utils/user.ts | 1 + test/auth.ts | 12 ++++++------ test/pen.test.ts | 4 ++-- test/user.test.ts | 4 ++-- vitest.config.ts | 8 -------- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/types/models.d.ts b/app/types/models.d.ts index f4022e6..808abd3 100644 --- a/app/types/models.d.ts +++ b/app/types/models.d.ts @@ -25,6 +25,11 @@ export interface User extends PrismaUser { hash: string } +export interface UserSession extends User { + session: User + cookie?: string +} + export interface Shot extends PrismaShot { cartridge?: Cartridge } diff --git a/server/utils/user.ts b/server/utils/user.ts index 81d5e5b..cd48b7f 100644 --- a/server/utils/user.ts +++ b/server/utils/user.ts @@ -34,6 +34,7 @@ export const createUser = async (info: UserInfo, provider: string, oauthPayload: }, }, }) as unknown as User + else await prisma.provider.upsert({ where: { diff --git a/test/auth.ts b/test/auth.ts index f3e0e2e..1755d55 100644 --- a/test/auth.ts +++ b/test/auth.ts @@ -1,11 +1,11 @@ import { $fetch } from '@nuxt/test-utils/e2e' -import type { User } from '~/types/models' +import type { User, UserSession } from '~/types/models' import { createUser } from '~~/server/utils/user' const users = [ { session: {} as User, - cookie: '', + cookie: undefined, email: 'test@test.com', name: 'Test User', avatar: 'https://avatars.githubusercontent.com/u/31337?v=4', @@ -27,12 +27,12 @@ const users = [ }, }, }, -] +] as UserSession[] async function setupUsers() { - await Promise.all(users.map(async (userData) => { - if (userData.session?.id) return - userData.session = await createUser(userData, 'github', {}) + await Promise.all(users.map(async (userData, index) => { + if (users[index].session?.id) return + users[index].session = await createUser(userData, 'github', {}) })) } diff --git a/test/pen.test.ts b/test/pen.test.ts index 324b108..2c30a65 100644 --- a/test/pen.test.ts +++ b/test/pen.test.ts @@ -1,4 +1,4 @@ -import { beforeAll, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { setup } from '@nuxt/test-utils' import { actingAs, setupUsers, userFromEmail } from './auth' import { setupConfig } from './config' @@ -6,8 +6,8 @@ import { penColors } from '~/utils/shared' import type { MetapiResponse } from '~/types/metapi' import type { Pen } from '~/types/models' -beforeAll(setupUsers) describe('/api/pen', async () => { + await setupUsers() await setup(setupConfig()) const pens: Pen[] = [] diff --git a/test/user.test.ts b/test/user.test.ts index b8080b1..6504974 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -1,12 +1,12 @@ -import { beforeAll, describe, expect, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { $fetch, setup } from '@nuxt/test-utils/e2e' import { actingAs, setupUsers, users } from './auth' import { setupConfig } from './config' import type { MetapiResponse } from '~/types/metapi' import type { User } from '~/types/models' -beforeAll(setupUsers) describe('/api/me and /api/user', async () => { + await setupUsers() await setup(setupConfig()) it('/api/me should 401', async () => { try { await $fetch('/api/me') } diff --git a/vitest.config.ts b/vitest.config.ts index 8017b9d..095c157 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -15,14 +15,6 @@ export default defineVitestConfig({ coverage: { reporter: ['text', 'json-summary', 'json'], reportOnFailure: true, - /* - thresholds: { - lines: 60, - branches: 60, - functions: 60, - statements: 60, - }, - */ }, }, }) From a0700db54cb97c1dfec39b78e167247f7156c85a Mon Sep 17 00:00:00 2001 From: kevin olson Date: Wed, 21 Aug 2024 17:26:58 -0500 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=92=9A=20create=20users=20when=20we?= =?UTF-8?q?=20need=20em=20-=20why=20complicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/types/models.d.ts | 3 ++- app/types/oauth.d.ts | 2 +- test/auth.ts | 22 ++++++++-------------- test/pen.test.ts | 5 ++--- test/user.test.ts | 3 +-- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/app/types/models.d.ts b/app/types/models.d.ts index 808abd3..a8fd7ba 100644 --- a/app/types/models.d.ts +++ b/app/types/models.d.ts @@ -1,5 +1,6 @@ import type { Token as PrismaToken, User as PrismaUser } from '@prisma/client' import type { Cartridge as PrismaCartridge, Pen as PrismaPen, Shot as PrismaShot } from '@prisma/client' +import type { UserInfo } from '~/types/oauth' export interface Token extends PrismaToken { client: import('ua-parser-js').IResult @@ -25,7 +26,7 @@ export interface User extends PrismaUser { hash: string } -export interface UserSession extends User { +export interface UserSession extends UserInfo { session: User cookie?: string } diff --git a/app/types/oauth.d.ts b/app/types/oauth.d.ts index 1b98803..0a62897 100644 --- a/app/types/oauth.d.ts +++ b/app/types/oauth.d.ts @@ -1,7 +1,7 @@ export interface UserInfo { + email: string name: string avatar: string - email: string payload?: UserPayload } diff --git a/test/auth.ts b/test/auth.ts index 1755d55..3151edc 100644 --- a/test/auth.ts +++ b/test/auth.ts @@ -29,22 +29,17 @@ const users = [ }, ] as UserSession[] -async function setupUsers() { - await Promise.all(users.map(async (userData, index) => { - if (users[index].session?.id) return - users[index].session = await createUser(userData, 'github', {}) - })) -} - -function userFromEmail(email: string) { - const user = users.find(user => user.email === email) - if (!user) throw new Error('User not found') - return user +async function userFromEmail(email: string): Promise { + const index = users.findIndex(user => user.email === email) + if (index === undefined) throw new Error(`User not found: ${email} - ${index}`) + if (!users[index]) throw new Error('User not found') + if (!users[index].session?.id) + users[index].session = await createUser(users[index], 'github', {}) + return users[index] } async function actingAs(email: string) { - const user = userFromEmail(email) - if (!user) throw new Error('User not found') + const user = await userFromEmail(email) const { data } = await $fetch('/api/test/session', { method: 'POST', body: { id: user?.session?.id.toString(), hash: user?.session?.hash } }) user.cookie = data.cookie[1].split(';')[0] as string const get = (url: string) => $fetch(url, { headers: { cookie: user.cookie as string } }) @@ -55,7 +50,6 @@ async function actingAs(email: string) { export { users, - setupUsers, actingAs, userFromEmail, } diff --git a/test/pen.test.ts b/test/pen.test.ts index 2c30a65..f5ad913 100644 --- a/test/pen.test.ts +++ b/test/pen.test.ts @@ -1,13 +1,12 @@ import { describe, expect, it } from 'vitest' import { setup } from '@nuxt/test-utils' -import { actingAs, setupUsers, userFromEmail } from './auth' +import { actingAs, userFromEmail } from './auth' import { setupConfig } from './config' import { penColors } from '~/utils/shared' import type { MetapiResponse } from '~/types/metapi' import type { Pen } from '~/types/models' describe('/api/pen', async () => { - await setupUsers() await setup(setupConfig()) const pens: Pen[] = [] @@ -17,7 +16,7 @@ describe('/api/pen', async () => { const { data: pen } = await post('/api/pen', { color: penColors[0] }) as MetapiResponse expect(pen.color).toBe(penColors[0]) pens.push(pen) - expect(pen.userId).toBe(userFromEmail('test@test.com').session.id.toString()) + expect(pen.userId).toBe((await userFromEmail('test@test.com')).session.id.toString()) }) it(' get /api/pen - list all pens', async () => { diff --git a/test/user.test.ts b/test/user.test.ts index 6504974..9bf4a13 100644 --- a/test/user.test.ts +++ b/test/user.test.ts @@ -1,12 +1,11 @@ import { describe, expect, it } from 'vitest' import { $fetch, setup } from '@nuxt/test-utils/e2e' -import { actingAs, setupUsers, users } from './auth' +import { actingAs, users } from './auth' import { setupConfig } from './config' import type { MetapiResponse } from '~/types/metapi' import type { User } from '~/types/models' describe('/api/me and /api/user', async () => { - await setupUsers() await setup(setupConfig()) it('/api/me should 401', async () => { try { await $fetch('/api/me') }