Update paths in sync.sh, master/main.go, and CLAUDE.md to reflect the directory rename. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
// Tests for auth/store.ts (InMemoryAuthStore)
|
|
// Pure unit tests - no database needed
|
|
|
|
import assert from "node:assert/strict";
|
|
import { after, before, beforeEach, describe, it } from "node:test";
|
|
import type { UserId } from "../user";
|
|
import { InMemoryAuthStore } from "./store";
|
|
import { hashToken } from "./token";
|
|
import type { TokenId } from "./types";
|
|
|
|
describe("InMemoryAuthStore", () => {
|
|
let store: InMemoryAuthStore;
|
|
|
|
beforeEach(() => {
|
|
store = new InMemoryAuthStore();
|
|
});
|
|
|
|
describe("createUser", () => {
|
|
it("creates a user with pending status", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
displayName: "Test User",
|
|
});
|
|
|
|
assert.equal(user.email, "test@example.com");
|
|
assert.equal(user.displayName, "Test User");
|
|
assert.equal(user.status, "pending");
|
|
});
|
|
|
|
it("creates a user without displayName", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
assert.equal(user.email, "test@example.com");
|
|
assert.equal(user.displayName, undefined);
|
|
});
|
|
|
|
it("generates a unique id", async () => {
|
|
const user1 = await store.createUser({
|
|
email: "test1@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
const user2 = await store.createUser({
|
|
email: "test2@example.com",
|
|
passwordHash: "hash456",
|
|
});
|
|
|
|
assert.notEqual(user1.id, user2.id);
|
|
});
|
|
});
|
|
|
|
describe("getUserByEmail", () => {
|
|
it("returns user when found", async () => {
|
|
await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const user = await store.getUserByEmail("test@example.com");
|
|
assert.notEqual(user, null);
|
|
assert.equal(user!.email, "test@example.com");
|
|
});
|
|
|
|
it("is case-insensitive", async () => {
|
|
await store.createUser({
|
|
email: "Test@Example.COM",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const user = await store.getUserByEmail("test@example.com");
|
|
assert.notEqual(user, null);
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
const user = await store.getUserByEmail("notfound@example.com");
|
|
assert.equal(user, null);
|
|
});
|
|
});
|
|
|
|
describe("getUserById", () => {
|
|
it("returns user when found", async () => {
|
|
const created = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const user = await store.getUserById(created.id);
|
|
assert.notEqual(user, null);
|
|
assert.equal(user!.id, created.id);
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
const user = await store.getUserById("nonexistent" as UserId);
|
|
assert.equal(user, null);
|
|
});
|
|
});
|
|
|
|
describe("getUserPasswordHash", () => {
|
|
it("returns hash when found", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const hash = await store.getUserPasswordHash(user.id);
|
|
assert.equal(hash, "hash123");
|
|
});
|
|
|
|
it("returns null when not found", async () => {
|
|
const hash = await store.getUserPasswordHash(
|
|
"nonexistent" as UserId,
|
|
);
|
|
assert.equal(hash, null);
|
|
});
|
|
});
|
|
|
|
describe("setUserPassword", () => {
|
|
it("updates password hash", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "oldhash",
|
|
});
|
|
|
|
await store.setUserPassword(user.id, "newhash");
|
|
|
|
const hash = await store.getUserPasswordHash(user.id);
|
|
assert.equal(hash, "newhash");
|
|
});
|
|
});
|
|
|
|
describe("updateUserEmailVerified", () => {
|
|
it("sets user status to active", async () => {
|
|
const created = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
assert.equal(created.status, "pending");
|
|
|
|
await store.updateUserEmailVerified(created.id);
|
|
|
|
const user = await store.getUserById(created.id);
|
|
assert.equal(user!.status, "active");
|
|
});
|
|
});
|
|
|
|
describe("createSession", () => {
|
|
it("creates a session with token", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token, session } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
assert.ok(token.length > 0);
|
|
assert.equal(session.userId, user.id);
|
|
assert.equal(session.tokenType, "session");
|
|
assert.equal(session.authMethod, "cookie");
|
|
});
|
|
|
|
it("stores metadata", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { session } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
userAgent: "Mozilla/5.0",
|
|
ipAddress: "127.0.0.1",
|
|
});
|
|
|
|
assert.equal(session.userAgent, "Mozilla/5.0");
|
|
assert.equal(session.ipAddress, "127.0.0.1");
|
|
});
|
|
});
|
|
|
|
describe("getSession", () => {
|
|
it("returns session when found and not expired", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000), // 1 hour from now
|
|
});
|
|
|
|
const tokenId = hashToken(token) as TokenId;
|
|
const session = await store.getSession(tokenId);
|
|
assert.notEqual(session, null);
|
|
assert.equal(session!.userId, user.id);
|
|
});
|
|
|
|
it("returns null for expired session", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() - 1000), // Expired 1 second ago
|
|
});
|
|
|
|
const tokenId = hashToken(token) as TokenId;
|
|
const session = await store.getSession(tokenId);
|
|
assert.equal(session, null);
|
|
});
|
|
|
|
it("returns null for nonexistent session", async () => {
|
|
const session = await store.getSession("nonexistent" as TokenId);
|
|
assert.equal(session, null);
|
|
});
|
|
});
|
|
|
|
describe("deleteSession", () => {
|
|
it("removes the session", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
const tokenId = hashToken(token) as TokenId;
|
|
await store.deleteSession(tokenId);
|
|
|
|
const session = await store.getSession(tokenId);
|
|
assert.equal(session, null);
|
|
});
|
|
});
|
|
|
|
describe("deleteUserSessions", () => {
|
|
it("removes all sessions for user", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token: token1 } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
const { token: token2 } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "bearer",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
const count = await store.deleteUserSessions(user.id);
|
|
assert.equal(count, 2);
|
|
|
|
const session1 = await store.getSession(
|
|
hashToken(token1) as TokenId,
|
|
);
|
|
const session2 = await store.getSession(
|
|
hashToken(token2) as TokenId,
|
|
);
|
|
assert.equal(session1, null);
|
|
assert.equal(session2, null);
|
|
});
|
|
|
|
it("returns 0 when user has no sessions", async () => {
|
|
const count = await store.deleteUserSessions(
|
|
"nonexistent" as UserId,
|
|
);
|
|
assert.equal(count, 0);
|
|
});
|
|
});
|
|
|
|
describe("updateLastUsed", () => {
|
|
it("updates lastUsedAt timestamp", async () => {
|
|
const user = await store.createUser({
|
|
email: "test@example.com",
|
|
passwordHash: "hash123",
|
|
});
|
|
|
|
const { token } = await store.createSession({
|
|
userId: user.id,
|
|
tokenType: "session",
|
|
authMethod: "cookie",
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
});
|
|
|
|
const tokenId = hashToken(token) as TokenId;
|
|
const beforeUpdate = await store.getSession(tokenId);
|
|
assert.equal(beforeUpdate!.lastUsedAt, undefined);
|
|
|
|
await store.updateLastUsed(tokenId);
|
|
|
|
const afterUpdate = await store.getSession(tokenId);
|
|
assert.ok(afterUpdate!.lastUsedAt instanceof Date);
|
|
});
|
|
});
|
|
});
|