Files
suaveplan/packages/security-stack/security/password-strength/src/assess/assess.test.ts
2026-05-27 23:47:53 +01:00

134 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { loadDictionaries } from "../dicts/load-dictionaries.js";
import {
PasswordStrengthError,
PWSTRENGTH_CODES,
} from "../errors/error-codes.js";
import { StrengthResultSchema } from "../schemas/schemas.js";
import { assess } from "./assess.js";
describe("assess", () => {
it("full pipeline returns canonical shape", { timeout: 5000 }, () => {
const r = assess("Password1!");
const parsed = StrengthResultSchema.safeParse(r);
expect(parsed.success).toBe(true);
expect(r.score).toBeLessThanOrEqual(1);
});
it("accepts 199-character input", { timeout: 5000 }, () => {
const r = assess("a".repeat(199));
expect(r.password.length).toBe(199);
});
it("rejects 200-character input", { timeout: 5000 }, () => {
expect(() => assess("a".repeat(200))).toThrowError(
PasswordStrengthError
);
});
it("rejects 200-character input with INPUT_TOO_LONG code", {
timeout: 5000,
}, () => {
try {
assess("a".repeat(200));
} catch (e) {
expect((e as PasswordStrengthError).code).toBe(
PWSTRENGTH_CODES.INPUT_TOO_LONG
);
}
});
it("userInputs surface as ranked matches", { timeout: 5000 }, () => {
const r = assess("hello-acme-corp", { userInputs: ["acme-corp"] });
expect(
r.sequence.some(
(m) =>
m.pattern === "dictionary" &&
m.dictionaryName === "user_inputs"
)
).toBe(true);
});
it("userInputs become a ranked dictionary", { timeout: 5000 }, () => {
const r = assess("alice@example.com-pwd", {
userInputs: ["alice@example.com"],
});
const hit = r.sequence.find(
(m) =>
m.pattern === "dictionary" && m.dictionaryName === "user_inputs"
);
expect(hit).toBeDefined();
expect(hit?.pattern === "dictionary" && hit.matchedWord).toBe(
"alice@example.com"
);
});
it("custom dictionaries take precedence", { timeout: 5000 }, () => {
const dicts = loadDictionaries({
passwords: new Map([["trustno1", 1]]),
});
const r = assess("trustno1", { dictionaries: dicts });
expect(r.score).toBe(0);
});
it("rejects malformed AssessOptions", { timeout: 5000 }, () => {
expect(() =>
assess("pwd", { userInputs: 42 as unknown as string[] })
).toThrowError(PasswordStrengthError);
});
it("emits feedback with warning + suggestions", { timeout: 5000 }, () => {
const r = assess("password");
expect(r.feedback.warning.length).toBeGreaterThan(0);
});
it("returns guesses ≥ 1", { timeout: 5000 }, () => {
const r = assess("anything-random-here");
expect(r.guesses).toBeGreaterThanOrEqual(1);
});
it("records non-negative calcTimeMs", { timeout: 5000 }, () => {
const r = assess("abc");
expect(r.calcTimeMs).toBeGreaterThanOrEqual(0);
});
it("accepts custom graphs option", { timeout: 5000 }, () => {
const r = assess("hello", { graphs: { qwerty: {} } });
expect(r).toBeDefined();
});
it("accepts custom referenceYear option", { timeout: 5000 }, () => {
const r = assess("1985", { referenceYear: 2026 });
expect(r.score).toBeLessThanOrEqual(1);
});
it("accepts empty userInputs", { timeout: 5000 }, () => {
const r = assess("hello", { userInputs: [] });
expect(r).toBeDefined();
});
it("deduplicates userInputs", { timeout: 5000 }, () => {
const r = assess("acme", {
userInputs: ["acme", "acme", "acme corp"],
});
expect(r).toBeDefined();
});
it("handles empty input", { timeout: 5000 }, () => {
const r = assess("");
expect(r.guesses).toBe(1);
});
it("accepts custom l33tTable", { timeout: 5000 }, () => {
const r = assess("p4ssword", { l33tTable: { a: ["4"] } });
expect(r).toBeDefined();
});
it("accepts feedback resolver via opts.feedback", { timeout: 5000 }, () => {
const r = assess("password", {
feedback: { resolver: (key) => `RESOLVED:${key}` },
});
expect(r.feedback.warning.startsWith("RESOLVED:")).toBe(true);
});
});