Files
MineGeo/tests/account.test.ts

117 lines
3.5 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
authActionSchema,
authDestination,
displayNameSchema,
newPasswordSchema,
profileUpdateSchema,
} from "../shared/account";
import {
createRecoveryGrant,
verifyRecoveryGrant,
} from "../shared/password-recovery";
test("display names allow Unicode and spaces, normalize surrounding whitespace, reject hidden controls", () => {
assert.equal(displayNameSchema.parse(" Pavel (: "), "Pavel (:");
assert.equal(displayNameSchema.parse("Исследователь"), "Исследователь");
for (const bad of [
"A",
"a".repeat(33),
"Name\nAdmin",
"Name\u202eadmin",
"\u0000Explorer",
])
assert.equal(displayNameSchema.safeParse(bad).success, false);
assert.equal(
profileUpdateSchema.safeParse({
displayName: "Explorer",
role: "moderator",
}).success,
false,
);
assert.equal(
profileUpdateSchema.safeParse({ displayName: "Explorer", id: "other-user" })
.success,
false,
);
});
test("new passwords require 12 characters and fit bcrypt UTF-8 input without trimming", () => {
assert.equal(newPasswordSchema.safeParse("short").success, false);
assert.equal(
newPasswordSchema.parse(" a passphrase with spaces "),
" a passphrase with spaces ",
);
assert.equal(newPasswordSchema.safeParse("x".repeat(72)).success, true);
assert.equal(newPasswordSchema.safeParse("x".repeat(73)).success, false);
assert.equal(newPasswordSchema.safeParse("🌍".repeat(20)).success, false);
// Existing accounts can log in using their original password policy.
assert.equal(
authActionSchema.safeParse({
action: "login",
email: "user@example.com",
password: "oldpass",
}).success,
true,
);
assert.equal(
authActionSchema.safeParse({
action: "signup",
email: "user@example.com",
password: "oldpass",
displayName: "Explorer",
}).success,
false,
);
});
test("authentication redirects accept supported local routes and reject open redirects", () => {
for (const next of [
"https://evil.example",
"//evil.example",
"/\\evil.example",
"/%2f%2fevil.example",
"/dashboard?next=https://evil.example",
"javascript:alert(1)",
])
assert.equal(authDestination(next), "/dashboard");
assert.equal(authDestination("/settings"), "/settings");
assert.equal(authDestination("/pair?code=1234ABCD"), "/pair?code=1234ABCD");
assert.equal(authDestination("/auth/reset-password"), "/auth/reset-password");
});
test("recovery grants bind account, session, signature and a ten-minute expiry", () => {
const now = 1700000000000,
key = "test-only-secret",
token = createRecoveryGrant("account", "session", key, now);
assert.equal(
verifyRecoveryGrant(token, "account", "session", key, now + 1000),
true,
);
for (const [user, session, secret, time] of [
["other", "session", key, now],
["account", "other", key, now],
["account", "session", "wrong", now],
["account", "session", key, now + 600000],
] as const)
assert.equal(
verifyRecoveryGrant(token, user, session, secret, time),
false,
);
assert.equal(
verifyRecoveryGrant(
token.replace("1700000600", "1700009999"),
"account",
"session",
key,
now,
),
false,
);
assert.equal(
verifyRecoveryGrant(undefined, "account", "session", key, now),
false,
);
assert.equal(
verifyRecoveryGrant("malformed", "account", "session", key, now),
false,
);
});