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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- Add unique constraint on (user_id, display_order) to prevent duplicate display orders per user
CREATE UNIQUE INDEX "platform_links_user_id_display_order_key" ON "platform_links"("user_id", "display_order");
1 change: 1 addition & 0 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ model PlatformLink {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
cardLinks CardLink[]

@@unique([userId, displayOrder])
@@map("platform_links")
}

Expand Down
11 changes: 6 additions & 5 deletions apps/backend/src/__tests__/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import Fastify, {
type FastifyInstance,
} from 'fastify';
import {
describe,
it,
Expand All @@ -7,13 +10,11 @@
vi,
} from 'vitest';

import Fastify, {
type FastifyInstance,
} from 'fastify';

import { analyticsRoutes } from '../routes/analytics';

import type { PrismaClient } from '@prisma/client';

import { analyticsRoutes } from '../routes/analytics';

// ─── Shared mock data ────────────────────────────────────────────────────────

Expand All @@ -34,7 +35,7 @@

// ─── App factory ─────────────────────────────────────────────────────────────

let mockJwtVerify = vi.fn();
const mockJwtVerify = vi.fn();

async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({
Expand Down Expand Up @@ -188,7 +189,7 @@

expect(
res.statusCode
).toBe(200);

Check failure on line 192 in apps/backend/src/__tests__/analytics.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/analytics.test.ts > Analytics API > GET /api/analytics/overview > 200 — returns analytics overview

AssertionError: expected 500 to be 200 // Object.is equality - Expected + Received - 200 + 500 ❯ src/__tests__/analytics.test.ts:192:15

const body =
res.json();
Expand Down
88 changes: 88 additions & 0 deletions apps/backend/src/__tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import cookie from '@fastify/cookie';
import jwt from '@fastify/jwt';
import Fastify from 'fastify';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { authRoutes } from '../routes/auth.js';

import type { PrismaClient } from '@prisma/client';

const mockUser = {
id: 'user-123',
username: 'devcard-demo',
};

const prismaMock = {
user: {
findUnique: vi.fn(),
},
};

async function buildApp(nodeEnv: string) {

Check warning on line 21 in apps/backend/src/__tests__/auth.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
vi.stubEnv('NODE_ENV', nodeEnv);

const app = Fastify();
await app.register(jwt, { secret: 'test-secret' });
await app.register(cookie);
app.decorate('prisma', prismaMock as unknown as PrismaClient);
app.decorate('authenticate', async () => {});
await app.register(authRoutes, { prefix: '/auth' });
await app.ready();
return app;
}

describe('auth dev-login route registration', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.unstubAllEnvs();
});

it('registers /auth/dev-login outside production', async () => {
prismaMock.user.findUnique.mockResolvedValue(mockUser);
const app = await buildApp('development');

const res = await app.inject({
method: 'POST',
url: '/auth/dev-login',
});

expect(res.statusCode).toBe(200);
expect(res.json()).toHaveProperty('token');
expect(prismaMock.user.findUnique).toHaveBeenCalledWith({
where: { username: 'devcard-demo' },
});

await app.close();
});

it('does not register /auth/dev-login in production', async () => {
const app = await buildApp('production');

const res = await app.inject({
method: 'POST',
url: '/auth/dev-login',
});

expect(res.statusCode).toBe(404);
expect(prismaMock.user.findUnique).not.toHaveBeenCalled();

await app.close();
});

it('keeps other auth routes registered in production', async () => {
const app = await buildApp('production');

const res = await app.inject({
method: 'POST',
url: '/auth/logout',
});

expect(res.statusCode).toBe(200);
expect(res.json()).toMatchObject({ message: 'Logged out' });

await app.close();
});
});
2 changes: 1 addition & 1 deletion apps/backend/src/__tests__/cards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function wireTransaction(): void {
);
}

