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 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,24 +27,26 @@ 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;
|
||||||
|
|
||||||
|
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;
|
export default router;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user