diff --git a/app/routes/users/$username/notes/+shared/note-editor.test.tsx b/app/routes/users/$username/notes/+shared/note-editor.test.tsx new file mode 100644 index 0000000..0b677d0 --- /dev/null +++ b/app/routes/users/$username/notes/+shared/note-editor.test.tsx @@ -0,0 +1,21 @@ +/** @vitest-environment jsdom */ +import { expect, test } from 'vitest' +import { NoteEditorSchema } from './note-editor.tsx' + +const title = NoteEditorSchema.shape.title + +test('a title made only of whitespace is rejected', () => { + expect(title.safeParse(' ').success).toBe(false) +}) +test('an empty title is rejected', () => { + expect(title.safeParse('').success).toBe(false) +}) +test('surrounding whitespace is normalized', () => { + expect(title.parse(' Workshop plan ')).toBe('Workshop plan') +}) +test('a normal title is preserved', () => { + expect(title.parse('Workshop plan')).toBe('Workshop plan') +}) +test('a title above the existing maximum stays invalid', () => { + expect(title.safeParse('x'.repeat(101)).success).toBe(false) +}) diff --git a/app/routes/users/$username/notes/+shared/note-editor.server.test.tsx b/app/routes/users/$username/notes/+shared/note-editor.server.test.tsx new file mode 100644 index 0000000..ee3f1e1 --- /dev/null +++ b/app/routes/users/$username/notes/+shared/note-editor.server.test.tsx @@ -0,0 +1,93 @@ +/** @vitest-environment jsdom */ +import setCookieParser from 'set-cookie-parser' +import { expect, test } from 'vitest' +import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts' +import { prisma } from '#app/utils/db.server.ts' +import { authSessionStorage } from '#app/utils/session.server.ts' +import { createUser } from '#tests/db-utils.ts' +import { action } from './note-editor.server.tsx' + +async function fixtures() { + const owner = await prisma.user.create({ data: createUser() }) + const other = await prisma.user.create({ data: createUser() }) + const note = await prisma.note.create({ + data: { + ownerId: owner.id, + title: 'Original title', + content: 'Original content', + }, + }) + const session = await prisma.session.create({ + data: { userId: owner.id, expirationDate: getSessionExpirationDate() }, + }) + const auth = await authSessionStorage.getSession() + auth.set(sessionKey, session.id) + const cookie = setCookieParser.parseString( + await authSessionStorage.commitSession(auth), + ) + return { owner, other, note, cookie: `${cookie.name}=${cookie.value}` } +} +async function submit(cookie: string, fields: Record) { + const body = new FormData() + for (const [key, value] of Object.entries(fields)) body.set(key, value) + return action({ + request: new Request('http://localhost/users/alice/notes/new', { + method: 'POST', + headers: { cookie }, + body, + }), + params: {}, + context: {}, + url: new URL('http://localhost/users/alice/notes/new'), + pattern: '/users/:username/notes/new', + }) +} + +test('an owner can update their own note', async () => { + const f = await fixtures() + const result = await submit(f.cookie, { + id: f.note.id, + title: 'Updated title', + content: 'Updated content', + }) + expect(result).toBeInstanceOf(Response) + expect((result as Response).status).toBe(302) + expect( + (await prisma.note.findUniqueOrThrow({ where: { id: f.note.id } })).title, + ).toBe('Updated title') +}) +test('a forged update cannot change another owner’s note', async () => { + const f = await fixtures() + const target = await prisma.note.create({ + data: { + ownerId: f.other.id, + title: 'Other owner title', + content: 'Keep this content', + }, + }) + const result = await submit(f.cookie, { + id: target.id, + title: 'Forged title', + content: 'Forged content', + }) + expect(result).toMatchObject({ init: { status: 400 } }) + expect( + await prisma.note.findUniqueOrThrow({ where: { id: target.id } }), + ).toMatchObject({ + ownerId: f.other.id, + title: 'Other owner title', + content: 'Keep this content', + }) +}) +test('the server rejects a whitespace title without inserting a note', async () => { + const f = await fixtures() + const before = await prisma.note.count({ where: { ownerId: f.owner.id } }) + const result = await submit(f.cookie, { + title: ' ', + content: 'Meaningful content', + }) + expect(result).toMatchObject({ init: { status: 400 } }) + expect(await prisma.note.count({ where: { ownerId: f.owner.id } })).toBe( + before, + ) +})