Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
import { describe, expect, it } from "vitest";
import { hashPassword, verifyPassword } from "../src/lib/passwords.js";
import { generateInviteCode, sha256 } from "../src/lib/helpers.js";
import { parse, ApiError } from "../src/lib/errors.js";
import { registerInputSchema, confirmScanInputSchema } from "@app/validation";
describe("lösenordshantering (scrypt)", () => {
it("hashar och verifierar", async () => {
const hash = await hashPassword("korrekt häst batteri 99");
expect(hash.startsWith("scrypt$")).toBe(true);
expect(await verifyPassword("korrekt häst batteri 99", hash)).toBe(true);
expect(await verifyPassword("fel lösenord", hash)).toBe(false);
});
it("unika salter ger olika hashar", async () => {
const a = await hashPassword("samma lösenord!");
const b = await hashPassword("samma lösenord!");
expect(a).not.toBe(b);
expect(await verifyPassword("samma lösenord!", a)).toBe(true);
expect(await verifyPassword("samma lösenord!", b)).toBe(true);
});
it("hanterar trasig lagrad hash utan att kasta", async () => {
expect(await verifyPassword("x", "inte-en-hash")).toBe(false);
expect(await verifyPassword("x", "scrypt$abc$8$1$AA$BB")).toBe(false);
});
});
describe("hjälpare", () => {
it("inbjudningskoder: 8 tecken utan förväxlingsbara", () => {
for (let i = 0; i < 20; i++) {
const code = generateInviteCode();
expect(code).toHaveLength(8);
expect(code).not.toMatch(/[01IO]/);
}
});
it("sha256 är deterministisk", () => {
expect(sha256("abc")).toBe(sha256("abc"));
expect(sha256("abc")).toHaveLength(64);
});
});
describe("validering via parse()", () => {
it("registrering: normaliserar e-post, kräver 10 teckens lösenord", () => {
const result = parse(registerInputSchema, {
email: " Johan@Example.COM",
password: "supersäkert lösen",
displayName: "Johan",
});
expect(result.email).toBe("johan@example.com");
expect(() =>
parse(registerInputSchema, { email: "a@b.se", password: "kort", displayName: "X" }),
).toThrowError(ApiError);
});
it("scan-bekräftelse: avvisar ogiltiga enheter", () => {
expect(() =>
parse(confirmScanInputSchema, {
items: [{ action: "accept", displayName: "Mjölk", quantity: 1, unit: "gallon" }],
}),
).toThrowError(ApiError);
});
it("ApiError bär status och kod", () => {
try {
parse(registerInputSchema, {});
} catch (err) {
expect(err).toBeInstanceOf(ApiError);
expect((err as ApiError).statusCode).toBe(400);
expect((err as ApiError).code).toBe("VALIDATION_ERROR");
}
});
});
describe("lösenordsåterställning (mejlmallar + tokensäkerhet)", () => {
it("renderar återställningsmejl per språk med brand ur konfig", async () => {
const { renderMail } = await import("../src/lib/mailer.js");
const sv = renderMail("auth.password_reset", "sv-SE", {
email: "a@b.se",
link: "app://reset-password?token=x",
});
const en = renderMail("auth.password_reset", "en-US", {
email: "a@b.se",
link: "app://reset-password?token=x",
});
expect(sv.subject).toContain("Återställ");
expect(en.subject).toContain("Reset");
expect(sv.text).toContain("30 minuter");
expect(en.text).toContain("30 minutes");
// Namnet kommer ur brand.config.json aldrig hårdkodat i mallen.
const { BRAND } = await import("@app/shared-types");
expect(sv.subject).toContain(BRAND.name);
});
it("alla tolv språk har mallar; okänt språk faller till svenska; okänd mall kastar", async () => {
const { renderMail } = await import("../src/lib/mailer.js");
const subjects = {
"sv-SE": "Återställ",
"en-US": "Reset",
"es-ES": "Restablece",
"it-IT": "Reimposta",
"de-DE": "Setze",
"fr-FR": "Réinitialisez",
"da-DK": "Nulstil",
"nb-NO": "Tilbakestill",
"fi-FI": "Nollaa",
"nl-NL": "wachtwoord opnieuw",
"pl-PL": "Zresetuj",
"pt-PT": "Reponha",
};
for (const [tag, word] of Object.entries(subjects)) {
expect(renderMail("auth.password_reset", tag, { email: "x", link: "y" }).subject).toContain(
word,
);
}
const unknown = renderMail("auth.password_reset", "el-GR", { email: "x", link: "y" });
expect(unknown.subject).toContain("Återställ"); // ostött språk -> sv
expect(() => renderMail("finns.inte", "sv", {})).toThrow();
});
it("återställningstoken lagras aldrig i klartext (sha256 är envägs)", () => {
const token = "a".repeat(64);
const hash = sha256(token);
expect(hash).not.toBe(token);
expect(hash).toHaveLength(64);
expect(sha256(token)).toBe(hash); // deterministisk uppslag fungerar
expect(sha256("b".repeat(64))).not.toBe(hash);
});
it("log-mailern skickar aldrig på riktigt; okonfigurerad SMTP vägrar högljutt", async () => {
const { createMailer } = await import("../src/lib/mailer.js");
await expect(
createMailer("log").send({ to: "x@y.se", subject: "t", text: "b" }),
).resolves.toBeUndefined();
// smtp utan inställningar/host/avsändare -> tydligt fel direkt (aldrig tyst tappade mejl)
expect(() => createMailer("smtp")).toThrow(/SMTP/);
expect(() =>
createMailer("smtp", {
host: "",
port: 587,
secure: false,
user: "",
pass: "",
from: "x@y.se",
}),
).toThrow(/SMTP_HOST/);
expect(() =>
createMailer("smtp", {
host: "smtp.x.se",
port: 587,
secure: false,
user: "",
pass: "",
from: "",
}),
).toThrow(/MAIL_FROM/);
});
});
describe("TOTP (RFC 6238) admin-2FA utan externa beroenden", () => {
it("klarar RFC 6238-testvektorn (SHA1, 6 siffror)", async () => {
const { base32Encode, totpCode } = await import("../src/lib/totp.js");
// RFC 6238 bilaga B: secret "12345678901234567890", T=59 s → 6 sista siffrorna av 94287082.
const secret = base32Encode(Buffer.from("12345678901234567890", "ascii"));
expect(totpCode(secret, 59_000)).toBe("287082");
expect(totpCode(secret, 1_111_111_109_000)).toBe("081804");
});
it("verifierar med ±1 stegs fönster men inte längre bort", async () => {
const { generateTotpSecret, totpCode, verifyTotp } = await import("../src/lib/totp.js");
const secret = generateTotpSecret();
const now = 1_700_000_000_000;
const code = totpCode(secret, now);
expect(verifyTotp(secret, code, now)).not.toBeNull();
expect(verifyTotp(secret, code, now + 30_000)).not.toBeNull(); // föregående steg ok
expect(verifyTotp(secret, code, now + 90_000)).toBeNull(); // för gammalt
expect(verifyTotp(secret, "000000", now)).toBeNull();
expect(verifyTotp(secret, "12345", now)).toBeNull(); // fel format
});
it("base32 rundtur + otpauth-URL utan hårdkodat namn", async () => {
const { base32Decode, base32Encode, otpauthUrl } = await import("../src/lib/totp.js");
const buf = Buffer.from("hemlig-nyckel-123", "ascii");
expect(base32Decode(base32Encode(buf)).equals(buf)).toBe(true);
const { BRAND } = await import("@app/shared-types");
const url = otpauthUrl("ABC234", "admin@example.com", `${BRAND.name} Admin`);
expect(url.startsWith("otpauth://totp/")).toBe(true);
expect(url).toContain("secret=ABC234");
expect(url).toContain(encodeURIComponent(BRAND.name));
});
});