mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 14:12:25 -05:00
Import, collection import and synced collections from MyMiniFactory, alongside MakerWorld, Printables and Thingiverse. - Backend: API v2 client with an instance-wide API key for metadata, collections and authors; files from MyMiniFactory's own download links (free models need no login). Paid models, and free ones whose files are archives, download as the user from their MyMiniFactory library through their session (PHPSESSID), fetching the model files MyMiniFactory unpacked from each archive. Paid models are left out of collection listings and syncs. - One migration: MyMiniFactory category ids, username and session columns, plus a data migration giving existing users' starter categories the new category ids. Category matching now prefers the most specific site category. - Frontend: provider everywhere the others appear (badges, Configuration and Administration providers, sync Browse tab, user menu, link detection, System Health, category ids), shared instance-key form for Thingiverse and MyMiniFactory, en and lt. - Thingport Grab: new provider sites are allowed once from the popup (optional all-sites access, content script registered at runtime), so adding one never makes the browser disable the extension; captures the signed-in MyMiniFactory username and session; paid-model notice. - Docs, website, store assets and screenshots updated.
145 lines
6.0 KiB
TypeScript
145 lines
6.0 KiB
TypeScript
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import request from "supertest";
|
|
import { createApp } from "../src/app";
|
|
import { prisma } from "../src/db";
|
|
import { resetClientTracking } from "../src/services/clientActivityService";
|
|
|
|
const app = createApp();
|
|
let admin: string;
|
|
let member: string;
|
|
|
|
async function register(role: "ADMIN" | "MEMBER") {
|
|
const res = await request(app)
|
|
.post("/api/register")
|
|
.send({ displayName: role, email: `health-${role}-${Date.now()}@example.com`, password: "password123" });
|
|
await prisma.user.update({ where: { id: res.body.user.id }, data: { role } });
|
|
return res.body.token as string;
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
admin = await register("ADMIN");
|
|
member = await register("MEMBER");
|
|
});
|
|
|
|
const as = (token: string) => ({ Authorization: `Bearer ${token}` });
|
|
type Report = {
|
|
components: {
|
|
component: string;
|
|
result: { status: string; code: string; detail: string | null; checked_at: string; facts: object } | null;
|
|
}[];
|
|
full_checked_at: string | null;
|
|
};
|
|
const resultOf = (report: Report, component: string) =>
|
|
report.components.find((c) => c.component === component)!.result;
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("system health", () => {
|
|
it("reports Grab as not seen until it makes a request of its own", async () => {
|
|
await prisma.setting.deleteMany({ where: { key: "client_seen_grab" } });
|
|
await prisma.grabConnection.deleteMany();
|
|
resetClientTracking();
|
|
const test = async () =>
|
|
resultOf(
|
|
(await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "grab" })).body,
|
|
"grab",
|
|
)!;
|
|
expect(await test()).toMatchObject({ status: "not_configured", code: "not_seen" });
|
|
|
|
await request(app).get("/api/health").set("X-Thingport-Client", "grab");
|
|
// Recorded without holding up the request.
|
|
await vi.waitFor(async () => {
|
|
expect(await test()).toMatchObject({ status: "ok", facts: { last_seen_at: expect.any(String) } });
|
|
});
|
|
});
|
|
|
|
it("lists every component by importance, the database first and SMTP, which is optional, last", async () => {
|
|
const res = await request(app).get("/api/admin/health").set(as(admin));
|
|
expect(res.status).toBe(200);
|
|
expect((res.body as Report).components.map((c) => c.component)).toEqual([
|
|
"database",
|
|
"storage",
|
|
"memory",
|
|
"internet",
|
|
"flaresolverr",
|
|
"thingiverse",
|
|
"myminifactory",
|
|
"grab",
|
|
"ai",
|
|
"smtp",
|
|
]);
|
|
});
|
|
|
|
it("tests one component when asked, leaving the others' last results as they were", async () => {
|
|
const before = (await request(app).get("/api/admin/health").set(as(admin))).body as Report;
|
|
const res = await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "database" });
|
|
expect(res.status).toBe(200);
|
|
const database = resultOf(res.body, "database")!;
|
|
expect(database).toMatchObject({ status: "ok", code: "ok" });
|
|
expect(database.facts).toMatchObject({ version: expect.stringMatching(/^PostgreSQL/) });
|
|
expect(resultOf(res.body, "smtp")).toEqual(resultOf(before, "smtp"));
|
|
expect(res.body.full_checked_at).toBe(before.full_checked_at);
|
|
});
|
|
|
|
it("tests everything in a full test, and says when it ran", async () => {
|
|
const res = await request(app).post("/api/admin/health/test").set(as(admin)).send({});
|
|
expect(res.status).toBe(200);
|
|
const report = res.body as Report;
|
|
expect(report.full_checked_at).not.toBeNull();
|
|
for (const { component, result } of report.components)
|
|
expect({ component, tested: Boolean(result) }).toEqual({ component, tested: true });
|
|
expect(resultOf(report, "storage")!.status).toBe("ok");
|
|
expect(resultOf(report, "memory")!.facts).toHaveProperty("available_bytes");
|
|
// Kept: opening the page again shows the same results, without testing anything.
|
|
const again = (await request(app).get("/api/admin/health").set(as(admin))).body as Report;
|
|
expect(again).toEqual(report);
|
|
});
|
|
|
|
it("keeps every result when tests overlap, as from two admins at once", async () => {
|
|
const before = (await request(app).get("/api/admin/health").set(as(admin))).body as Report;
|
|
const started = new Date().toISOString();
|
|
await Promise.all(
|
|
["database", "storage", "memory"].map((component) =>
|
|
request(app).post("/api/admin/health/test").set(as(admin)).send({ component }),
|
|
),
|
|
);
|
|
const after = (await request(app).get("/api/admin/health").set(as(admin))).body as Report;
|
|
for (const component of ["database", "storage", "memory"]) {
|
|
expect({ component, fresh: resultOf(after, component)!.checked_at >= started }).toEqual({
|
|
component,
|
|
fresh: true,
|
|
});
|
|
}
|
|
expect(after.full_checked_at).toBe(before.full_checked_at);
|
|
});
|
|
|
|
it("reports the server offline when nothing outside answers", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(
|
|
Object.assign(new TypeError("fetch failed"), { cause: { code: "ENOTFOUND" } }),
|
|
);
|
|
const res = await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "internet" });
|
|
expect(resultOf(res.body, "internet")).toMatchObject({
|
|
status: "error",
|
|
code: "offline",
|
|
detail: "ENOTFOUND",
|
|
facts: { host: expect.any(String) },
|
|
});
|
|
});
|
|
|
|
it("reports the server online on any answer", async () => {
|
|
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response("[]", { status: 200 }));
|
|
const res = await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "internet" });
|
|
expect(resultOf(res.body, "internet")).toMatchObject({ status: "ok", code: "ok" });
|
|
});
|
|
|
|
it("is for admins only, and refuses a component it doesn't know", async () => {
|
|
expect((await request(app).get("/api/admin/health").set(as(member))).status).toBe(403);
|
|
expect((await request(app).post("/api/admin/health/test").set(as(member)).send({})).status).toBe(403);
|
|
expect(
|
|
(await request(app).post("/api/admin/health/test").set(as(admin)).send({ component: "toaster" })).status,
|
|
).toBe(400);
|
|
});
|
|
});
|