Skip to content

Commit 35b6905

Browse files
committed
chore(api): added pages e2e tests
1 parent 261b5e7 commit 35b6905

File tree

12 files changed

+288
-2
lines changed

12 files changed

+288
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"vitest": "vitest",
3636
"vitest:expected-cov": "vitest run --coverage.enabled --coverage.thresholds.lines=90 --coverage.thresholds.functions=90 --coverage.thresholds.statements=90",
3737
"vitest:cov": "vitest run --coverage.enabled",
38+
"vitest:changed": "vitest --changed=HEAD^1",
3839
"knip": "VITE_VALIDATE_ENV=false knip"
3940
},
4041
"devDependencies": {
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import * as http from "@liexp/shared/lib/io/http/index.js";
2+
import { toInitialValue } from "@liexp/shared/lib/providers/blocknote/utils";
23
import { Arbitrary } from "effect";
3-
import type fc from "fast-check";
4+
import fc from "fast-check";
5+
import { HumanReadableStringArb } from "./HumanReadableString.arbitrary";
46

57
export const PageArb: fc.Arbitrary<http.Page.Page> = Arbitrary.make(
68
http.Page.Page,
79
).map((p) => ({
810
...p,
11+
title: fc
12+
.sample(HumanReadableStringArb({ count: 10 }), 1)
13+
.reduce((acc, s) => acc.concat(s), ""),
14+
excerpt: toInitialValue(
15+
fc
16+
.sample(HumanReadableStringArb({ count: 40 }))
17+
.reduce((acc, s) => acc.concat(s), ""),
18+
),
19+
body2: toInitialValue(
20+
fc
21+
.sample(HumanReadableStringArb({ count: 100 }))
22+
.reduce((acc, s) => acc.concat(s), ""),
23+
),
924
deletedAt: undefined,
1025
}));
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
2+
3+
describe("Healthcheck route", () => {
4+
let Test: AppTest;
5+
6+
beforeAll(async () => {
7+
Test = await GetAppTest();
8+
});
9+
10+
test("GET /v1/healthcheck should return 200 and OK status", async () => {
11+
const res = await Test.req.get("/v1/healthcheck");
12+
expect(res.status).toEqual(200);
13+
expect(res.body).toHaveProperty("data");
14+
expect(res.body.data).toMatchObject({ status: "OK" });
15+
});
16+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
2+
3+
describe("OpenGraph metadata route", () => {
4+
let Test: AppTest;
5+
6+
beforeAll(async () => {
7+
Test = await GetAppTest();
8+
// Ensure the urlMetadata mock returns a Promise-compatible result
9+
// to avoid TaskEither/then TypeError seen in CI runs.
10+
Test.mocks.urlMetadata.fetchMetadata.mockResolvedValue({
11+
title: "Example Article",
12+
description: "An example article used in tests",
13+
url: "https://example.com/article/1",
14+
images: [],
15+
});
16+
});
17+
18+
test("GET /v1/open-graph/metadata should return 200 and metadata for URL", async () => {
19+
const url = "https://example.com/article/1";
20+
const res = await Test.req.get(
21+
`/v1/open-graph/metadata?url=${encodeURIComponent(url)}&type=Link`,
22+
);
23+
24+
expect(res.status).toEqual(200);
25+
expect(res.body).toHaveProperty("data");
26+
// data.metadata should exist (may be null if not found)
27+
expect(res.body.data).toHaveProperty("metadata");
28+
});
29+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { saveUser } from "@liexp/backend/lib/test/utils/user.utils.js";
2+
import { PageArb } from "@liexp/test/lib/arbitrary/Page.arbitrary.js";
3+
import fc from "fast-check";
4+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
5+
import { loginUser } from "../../../../test/utils/user.utils.js";
6+
7+
describe("AddPage route", () => {
8+
let Test: AppTest;
9+
10+
beforeAll(async () => {
11+
Test = await GetAppTest();
12+
});
13+
14+
test("unauthorized POST /v1/pages should return 401", async () => {
15+
const payload = { title: "t", content: "c", url: "https://ex.com/p" };
16+
const res = await Test.req.post("/v1/pages").send(payload);
17+
expect(res.status).toEqual(401);
18+
});
19+
20+
test("create page happy path (property-based)", async () => {
21+
const user = await saveUser(Test.ctx, ["admin:create"]);
22+
const { authorization } = await loginUser(Test)(user);
23+
24+
// sample one payload from the PageArb
25+
const [payload] = fc.sample(PageArb, 1);
26+
27+
const res = await Test.req
28+
.post("/v1/pages")
29+
.set("Authorization", authorization)
30+
.send(payload);
31+
32+
expect(201).toBe(res.status);
33+
expect(res.body).toHaveProperty("data");
34+
});
35+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { PageEntity } from "@liexp/backend/lib/entities/Page.entity.js";
2+
import { saveUser } from "@liexp/backend/lib/test/utils/user.utils.js";
3+
import { AdminDelete } from "@liexp/shared/lib/io/http/User.js";
4+
import { throwTE } from "@liexp/shared/lib/utils/task.utils.js";
5+
import { PageArb } from "@liexp/test/lib/arbitrary/Page.arbitrary.js";
6+
import fc from "fast-check";
7+
import { pipe } from "fp-ts/lib/function.js";
8+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
9+
import { loginUser } from "../../../../test/utils/user.utils.js";
10+
11+
describe("DeleteManyPage route", () => {
12+
let Test: AppTest;
13+
14+
beforeAll(async () => {
15+
Test = await GetAppTest();
16+
});
17+
18+
test("delete many pages happy path", async () => {
19+
const user = await saveUser(Test.ctx, [AdminDelete.literals[0]]);
20+
const { authorization } = await loginUser(Test)(user);
21+
22+
const pages = fc.sample(PageArb, 2).map((p) => ({
23+
...p,
24+
createdAt: new Date(),
25+
updatedAt: new Date(),
26+
}));
27+
await pipe(Test.ctx.db.save(PageEntity, pages), throwTE);
28+
29+
const ids = pages.map((p) => p.id);
30+
31+
const res = await Test.req
32+
.delete(`/v1/pages?${ids.map((id) => `ids=${id}`).join("&")}`)
33+
.set("Authorization", authorization);
34+
35+
expect(res.status).toEqual(200);
36+
});
37+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { PageEntity } from "@liexp/backend/lib/entities/Page.entity.js";
2+
import { saveUser } from "@liexp/backend/lib/test/utils/user.utils.js";
3+
import { AdminDelete, AdminRead } from "@liexp/shared/lib/io/http/User.js";
4+
import { throwTE } from "@liexp/shared/lib/utils/task.utils.js";
5+
import { PageArb } from "@liexp/test/lib/arbitrary/Page.arbitrary.js";
6+
import fc from "fast-check";
7+
import { pipe } from "fp-ts/lib/function.js";
8+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
9+
import { loginUser } from "../../../../test/utils/user.utils.js";
10+
11+
describe("DeletePage route", () => {
12+
let Test: AppTest;
13+
14+
beforeAll(async () => {
15+
Test = await GetAppTest();
16+
});
17+
18+
test("admin with read permissions should not delete page", async () => {
19+
const user = await saveUser(Test.ctx, [AdminRead.literals[0]]);
20+
const { authorization } = await loginUser(Test)(user);
21+
22+
const [payload] = fc.sample(PageArb, 1);
23+
24+
await Test.req
25+
.post("/v1/pages")
26+
.set("Authorization", authorization)
27+
.send(payload)
28+
.expect(401);
29+
});
30+
31+
test("delete page happy path", async () => {
32+
const user = await saveUser(Test.ctx, [AdminDelete.literals[0]]);
33+
const { authorization } = await loginUser(Test)(user);
34+
35+
const [payload] = fc.sample(PageArb, 1);
36+
37+
await pipe(
38+
Test.ctx.db.save(PageEntity, [
39+
{ ...payload, createdAt: new Date(), updatedAt: new Date() },
40+
]),
41+
throwTE,
42+
);
43+
44+
const delRes = await Test.req
45+
.delete(`/v1/pages/${payload.id}`)
46+
.set("Authorization", authorization);
47+
48+
expect(delRes.status).toEqual(200);
49+
});
50+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { PageEntity } from "@liexp/backend/lib/entities/Page.entity.js";
2+
import { saveUser } from "@liexp/backend/lib/test/utils/user.utils.js";
3+
import { AdminEdit } from "@liexp/shared/lib/io/http/User.js";
4+
import { throwTE } from "@liexp/shared/lib/utils/task.utils.js";
5+
import { PageArb } from "@liexp/test/lib/arbitrary/Page.arbitrary.js";
6+
import fc from "fast-check";
7+
import { pipe } from "fp-ts/lib/function.js";
8+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
9+
import { loginUser } from "../../../../test/utils/user.utils.js";
10+
11+
describe("EditPage route", () => {
12+
let Test: AppTest;
13+
14+
beforeAll(async () => {
15+
Test = await GetAppTest();
16+
});
17+
18+
test("edit page happy path", async () => {
19+
const user = await saveUser(Test.ctx, [AdminEdit.literals[0]]);
20+
const { authorization } = await loginUser(Test)(user);
21+
22+
const payload = fc.sample(PageArb, 1)[0];
23+
24+
await pipe(
25+
Test.ctx.db.save(PageEntity, [
26+
{ ...payload, createdAt: new Date(), updatedAt: new Date() },
27+
]),
28+
throwTE,
29+
);
30+
31+
const editRes = await Test.req
32+
.put(`/v1/pages/${payload.id}`)
33+
.set("Authorization", authorization)
34+
.send({ ...payload, title: "edited" });
35+
36+
expect(editRes.status).toEqual(200);
37+
expect(editRes.body.data).toMatchObject({
38+
id: payload.id,
39+
title: "edited",
40+
});
41+
});
42+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { saveUser } from "@liexp/backend/lib/test/utils/user.utils.js";
2+
import { PageArb } from "@liexp/test/lib/arbitrary/Page.arbitrary.js";
3+
import fc from "fast-check";
4+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
5+
import { loginUser } from "../../../../test/utils/user.utils.js";
6+
7+
describe("GetPage route", () => {
8+
let Test: AppTest;
9+
10+
beforeAll(async () => {
11+
Test = await GetAppTest();
12+
});
13+
14+
test("get page by id returns 200", async () => {
15+
const user = await saveUser(Test.ctx, ["admin:create"]);
16+
const { authorization } = await loginUser(Test)(user);
17+
18+
const [payload] = fc.sample(PageArb, 1);
19+
20+
const createRes = await Test.req
21+
.post("/v1/pages")
22+
.set("Authorization", authorization)
23+
.send(payload);
24+
25+
expect([200, 201]).toContain(createRes.status);
26+
const id = createRes.body.data.id;
27+
28+
const getRes = await Test.req.get(`/v1/pages/${id}`);
29+
expect(getRes.status).toEqual(200);
30+
expect(getRes.body.data).toHaveProperty("id", id);
31+
});
32+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { type AppTest, GetAppTest } from "../../../../test/AppTest.js";
2+
3+
describe("ListPage route", () => {
4+
let Test: AppTest;
5+
6+
beforeAll(async () => {
7+
Test = await GetAppTest();
8+
});
9+
10+
test("GET /v1/pages returns 200 and data array", async () => {
11+
const res = await Test.req.get("/v1/pages");
12+
expect(res.status).toEqual(200);
13+
expect(res.body.data).toBeInstanceOf(Array);
14+
expect(res.body.total).toBeTypeOf("number");
15+
});
16+
});

0 commit comments

Comments
 (0)