420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
// Tests for auth/service.ts
|
|
// Uses InMemoryAuthStore - no database needed
|
|
|
|
import assert from "node:assert/strict";
|
|
import { beforeEach, describe, it } from "node:test";
|
|
import { AuthService } from "./service";
|
|
import { InMemoryAuthStore } from "./store";
|
|
|
|
describe("AuthService", () => {
|
|
let store: InMemoryAuthStore;
|
|
let service: AuthService;
|
|
|
|
beforeEach(() => {
|
|
store = new InMemoryAuthStore();
|
|
service = new AuthService(store);
|
|
});
|
|
|
|
describe("register", () => {
|
|
it("creates a new user", async () => {
|
|
const result = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
"Test User",
|
|
);
|
|
|
|
assert.equal(result.success, true);
|
|
if (result.success) {
|
|
assert.equal(result.user.email, "test@example.com");
|
|
assert.equal(result.user.displayName, "Test User");
|
|
assert.ok(result.verificationToken.length > 0);
|
|
}
|
|
});
|
|
|
|
it("fails when email already registered", async () => {
|
|
await service.register("test@example.com", "password123");
|
|
const result = await service.register(
|
|
"test@example.com",
|
|
"password456",
|
|
);
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(result.error, "Email already registered");
|
|
}
|
|
});
|
|
|
|
it("creates user without displayName", async () => {
|
|
const result = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
|
|
assert.equal(result.success, true);
|
|
if (result.success) {
|
|
assert.equal(result.user.displayName, undefined);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("login", () => {
|
|
beforeEach(async () => {
|
|
// Create and verify a user
|
|
const result = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
"Test User",
|
|
);
|
|
if (result.success) {
|
|
// Verify email to activate user
|
|
await service.verifyEmail(result.verificationToken);
|
|
}
|
|
});
|
|
|
|
it("succeeds with correct credentials", async () => {
|
|
const result = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
|
|
assert.equal(result.success, true);
|
|
if (result.success) {
|
|
assert.ok(result.token.length > 0);
|
|
assert.equal(result.user.email, "test@example.com");
|
|
}
|
|
});
|
|
|
|
it("fails with wrong password", async () => {
|
|
const result = await service.login(
|
|
"test@example.com",
|
|
"wrongpassword",
|
|
"cookie",
|
|
);
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(result.error, "Invalid credentials");
|
|
}
|
|
});
|
|
|
|
it("fails with unknown email", async () => {
|
|
const result = await service.login(
|
|
"unknown@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(result.error, "Invalid credentials");
|
|
}
|
|
});
|
|
|
|
it("fails for inactive user", async () => {
|
|
// Create a user but don't verify email (stays pending)
|
|
await service.register("pending@example.com", "password123");
|
|
|
|
const result = await service.login(
|
|
"pending@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(result.error, "Account is not active");
|
|
}
|
|
});
|
|
|
|
it("stores metadata", async () => {
|
|
const result = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
{ userAgent: "TestAgent", ipAddress: "192.168.1.1" },
|
|
);
|
|
|
|
assert.equal(result.success, true);
|
|
});
|
|
});
|
|
|
|
describe("validateToken", () => {
|
|
let token: string;
|
|
|
|
beforeEach(async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
if (regResult.success) {
|
|
await service.verifyEmail(regResult.verificationToken);
|
|
}
|
|
|
|
const loginResult = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
if (loginResult.success) {
|
|
token = loginResult.token;
|
|
}
|
|
});
|
|
|
|
it("returns authenticated for valid token", async () => {
|
|
const result = await service.validateToken(token);
|
|
|
|
assert.equal(result.authenticated, true);
|
|
if (result.authenticated) {
|
|
assert.equal(result.user.email, "test@example.com");
|
|
assert.notEqual(result.session, null);
|
|
}
|
|
});
|
|
|
|
it("returns unauthenticated for invalid token", async () => {
|
|
const result = await service.validateToken("invalid-token");
|
|
|
|
assert.equal(result.authenticated, false);
|
|
assert.equal(result.user.isAnonymous(), true);
|
|
assert.equal(result.session, null);
|
|
});
|
|
});
|
|
|
|
describe("logout", () => {
|
|
it("invalidates the session", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
if (regResult.success) {
|
|
await service.verifyEmail(regResult.verificationToken);
|
|
}
|
|
|
|
const loginResult = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
assert.equal(loginResult.success, true);
|
|
if (!loginResult.success) return;
|
|
|
|
const token = loginResult.token;
|
|
|
|
// Token should be valid before logout
|
|
const beforeLogout = await service.validateToken(token);
|
|
assert.equal(beforeLogout.authenticated, true);
|
|
|
|
// Logout
|
|
await service.logout(token);
|
|
|
|
// Token should be invalid after logout
|
|
const afterLogout = await service.validateToken(token);
|
|
assert.equal(afterLogout.authenticated, false);
|
|
});
|
|
});
|
|
|
|
describe("logoutAllSessions", () => {
|
|
it("invalidates all user sessions", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
if (regResult.success) {
|
|
await service.verifyEmail(regResult.verificationToken);
|
|
}
|
|
|
|
// Create multiple sessions
|
|
const login1 = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
const login2 = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"bearer",
|
|
);
|
|
|
|
assert.equal(login1.success, true);
|
|
assert.equal(login2.success, true);
|
|
if (!login1.success || !login2.success) return;
|
|
|
|
// Both should be valid
|
|
const before1 = await service.validateToken(login1.token);
|
|
const before2 = await service.validateToken(login2.token);
|
|
assert.equal(before1.authenticated, true);
|
|
assert.equal(before2.authenticated, true);
|
|
|
|
// Logout all
|
|
const user = await store.getUserByEmail("test@example.com");
|
|
const count = await service.logoutAllSessions(user!.id);
|
|
assert.equal(count, 2);
|
|
|
|
// Both should be invalid
|
|
const after1 = await service.validateToken(login1.token);
|
|
const after2 = await service.validateToken(login2.token);
|
|
assert.equal(after1.authenticated, false);
|
|
assert.equal(after2.authenticated, false);
|
|
});
|
|
});
|
|
|
|
describe("verifyEmail", () => {
|
|
it("activates user with valid token", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
assert.equal(regResult.success, true);
|
|
if (!regResult.success) return;
|
|
|
|
const result = await service.verifyEmail(
|
|
regResult.verificationToken,
|
|
);
|
|
assert.equal(result.success, true);
|
|
|
|
// User should now be active and can login
|
|
const loginResult = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
assert.equal(loginResult.success, true);
|
|
});
|
|
|
|
it("fails with invalid token", async () => {
|
|
const result = await service.verifyEmail("invalid-token");
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(
|
|
result.error,
|
|
"Invalid or expired verification token",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("fails when token already used", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
assert.equal(regResult.success, true);
|
|
if (!regResult.success) return;
|
|
|
|
// First verification succeeds
|
|
const result1 = await service.verifyEmail(
|
|
regResult.verificationToken,
|
|
);
|
|
assert.equal(result1.success, true);
|
|
|
|
// Second verification fails (token deleted)
|
|
const result2 = await service.verifyEmail(
|
|
regResult.verificationToken,
|
|
);
|
|
assert.equal(result2.success, false);
|
|
});
|
|
});
|
|
|
|
describe("createPasswordResetToken", () => {
|
|
it("returns token for existing user", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
assert.equal(regResult.success, true);
|
|
|
|
const result =
|
|
await service.createPasswordResetToken("test@example.com");
|
|
assert.notEqual(result, null);
|
|
assert.ok(result!.token.length > 0);
|
|
});
|
|
|
|
it("returns null for unknown email", async () => {
|
|
const result = await service.createPasswordResetToken(
|
|
"unknown@example.com",
|
|
);
|
|
assert.equal(result, null);
|
|
});
|
|
});
|
|
|
|
describe("resetPassword", () => {
|
|
it("changes password with valid token", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"oldpassword",
|
|
);
|
|
if (regResult.success) {
|
|
await service.verifyEmail(regResult.verificationToken);
|
|
}
|
|
|
|
const resetToken =
|
|
await service.createPasswordResetToken("test@example.com");
|
|
assert.notEqual(resetToken, null);
|
|
|
|
const result = await service.resetPassword(
|
|
resetToken!.token,
|
|
"newpassword",
|
|
);
|
|
assert.equal(result.success, true);
|
|
|
|
// Old password should no longer work
|
|
const loginOld = await service.login(
|
|
"test@example.com",
|
|
"oldpassword",
|
|
"cookie",
|
|
);
|
|
assert.equal(loginOld.success, false);
|
|
|
|
// New password should work
|
|
const loginNew = await service.login(
|
|
"test@example.com",
|
|
"newpassword",
|
|
"cookie",
|
|
);
|
|
assert.equal(loginNew.success, true);
|
|
});
|
|
|
|
it("fails with invalid token", async () => {
|
|
const result = await service.resetPassword(
|
|
"invalid-token",
|
|
"newpassword",
|
|
);
|
|
|
|
assert.equal(result.success, false);
|
|
if (!result.success) {
|
|
assert.equal(result.error, "Invalid or expired reset token");
|
|
}
|
|
});
|
|
|
|
it("invalidates all existing sessions", async () => {
|
|
const regResult = await service.register(
|
|
"test@example.com",
|
|
"password123",
|
|
);
|
|
if (regResult.success) {
|
|
await service.verifyEmail(regResult.verificationToken);
|
|
}
|
|
|
|
// Create a session
|
|
const loginResult = await service.login(
|
|
"test@example.com",
|
|
"password123",
|
|
"cookie",
|
|
);
|
|
assert.equal(loginResult.success, true);
|
|
if (!loginResult.success) return;
|
|
|
|
const sessionToken = loginResult.token;
|
|
|
|
// Reset password
|
|
const resetToken =
|
|
await service.createPasswordResetToken("test@example.com");
|
|
await service.resetPassword(resetToken!.token, "newpassword");
|
|
|
|
// Old session should be invalid
|
|
const validateResult = await service.validateToken(sessionToken);
|
|
assert.equal(validateResult.authenticated, false);
|
|
});
|
|
});
|
|
});
|