chore: request validation for searches (#5460)
This commit is contained in:
1
server/routes/api/searches/index.ts
Normal file
1
server/routes/api/searches/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from "./searches";
|
||||
14
server/routes/api/searches/schema.ts
Normal file
14
server/routes/api/searches/schema.ts
Normal 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>;
|
||||
114
server/routes/api/searches/searches.test.ts
Normal file
114
server/routes/api/searches/searches.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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<T.SearchesDeleteReq>) => {
|
||||
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;
|
||||
@@ -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<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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user