chore: request validation for searches (#5460)

This commit is contained in:
Apoorv Mishra
2023-06-21 10:38:38 +05:30
committed by GitHub
parent 6d556c7a55
commit 8d69de1be0
5 changed files with 184 additions and 21 deletions

View File

@@ -0,0 +1 @@
export { default } from "./searches";

View File

@@ -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<typeof SearchesDeleteSchema>;

View File

@@ -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);
});
});

View File

@@ -1,10 +1,11 @@
import Router from "koa-router"; import Router from "koa-router";
import auth from "@server/middlewares/authentication"; import auth from "@server/middlewares/authentication";
import validate from "@server/middlewares/validate";
import { SearchQuery } from "@server/models"; import { SearchQuery } from "@server/models";
import { presentSearchQuery } from "@server/presenters"; import { presentSearchQuery } from "@server/presenters";
import { APIContext } from "@server/types"; 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(); const router = new Router();
@@ -26,14 +27,15 @@ router.post("searches.list", auth(), pagination(), async (ctx: APIContext) => {
}; };
}); });
router.post("searches.delete", auth(), async (ctx: APIContext) => { router.post(
const { id, query } = ctx.request.body; "searches.delete",
assertPresent(id || query, "id or query is required"); auth(),
if (id) { validate(T.SearchesDeleteSchema),
assertUuid(id, "id is must be a uuid"); async (ctx: APIContext<T.SearchesDeleteReq>) => {
} const { id, query } = ctx.input.body;
const { user } = ctx.state.auth; const { user } = ctx.state.auth;
await SearchQuery.destroy({ await SearchQuery.destroy({
where: { where: {
...(id ? { id } : { query }), ...(id ? { id } : { query }),
@@ -44,6 +46,7 @@ router.post("searches.delete", auth(), async (ctx: APIContext) => {
ctx.body = { ctx.body = {
success: true, success: true,
}; };
}); }
);
export default router; export default router;

View File

@@ -1,4 +1,4 @@
import { isNull } from "lodash"; import { isNil, isNull } from "lodash";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { import {
CollectionPermission, CollectionPermission,
@@ -28,6 +28,7 @@ import {
ApiKey, ApiKey,
Subscription, Subscription,
Notification, Notification,
SearchQuery,
} from "@server/models"; } from "@server/models";
let count = 1; let count = 1;
@@ -517,3 +518,33 @@ export async function buildNotification(
return Notification.create(overrides); return Notification.create(overrides);
} }
export async function buildSearchQuery(
overrides: Partial<SearchQuery> = {}
): Promise<SearchQuery> {
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);
}