Skip to content

Commit

Permalink
wip: test api route handler
Browse files Browse the repository at this point in the history
  • Loading branch information
mrkarimoff committed Jan 30, 2024
1 parent 8a85637 commit f01c5af
Show file tree
Hide file tree
Showing 7 changed files with 1,733 additions and 150 deletions.
107 changes: 107 additions & 0 deletions apps/analytics/app/api/event/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import insertEvent from "~/db/insertEvent";
import { Event, POST } from "./route";

import { createMocks as _createMocks } from "node-mocks-http";
import type { RequestOptions, ResponseOptions } from "node-mocks-http";
import { NextRequest, NextResponse } from "next/server";

const createMocks = _createMocks as (
reqOptions?: RequestOptions,
resOptions?: ResponseOptions
// @ts-ignore: Fixing this: https://github.com/howardabrams/node-mocks-http/issues/245
) => Mocks<NextRequest, NextResponse>;

jest.mock("~/db/insertEvent", () => jest.fn());

describe("/api/event", () => {
test("should insert event to the database", async () => {
const eventData = {
type: "AppSetup",
metadata: {
details: "testing details",
path: "testing path",
},
installationId: "21321546453213123",
timestamp: new Date(),
isocode: "US",
};

const mockInsertEvent = async (eventData: Event) => {
return { data: eventData, error: null };
};

const { req } = createMocks({
method: "POST",
body: eventData,
});

const response = await POST(req);
expect(mockInsertEvent).toHaveBeenCalledWith(eventData);
expect(response.status).toBe(200);
expect(response.headers).toEqual({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
expect(await response.json()).toEqual(eventData);
}),
test("should return 401 if event is invalid", async () => {
const eventData = {
type: "InvalidEvent",
metadata: {
details: "testing details",
path: "testing path",
},
timestamp: new Date(),
isocode: "US",
};

const { req } = createMocks({
method: "POST",
body: eventData,
});

const response = await POST(req as any);
expect(insertEvent).not.toHaveBeenCalled();
expect(response.status).toBe(401);
expect(response.headers).toEqual({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
expect(await response.json()).toEqual({ error: "Invalid event" });
}),
test("should return 500 if there is an error inserting the event to the database", async () => {
const eventData = {
type: "AppSetup",
metadata: {
details: "testing details",
path: "testing path",
},
installationId: "21321546453213123",
timestamp: new Date(),
isocode: "US",
};

const mockInsertEvent = async (eventData: Event) => {
return { data: null, error: "Error inserting events" };
};

const { req } = createMocks({
method: "POST",
body: eventData,
});

const response = await POST(req as any);
expect(mockInsertEvent).toHaveBeenCalledWith(eventData);
expect(response.status).toBe(500);
expect(response.headers).toEqual({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
expect(await response.json()).toEqual({
error: "Error inserting events",
});
});
});
59 changes: 41 additions & 18 deletions apps/analytics/app/api/event/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ import { type DispatchableAnalyticsEvent } from "@codaco/analytics";
import { NextRequest, NextResponse } from "next/server";
import { type EventInsertType } from "~/db/db";
import insertEvent from "~/db/insertEvent";
import z from "zod";

export const eventTypes = [
"AppSetup",
"ProtocolInstalled",
"InterviewStarted",
"InterviewCompleted",
"InterviewCompleted",
"DataExported",
"Error",
] as const;

export type EventType = (typeof eventTypes)[number];

export const EventsSchema = z.object({
type: z.enum(eventTypes),
installationId: z.string(),
timestamp: z.date(),
isocode: z.string().optional(),
message: z.string().optional(),
name: z.string().optional(),
stack: z.string().optional(),
metadata: z
.object({
details: z.string(),
path: z.string(),
})
.optional(),
});

export type Event = z.infer<typeof EventsSchema>;

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
Expand All @@ -12,27 +43,19 @@ const corsHeaders = {
export const runtime = "edge";

export async function POST(request: NextRequest) {
const event = (await request.json()) as DispatchableAnalyticsEvent;

let generalEvent: EventInsertType = {
type: event.type,
metadata: event.metadata,
timestamp: new Date(event.timestamp),
installationId: event.installationId,
isocode: event.geolocation?.countryCode,
};

if (event.type === "Error") {
generalEvent = {
...generalEvent,
message: event.error.message,
name: event.error.name,
stack: event.error.stack,
};
const event = (await request.json()) as unknown;

const parsedEvent = EventsSchema.safeParse(event);

if (parsedEvent.success === false) {
return NextResponse.json(
{ error: "Invalid event" },
{ status: 401, headers: corsHeaders }
);
}

try {
const result = await insertEvent(generalEvent);
const result = await insertEvent(parsedEvent.data);
if (result.error) throw new Error(result.error);

return NextResponse.json({ event }, { status: 200, headers: corsHeaders });
Expand Down
2 changes: 2 additions & 0 deletions apps/analytics/db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ import { eventsTable } from "./schema";
export const db = drizzle(sql, { schema });

export type EventInsertType = typeof eventsTable.$inferInsert;

// derive a zod schema from the table schema and use it inside analytics package
8 changes: 8 additions & 0 deletions apps/analytics/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
moduleNameMapper: {
"^~/(.*)$": "<rootDir>/$1",
},
};
10 changes: 8 additions & 2 deletions apps/analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,24 @@
"react": "^18",
"react-dom": "^18",
"tailwind-merge": "^2.0.0",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@faker-js/faker": "^8.2.0",
"@next/eslint-plugin-next": "^13.4.19",
"@types/node": "^20",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/node": "^20.5.2",
"@types/papaparse": "^5.3.14",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"drizzle-kit": "^0.20.13",
"eslint-config-custom": "workspace:*",
"jest": "^29.7.0",
"node-mocks-http": "^1.14.1",
"tailwindcss": "^3.3.0",
"ts-jest": "^29.1.2",
"tsconfig": "workspace:*",
"typescript": "^5.2.2"
}
Expand Down
14 changes: 2 additions & 12 deletions apps/analytics/scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,15 @@ import dotenv from "dotenv";
dotenv.config();

import { faker } from "@faker-js/faker";
import { db } from "~/db/db";
import { eventTypes } from "~/app/api/event/route";
import { db, type EventInsertType } from "~/db/db";
import { eventsTable } from "~/db/schema";
import { type EventInsertType } from "~/db/db";

let installationIds: string[] = [];
for (let i = 0; i < 20; i++) {
installationIds.push(faker.string.uuid());
}

const eventTypes = [
"AppSetup",
"ProtocolInstalled",
"InterviewStarted",
"InterviewCompleted",
"InterviewCompleted",
"DataExported",
"Error",
];

async function seedEvents() {
console.info("Starting to seed events");

Expand Down
Loading

0 comments on commit f01c5af

Please sign in to comment.