From 12dbb0ad032f2ca4e68b874227d169ae359c00b8 Mon Sep 17 00:00:00 2001 From: Agusti Fernandez Pardo Date: Wed, 20 Apr 2022 16:06:37 +0200 Subject: [PATCH] feat: final securing of all endpoints, update readme --- README.md | 14 +++ tests/api-keys/[id]/api-key.id.delete.test.ts | 86 --------------- tests/api-keys/[id]/api-key.id.edit.test.ts | 103 ------------------ tests/api-keys/[id]/api-key.id.index.test.ts | 83 -------------- tests/api-keys/api-key.index.test.ts | 39 ------- tests/api-keys/api-key.new.test.ts | 77 ------------- 6 files changed, 14 insertions(+), 388 deletions(-) delete mode 100644 tests/api-keys/[id]/api-key.id.delete.test.ts delete mode 100644 tests/api-keys/[id]/api-key.id.edit.test.ts delete mode 100644 tests/api-keys/[id]/api-key.id.index.test.ts delete mode 100644 tests/api-keys/api-key.index.test.ts delete mode 100644 tests/api-keys/api-key.new.test.ts diff --git a/README.md b/README.md index a79bb64f37..5aed701aff 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,17 @@ This is the public REST api for cal.com. It exposes CRUD Endpoints of all our mo - Prisma - No tRPC (for now) We hook directly into prisma client, but probably should look into adding a new @calcom/trpc package that adds pagination and such stuff and can be shared between webapp and API. + +## How to run it + +First clone the main repo with --recursive-submodules flag. This will clone our monorepo, and all the private git submodules within it. +`` +Be sure to be authenticated in gh-cli or via PAT in your shell, as this will clone private repos that requires this (website, api) +`` + +`cp .env.example .env` + +`yarn workspace @calcom/api dev ## API Authentication (API Keys) The API requires a valid apiKey query param to be passed: @@ -133,6 +144,9 @@ mostly because they're deemed too sensitive can be revisited if needed. - [] Api Keys - [] Credentials +- [] Webhooks +- [] ResetPasswordRequest +- [] VerificationToken ## Documentation (OpenAPI) diff --git a/tests/api-keys/[id]/api-key.id.delete.test.ts b/tests/api-keys/[id]/api-key.id.delete.test.ts deleted file mode 100644 index d2abfecb7a..0000000000 --- a/tests/api-keys/[id]/api-key.id.delete.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import handleDeleteApiKey from "@api/api-keys/[id]/delete"; -import { createMocks } from "node-mocks-http"; - -import prisma from "@calcom/prisma"; - -describe("DELETE /api/api-keys/[id]/delete with valid id as string returns an apiKey", () => { - it("returns a message with the specified apiKeys", async () => { - const apiKey = await prisma.apiKey.findFirst(); - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: apiKey?.id, - }, - }); - // const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id} }); - await handleDeleteApiKey(req, res); - expect(res._getStatusCode()).toBe(204); - expect(JSON.parse(res._getData())).toEqual({ - message: `api-key with id: ${apiKey?.id} deleted successfully`, - }); - }); -}); - -// 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("DELETE /api/api-keys/[id]/delete errors if query id is number, requires a string", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "DELETE", - query: { - id: 1, // passing query as a number, which should fail as nextjs will try to parse it as a string - }, - }); - await handleDeleteApiKey(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("DELETE /api/api-keys/[id]/delete 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: "DELETE", - query: { - id: "0", // There's no apiKey with id 0 - }, - }); - await handleDeleteApiKey(req, res); - - expect(res._getStatusCode()).toBe(404); - expect(JSON.parse(res._getData())).toStrictEqual({ - error: { - clientVersion: "3.10.0", - code: "P2025", - meta: { - cause: "Record to delete does not exist.", - }, - }, - message: "Resource with id:0 was not found", - }); - }); -}); - -describe("POST /api/api-keys/[id]/delete fails, only DELETE allowed", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - query: { - id: "1", - }, - }); - await handleDeleteApiKey(req, res); - - expect(res._getStatusCode()).toBe(405); - expect(JSON.parse(res._getData())).toStrictEqual({ message: "Only DELETE Method allowed" }); - }); -}); diff --git a/tests/api-keys/[id]/api-key.id.edit.test.ts b/tests/api-keys/[id]/api-key.id.edit.test.ts deleted file mode 100644 index 1007181179..0000000000 --- a/tests/api-keys/[id]/api-key.id.edit.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -import handleapiKeyEdit from "@api/api-keys/[id]/edit"; -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 handleapiKeyEdit(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 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", - }); - }); -}); - -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 handleapiKeyEdit(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/[id]/edit fails, only PATCH allowed", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "POST", // This POST method is not allowed - query: { - id: "cl16zg6860000wwylnsgva00b", - }, - body: { - note: "Updated note", - }, - }); - await handleapiKeyEdit(req, res); - - expect(res._getStatusCode()).toBe(405); - expect(JSON.parse(res._getData())).toStrictEqual({ - message: "Only PATCH Method allowed for updating API keys", - }); - }); -}); diff --git a/tests/api-keys/[id]/api-key.id.index.test.ts b/tests/api-keys/[id]/api-key.id.index.test.ts deleted file mode 100644 index 8d3c2655af..0000000000 --- a/tests/api-keys/[id]/api-key.id.index.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import handleApiKey from "@api/api-keys/[id]"; -import { createMocks } from "node-mocks-http"; - -import prisma from "@calcom/prisma"; - -import { stringifyISODate } from "@lib/utils/stringifyISODate"; - -describe("GET /api/api-keys/[id] with valid id as string returns an apiKey", () => { - it("returns a message with the specified apiKeys", async () => { - const { req, res } = createMocks({ - method: "GET", - query: { - id: "cl16zg6860000wwylnsgva00b", - }, - }); - const apiKey = await prisma.apiKey.findUnique({ where: { id: req.query.id } }); - await handleApiKey(req, res); - - expect(res._getStatusCode()).toBe(200); - expect(JSON.parse(res._getData())).toEqual({ - data: { - ...apiKey, - createdAt: stringifyISODate(apiKey?.createdAt), - expiresAt: stringifyISODate(apiKey?.expiresAt), - }, - }); - }); -}); - -// 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 handleApiKey(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/[id] 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 - query: { - id: "1", - }, - }); - await handleApiKey(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.index.test.ts b/tests/api-keys/api-key.index.test.ts deleted file mode 100644 index 957b749b88..0000000000 --- a/tests/api-keys/api-key.index.test.ts +++ /dev/null @@ -1,39 +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 } }))); - }); -}); - -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 deleted file mode 100644 index 339b4c146b..0000000000 --- a/tests/api-keys/api-key.new.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -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: [], - }, - ]); - }); -});