Add tests for invite router (#1456)

*  Add test for invite router

*  Add cleanup to invite-router tests, add tests for creation and deletion of invites

* ♻️ Fix typo

* ♻️ Remove nullish for limit of invite router all procedure
This commit is contained in:
Meier Lukas
2023-10-18 20:41:59 +02:00
committed by GitHub
parent d60cd2ed8d
commit 0a98be4553
5 changed files with 249 additions and 14 deletions

View File

@@ -29,7 +29,7 @@ export const DeleteInviteModal = ({ id, innerProps }: ContextModalProps<{ tokenI
<Button <Button
onClick={async () => { onClick={async () => {
await deleteAsync({ await deleteAsync({
tokenId: innerProps.tokenId, id: innerProps.tokenId,
}); });
}} }}
disabled={isLoading} disabled={isLoading}

View File

@@ -1,24 +1,25 @@
import { createTRPCRouter } from '~/server/api/trpc';
import { appRouter } from './routers/app'; import { appRouter } from './routers/app';
import { boardRouter } from './routers/board'; import { boardRouter } from './routers/board';
import { calendarRouter } from './routers/calendar'; import { calendarRouter } from './routers/calendar';
import { configRouter } from './routers/config'; import { configRouter } from './routers/config';
import { dashDotRouter } from './routers/dash-dot'; import { dashDotRouter } from './routers/dash-dot';
import { dnsHoleRouter } from './routers/dns-hole/router'; import { dnsHoleRouter } from './routers/dns-hole/router';
import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download'; import { downloadRouter } from './routers/download';
import { iconRouter } from './routers/icon'; import { iconRouter } from './routers/icon';
import { inviteRouter } from './routers/invite'; import { inviteRouter } from './routers/invite/invite-router';
import { mediaRequestsRouter } from './routers/media-request'; import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server'; import { mediaServerRouter } from './routers/media-server';
import { notebookRouter } from './routers/notebook';
import { overseerrRouter } from './routers/overseerr'; import { overseerrRouter } from './routers/overseerr';
import { passwordRouter } from './routers/password'; import { passwordRouter } from './routers/password';
import { rssRouter } from './routers/rss'; import { rssRouter } from './routers/rss';
import { timezoneRouter } from './routers/timezone';
import { usenetRouter } from './routers/usenet/router';
import { userRouter } from './routers/user'; import { userRouter } from './routers/user';
import { weatherRouter } from './routers/weather'; import { weatherRouter } from './routers/weather';
import { dockerRouter } from './routers/docker/router';
import { usenetRouter } from './routers/usenet/router';
import { createTRPCRouter } from '~/server/api/trpc';
import { timezoneRouter } from './routers/timezone';
import { notebookRouter } from './routers/notebook';
/** /**
* This is the primary router for your server. * This is the primary router for your server.
@@ -45,7 +46,7 @@ export const rootRouter = createTRPCRouter({
invites: inviteRouter, invites: inviteRouter,
boards: boardRouter, boards: boardRouter,
password: passwordRouter, password: passwordRouter,
notebook: notebookRouter notebook: notebookRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -0,0 +1,234 @@
import dayjs from 'dayjs';
import { eq } from 'drizzle-orm';
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
import { Session, User } from 'next-auth';
import { v4 } from 'uuid';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { db, sqlite } from '~/server/db';
import { invites, users } from '~/server/db/schema';
import { rootRouter } from '../../root';
import { inviteRouter } from './invite-router';
const sessionMock = (user?: Partial<User>): Session => ({
user: {
id: user?.id ?? '123',
name: user?.name ?? 'John Doe',
language: user?.language ?? 'en',
colorScheme: user?.colorScheme ?? 'dark',
autoFocusSearch: user?.autoFocusSearch ?? false,
isAdmin: user?.isAdmin ?? false,
},
expires: '1',
});
describe('invite router', () => {
beforeEach(async () => {
vi.stubEnv('DATABASE_URL', ':memory:');
migrate(db, { migrationsFolder: './drizzle' });
});
afterEach(async () => {
// Delete all data from database
const tables = sqlite.prepare(`select name from sqlite_master where type = 'table';`).all() as {
name: string;
}[];
for (const table of tables) {
if (table.name.startsWith('__')) continue;
sqlite.prepare(`delete from ${table.name}`).run();
}
});
test('Admin procedure check', async () => {
const caller = inviteRouter.createCaller({
session: sessionMock(),
cookies: {},
});
await expect(
(async () => {
await caller.all({ page: 0 });
})()
).rejects.toThrowError('FORBIDDEN');
});
test('All invites should return invites from database', async () => {
const expireDate = new Date(2021, 1, 1);
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
await db.insert(invites).values({
id: '123',
createdById: '123',
expires: expireDate,
token: 'token',
});
await db.insert(invites).values({
id: v4(),
createdById: '123',
expires: expireDate,
token: v4(),
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
const result = await caller.all({ page: 0, limit: 1 });
expect(result.countPages).toEqual(2);
expect(result.invites[0]).toStrictEqual({
id: '123',
creator: 'John Doe',
expires: expireDate,
});
});
test('Create should create new invite in database with expiration in 6 minutes', async () => {
const expireDate = dayjs().add(6, 'minutes').set('milliseconds', 0).toDate();
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
const result = await caller.create({ expiration: expireDate });
const dbInvite = await db.query.invites.findFirst({
where: eq(invites.id, result.id),
});
expect(result.id).toBeDefined();
expect(result.expires).toEqual(expireDate);
expect(result.token).toHaveLength(40);
expect(dbInvite).toStrictEqual({
id: result.id,
createdById: '123',
expires: expireDate,
token: result.token,
});
});
test('Create should create new invite in database with expiration in 30 days', async () => {
const expireDate = dayjs().add(6, 'months').add(-1, 'minute').set('milliseconds', 0).toDate();
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
const result = await caller.create({ expiration: expireDate });
const dbInvite = await db.query.invites.findFirst({
where: eq(invites.id, result.id),
});
expect(result.id).toBeDefined();
expect(result.expires).toEqual(expireDate);
expect(result.token).toHaveLength(40);
expect(dbInvite).toStrictEqual({
id: result.id,
createdById: '123',
expires: expireDate,
token: result.token,
});
});
test('Create should throw too_small with expiration in 4 minutes', async () => {
const expireDate = dayjs().add(4, 'minutes').set('milliseconds', 0).toDate();
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
const act = async () => await caller.create({ expiration: expireDate });
expect(act).rejects.toThrowError(/"code": "too_small"/);
});
test('Create should throw too_big with expiration in 7 months', async () => {
const expireDate = dayjs().add(7, 'months').set('milliseconds', 0).toDate();
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
const act = async () => await caller.create({ expiration: expireDate });
expect(act).rejects.toThrowError(/"code": "too_big"/);
});
test('Delete should delete invite from database', async () => {
const inviteId = '123';
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
await db.insert(invites).values({
id: inviteId,
createdById: '123',
expires: new Date(2023, 1, 1),
token: 'token',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
await caller.delete({ id: inviteId });
const dbInvite = await db.query.invites.findFirst({
where: eq(invites.id, inviteId),
});
expect(dbInvite).toBeUndefined();
});
test('Delete should delete invite from database', async () => {
const inviteId = '123';
await db.insert(users).values({
id: '123',
name: 'John Doe',
});
await db.insert(invites).values({
id: inviteId,
createdById: '123',
expires: new Date(2023, 1, 1),
token: 'token',
});
const caller = inviteRouter.createCaller({
session: sessionMock({ isAdmin: true }),
cookies: {},
});
await caller.delete({ id: inviteId });
const dbInvite = await db.query.invites.findFirst({
where: eq(invites.id, inviteId),
});
expect(dbInvite).toBeUndefined();
});
});

View File

@@ -5,18 +5,18 @@ import { z } from 'zod';
import { db } from '~/server/db'; import { db } from '~/server/db';
import { invites } from '~/server/db/schema'; import { invites } from '~/server/db/schema';
import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc'; import { adminProcedure, createTRPCRouter, publicProcedure } from '../../trpc';
export const inviteRouter = createTRPCRouter({ export const inviteRouter = createTRPCRouter({
all: adminProcedure all: adminProcedure
.input( .input(
z.object({ z.object({
limit: z.number().min(1).max(100).nullish().default(10), limit: z.number().min(1).max(100).default(10),
page: z.number().min(0), page: z.number().min(0),
}) })
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const limit = input.limit ?? 50; const limit = input.limit;
const dbInvites = await db.query.invites.findMany({ const dbInvites = await db.query.invites.findMany({
limit: limit, limit: limit,
offset: limit * input.page, offset: limit * input.page,
@@ -67,7 +67,7 @@ export const inviteRouter = createTRPCRouter({
expires: inviteToInsert.expires, expires: inviteToInsert.expires,
}; };
}), }),
delete: adminProcedure.input(z.object({ tokenId: z.string() })).mutation(async ({ input }) => { delete: adminProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => {
await db.delete(invites).where(eq(invites.id, input.tokenId)); await db.delete(invites).where(eq(invites.id, input.id));
}), }),
}); });

View File

@@ -4,6 +4,6 @@ import { env } from '~/env';
import * as schema from './schema'; import * as schema from './schema';
const sqlite = new Database(env.DATABASE_URL?.replace('file:', '')); export const sqlite = new Database(env.DATABASE_URL?.replace('file:', ''));
export const db = drizzle(sqlite, { schema }); export const db = drizzle(sqlite, { schema });