diff --git a/README.md b/README.md index f8834ae82d..dc598bf1e8 100644 --- a/README.md +++ b/README.md @@ -41,4 +41,47 @@ Since this will only support an API, we redirect the requests to root to the /ap We also added a redirect for future-proofing API versioning when we might need it, without having to resort to dirty hacks like a v1/v2 folders with lots of duplicated code, instead we redirect /api/v*/:rest to /api/:rest?version=* -The priority is the booking-related API routes so people can build their own booking flow, then event type management routes, then availability management routes etc \ No newline at end of file +The priority is the booking-related API routes so people can build their own booking flow, then event type management routes, then availability management routes etc + + +How to add a new model or endpoint + +Basically there's three places of the codebase you need to think about for each feature. + +/pages/api/ +- This is the most important one, and where your endpoint will live. You will leverage nextjs dynamic routes and expose one file for each endpoint you want to support ideally. + +## How the codebase is organized. + +## The example resource -model- and it's endpoints + +### `pages/api/endpoint/` + +GET pages/api/endpoint/index.ts - Read All of your resource +POST pages/api/endpoint/new.ts - Create new resource + +### `pages/api/endpoint/[id]/` + +GET pages/api/endpoint/[id]/index.ts - Read All of your resource +PATCH pages/api/endpoint/[id]/edit.ts - Create new resource +DELETE pages/api/endpoint/[id]/delete.ts - Create new resource + + +## `/tests/` + +This is where all your endpoint's tests live, we mock prisma calls. We aim for at least 50% global coverage. Test each of your endpoints. + +### `/tests/endpoint/` + +/tests/endpoint/resource.index.test.ts - Test for your pages/api/endpoint/index.ts file +tests/endpoint/resource.new.test.ts - Create new resource + +### `/tests/endpoint/[id]/` + +`/tests/endpoint/[id]/resource.index.test.ts` - Read All of your resource +`/tests/endpoint/[id]/resource.edit.test.ts` - Create new resource +`/tests/endpoint/[id]/resource.delete.test.ts` - Create new resource + +## `/lib/validations/yourEndpoint.ts` + +- This is where our model validations, live, we try to make a 1:1 for db models, and also extract out any re-usable code into the /lib/validations/shared/ sub-folder. diff --git a/__tests__/api-keys/api-key.index.test.ts b/__tests__/api-keys/api-key.index.test.ts deleted file mode 100644 index 5c54a88d8d..0000000000 --- a/__tests__/api-keys/api-key.index.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import handleApiKeys from "@api/api-keys"; -import { createMocks } from "node-mocks-http"; - -import prisma from "@calcom/prisma"; -import {stringifyISODate} from "@lib/utils/stringifyISODate"; - -describe("GET /api/api-keys without any params", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "GET", - query: {}, - }); - let apiKeys = await prisma.apiKey.findMany(); - await handleApiKeys(req, res); - - expect(res._getStatusCode()).toBe(200); - apiKeys = apiKeys.map(apiKey => (apiKey = {...apiKey, createdAt: stringifyISODate(apiKey?.createdAt), expiresAt: stringifyISODate(apiKey?.expiresAt)})); - expect(JSON.parse(res._getData())).toStrictEqual(JSON.parse(JSON.stringify({ data: {...apiKeys} }))); - }); -}); - -// This can never happen under our normal nextjs setup where query is always a string | string[]. -// But seemed a good example for testing an error validation -// describe("GET /api/api-keys/[id] errors if query id is number, requires a string", () => { -// it("returns a message with the specified apiKeys", async () => { -// const { req, res } = createMocks({ -// method: "GET", -// query: { -// id: 1, // passing query as a number, which should fail as nextjs will try to parse it as a string -// }, -// }); -// await handleApiKeys(req, res); - -// expect(res._getStatusCode()).toBe(400); -// expect(JSON.parse(res._getData())).toStrictEqual([ -// { -// code: "invalid_type", -// expected: "string", -// received: "number", -// path: ["id"], -// message: "Expected string, received number", -// }, -// ]); -// }); -// }); - -// describe("GET /api/api-keys/[id] an id not present in db like 0, throws 404 not found", () => { -// it("returns a message with the specified apiKeys", async () => { -// const { req, res } = createMocks({ -// method: "GET", -// query: { -// id: "0", // There's no apiKey with id 0 -// }, -// }); -// await handleApiKey(req, res); - -// expect(res._getStatusCode()).toBe(404); -// expect(JSON.parse(res._getData())).toStrictEqual({ message: "API key was not found" }); -// }); -// }); - -describe("POST /api/api-keys/ fails, only GET allowed", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - }); - await handleApiKeys(req, res); - expect(res._getStatusCode()).toBe(405); - expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only GET Method allowed" }); - }); -}); - -// describe("GET /api/api-keys/ without prisma", () => { -// it("returns a message with the specified apiKeys", async () => { -// prisma.$disconnect().then(); - -// const { req, res } = createMocks({ -// method: "GET", -// }); -// await handleApiKeys(req, res); - -// expect(res._getStatusCode()).toBe(400); -// expect(JSON.parse(res._getData())).toStrictEqual({ message: "API key was not found" }); -// }); -// }); - - diff --git a/__tests__/api-keys/api-key.new.test.ts b/__tests__/api-keys/api-key.new.test.ts deleted file mode 100644 index e5763f83d5..0000000000 --- a/__tests__/api-keys/api-key.new.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import handleNewApiKey from "@api/api-keys/new"; -import { createMocks } from "node-mocks-http"; - -import prisma from "@calcom/prisma"; -// import {stringifyISODate} from "@lib/utils/stringifyISODate"; - -// describe("PATCH /api/api-keys/[id]/edit with valid id and body with note", () => { -// it("returns a 200 and the updated apiKey note", async () => { -// const { req, res } = createMocks({ -// method: "PATCH", -// query: { -// id: "cl16zg6860000wwylnsgva00b", -// }, -// body: { -// note: "Updated note", -// }, -// }); -// const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id } }); -// await handleNewApiKey(req, res); - -// expect(res._getStatusCode()).toBe(200); -// expect(JSON.parse(res._getData())).toEqual({ data: {...apiKey, createdAt: stringifyISODate(apiKey?.createdAt), expiresAt: stringifyISODate(apiKey?.expiresAt)} }); -// }); -// }); - -// // describe("PATCH /api/api-keys/[id]/edit with invalid id returns 404", () => { -// // it("returns a message with the specified apiKeys", async () => { -// // const { req, res } = createMocks({ -// // method: "PATCH", -// // query: { -// // id: "cl16zg6860000wwylnsgva00a", -// // }, -// // body: { -// // note: "Updated note", -// // }, -// // }); -// // const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id } }); -// // await handleNewApiKey(req, res); - -// // expect(res._getStatusCode()).toBe(404); -// // if (apiKey) apiKey.note = "Updated note"; -// // expect(JSON.parse(res._getData())).toStrictEqual({ "error": { -// // "clientVersion": "3.10.0", -// // "code": "P2025", -// // "meta": { -// // "cause": "Record to update not found.", -// // }, -// // }, -// // "message": "apiKey with ID cl16zg6860000wwylnsgva00a not found and wasn't updated", }); -// // }); -// // }); - -// describe("PATCH /api/api-keys/[id]/edit with valid id and no body returns 200 with an apiKey with no note and default expireAt", () => { -// it("returns a message with the specified apiKeys", async () => { -// const apiKey = await prisma.apiKey.create({data:{} }); -// const { req, res } = createMocks({ -// method: "PATCH", -// query: { -// id: apiKey?.id, -// }, -// }); -// await handleNewApiKey(req, res); - -// expect(apiKey?.note).toBeNull(); -// expect(res._getStatusCode()).toBe(200); -// expect(JSON.parse(res._getData())).toEqual({ data: {...apiKey, createdAt: stringifyISODate(apiKey?.createdAt), expiresAt: stringifyISODate(apiKey?.expiresAt)} }); - -// }); -// }); - -describe("POST /api/api-keys/new with a note", () => { - it("returns a 201, and the created api key", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - body: { - note: "Updated note", - }, - }); - await handleNewApiKey(req, res); - - expect(res._getStatusCode()).toBe(201); - expect(JSON.parse(res._getData()).data.note).toStrictEqual("Updated note"); - }); -}); - - -describe("POST /api/api-keys/new with a slug param", () => { - it("returns error 400, and the details about invalid slug body param", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - body: { - note: "Updated note", - slug: "slug", - }, - }); - await handleNewApiKey(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toStrictEqual( - [{"code": "unrecognized_keys", "keys": ["slug"], "message": "Unrecognized key(s) in object: 'slug'", "path": []}] - ); - }); -}); - - -describe("GET /api/api-keys/new fails, only POST allowed", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "GET", // This POST method is not allowed - }); - await handleNewApiKey(req, res); - - expect(res._getStatusCode()).toBe(405); - expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); - }); -}); - -describe("GET /api/api-keys/new fails, only POST allowed", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - body: { - fail: true - // note: '123', - // slug: 12, - }, - }); - await handleNewApiKey(req, res); - - expect(res._getStatusCode()).toBe(400); - expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); - }); -}); \ No newline at end of file diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts new file mode 100644 index 0000000000..fb50c9c9ad --- /dev/null +++ b/lib/validations/booking.ts @@ -0,0 +1,65 @@ +import { withValidation } from "next-validations"; +import { z } from "zod"; + +const schemaBooking = z + .object({ + uid: z.string().min(3), + title: z.string().min(3), + description: z.string().min(3).optional(), + startTime: z.date().or(z.string()), + endTime: z.date(), + location: z.string().min(3).optional(), + createdAt: z.date().or(z.string()), + updatedAt: z.date(), + confirmed: z.boolean().default(true), + rejected: z.boolean().default(false), + paid: z.boolean().default(false), + + // bufferTime: z.number().default(0), + // // attendees: z.array((schemaSchedule)).optional(), + + // startTime: z.string().min(3), + // endTime: z.string().min(3), + // email: z.string().email(), // max is a full day. + // emailVerified: z.date().optional(), + // password: z.string().optional(), + // bio: z.string().min(3).optional(), + // avatar: z.string().optional(), + // timeZone: z.string().default("Europe/London"), + // weekStart: z.string().default("Sunday"), + // bufferTime: z.number().default(0), + // theme: z.string().optional(), + // trialEndsAt: z.date().optional(), + // eventTypes: z.array((schemaEventType)).optional(), + // // credentials: z.array((schemaCredentials)).optional(), + // // teams: z.array((schemaMembership)).optional(), + // // bookings: z.array((schemaBooking)).optional(), + // // schedules: z.array((schemaSchedule)).optional(), + // defaultScheduleId: z.number().optional(), + // // selectedCalendars: z.array((schemaSelectedCalendar)).optional(), + // completedOnboarding: z.boolean().default(false), + // locale: z.string().optional(), + // timeFormat: z.number().optional().default(12), + // twoFactorEnabled: z.boolean().default(false), + // twoFactorSecret: z.string().optional(), + // identityProvider: z.enum(["CAL", "SAML", "GOOGLE"]).optional().default("CAL"), + // identityProviderId: z.string().optional(), + // // availavility: z.array((schemaAvailavility)).optional(), + // invitedTo: z.number().optional(), + // plan: z.enum(['FREE', 'TRIAL', 'PRO']).default("TRIAL"), + // // webhooks: z.array((schemaWebhook)).optional(), + // brandColor: z.string().default("#292929"), + // darkBrandColor: z.string().default("#fafafa"), + // // destinationCalendar: z.instanceof(schemaEventType).optional(), // FIXME: instanceof doesnt work here + // away: z.boolean().default(false), + // metadata: z.object({}).optional(), + // verified: z.boolean().default(false), + }) + .strict(); // Adding strict so that we can disallow passing in extra fields +const withValidBooking = withValidation({ + schema: schemaBooking, + type: "Zod", + mode: "body", +}); + +export { schemaBooking, withValidBooking }; diff --git a/lib/validations/queryIdString.ts b/lib/validations/shared/queryIdString.ts similarity index 100% rename from lib/validations/queryIdString.ts rename to lib/validations/shared/queryIdString.ts diff --git a/lib/validations/queryIdTransformParseInt.ts b/lib/validations/shared/queryIdTransformParseInt.ts similarity index 100% rename from lib/validations/queryIdTransformParseInt.ts rename to lib/validations/shared/queryIdTransformParseInt.ts diff --git a/pages/api/api-keys/[id]/delete.ts b/pages/api/api-keys/[id]/delete.ts index 83b75ffd18..dbd64971f9 100644 --- a/pages/api/api-keys/[id]/delete.ts +++ b/pages/api/api-keys/[id]/delete.ts @@ -1,7 +1,7 @@ import prisma from "@calcom/prisma"; import { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/queryIdString"; +import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; type ResponseData = { message?: string; diff --git a/pages/api/api-keys/[id]/edit.ts b/pages/api/api-keys/[id]/edit.ts index 445cb429b4..117e494eb9 100644 --- a/pages/api/api-keys/[id]/edit.ts +++ b/pages/api/api-keys/[id]/edit.ts @@ -4,7 +4,7 @@ import { ApiKey } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaApiKey, withValidApiKey } from "@lib/validations/apiKey"; -import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/queryIdString"; +import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; type ResponseData = { data?: ApiKey; @@ -17,8 +17,7 @@ export async function editApiKey(req: NextApiRequest, res: NextApiResponse { res.status(404).json({ message: `apiKey with ID ${safeQuery.data.id} not found and wasn't updated`, error }) }); - } } else { // Reject any other HTTP method than POST res.status(405).json({ message: "Only PATCH Method allowed for updating API keys" }); diff --git a/pages/api/api-keys/[id]/index.ts b/pages/api/api-keys/[id]/index.ts index e7af7b15d0..169e1c121d 100644 --- a/pages/api/api-keys/[id]/index.ts +++ b/pages/api/api-keys/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { ApiKey } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/queryIdString"; +import { schemaQueryIdAsString, withValidQueryIdString } from "@lib/validations/shared/queryIdString"; type ResponseData = { data?: ApiKey; @@ -13,14 +13,11 @@ type ResponseData = { export async function apiKey(req: NextApiRequest, res: NextApiResponse) { const { query, method } = req; - if (method === "GET") { const safe = await schemaQueryIdAsString.safeParse(query); - if (safe.success) { + if (method === "GET" && safe.success) { const apiKey = await prisma.apiKey.findUnique({ where: { id: safe.data.id } }); - - if (apiKey) res.status(200).json({ data: apiKey }); if (!apiKey) res.status(404).json({ message: "API key was not found" }); - } + else res.status(200).json({ data: apiKey }); } else { // Reject any other HTTP method than POST res.status(405).json({ message: "Only GET Method allowed" }); diff --git a/pages/api/api-keys/new.ts b/pages/api/api-keys/new.ts index 500c9219c3..4a88584d67 100644 --- a/pages/api/api-keys/new.ts +++ b/pages/api/api-keys/new.ts @@ -13,9 +13,8 @@ type ResponseData = { async function createApiKey(req: NextApiRequest, res: NextApiResponse) { const { body, method } = req; - if (method === "POST") { - const safe = schemaApiKey.safeParse(body); - if (safe.success && safe.data) { + const safe = schemaApiKey.safeParse(body); + if (method === "POST" && safe.success) { const apiKey = await prisma.apiKey .create({ data: { @@ -24,17 +23,9 @@ async function createApiKey(req: NextApiRequest, res: NextApiResponse res.status(201).json({ data: apiKey })) - // .catch((error) => { - // res.status(400).json({ message: "Could not create apiKey", error: error }) - // } - // ) - } + } else { + res.status(404).json({message: "API Key not created"}); + } } else { // Reject any other HTTP method than POST res.status(405).json({ error: "Only POST Method allowed" }); diff --git a/pages/api/bookings/[id]/delete.ts b/pages/api/bookings/[id]/delete.ts new file mode 100644 index 0000000000..1196e505eb --- /dev/null +++ b/pages/api/bookings/[id]/delete.ts @@ -0,0 +1,36 @@ +import prisma from "@calcom/prisma"; + +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + + +type ResponseData = { + message?: string; + error?: unknown; +}; + +export async function booking(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryId.safeParse(query); + if (safe.success) { + if (method === "DELETE") { + // DELETE WILL DELETE THE EVENT TYPE + prisma.booking + .delete({ where: { id: safe.data.id } }) + .then(() => { + // We only remove the booking type from the database if there's an existing resource. + res.status(200).json({ message: `booking-type with id: ${safe.data.id} deleted successfully` }); + }) + .catch((error) => { + // This catches the error thrown by prisma.booking.delete() if the resource is not found. + res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error }); + }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only DELETE Method allowed in /booking-types/[id]/delete endpoint" }); + } + } +} + +export default withValidQueryIdTransformParseInt(booking); diff --git a/pages/api/bookings/[id]/edit.ts b/pages/api/bookings/[id]/edit.ts new file mode 100644 index 0000000000..1fc77f9c25 --- /dev/null +++ b/pages/api/bookings/[id]/edit.ts @@ -0,0 +1,37 @@ +import prisma from "@calcom/prisma"; + +import { Booking } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaBooking, withValidBooking } from "@lib/validations/booking"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Booking; + message?: string; + error?: unknown; +}; + +export async function editBooking(req: NextApiRequest, res: NextApiResponse) { + const { query, body, method } = req; + const safeQuery = await schemaQueryId.safeParse(query); + const safeBody = await schemaBooking.safeParse(body); + + if (method === "PATCH") { + if (safeQuery.success && safeBody.success) { + await prisma.booking.update({ + where: { id: safeQuery.data.id }, + data: safeBody.data, + }).then(booking => { + res.status(200).json({ data: booking }); + }).catch(error => { + res.status(404).json({ message: `Event type with ID ${safeQuery.data.id} not found and wasn't updated`, error }) + }); + } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only PATCH Method allowed for updating bookings" }); + } +} + +export default withValidQueryIdTransformParseInt(withValidBooking(editBooking)); diff --git a/pages/api/bookings/[id]/index.ts b/pages/api/bookings/[id]/index.ts new file mode 100644 index 0000000000..11c84ba5c8 --- /dev/null +++ b/pages/api/bookings/[id]/index.ts @@ -0,0 +1,31 @@ +import prisma from "@calcom/prisma"; + +import { Booking } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; + +type ResponseData = { + data?: Booking; + message?: string; + error?: unknown; +}; + +export async function booking(req: NextApiRequest, res: NextApiResponse) { + const { query, method } = req; + const safe = await schemaQueryId.safeParse(query); + if (safe.success) { + if (method === "GET") { + const booking = await prisma.booking.findUnique({ where: { id: safe.data.id } }); + + if (booking) res.status(200).json({ data: booking }); + if (!booking) res.status(404).json({ message: "Event type not found" }); + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); + } + } +} + + +export default withValidQueryIdTransformParseInt(booking); diff --git a/pages/api/bookings/index.ts b/pages/api/bookings/index.ts new file mode 100644 index 0000000000..20b9a41a7e --- /dev/null +++ b/pages/api/bookings/index.ts @@ -0,0 +1,19 @@ +import prisma from "@calcom/prisma"; + +import { Booking } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +type ResponseData = { + data?: Booking[]; + error?: unknown; +}; + +export default async function booking(req: NextApiRequest, res: NextApiResponse) { + try { + const bookings = await prisma.booking.findMany(); + res.status(200).json({ data: { ...bookings } }); + } catch (error) { + // FIXME: Add zod for validation/error handling + res.status(400).json({ error: error }); + } +} diff --git a/pages/api/bookings/new.ts b/pages/api/bookings/new.ts new file mode 100644 index 0000000000..296875608f --- /dev/null +++ b/pages/api/bookings/new.ts @@ -0,0 +1,30 @@ +import prisma from "@calcom/prisma"; + +import { Booking } from "@calcom/prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { schemaBooking, withValidBooking } from "@lib/validations/booking"; + +type ResponseData = { + data?: Booking; + message?: string; + error?: string; +}; + +async function createBooking(req: NextApiRequest, res: NextApiResponse) { + const { body, method } = req; + if (method === "POST") { + const safe = schemaBooking.safeParse(body); + if (safe.success && safe.data) { + await prisma.booking + .create({ data: safe.data }) + .then((booking) => res.status(201).json({ data: booking })) + .catch((error) => res.status(400).json({ message: "Could not create booking type", error: error })); + } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ error: "Only POST Method allowed" }); + } +} + +export default withValidBooking(createBooking); diff --git a/pages/api/event-types/[id]/delete.ts b/pages/api/event-types/[id]/delete.ts index 52f2cd74b7..f6341df094 100644 --- a/pages/api/event-types/[id]/delete.ts +++ b/pages/api/event-types/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { diff --git a/pages/api/event-types/[id]/edit.ts b/pages/api/event-types/[id]/edit.ts index 0194678226..276b0ba34a 100644 --- a/pages/api/event-types/[id]/edit.ts +++ b/pages/api/event-types/[id]/edit.ts @@ -4,7 +4,7 @@ import { EventType } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaEventType, withValidEventType } from "@lib/validations/eventType"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: EventType; diff --git a/pages/api/event-types/[id]/index.ts b/pages/api/event-types/[id]/index.ts index 1c4bc056fb..44eaa46c77 100644 --- a/pages/api/event-types/[id]/index.ts +++ b/pages/api/event-types/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { EventType } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: EventType; diff --git a/pages/api/event-types/index.ts b/pages/api/event-types/index.ts index 2ea925d76b..534ebf828f 100644 --- a/pages/api/event-types/index.ts +++ b/pages/api/event-types/index.ts @@ -5,15 +5,17 @@ import type { NextApiRequest, NextApiResponse } from "next"; type ResponseData = { data?: EventType[]; + message?: string; error?: unknown; }; export default async function eventType(req: NextApiRequest, res: NextApiResponse) { - try { + const { method } = req; + if (method === "GET") { const eventTypes = await prisma.eventType.findMany(); res.status(200).json({ data: { ...eventTypes } }); - } catch (error) { - // FIXME: Add zod for validation/error handling - res.status(400).json({ error: error }); - } + } else { + // Reject any other HTTP method than POST + res.status(405).json({ message: "Only GET Method allowed" }); + } } diff --git a/pages/api/teams/[id]/delete.ts b/pages/api/teams/[id]/delete.ts index aee327a197..26a5df4919 100644 --- a/pages/api/teams/[id]/delete.ts +++ b/pages/api/teams/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { diff --git a/pages/api/teams/[id]/edit.ts b/pages/api/teams/[id]/edit.ts index daaf27657a..eb3636aac4 100644 --- a/pages/api/teams/[id]/edit.ts +++ b/pages/api/teams/[id]/edit.ts @@ -4,7 +4,7 @@ import { Team } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaTeam, withValidTeam } from "@lib/validations/team"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Team; diff --git a/pages/api/teams/[id]/index.ts b/pages/api/teams/[id]/index.ts index 50a600ca58..d21c33e266 100644 --- a/pages/api/teams/[id]/index.ts +++ b/pages/api/teams/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { Team } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: Team; diff --git a/pages/api/users/[id]/delete.ts b/pages/api/users/[id]/delete.ts index 3c35fd5844..860b684a64 100644 --- a/pages/api/users/[id]/delete.ts +++ b/pages/api/users/[id]/delete.ts @@ -2,7 +2,7 @@ import prisma from "@calcom/prisma"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { diff --git a/pages/api/users/[id]/edit.ts b/pages/api/users/[id]/edit.ts index 877cd7f35a..7b26f306f8 100644 --- a/pages/api/users/[id]/edit.ts +++ b/pages/api/users/[id]/edit.ts @@ -4,7 +4,7 @@ import { User } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { schemaUser, withValidUser } from "@lib/validations/user"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: User; diff --git a/pages/api/users/[id]/index.ts b/pages/api/users/[id]/index.ts index 7b6c991f2b..a5fcf369fa 100644 --- a/pages/api/users/[id]/index.ts +++ b/pages/api/users/[id]/index.ts @@ -3,7 +3,7 @@ import prisma from "@calcom/prisma"; import { User } from "@calcom/prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; -import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/queryIdTransformParseInt"; +import { schemaQueryId, withValidQueryIdTransformParseInt } from "@lib/validations/shared/queryIdTransformParseInt"; type ResponseData = { data?: User; diff --git a/__tests__/api-keys/[id]/api-key.id.delete.test.ts b/tests/api-keys/[id]/api-key.id.delete.test.ts similarity index 100% rename from __tests__/api-keys/[id]/api-key.id.delete.test.ts rename to tests/api-keys/[id]/api-key.id.delete.test.ts diff --git a/__tests__/api-keys/[id]/api-key.id.edit.test.ts b/tests/api-keys/[id]/api-key.id.edit.test.ts similarity index 69% rename from __tests__/api-keys/[id]/api-key.id.edit.test.ts rename to tests/api-keys/[id]/api-key.id.edit.test.ts index 3d4b4f35dc..c0d2e3631c 100644 --- a/__tests__/api-keys/[id]/api-key.id.edit.test.ts +++ b/tests/api-keys/[id]/api-key.id.edit.test.ts @@ -23,32 +23,32 @@ describe("PATCH /api/api-keys/[id]/edit with valid id and body with note", () => }); }); -// describe("PATCH /api/api-keys/[id]/edit with invalid id returns 404", () => { -// it("returns a message with the specified apiKeys", async () => { -// const { req, res } = createMocks({ -// method: "PATCH", -// query: { -// id: "cl16zg6860000wwylnsgva00a", -// }, -// body: { -// note: "Updated note", -// }, -// }); -// const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id } }); -// await handleapiKeyEdit(req, res); +describe("PATCH /api/api-keys/[id]/edit with invalid id returns 404", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "PATCH", + query: { + id: "cl16zg6860000wwylnsgva00a", + }, + body: { + note: "Updated note", + }, + }); + const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id } }); + await handleapiKeyEdit(req, res); -// expect(res._getStatusCode()).toBe(404); -// if (apiKey) apiKey.note = "Updated note"; -// expect(JSON.parse(res._getData())).toStrictEqual({ "error": { -// "clientVersion": "3.10.0", -// "code": "P2025", -// "meta": { -// "cause": "Record to update not found.", -// }, -// }, -// "message": "apiKey with ID cl16zg6860000wwylnsgva00a not found and wasn't updated", }); -// }); -// }); + expect(res._getStatusCode()).toBe(404); + if (apiKey) apiKey.note = "Updated note"; + expect(JSON.parse(res._getData())).toStrictEqual({ "error": { + "clientVersion": "3.10.0", + "code": "P2025", + "meta": { + "cause": "Record to update not found.", + }, + }, + "message": "apiKey with ID cl16zg6860000wwylnsgva00a not found and wasn't updated", }); + }); +}); describe("PATCH /api/api-keys/[id]/edit with valid id and no body returns 200 with an apiKey with no note and default expireAt", () => { it("returns a message with the specified apiKeys", async () => { diff --git a/__tests__/api-keys/[id]/api-key.id.index.test.ts b/tests/api-keys/[id]/api-key.id.index.test.ts similarity index 100% rename from __tests__/api-keys/[id]/api-key.id.index.test.ts rename to tests/api-keys/[id]/api-key.id.index.test.ts diff --git a/tests/api-keys/api-key.index.test.ts b/tests/api-keys/api-key.index.test.ts new file mode 100644 index 0000000000..87e1bd24b5 --- /dev/null +++ b/tests/api-keys/api-key.index.test.ts @@ -0,0 +1,31 @@ +import handleApiKeys from "@api/api-keys"; +import { createMocks } from "node-mocks-http"; + +import prisma from "@calcom/prisma"; +import {stringifyISODate} from "@lib/utils/stringifyISODate"; + +describe("GET /api/api-keys without any params", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "GET", + query: {}, + }); + let apiKeys = await prisma.apiKey.findMany(); + await handleApiKeys(req, res); + + expect(res._getStatusCode()).toBe(200); + apiKeys = apiKeys.map(apiKey => (apiKey = {...apiKey, createdAt: stringifyISODate(apiKey?.createdAt), expiresAt: stringifyISODate(apiKey?.expiresAt)})); + expect(JSON.parse(res._getData())).toStrictEqual(JSON.parse(JSON.stringify({ data: {...apiKeys} }))); + }); +}); + +describe("POST /api/api-keys/ fails, only GET allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + }); + await handleApiKeys(req, res); + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only GET Method allowed" }); + }); +}); diff --git a/tests/api-keys/api-key.new.test.ts b/tests/api-keys/api-key.new.test.ts new file mode 100644 index 0000000000..9507bdbd71 --- /dev/null +++ b/tests/api-keys/api-key.new.test.ts @@ -0,0 +1,71 @@ +import handleNewApiKey from "@api/api-keys/new"; +import { createMocks } from "node-mocks-http"; + +describe("POST /api/api-keys/new with a note", () => { + it("returns a 201, and the created api key", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(201); + expect(JSON.parse(res._getData()).data.note).toStrictEqual("Updated note"); + }); +}); + +describe("POST /api/api-keys/new with a slug param", () => { + it("returns error 400, and the details about invalid slug body param", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + slug: "slug", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual( + [{"code": "unrecognized_keys", "keys": ["slug"], "message": "Unrecognized key(s) in object: 'slug'", "path": []}] + ); + }); +}); + + +describe("GET /api/api-keys/new fails, only POST allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "GET", // This POST method is not allowed + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); + }); +}); + + +// FIXME: test 405 when prisma fails look for how to test prisma errors +describe("GET /api/api-keys/new fails, only POST allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + nonExistentParam: true + // note: '123', + // slug: 12, + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual([{ + "code": "unrecognized_keys", + "keys": ["nonExistentParam"], + "message": "Unrecognized key(s) in object: 'nonExistentParam'", "path": [] + }]); + }); +}); \ No newline at end of file diff --git a/__tests__/event-types/[id]/event-type.id.edit.test.ts b/tests/event-types/[id]/event-type.id.edit.test.ts similarity index 100% rename from __tests__/event-types/[id]/event-type.id.edit.test.ts rename to tests/event-types/[id]/event-type.id.edit.test.ts diff --git a/__tests__/event-types/[id]/event-type.id.index.test.ts b/tests/event-types/[id]/event-type.id.index.test.ts similarity index 100% rename from __tests__/event-types/[id]/event-type.id.index.test.ts rename to tests/event-types/[id]/event-type.id.index.test.ts diff --git a/tests/event-types/event-type.index.test.ts b/tests/event-types/event-type.index.test.ts new file mode 100644 index 0000000000..668d88a9d7 --- /dev/null +++ b/tests/event-types/event-type.index.test.ts @@ -0,0 +1,31 @@ +import handleApiKeys from "@api/event-types"; +import { createMocks } from "node-mocks-http"; + +import prisma from "@calcom/prisma"; +// import {stringifyISODate} from "@lib/utils/stringifyISODate"; + +describe("GET /api/event-types without any params", () => { + it("returns a message with the specified eventTypes", async () => { + const { req, res } = createMocks({ + method: "GET", + query: {}, + }); + const eventTypes = await prisma.eventType.findMany(); + await handleApiKeys(req, res); + + expect(res._getStatusCode()).toBe(200); + // eventTypes = eventTypes.map(eventType => (eventType = {...eventType, createdAt: stringifyISODate(eventType?.createdAt), expiresAt: stringifyISODate(eventType?.expiresAt)})); + expect(JSON.parse(res._getData())).toStrictEqual(JSON.parse(JSON.stringify({ data: {...eventTypes} }))); + }); +}); + +describe("POST /api/event-types/ fails, only GET allowed", () => { + it("returns a message with the specified eventTypes", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + }); + await handleApiKeys(req, res); + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only GET Method allowed" }); + }); +}); diff --git a/tests/event-types/event-type.new.test.ts b/tests/event-types/event-type.new.test.ts new file mode 100644 index 0000000000..01dab107a9 --- /dev/null +++ b/tests/event-types/event-type.new.test.ts @@ -0,0 +1,68 @@ +import handleNewApiKey from "@api/api-keys/new"; +import { createMocks } from "node-mocks-http"; + +describe("POST /api/api-keys/new with a note", () => { + it("returns a 201, and the created api key", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(201); + expect(JSON.parse(res._getData()).data.note).toStrictEqual("Updated note"); + }); +}); + + +describe("POST /api/api-keys/new with a slug param", () => { + it("returns error 400, and the details about invalid slug body param", async () => { + const { req, res } = createMocks({ + method: "POST", // This POST method is not allowed + body: { + note: "Updated note", + slug: "slug", + }, + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(400); + expect(JSON.parse(res._getData())).toStrictEqual( + [{"code": "unrecognized_keys", "keys": ["slug"], "message": "Unrecognized key(s) in object: 'slug'", "path": []}] + ); + }); +}); + + +describe("GET /api/api-keys/new fails, only POST allowed", () => { + it("returns a message with the specified apiKeys", async () => { + const { req, res } = createMocks({ + method: "GET", // This POST method is not allowed + }); + await handleNewApiKey(req, res); + + expect(res._getStatusCode()).toBe(405); + expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); + }); +}); + + +// FIXME: test 405 when prisma fails look for how to test prisma errors +// describe("GET /api/api-keys/new fails, only POST allowed", () => { +// it("returns a message with the specified apiKeys", async () => { +// const { req, res } = createMocks({ +// method: "POST", // This POST method is not allowed +// body: { +// fail: true +// // note: '123', +// // slug: 12, +// }, +// }); +// await handleNewApiKey(req, res); + +// expect(res._getStatusCode()).toBe(400); +// expect(JSON.parse(res._getData())).toStrictEqual({ error: "Only POST Method allowed" }); +// }); +// }); \ No newline at end of file diff --git a/__tests__/teams/[id]/team.id.test.edit.ts b/tests/teams/[id]/team.id.test.edit.ts similarity index 100% rename from __tests__/teams/[id]/team.id.test.edit.ts rename to tests/teams/[id]/team.id.test.edit.ts diff --git a/__tests__/teams/[id]/team.id.test.index.ts b/tests/teams/[id]/team.id.test.index.ts similarity index 100% rename from __tests__/teams/[id]/team.id.test.index.ts rename to tests/teams/[id]/team.id.test.index.ts diff --git a/__tests__/users/[id]/user.id.edit.test.ts b/tests/users/[id]/user.id.edit.test.ts similarity index 100% rename from __tests__/users/[id]/user.id.edit.test.ts rename to tests/users/[id]/user.id.edit.test.ts diff --git a/__tests__/users/[id]/user.id.index.test.ts b/tests/users/[id]/user.id.index.test.ts similarity index 100% rename from __tests__/users/[id]/user.id.index.test.ts rename to tests/users/[id]/user.id.index.test.ts