mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 14:12:25 -05:00
109 lines
4.7 KiB
TypeScript
109 lines
4.7 KiB
TypeScript
import { beforeAll, describe, expect, it } from "vitest";
|
|
import request from "supertest";
|
|
import { createApp } from "../src/app";
|
|
import { prisma } from "../src/db";
|
|
|
|
const app = createApp();
|
|
let session: string;
|
|
let userId: string;
|
|
|
|
function auth(t: string) {
|
|
return { Authorization: `Bearer ${t}` };
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
const email = `api-token-test-${Date.now()}@example.com`;
|
|
const registered = await request(app)
|
|
.post("/api/register")
|
|
.send({ displayName: "API Token Test", email, password: "password123" });
|
|
if (registered.status !== 200) {
|
|
throw new Error(`Failed to register during test setup: ${registered.status} ${JSON.stringify(registered.body)}`);
|
|
}
|
|
// No SMTP in tests, so registration signs in immediately.
|
|
session = registered.body.token;
|
|
userId = registered.body.user.id;
|
|
});
|
|
|
|
describe("API token", () => {
|
|
it("reports no token for a new account", async () => {
|
|
const res = await request(app).get("/api/profile/api-token").set(auth(session));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body).toEqual({ configured: false, hint: null, created_at: null, last_used_at: null });
|
|
});
|
|
|
|
it("generates a token that is shown once and stored only as a hash", async () => {
|
|
const res = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.token).toMatch(/^tp_[A-Za-z0-9_-]{43}$/);
|
|
expect(res.body.hint).toBe(res.body.token.slice(-4));
|
|
|
|
const user = await prisma.user.findUniqueOrThrow({ where: { id: userId } });
|
|
expect(user.apiTokenHash).not.toContain(res.body.token);
|
|
|
|
const info = await request(app).get("/api/profile/api-token").set(auth(session));
|
|
expect(info.body.configured).toBe(true);
|
|
expect(info.body.token).toBeUndefined();
|
|
});
|
|
|
|
it("reads as its owner and records when it was last used", async () => {
|
|
const { body } = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const res = await request(app).get("/api/dashboard/summary").set(auth(body.token));
|
|
expect(res.status).toBe(200);
|
|
|
|
// The last-used write is fire-and-forget, so give it a moment.
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
const info = await request(app).get("/api/profile/api-token").set(auth(session));
|
|
expect(info.body.last_used_at).not.toBeNull();
|
|
});
|
|
|
|
it("can't write, mint a session or replace itself", async () => {
|
|
const { body } = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const refresh = await request(app).post("/api/refresh").set(auth(body.token));
|
|
expect(refresh.status).toBe(403);
|
|
const regenerate = await request(app).post("/api/profile/api-token").set(auth(body.token));
|
|
expect(regenerate.status).toBe(403);
|
|
const revoke = await request(app).delete("/api/profile/api-token").set(auth(body.token));
|
|
expect(revoke.status).toBe(403);
|
|
});
|
|
|
|
it("is only accepted in the Authorization header, not ?token=", async () => {
|
|
const { body } = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const res = await request(app).get("/api/profile/api-token").query({ token: body.token });
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("stops working once regenerated", async () => {
|
|
const first = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const second = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
expect(second.body.token).not.toBe(first.body.token);
|
|
|
|
expect((await request(app).get("/api/profile/api-token").set(auth(first.body.token))).status).toBe(401);
|
|
expect((await request(app).get("/api/profile/api-token").set(auth(second.body.token))).status).toBe(200);
|
|
});
|
|
|
|
it("stops working once revoked", async () => {
|
|
const { body } = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const revoked = await request(app).delete("/api/profile/api-token").set(auth(session));
|
|
expect(revoked.status).toBe(200);
|
|
expect(revoked.body.configured).toBe(false);
|
|
|
|
expect((await request(app).get("/api/profile/api-token").set(auth(body.token))).status).toBe(401);
|
|
});
|
|
|
|
it("rejects a made-up token", async () => {
|
|
const res = await request(app).get("/api/profile/api-token").set(auth("tp_not-a-real-token"));
|
|
expect(res.status).toBe(401);
|
|
});
|
|
|
|
it("can't reach admin routes, even for an admin", async () => {
|
|
await prisma.user.update({ where: { id: userId }, data: { role: "ADMIN" } });
|
|
try {
|
|
const { body } = await request(app).post("/api/profile/api-token").set(auth(session));
|
|
const res = await request(app).get("/api/admin/users").set(auth(body.token));
|
|
expect(res.status).toBe(403);
|
|
} finally {
|
|
await prisma.user.update({ where: { id: userId }, data: { role: "MEMBER" } });
|
|
}
|
|
});
|
|
});
|