84 lines
3.6 KiB
TypeScript
84 lines
3.6 KiB
TypeScript
function assert(condition: unknown, message = "assertion failed"): asserts condition {
|
|
if (!condition) throw new Error(message);
|
|
}
|
|
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import PasswordField from "../src/components/PasswordField";
|
|
import PasswordGeneratorDialog from "../src/components/PasswordGeneratorDialog";
|
|
import {
|
|
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
generateSecurePassword,
|
|
type SecureRandomFill
|
|
} from "../src/components/passwordGenerator";
|
|
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
|
|
|
|
function sequenceFill(sequence: number[]): SecureRandomFill {
|
|
let index = 0;
|
|
return (values) => {
|
|
values[0] = sequence[index % sequence.length] >>> 0;
|
|
index += 1;
|
|
return values;
|
|
};
|
|
}
|
|
|
|
const password = generateSecurePassword(
|
|
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
|
|
sequenceFill(Array.from({ length: 64 }, (_, index) => index * 7919))
|
|
);
|
|
assert(password.length === 20, "the requested password length is preserved");
|
|
assert(/[a-z]/.test(password), "selected lowercase characters are represented");
|
|
assert(/[A-Z]/.test(password), "selected uppercase characters are represented");
|
|
assert(/[0-9]/.test(password), "selected digits are represented");
|
|
assert(/[!#$%&*+\-=?@^_]/.test(password), "selected symbols are represented");
|
|
|
|
let rejectedDraws = 0;
|
|
const rejectionAware = generateSecurePassword(
|
|
{ length: 12, lowercase: true, uppercase: false, digits: false, symbols: false },
|
|
(values) => {
|
|
values[0] = rejectedDraws++ % 2 === 0 ? 0xffff_ffff : 7;
|
|
return values;
|
|
}
|
|
);
|
|
assert(rejectionAware.length === 12, "out-of-range random draws are rejected without shortening output");
|
|
assert(rejectedDraws > 12, "modulo-bias rejection consumes a replacement secure draw");
|
|
|
|
for (const [options, expected] of [
|
|
[{ ...DEFAULT_PASSWORD_GENERATOR_OPTIONS, length: 11 }, "between 12 and 128"],
|
|
[{ ...DEFAULT_PASSWORD_GENERATOR_OPTIONS, lowercase: false, uppercase: false, digits: false, symbols: false }, "at least one character set"]
|
|
] as const) {
|
|
let message = "";
|
|
try {
|
|
generateSecurePassword(options, sequenceFill([1]));
|
|
} catch (error) {
|
|
message = error instanceof Error ? error.message : String(error);
|
|
}
|
|
assert(message.includes(expected), `invalid generator options should explain ${expected}`);
|
|
}
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<PlatformLanguageProvider>
|
|
<PasswordField value="" onValueChange={() => undefined} generator helpContextId="access.authentication.password" helpModuleId="access" />
|
|
</PlatformLanguageProvider>
|
|
);
|
|
assert(markup.includes('aria-label="Generate password"'), "the opt-in generator action is accessible");
|
|
assert(markup.includes("lucide-dice-5"), "the familiar generator icon is used");
|
|
assert(!markup.includes("password-generator-dialog"), "the generator dialog stays closed until explicitly requested");
|
|
assert(markup.includes('data-help-context-id="access.authentication.password"'), "the owner context reaches the password field and its actions");
|
|
assert(markup.includes('data-help-module-id="access"'), "the password field retains its documentation owner");
|
|
|
|
const dialogMarkup = renderToStaticMarkup(
|
|
<PlatformLanguageProvider>
|
|
<PasswordGeneratorDialog
|
|
open
|
|
helpContextId="access.authentication.password"
|
|
helpModuleId="access"
|
|
onUse={() => undefined}
|
|
onClose={() => undefined}
|
|
/>
|
|
</PlatformLanguageProvider>
|
|
);
|
|
assert(dialogMarkup.includes('data-help-context-id="access.authentication.password"'), "the generator dialog inherits the calling credential context");
|
|
assert(dialogMarkup.includes('data-help-module-id="access"'), "generated-password controls retain the credential owner's module");
|
|
|
|
console.log("Password generator contract passed.");
|