async function buildApp():Promise<FastifyInstance> {
async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({ logger: false });
app.decorate('prisma', mockPrisma as unknown as PrismaClient);
app.decorate('authenticate', async (request: any) => {
Expand Down
8 changes: 5 additions & 3 deletions apps/backend/src/__tests__/event.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Fastify, { type FastifyInstance } from 'fastify';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Fastify, { FastifyInstance } from 'fastify';
import { PrismaClient } from '@prisma/client';

import { eventRoutes } from '../routes/event';

import type { PrismaClient } from '@prisma/client';

// ─── Shared mock data ────────────────────────────────────────────────────────

const MOCK_USER_ID = 'user-uuid-001';
Expand Down Expand Up @@ -64,7 +66,7 @@
//
// This mirrors the real app setup without touching a real DB or real JWT keys.

let mockJwtVerify = vi.fn();
const mockJwtVerify = vi.fn();

async function buildApp(): Promise<FastifyInstance> {
const app = Fastify({ logger: false });
Expand Down Expand Up @@ -93,7 +95,7 @@
}

/** Injects a POST /api/events request */
async function createEvent(

Check warning on line 98 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app: FastifyInstance,
body: Record<string, unknown>,
authenticated = true,
Expand Down Expand Up @@ -140,7 +142,7 @@

const res = await createEvent(app, validBody);

expect(res.statusCode).toBe(201);

Check failure on line 145 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 201 — creates event and returns it for authenticated organizer

AssertionError: expected 500 to be 201 // Object.is equality - Expected + Received - 201 + 500 ❯ src/__tests__/event.test.ts:145:30
const body = res.json();
expect(body.slug).toBe('devcard-conf-2025');
expect(body.organizerId).toBe(MOCK_USER_ID);
Expand All @@ -165,34 +167,34 @@

it('400 — rejects missing required fields (no dates, no location)', async () => {
const res = await createEvent(app, { name: 'Hello World' }); // missing dates + location
expect(res.statusCode).toBe(400);

Check failure on line 170 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects missing required fields (no dates, no location)

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:170:30
});

it('400 — rejects missing location', async () => {
const { location: _omit, ...bodyWithoutLocation } = validBody;
const res = await createEvent(app, bodyWithoutLocation);
expect(res.statusCode).toBe(400);

Check failure on line 176 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects missing location

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:176:30
});

it('400 — rejects location shorter than 2 characters', async () => {
const res = await createEvent(app, { ...validBody, location: 'A' });
expect(res.statusCode).toBe(400);

Check failure on line 181 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects location shorter than 2 characters

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:181:30
});

it('400 — rejects location longer than 100 characters', async () => {
const res = await createEvent(app, { ...validBody, location: 'A'.repeat(101) });
expect(res.statusCode).toBe(400);

Check failure on line 186 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects location longer than 100 characters

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:186:30
});

it('400 — rejects event name shorter than 3 characters', async () => {
const res = await createEvent(app, { ...validBody, name: 'Hi' });
expect(res.statusCode).toBe(400);

Check failure on line 191 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects event name shorter than 3 characters

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:191:30
});

it('400 — rejects event name longer than 100 characters', async () => {
const longName = 'A'.repeat(101);
const res = await createEvent(app, { ...validBody, name: longName });
expect(res.statusCode).toBe(400);

Check failure on line 197 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects event name longer than 100 characters

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:197:30
});

it('400 — rejects invalid date format', async () => {
Expand All @@ -200,7 +202,7 @@
...validBody,
startDate: 'not-a-date',
});
expect(res.statusCode).toBe(400);

Check failure on line 205 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 400 — rejects invalid date format

AssertionError: expected 500 to be 400 // Object.is equality - Expected + Received - 400 + 500 ❯ src/__tests__/event.test.ts:205:30
});

it('201 — generates a unique slug when the first candidate is taken', async () => {
Expand All @@ -216,7 +218,7 @@

const res = await createEvent(app, validBody);

expect(res.statusCode).toBe(201);

Check failure on line 221 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

src/__tests__/event.test.ts > Events API > POST /api/events — create event > 201 — generates a unique slug when the first candidate is taken

AssertionError: expected 500 to be 201 // Object.is equality - Expected + Received - 201 + 500 ❯ src/__tests__/event.test.ts:221:30
// create was eventually called with a slug different from the base one
const createdSlug: string = prismaMock.event.create.mock.calls[0][0].data.slug;
expect(createdSlug).toMatch(/^devcard-conf-2025-[a-z0-9]+$/);
Expand Down Expand Up @@ -474,7 +476,7 @@

describe('GET /api/events/:slug/attendees — paginated attendee list', () => {
/** Builds a raw EventAttendee row as Prisma returns it (with nested user) */
function makeAttendeeRow(

Check warning on line 479 in apps/backend/src/__tests__/event.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
profile: typeof MOCK_USER_PROFILE | typeof MOCK_OTHER_USER_PROFILE,
) {
return {
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/__tests__/follow.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Fastify, { FastifyInstance } from 'fastify';
import Fastify, { type FastifyInstance } from 'fastify';
import { describe, expect, it, vi, beforeAll, beforeEach, afterAll } from 'vitest';

import { followRoutes } from '../routes/follow.js';
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/__tests__/oauth-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
* flow so the two records are independent and can never overwrite each other.
*/

import { describe, it, expect, beforeEach, vi } from 'vitest';
import Fastify from 'fastify';
import { describe, it, expect, beforeEach, vi } from 'vitest';

import { connectRoutes } from '../routes/connect.js';
import { followRoutes } from '../routes/follow.js';

import type { PrismaClient } from '@prisma/client';

// ── Mocks ─────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -42,20 +44,20 @@
return Buffer.from(JSON.stringify({ userId, nonce: 'test-nonce' })).toString('base64');
}

function buildConnectApp(mockPrisma: Partial<PrismaClient>) {

Check warning on line 47 in apps/backend/src/__tests__/oauth-scope.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
const app = Fastify({ logger: false });
app.decorate('prisma', mockPrisma as PrismaClient);
app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; });

Check failure on line 50 in apps/backend/src/__tests__/oauth-scope.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Assignment to property of function parameter 'req'
app.register(connectRoutes, { prefix: '/api/connect' });
return app.ready().then(() => app);
}

// ── Follow-route test harness ─────────────────────────────────────────────────

function buildFollowApp(mockPrisma: Partial<PrismaClient>) {

Check warning on line 57 in apps/backend/src/__tests__/oauth-scope.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
const app = Fastify({ logger: false });
app.decorate('prisma', mockPrisma as PrismaClient);
app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; });

Check failure on line 60 in apps/backend/src/__tests__/oauth-scope.test.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Assignment to property of function parameter 'req'
app.register(followRoutes, { prefix: '/api/follow' });
return app.ready().then(() => app);
}
Expand Down
Loading
Loading