Merge pull request #6 from calcom/chore/more-testing

feat: 50% almost code coverage
This commit is contained in:
Agusti Fernandez
2022-03-26 05:30:25 +01:00
committed by GitHub
25 changed files with 393 additions and 72 deletions
@@ -0,0 +1,88 @@
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);
// console.log(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" });
});
});
@@ -4,8 +4,8 @@ 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 updates an apiKey-type", () => {
it("returns a message with the specified apiKeys", async () => {
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: {
@@ -86,7 +86,4 @@ describe("POST /api/api-keys/[id]/edit fails, only PATCH allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
@@ -75,7 +75,4 @@ describe("POST /api/api-keys/[id] fails, only GET allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
+87
View File
@@ -0,0 +1,87 @@
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" });
// });
// });
+133
View File
@@ -0,0 +1,133 @@
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" });
});
});
@@ -89,7 +89,4 @@ describe("POST /api/event-types/[id]/edit fails, only PATCH allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
@@ -74,7 +74,4 @@ describe("POST /api/event-types/[id] fails, only GET allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
+1 -4
View File
@@ -87,7 +87,4 @@ describe("POST /api/teams/[id]/edit fails, only PATCH allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
+1 -4
View File
@@ -74,7 +74,4 @@ describe("POST /api/teams/[id] fails, only GET allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
@@ -89,7 +89,4 @@ describe("POST /api/event-types/[id]/edit fails, only PATCH allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
@@ -75,7 +75,4 @@ describe("POST /api/users/[id] fails, only GET allowed", () => {
});
});
afterAll((done) => {
prisma.$disconnect().then();
done();
});
+11 -4
View File
@@ -10,6 +10,7 @@ const config = {
"collectCoverageFrom": [
"pages/api/**/*.ts"
],
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
@@ -27,8 +28,14 @@ const config = {
],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
coverageThreshold: {
global: {
lines: 50,
functions: 40,
branches: 50,
statements: 50,
},
},
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
@@ -62,10 +69,10 @@ const config = {
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
// The number of seconds after which a test is considered as slow and reported as such in the results.
slowTestThreshold: 1,
slowTestThreshold: 0.1,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
+6
View File
@@ -0,0 +1,6 @@
import prisma from "@calcom/prisma";
afterEach((done) => {
prisma.$disconnect().then();
done();
});
+5
View File
@@ -1,3 +1,8 @@
export const stringifyISODate = (date: Date|undefined): string => {
return `${date?.toISOString()}`
}
export const autoStringifyDateValues = ([key, value]: [string, unknown]): [string, unknown] => {
console.log(key,value)
return [key, typeof value === "object" && value instanceof Date ? stringifyISODate(value) : value]
}
+3
View File
@@ -3,6 +3,9 @@ import { z } from "zod";
const schemaApiKey = z
.object({
// We need to cast the date as strings as when we get it from the json response
// we serve in api it is a string too (JSON doesn't directly support Date types)
createdAt: z.date().optional().or(z.string().optional()),
expiresAt: z.date().optional(), // default is 30 days
note: z.string().min(1).optional(),
})
+1 -1
View File
@@ -12,7 +12,7 @@
"build": "next build",
"lint": "next lint",
"lint-fix": "next lint --fix",
"test": "jest --detectOpenHandles --verbose",
"test": "jest --detectOpenHandles",
"type-check": "tsc --pretty --noEmit",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
"prepare": "husky install",
+6 -8
View File
@@ -11,24 +11,22 @@ type ResponseData = {
export async function apiKey(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
const { query, method } = req;
const safe = await schemaQueryIdAsString.safeParse(query);
if (safe.success) {
if (method === "DELETE") {
if (method === "DELETE" && safe.success) {
// DELETE WILL DELETE THE EVENT TYPE
prisma.apiKey
await prisma.apiKey
.delete({ where: { id: safe.data.id } })
.then(() => {
// We only remove the api key from the database if there's an existing resource.
res.status(200).json({ message: `api-key with id: ${safe.data.id} deleted successfully` });
res.status(204).json({ message: `api-key with id: ${safe.data.id} deleted successfully` });
})
.catch((error) => {
// This catches the error thrown by prisma.apiKey.delete() if the resource is not found.
res.status(400).json({ message: `Resource with id:${safe.data.id} was not found`, error: error });
res.status(404).json({ message: `Resource with id:${safe.data.id} was not found`, error: error });
});
} else {
} else {
// Reject any other HTTP method than POST
res.status(405).json({ message: "Only DELETE Method allowed in /api-keys/[id]/delete endpoint" });
res.status(405).json({ message: "Only DELETE Method allowed" });
}
}
}
export default withValidQueryIdString(apiKey);
+3 -3
View File
@@ -13,18 +13,18 @@ type ResponseData = {
export async function apiKey(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
const { query, method } = req;
if (method === "GET") {
const safe = await schemaQueryIdAsString.safeParse(query);
if (safe.success) {
if (method === "GET") {
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 {
}
} else {
// Reject any other HTTP method than POST
res.status(405).json({ message: "Only GET Method allowed" });
}
}
}
+17 -8
View File
@@ -1,19 +1,28 @@
import prisma from "@calcom/prisma";
import { ApiKey } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next";
import { ApiKey } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
type ResponseData = {
data?: ApiKey[];
error?: unknown;
message?: string;
};
export default async function apiKey(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
try {
export default async function apiKeys(req: NextApiRequest, res: NextApiResponse<ResponseData>) {
const { method } = req;
if (method === "GET") {
// try {
const apiKeys = await prisma.apiKey.findMany({});
res.status(200).json({ data: { ...apiKeys } });
} catch (error) {
// console.log(error);
// FIXME: Add zod for validation/error handling
res.status(400).json({ error: error });
}
// Without any params this never fails. not sure how to force test unavailable prisma query
// } 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" });
}
}
+14 -7
View File
@@ -1,6 +1,6 @@
import prisma from "@calcom/prisma";
import { ApiKey } from "@calcom/prisma/client";
import { ApiKey } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { schemaApiKey, withValidApiKey } from "@lib/validations/apiKey";
@@ -16,17 +16,24 @@ async function createApiKey(req: NextApiRequest, res: NextApiResponse<ResponseDa
if (method === "POST") {
const safe = schemaApiKey.safeParse(body);
if (safe.success && safe.data) {
await prisma.apiKey
const apiKey = await prisma.apiKey
.create({
data: {
...safe.data, user: { connect: { id: 1 } }
}
})
.then((apiKey) => res.status(201).json({ data: apiKey }))
.catch((error) => {
res.status(400).json({ message: "Could not create apiKey", error: error })
}
)
if (apiKey) {
res.status(201).json({ data: apiKey });
} else {
// Reject any other HTTP method than POST
res.status(405).json({ error: "Only POST Method allowed" });
}
// .then((apiKey) => res.status(201).json({ data: apiKey }))
// .catch((error) => {
// res.status(400).json({ message: "Could not create apiKey", error: error })
// }
// )
}
} else {
// Reject any other HTTP method than POST
+2 -1
View File
@@ -1,6 +1,7 @@
import prisma from "@calcom/prisma";
import { EventType } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next";
import { EventType } from "@calcom/prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
type ResponseData = {
data?: EventType[];
+2 -1
View File
@@ -1,6 +1,7 @@
import prisma from "@calcom/prisma";
import { EventType } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next";
import { EventType } from "@calcom/prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { schemaEventType, withValidEventType } from "@lib/validations/eventType";
+2 -1
View File
@@ -1,6 +1,7 @@
import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next";
import { User } from "@calcom/prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
type ResponseData = {
data?: User[];
+2 -1
View File
@@ -1,6 +1,7 @@
import prisma from "@calcom/prisma";
import { User } from "@calcom/prisma/client";import type { NextApiRequest, NextApiResponse } from "next";
import { User } from "@calcom/prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { schemaUser, withValidUser } from "@lib/validations/user";
+1 -3
View File
@@ -25,7 +25,5 @@
},
},
"include": [
"./**/*.ts"
]
"include": ["./**/*.ts"]
}