diff --git a/server/routes/api/searches/index.ts b/server/routes/api/searches/index.ts new file mode 100644 index 000000000..5c9c41a50 --- /dev/null +++ b/server/routes/api/searches/index.ts @@ -0,0 +1 @@ +export { default } from "./searches"; diff --git a/server/routes/api/searches/schema.ts b/server/routes/api/searches/schema.ts new file mode 100644 index 000000000..c99fc4973 --- /dev/null +++ b/server/routes/api/searches/schema.ts @@ -0,0 +1,14 @@ +import { isEmpty } from "lodash"; +import { z } from "zod"; +import BaseSchema from "../BaseSchema"; + +export const SearchesDeleteSchema = BaseSchema.extend({ + body: z.object({ + id: z.string().uuid().optional(), + query: z.string().optional(), + }), +}).refine((req) => !(isEmpty(req.body.id) && isEmpty(req.body.query)), { + message: "id or query is required", +}); + +export type SearchesDeleteReq = z.infer; diff --git a/server/routes/api/searches/searches.test.ts b/server/routes/api/searches/searches.test.ts new file mode 100644 index 000000000..37d8a88b0 --- /dev/null +++ b/server/routes/api/searches/searches.test.ts @@ -0,0 +1,114 @@ +import { SearchQuery, User } from "@server/models"; +import { buildSearchQuery, buildUser } from "@server/test/factories"; +import { getTestServer } from "@server/test/support"; + +const server = getTestServer(); + +describe("#searches.list", () => { + let user: User; + + beforeEach(async () => { + user = await buildUser(); + + await Promise.all([ + buildSearchQuery({ + userId: user.id, + teamId: user.teamId, + }), + buildSearchQuery({ + userId: user.id, + teamId: user.teamId, + query: "foo", + }), + buildSearchQuery({ + userId: user.id, + teamId: user.teamId, + query: "bar", + }), + ]); + }); + + it("should succeed with status 200 ok returning results", async () => { + const res = await server.post("/api/searches.list", { + body: { + token: user.getJwtToken(), + }, + }); + const body = await res.json(); + expect(res.status).toEqual(200); + expect(body.data).toHaveLength(3); + const queries = body.data.map((d: any) => d.query); + expect(queries).toContain("query"); + expect(queries).toContain("foo"); + expect(queries).toContain("bar"); + }); +}); + +describe("#searches.delete", () => { + let user: User; + let searchQuery: SearchQuery; + + beforeEach(async () => { + user = await buildUser(); + + searchQuery = await buildSearchQuery({ + userId: user.id, + teamId: user.teamId, + }); + }); + + it("should fail with status 400 bad request when no id or query is provided", async () => { + const res = await server.post("/api/searches.delete", { + body: { + token: user.getJwtToken(), + }, + }); + + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toEqual("id or query is required"); + }); + + it("should fail with status 400 bad request when an invalid id is provided", async () => { + const res = await server.post("/api/searches.delete", { + body: { + token: user.getJwtToken(), + id: "id", + }, + }); + + const body = await res.json(); + expect(res.status).toEqual(400); + expect(body.message).toEqual("id: Invalid uuid"); + }); + + it("should succeed with status 200 ok and successfully delete the query", async () => { + let searchQueries = await SearchQuery.findAll({ + where: { + userId: user.id, + teamId: user.teamId, + }, + }); + expect(searchQueries).toHaveLength(1); + + const res = await server.post("/api/searches.delete", { + body: { + token: user.getJwtToken(), + id: searchQuery.id, + }, + }); + + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.success).toEqual(true); + + searchQueries = await SearchQuery.findAll({ + where: { + userId: user.id, + teamId: user.teamId, + }, + }); + expect(searchQueries).toHaveLength(0); + }); +}); diff --git a/server/routes/api/searches.ts b/server/routes/api/searches/searches.ts similarity index 57% rename from server/routes/api/searches.ts rename to server/routes/api/searches/searches.ts index 5caf588cc..1126ffd2f 100644 --- a/server/routes/api/searches.ts +++ b/server/routes/api/searches/searches.ts @@ -1,10 +1,11 @@ import Router from "koa-router"; import auth from "@server/middlewares/authentication"; +import validate from "@server/middlewares/validate"; import { SearchQuery } from "@server/models"; import { presentSearchQuery } from "@server/presenters"; import { APIContext } from "@server/types"; -import { assertPresent, assertUuid } from "@server/validation"; -import pagination from "./middlewares/pagination"; +import pagination from "../middlewares/pagination"; +import * as T from "./schema"; const router = new Router(); @@ -26,24 +27,26 @@ router.post("searches.list", auth(), pagination(), async (ctx: APIContext) => { }; }); -router.post("searches.delete", auth(), async (ctx: APIContext) => { - const { id, query } = ctx.request.body; - assertPresent(id || query, "id or query is required"); - if (id) { - assertUuid(id, "id is must be a uuid"); +router.post( + "searches.delete", + auth(), + validate(T.SearchesDeleteSchema), + async (ctx: APIContext) => { + const { id, query } = ctx.input.body; + + const { user } = ctx.state.auth; + + await SearchQuery.destroy({ + where: { + ...(id ? { id } : { query }), + userId: user.id, + }, + }); + + ctx.body = { + success: true, + }; } - - const { user } = ctx.state.auth; - await SearchQuery.destroy({ - where: { - ...(id ? { id } : { query }), - userId: user.id, - }, - }); - - ctx.body = { - success: true, - }; -}); +); export default router; diff --git a/server/test/factories.ts b/server/test/factories.ts index bb4b67318..c3ad2c93a 100644 --- a/server/test/factories.ts +++ b/server/test/factories.ts @@ -1,4 +1,4 @@ -import { isNull } from "lodash"; +import { isNil, isNull } from "lodash"; import { v4 as uuidv4 } from "uuid"; import { CollectionPermission, @@ -28,6 +28,7 @@ import { ApiKey, Subscription, Notification, + SearchQuery, } from "@server/models"; let count = 1; @@ -517,3 +518,33 @@ export async function buildNotification( return Notification.create(overrides); } + +export async function buildSearchQuery( + overrides: Partial = {} +): Promise { + if (!overrides.teamId) { + const team = await buildTeam(); + overrides.teamId = team.id; + } + + if (!overrides.userId) { + const user = await buildUser({ + teamId: overrides.teamId, + }); + overrides.userId = user.id; + } + + if (!overrides.source) { + overrides.source = "app"; + } + + if (isNil(overrides.query)) { + overrides.query = "query"; + } + + if (isNil(overrides.results)) { + overrides.results = 1; + } + + return SearchQuery.create(overrides); +}