refactor: strip Phase/Plan/R-number references from source comments
This commit is contained in:
@@ -3,89 +3,86 @@ import { defineContractSuite } from "@repo/core-testing/contract";
|
||||
import type { IUsersRepository } from "../application/repositories/users.repository.interface.js";
|
||||
import { userFactory } from "../__factories__/user.factory.js";
|
||||
|
||||
export const usersRepositoryContract =
|
||||
defineContractSuite<IUsersRepository>(
|
||||
"IUsersRepository",
|
||||
({ buildSubject, getTracer }) => {
|
||||
let repo: IUsersRepository;
|
||||
export const usersRepositoryContract = defineContractSuite<IUsersRepository>(
|
||||
"IUsersRepository",
|
||||
({ buildSubject, getTracer }) => {
|
||||
let repo: IUsersRepository;
|
||||
|
||||
beforeEach(async () => {
|
||||
userFactory.reset();
|
||||
repo = await buildSubject();
|
||||
beforeEach(async () => {
|
||||
userFactory.reset();
|
||||
repo = await buildSubject();
|
||||
});
|
||||
|
||||
// --- getUser ---
|
||||
|
||||
it("createUser then getUser returns it by id", async () => {
|
||||
const seed = userFactory.build();
|
||||
await repo.createUser(seed);
|
||||
const result = await repo.getUser(seed.id);
|
||||
expect(result?.id).toBe(seed.id);
|
||||
expect(result?.username).toBe(seed.username);
|
||||
});
|
||||
|
||||
it("getUser returns undefined for missing id", async () => {
|
||||
expect(await repo.getUser("does-not-exist")).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- getUserByUsername ---
|
||||
|
||||
it("createUser then getUserByUsername returns it by username", async () => {
|
||||
const seed = userFactory.build({ username: "alice" });
|
||||
await repo.createUser(seed);
|
||||
const result = await repo.getUserByUsername("alice");
|
||||
expect(result?.id).toBe(seed.id);
|
||||
expect(result?.username).toBe("alice");
|
||||
});
|
||||
|
||||
it("getUserByUsername returns undefined for missing username", async () => {
|
||||
expect(await repo.getUserByUsername("no-such-user")).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- createUser ---
|
||||
|
||||
it("createUser returns the created user", async () => {
|
||||
const seed = userFactory.build({ username: "carol" });
|
||||
const created = await repo.createUser(seed);
|
||||
expect(created.id).toBe(seed.id);
|
||||
expect(created.username).toBe("carol");
|
||||
});
|
||||
|
||||
describe("span emission", () => {
|
||||
it("getUser emits users.getUser span with id attribute", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
await repo.getUser("nonexistent");
|
||||
const span = tracer.findSpan("users.getUser");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
expect(span!.attributes.id).toBe("nonexistent");
|
||||
});
|
||||
|
||||
// --- getUser ---
|
||||
it("getUserByUsername emits users.getUserByUsername span", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
await repo.getUserByUsername("alice");
|
||||
const span = tracer.findSpan("users.getUserByUsername");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
});
|
||||
|
||||
it("createUser then getUser returns it by id", async () => {
|
||||
const seed = userFactory.build();
|
||||
it("createUser emits users.createUser span with id attribute", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
const seed = userFactory.build({ username: "span-test-user" });
|
||||
await repo.createUser(seed);
|
||||
const result = await repo.getUser(seed.id);
|
||||
expect(result?.id).toBe(seed.id);
|
||||
expect(result?.username).toBe(seed.username);
|
||||
const span = tracer.findSpan("users.createUser");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
expect(span!.attributes.id).toBe(seed.id);
|
||||
});
|
||||
|
||||
it("getUser returns undefined for missing id", async () => {
|
||||
expect(await repo.getUser("does-not-exist")).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- getUserByUsername ---
|
||||
|
||||
it("createUser then getUserByUsername returns it by username", async () => {
|
||||
const seed = userFactory.build({ username: "alice" });
|
||||
await repo.createUser(seed);
|
||||
const result = await repo.getUserByUsername("alice");
|
||||
expect(result?.id).toBe(seed.id);
|
||||
expect(result?.username).toBe("alice");
|
||||
});
|
||||
|
||||
it("getUserByUsername returns undefined for missing username", async () => {
|
||||
expect(
|
||||
await repo.getUserByUsername("no-such-user"),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
// --- createUser ---
|
||||
|
||||
it("createUser returns the created user", async () => {
|
||||
const seed = userFactory.build({ username: "carol" });
|
||||
const created = await repo.createUser(seed);
|
||||
expect(created.id).toBe(seed.id);
|
||||
expect(created.username).toBe("carol");
|
||||
});
|
||||
|
||||
describe("span emission (R50)", () => {
|
||||
it("getUser emits users.getUser span with id attribute", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
await repo.getUser("nonexistent");
|
||||
const span = tracer.findSpan("users.getUser");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
expect(span!.attributes.id).toBe("nonexistent");
|
||||
});
|
||||
|
||||
it("getUserByUsername emits users.getUserByUsername span", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
await repo.getUserByUsername("alice");
|
||||
const span = tracer.findSpan("users.getUserByUsername");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
});
|
||||
|
||||
it("createUser emits users.createUser span with id attribute", async () => {
|
||||
if (!getTracer) return;
|
||||
const tracer = getTracer();
|
||||
tracer.reset();
|
||||
const seed = userFactory.build({ username: "span-test-user" });
|
||||
await repo.createUser(seed);
|
||||
const span = tracer.findSpan("users.createUser");
|
||||
expect(span).toBeDefined();
|
||||
expect(span!.op).toBe("repository");
|
||||
expect(span!.attributes.id).toBe(seed.id);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
import { signInUseCase, signInOutputSchema } from "@/application/use-cases/sign-in.use-case";
|
||||
import {
|
||||
signInUseCase,
|
||||
signInOutputSchema,
|
||||
} from "@/application/use-cases/sign-in.use-case";
|
||||
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
|
||||
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
|
||||
import { AuthenticationError } from "@/entities/errors/auth";
|
||||
@@ -18,7 +21,10 @@ describe("signInUseCase", () => {
|
||||
await users.createUser(seedUser);
|
||||
|
||||
const useCase = signInUseCase(users, auth);
|
||||
const result = await useCase({ username: "alice", password: "testpassword" });
|
||||
const result = await useCase({
|
||||
username: "alice",
|
||||
password: "testpassword",
|
||||
});
|
||||
|
||||
expect(result.session.userId).toBe(seedUser.id);
|
||||
expect(result.cookie.name).toBe("session");
|
||||
@@ -38,7 +44,10 @@ describe("signInUseCase", () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
await users.createUser(
|
||||
userFactory.build({ username: "alice", passwordHash: "hashed_correctpassword" }),
|
||||
userFactory.build({
|
||||
username: "alice",
|
||||
passwordHash: "hashed_correctpassword",
|
||||
}),
|
||||
);
|
||||
|
||||
const useCase = signInUseCase(users, auth);
|
||||
@@ -48,7 +57,7 @@ describe("signInUseCase", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("signInUseCase output validation (R25)", () => {
|
||||
describe("signInUseCase output validation", () => {
|
||||
it("throws when authenticationService returns a malformed session", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const seed = userFactory.build({ username: "alice" });
|
||||
@@ -61,7 +70,9 @@ describe("signInUseCase output validation (R25)", () => {
|
||||
} as unknown as IAuthenticationService;
|
||||
|
||||
const useCase = signInUseCase(users, auth);
|
||||
await expect(useCase({ username: "alice", password: "x" })).rejects.toBeInstanceOf(ZodError);
|
||||
await expect(
|
||||
useCase({ username: "alice", password: "x" }),
|
||||
).rejects.toBeInstanceOf(ZodError);
|
||||
});
|
||||
|
||||
it("exports an output schema that mirrors the success shape", () => {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
import { RecordingEventBus } from "@repo/core-testing/instrumentation";
|
||||
import { signUpUseCase, signUpOutputSchema } from "@/application/use-cases/sign-up.use-case";
|
||||
import {
|
||||
signUpUseCase,
|
||||
signUpOutputSchema,
|
||||
} from "@/application/use-cases/sign-up.use-case";
|
||||
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
|
||||
import { MockAuthenticationService } from "@/infrastructure/services/authentication.service.mock";
|
||||
import { AuthenticationError } from "@/entities/errors/auth";
|
||||
@@ -33,7 +36,11 @@ describe("signUpUseCase", () => {
|
||||
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
await expect(
|
||||
useCase({ username: "alice", password: "secret_password", confirmPassword: "secret_password" }),
|
||||
useCase({
|
||||
username: "alice",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(AuthenticationError);
|
||||
});
|
||||
|
||||
@@ -83,13 +90,17 @@ describe("signUpUseCase", () => {
|
||||
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
await expect(
|
||||
useCase({ username: "eve", password: "secret_password", confirmPassword: "secret_password" }),
|
||||
useCase({
|
||||
username: "eve",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(AuthenticationError);
|
||||
expect(bus.published).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signUpUseCase output validation (R25)", () => {
|
||||
describe("signUpUseCase output validation", () => {
|
||||
it("throws when authenticationService returns a malformed session", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = {
|
||||
@@ -103,7 +114,11 @@ describe("signUpUseCase output validation (R25)", () => {
|
||||
const bus = new RecordingEventBus();
|
||||
const useCase = signUpUseCase(users, auth, bus);
|
||||
await expect(
|
||||
useCase({ username: "carol", password: "secret_password", confirmPassword: "secret_password" }),
|
||||
useCase({
|
||||
username: "carol",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(ZodError);
|
||||
});
|
||||
|
||||
|
||||
@@ -39,12 +39,16 @@ export const signUpUseCase =
|
||||
bus: EventBusProtocol | undefined,
|
||||
) =>
|
||||
async (input: SignUpInput): Promise<SignUpOutput> => {
|
||||
const existingUser = await usersRepository.getUserByUsername(input.username);
|
||||
const existingUser = await usersRepository.getUserByUsername(
|
||||
input.username,
|
||||
);
|
||||
if (existingUser) {
|
||||
throw new AuthenticationError("Username taken");
|
||||
}
|
||||
|
||||
const passwordHash = await authenticationService.hashPassword(input.password);
|
||||
const passwordHash = await authenticationService.hashPassword(
|
||||
input.password,
|
||||
);
|
||||
const userId = authenticationService.generateUserId();
|
||||
|
||||
const newUser = await usersRepository.createUser({
|
||||
@@ -53,11 +57,12 @@ export const signUpUseCase =
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
const { cookie, session } = await authenticationService.createSession(newUser);
|
||||
const { cookie, session } =
|
||||
await authenticationService.createSession(newUser);
|
||||
|
||||
// Auth is username-based — synthesize a deterministic email so the event
|
||||
// payload validates against userSignedUpEventSchema.email().
|
||||
// bus is optional: absent when core-events is not wired (Phase 3+).
|
||||
// bus is optional: absent when core-events is not wired.
|
||||
if (bus) {
|
||||
await bus.publish(userSignedUpEvent, {
|
||||
userId: newUser.id,
|
||||
|
||||
@@ -44,8 +44,12 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
|
||||
authContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
|
||||
}
|
||||
authContainer.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
|
||||
authContainer.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
|
||||
authContainer
|
||||
.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER)
|
||||
.toConstantValue(tracer);
|
||||
authContainer
|
||||
.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER)
|
||||
.toConstantValue(logger);
|
||||
|
||||
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
|
||||
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
|
||||
@@ -61,7 +65,9 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
.toConstantValue(repo);
|
||||
|
||||
// Need auth service from container for use cases
|
||||
const authService = authContainer.get<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService);
|
||||
const authService = authContainer.get<IAuthenticationService>(
|
||||
AUTH_SYMBOLS.IAuthenticationService,
|
||||
);
|
||||
|
||||
// Wrap use cases + controllers identically to bind-production
|
||||
const wrappedSignIn = withSpan(
|
||||
@@ -102,9 +108,15 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
]) {
|
||||
if (authContainer.isBound(sym)) authContainer.unbind(sym);
|
||||
}
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignInUseCase).toConstantValue(wrappedSignIn);
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignUpUseCase).toConstantValue(wrappedSignUp);
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignOutUseCase).toConstantValue(wrappedSignOut);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignInUseCase)
|
||||
.toConstantValue(wrappedSignIn);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignUpUseCase)
|
||||
.toConstantValue(wrappedSignUp);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignOutUseCase)
|
||||
.toConstantValue(wrappedSignOut);
|
||||
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignInController)
|
||||
@@ -145,8 +157,7 @@ export async function bindDevSeedAuth(ctx: BindContext): Promise<void> {
|
||||
),
|
||||
),
|
||||
);
|
||||
// bus + queue are accept-and-forward in Phase 6; consumed by Phase 7 generator
|
||||
// output at the <gen:event-handlers> / <gen:jobs> anchors below.
|
||||
// bus + queue are passed through; generated handlers consume them at the anchors below.
|
||||
void bus;
|
||||
void queue;
|
||||
void realtime;
|
||||
|
||||
@@ -33,7 +33,8 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
|
||||
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
|
||||
const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } =
|
||||
ctx;
|
||||
|
||||
// Bind shared instrumentation into feature container
|
||||
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) {
|
||||
@@ -42,15 +43,21 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
if (authContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) {
|
||||
authContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER);
|
||||
}
|
||||
authContainer.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer);
|
||||
authContainer.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger);
|
||||
authContainer
|
||||
.bind<ITracer>(INSTRUMENTATION_SYMBOLS.TRACER)
|
||||
.toConstantValue(tracer);
|
||||
authContainer
|
||||
.bind<ILogger>(INSTRUMENTATION_SYMBOLS.LOGGER)
|
||||
.toConstantValue(logger);
|
||||
|
||||
// Real repositories
|
||||
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
|
||||
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
|
||||
}
|
||||
const repo = new UsersRepository(config, tracer, logger);
|
||||
authContainer.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).toConstantValue(repo);
|
||||
authContainer
|
||||
.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
|
||||
.toConstantValue(repo);
|
||||
|
||||
if (authContainer.isBound(AUTH_SYMBOLS.IAuthenticationService)) {
|
||||
authContainer.unbind(AUTH_SYMBOLS.IAuthenticationService);
|
||||
@@ -100,9 +107,15 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
]) {
|
||||
if (authContainer.isBound(sym)) authContainer.unbind(sym);
|
||||
}
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignInUseCase).toConstantValue(wrappedSignIn);
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignUpUseCase).toConstantValue(wrappedSignUp);
|
||||
authContainer.bind(AUTH_SYMBOLS.ISignOutUseCase).toConstantValue(wrappedSignOut);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignInUseCase)
|
||||
.toConstantValue(wrappedSignIn);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignUpUseCase)
|
||||
.toConstantValue(wrappedSignUp);
|
||||
authContainer
|
||||
.bind(AUTH_SYMBOLS.ISignOutUseCase)
|
||||
.toConstantValue(wrappedSignOut);
|
||||
|
||||
// Controllers — wrapped with span at bind time
|
||||
for (const sym of [
|
||||
@@ -151,8 +164,7 @@ export function bindProductionAuth(ctx: BindProductionContext): void {
|
||||
),
|
||||
),
|
||||
);
|
||||
// bus + queue are accept-and-forward in Phase 6; consumed by Phase 7 generator
|
||||
// output at the <gen:event-handlers> / <gen:jobs> anchors below.
|
||||
// bus + queue are passed through; generated handlers consume them at the anchors below.
|
||||
void bus;
|
||||
void queue;
|
||||
void realtime;
|
||||
|
||||
@@ -10,7 +10,7 @@ export {
|
||||
export { InputParseError } from "./entities/errors/common";
|
||||
export { SESSION_COOKIE } from "./config";
|
||||
|
||||
// Use case schemas + types (Plan 9 R18)
|
||||
// Use case schemas + types
|
||||
export {
|
||||
signInInputSchema,
|
||||
signInOutputSchema,
|
||||
@@ -44,7 +44,6 @@ export {
|
||||
} from "./events/user-signed-up.event";
|
||||
// <gen:realtime-channels>
|
||||
|
||||
// Feature conformance manifest (added in conformance milestone i, exposed
|
||||
// here in milestone ii so the boot-time assertion and future tooling can
|
||||
// read the contract from the package boundary).
|
||||
// Feature conformance manifest — declares this feature's use cases, audits,
|
||||
// publishes, and consumes. Read by the boot-time assertion + ESLint rules.
|
||||
export { authManifest, type AuthManifest } from "./feature.manifest";
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
|
||||
import {
|
||||
RecordingTracer,
|
||||
RecordingLogger,
|
||||
} from "@repo/core-testing/instrumentation";
|
||||
import { MockUsersRepository } from "@/infrastructure/repositories/users.repository.mock";
|
||||
|
||||
// Mock repo also wraps in spans (R42); easier to assert without booting Payload.
|
||||
describe("MockUsersRepository emits spans (R42)", () => {
|
||||
// Mock repo also wraps in spans; easier to assert without booting Payload.
|
||||
describe("MockUsersRepository emits spans", () => {
|
||||
it("getUser emits one span with op='repository'", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
const logger = new RecordingLogger();
|
||||
@@ -26,13 +29,19 @@ describe("MockUsersRepository emits spans (R42)", () => {
|
||||
);
|
||||
await repo.getUserByUsername("alice");
|
||||
expect(tracer.findSpan("users.getUserByUsername")).toBeDefined();
|
||||
expect(tracer.findSpan("users.getUserByUsername")!.attributes.found).toBe(true);
|
||||
expect(tracer.findSpan("users.getUserByUsername")!.attributes.found).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("createUser records created=true", async () => {
|
||||
const tracer = new RecordingTracer();
|
||||
const repo = new MockUsersRepository([], tracer);
|
||||
await repo.createUser({ id: "u1", username: "charlie", passwordHash: "hash" });
|
||||
await repo.createUser({
|
||||
id: "u1",
|
||||
username: "charlie",
|
||||
passwordHash: "hash",
|
||||
});
|
||||
expect(tracer.findSpan("users.createUser")).toBeDefined();
|
||||
expect(tracer.findSpan("users.createUser")!.attributes.created).toBe(true);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { User } from "../../entities/models/user";
|
||||
// generic session interface without deep integration with Payload's REST/local
|
||||
// API and cookie infrastructure.
|
||||
//
|
||||
// TODO(lazar-conformance §7): Implement these three methods once the session
|
||||
// TODO: Implement these three methods once the session
|
||||
// cookie strategy is settled. Until then they throw NotImplementedError to
|
||||
// keep the production-shaped file in place without silently no-oping.
|
||||
//
|
||||
@@ -21,7 +21,7 @@ import type { User } from "../../entities/models/user";
|
||||
|
||||
class NotImplementedError extends Error {
|
||||
constructor(method: string) {
|
||||
super(`NotImplemented: AuthenticationService.${method} — see refactor log §7`);
|
||||
super(`NotImplemented: AuthenticationService.${method}`);
|
||||
this.name = "NotImplementedError";
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,17 @@ export class AuthenticationService implements IAuthenticationService {
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
const salt = crypto.randomBytes(SALT_LENGTH).toString("hex");
|
||||
const hash = await new Promise<string>((resolve, reject) => {
|
||||
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
else resolve(derivedKey.toString("hex"));
|
||||
});
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
ITERATIONS,
|
||||
KEY_LENGTH,
|
||||
DIGEST,
|
||||
(err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
else resolve(derivedKey.toString("hex"));
|
||||
},
|
||||
);
|
||||
});
|
||||
return `${salt}${SEPARATOR}${hash}`;
|
||||
}
|
||||
@@ -56,10 +63,17 @@ export class AuthenticationService implements IAuthenticationService {
|
||||
const salt = parts[0]!;
|
||||
const expectedHash = parts[1]!;
|
||||
const actualHash = await new Promise<string>((resolve, reject) => {
|
||||
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
else resolve(derivedKey.toString("hex"));
|
||||
});
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
ITERATIONS,
|
||||
KEY_LENGTH,
|
||||
DIGEST,
|
||||
(err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
else resolve(derivedKey.toString("hex"));
|
||||
},
|
||||
);
|
||||
});
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expectedHash, "hex"),
|
||||
@@ -67,23 +81,27 @@ export class AuthenticationService implements IAuthenticationService {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO(lazar-conformance §7): Implement using Payload's local.login / JWT session issuance.
|
||||
// TODO:Implement using Payload's local.login / JWT session issuance.
|
||||
// Payload creates sessions via its REST auth endpoint; mapping that to a
|
||||
// generic { session: Session; cookie: Cookie } shape requires understanding
|
||||
// the JWT payload structure and the cookie name/attributes Payload uses.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async createSession(user: User): Promise<{ session: Session; cookie: Cookie }> {
|
||||
async createSession(
|
||||
user: User,
|
||||
): Promise<{ session: Session; cookie: Cookie }> {
|
||||
throw new NotImplementedError("createSession");
|
||||
}
|
||||
|
||||
// TODO(lazar-conformance §7): Implement using Payload's JWT verify mechanism.
|
||||
// TODO:Implement using Payload's JWT verify mechanism.
|
||||
// Need to call Payload's local API to verify the token and retrieve the user.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async validateSession(sessionId: string): Promise<{ user: User; session: Session }> {
|
||||
async validateSession(
|
||||
sessionId: string,
|
||||
): Promise<{ user: User; session: Session }> {
|
||||
throw new NotImplementedError("validateSession");
|
||||
}
|
||||
|
||||
// TODO(lazar-conformance §7): Implement by clearing the session token.
|
||||
// TODO:Implement by clearing the session token.
|
||||
// Payload does not have a server-side session store by default; invalidation
|
||||
// is typically done client-side by clearing the cookie.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("authRouter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("authRouter (R26 error mapping)", () => {
|
||||
describe("authRouter error mapping", () => {
|
||||
beforeEach(() => {
|
||||
if (authContainer.isBound(AUTH_SYMBOLS.IUsersRepository)) {
|
||||
authContainer.unbind(AUTH_SYMBOLS.IUsersRepository);
|
||||
@@ -39,7 +39,9 @@ describe("authRouter (R26 error mapping)", () => {
|
||||
}
|
||||
const users = new MockUsersRepository();
|
||||
const auth = new MockAuthenticationService(users);
|
||||
authContainer.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository).toConstantValue(users);
|
||||
authContainer
|
||||
.bind<IUsersRepository>(AUTH_SYMBOLS.IUsersRepository)
|
||||
.toConstantValue(users);
|
||||
authContainer
|
||||
.bind<IAuthenticationService>(AUTH_SYMBOLS.IAuthenticationService)
|
||||
.toConstantValue(auth);
|
||||
|
||||
Reference in New Issue
Block a user