kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ci): add api task integration tests

Tin 831effdc 8559555e

+275 -41
+3
apps/api/vitest.integration.config.ts
··· 5 5 environment: "node", 6 6 include: ["../../tests/api-integration/**/*.test.ts"], 7 7 setupFiles: ["../../tests/api-integration/setup.ts"], 8 + fileParallelism: false, 9 + maxWorkers: 1, 10 + minWorkers: 1, 8 11 coverage: { 9 12 enabled: false, 10 13 },
+99
tests/api-integration/helpers/fixtures.ts
··· 1 + import { randomUUID } from "node:crypto"; 2 + import db, { schema } from "../../../apps/api/src/database"; 3 + 4 + export type SeededMemberContext = { 5 + user: typeof schema.userTable.$inferSelect; 6 + workspace: typeof schema.workspaceTable.$inferSelect; 7 + }; 8 + 9 + export async function createWorkspaceMember( 10 + overrides?: Partial<{ 11 + userName: string; 12 + workspaceName: string; 13 + role: string; 14 + }>, 15 + ): Promise<SeededMemberContext> { 16 + const userId = `user-${randomUUID()}`; 17 + const workspaceId = `workspace-${randomUUID()}`; 18 + 19 + const [user] = await db 20 + .insert(schema.userTable) 21 + .values({ 22 + id: userId, 23 + email: `${userId}@example.com`, 24 + emailVerified: true, 25 + name: overrides?.userName || "Integration Test User", 26 + }) 27 + .returning(); 28 + 29 + const [workspace] = await db 30 + .insert(schema.workspaceTable) 31 + .values({ 32 + id: workspaceId, 33 + createdAt: new Date(), 34 + name: overrides?.workspaceName || "Integration Test Workspace", 35 + slug: `workspace-${randomUUID()}`, 36 + }) 37 + .returning(); 38 + 39 + await db.insert(schema.workspaceUserTable).values({ 40 + workspaceId: workspace.id, 41 + userId: user.id, 42 + role: overrides?.role || "owner", 43 + joinedAt: new Date(), 44 + }); 45 + 46 + return { user, workspace }; 47 + } 48 + 49 + export async function createProjectFixture({ 50 + workspaceId, 51 + name = "Integration Project", 52 + icon = "Folder", 53 + slug = `project-${randomUUID()}`, 54 + }: { 55 + workspaceId: string; 56 + name?: string; 57 + icon?: string; 58 + slug?: string; 59 + }) { 60 + const [project] = await db 61 + .insert(schema.projectTable) 62 + .values({ 63 + workspaceId, 64 + name, 65 + icon, 66 + slug, 67 + }) 68 + .returning(); 69 + 70 + const [todoColumn] = await db 71 + .insert(schema.columnTable) 72 + .values({ 73 + projectId: project.id, 74 + name: "To Do", 75 + slug: "to-do", 76 + position: 0, 77 + isFinal: false, 78 + }) 79 + .returning(); 80 + 81 + const [doneColumn] = await db 82 + .insert(schema.columnTable) 83 + .values({ 84 + projectId: project.id, 85 + name: "Done", 86 + slug: "done", 87 + position: 1, 88 + isFinal: true, 89 + }) 90 + .returning(); 91 + 92 + return { 93 + project, 94 + columns: { 95 + todo: todoColumn, 96 + done: doneColumn, 97 + }, 98 + }; 99 + }
+2 -41
tests/api-integration/project.test.ts
··· 1 - import { randomUUID } from "node:crypto"; 2 1 import { eq } from "drizzle-orm"; 3 2 import { beforeEach, describe, expect, it } from "vitest"; 4 3 import db, { schema } from "../../apps/api/src/database"; 5 4 import { createApp } from "../../apps/api/src/index"; 6 5 import { mockAnonymousSession, mockAuthenticatedSession } from "./helpers/auth"; 7 6 import { resetTestDatabase } from "./helpers/database"; 8 - 9 - type SeededMemberContext = { 10 - user: typeof schema.userTable.$inferSelect; 11 - workspace: typeof schema.workspaceTable.$inferSelect; 12 - }; 13 - 14 - async function createWorkspaceMember(): Promise<SeededMemberContext> { 15 - const userId = `user-${randomUUID()}`; 16 - const workspaceId = `workspace-${randomUUID()}`; 17 - 18 - const [user] = await db 19 - .insert(schema.userTable) 20 - .values({ 21 - id: userId, 22 - email: `${userId}@example.com`, 23 - emailVerified: true, 24 - name: "Integration Test User", 25 - }) 26 - .returning(); 27 - 28 - const [workspace] = await db 29 - .insert(schema.workspaceTable) 30 - .values({ 31 - id: workspaceId, 32 - createdAt: new Date(), 33 - name: "Integration Test Workspace", 34 - slug: `workspace-${randomUUID()}`, 35 - }) 36 - .returning(); 37 - 38 - await db.insert(schema.workspaceUserTable).values({ 39 - workspaceId: workspace.id, 40 - userId: user.id, 41 - role: "owner", 42 - joinedAt: new Date(), 43 - }); 44 - 45 - return { user, workspace }; 46 - } 7 + import { createWorkspaceMember } from "./helpers/fixtures"; 47 8 48 9 describe("API integration: project creation", () => { 49 10 beforeEach(async () => { ··· 133 94 134 95 it("rejects project creation for users outside the workspace", async () => { 135 96 const member = await createWorkspaceMember(); 136 - const outsiderId = `user-${randomUUID()}`; 97 + const outsiderId = "user-outsider"; 137 98 138 99 const [outsider] = await db 139 100 .insert(schema.userTable)
+171
tests/api-integration/task.test.ts
··· 1 + import { randomUUID } from "node:crypto"; 2 + import { and, eq } from "drizzle-orm"; 3 + import { beforeEach, describe, expect, it } from "vitest"; 4 + import db, { schema } from "../../apps/api/src/database"; 5 + import { createApp } from "../../apps/api/src/index"; 6 + import { mockAnonymousSession, mockAuthenticatedSession } from "./helpers/auth"; 7 + import { resetTestDatabase } from "./helpers/database"; 8 + import { 9 + createProjectFixture, 10 + createWorkspaceMember, 11 + } from "./helpers/fixtures"; 12 + 13 + describe("API integration: task creation", () => { 14 + beforeEach(async () => { 15 + await resetTestDatabase(); 16 + }); 17 + 18 + it("rejects unauthenticated task creation requests", async () => { 19 + const member = await createWorkspaceMember(); 20 + const { project } = await createProjectFixture({ 21 + workspaceId: member.workspace.id, 22 + }); 23 + 24 + mockAnonymousSession(); 25 + const { app } = createApp(); 26 + 27 + const response = await app.request(`/api/task/${project.id}`, { 28 + method: "POST", 29 + headers: { 30 + "content-type": "application/json", 31 + }, 32 + body: JSON.stringify({ 33 + title: "Unauthorized task", 34 + description: "Should not be created", 35 + priority: "low", 36 + status: "to-do", 37 + }), 38 + }); 39 + 40 + expect(response.status).toBe(401); 41 + await expect(response.text()).resolves.toBe("Unauthorized"); 42 + }); 43 + 44 + it("creates a task with the matching column, assignee, and next number", async () => { 45 + const member = await createWorkspaceMember(); 46 + const { project, columns } = await createProjectFixture({ 47 + workspaceId: member.workspace.id, 48 + name: "Delivery", 49 + slug: "delivery", 50 + }); 51 + 52 + await db.insert(schema.taskTable).values({ 53 + projectId: project.id, 54 + userId: member.user.id, 55 + title: "Existing task", 56 + description: "Already there", 57 + status: "to-do", 58 + columnId: columns.todo.id, 59 + priority: "medium", 60 + number: 1, 61 + position: 1, 62 + }); 63 + 64 + mockAuthenticatedSession(member.user); 65 + const { app } = createApp(); 66 + 67 + const response = await app.request(`/api/task/${project.id}`, { 68 + method: "POST", 69 + headers: { 70 + "content-type": "application/json", 71 + }, 72 + body: JSON.stringify({ 73 + title: "Ship integration flow", 74 + description: "Cover the first create-task path", 75 + priority: "high", 76 + status: "to-do", 77 + userId: member.user.id, 78 + }), 79 + }); 80 + 81 + expect(response.status).toBe(200); 82 + const payload = (await response.json()) as { 83 + id: string; 84 + projectId: string; 85 + title: string; 86 + description: string; 87 + priority: string; 88 + status: string; 89 + userId: string | null; 90 + number: number | null; 91 + position: number | null; 92 + assigneeName?: string; 93 + }; 94 + 95 + expect(payload).toMatchObject({ 96 + projectId: project.id, 97 + title: "Ship integration flow", 98 + description: "Cover the first create-task path", 99 + priority: "high", 100 + status: "to-do", 101 + userId: member.user.id, 102 + number: 2, 103 + position: 2, 104 + assigneeName: member.user.name, 105 + }); 106 + 107 + const persistedTask = await db.query.taskTable.findFirst({ 108 + where: eq(schema.taskTable.id, payload.id), 109 + }); 110 + 111 + expect(persistedTask).toMatchObject({ 112 + id: payload.id, 113 + projectId: project.id, 114 + columnId: columns.todo.id, 115 + userId: member.user.id, 116 + title: "Ship integration flow", 117 + priority: "high", 118 + status: "to-do", 119 + number: 2, 120 + position: 2, 121 + }); 122 + }); 123 + 124 + it("rejects task creation for users outside the project workspace", async () => { 125 + const member = await createWorkspaceMember(); 126 + const outsiderId = `user-${randomUUID()}`; 127 + const { project } = await createProjectFixture({ 128 + workspaceId: member.workspace.id, 129 + }); 130 + 131 + const [outsider] = await db 132 + .insert(schema.userTable) 133 + .values({ 134 + id: outsiderId, 135 + email: `${outsiderId}@example.com`, 136 + emailVerified: true, 137 + name: "Task Outsider", 138 + }) 139 + .returning(); 140 + 141 + mockAuthenticatedSession(outsider); 142 + const { app } = createApp(); 143 + 144 + const response = await app.request(`/api/task/${project.id}`, { 145 + method: "POST", 146 + headers: { 147 + "content-type": "application/json", 148 + }, 149 + body: JSON.stringify({ 150 + title: "Forbidden task", 151 + description: "Should not be created", 152 + priority: "low", 153 + status: "to-do", 154 + }), 155 + }); 156 + 157 + expect(response.status).toBe(403); 158 + await expect(response.text()).resolves.toBe( 159 + "You don't have access to this workspace", 160 + ); 161 + 162 + const persistedTask = await db.query.taskTable.findFirst({ 163 + where: and( 164 + eq(schema.taskTable.projectId, project.id), 165 + eq(schema.taskTable.title, "Forbidden task"), 166 + ), 167 + }); 168 + 169 + expect(persistedTask).toBeUndefined(); 170 + }); 171 + });