chore: upgrade Node.js runtime to v24 across codebase #419

Merged
jason.woltje merged 438 commits from fix/auth-frontend-remediation into main 2026-02-17 01:04:47 +00:00
2 changed files with 101 additions and 2 deletions
Showing only changes of commit c38271da3b - Show all commits

View File

@@ -0,0 +1,96 @@
import { describe, it, expect } from "vitest";
import { ExecutionContext, UnauthorizedException } from "@nestjs/common";
import { ROUTE_ARGS_METADATA } from "@nestjs/common/constants";
import { CurrentUser } from "./current-user.decorator";
import type { AuthUser } from "@mosaic/shared";
/**
* Extract the factory function from a NestJS param decorator created with createParamDecorator.
* NestJS stores param decorator factories in metadata on a dummy class.
*/
function getParamDecoratorFactory(): (data: unknown, ctx: ExecutionContext) => AuthUser {
class TestController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
testMethod(@CurrentUser() _user: AuthUser): void {
// no-op
}
}
const metadata = Reflect.getMetadata(ROUTE_ARGS_METADATA, TestController, "testMethod");
// The metadata keys are in the format "paramtype:index"
const key = Object.keys(metadata)[0];
return metadata[key].factory;
}
function createMockExecutionContext(user?: AuthUser): ExecutionContext {
const mockRequest = {
...(user !== undefined ? { user } : {}),
};
return {
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as ExecutionContext;
}
describe("CurrentUser decorator", () => {
const factory = getParamDecoratorFactory();
const mockUser: AuthUser = {
id: "user-123",
email: "test@example.com",
name: "Test User",
};
it("should return the user when present on the request", () => {
const ctx = createMockExecutionContext(mockUser);
const result = factory(undefined, ctx);
expect(result).toEqual(mockUser);
});
it("should return the user with optional fields", () => {
const userWithOptionalFields: AuthUser = {
...mockUser,
image: "https://example.com/avatar.png",
workspaceId: "ws-123",
workspaceRole: "owner",
};
const ctx = createMockExecutionContext(userWithOptionalFields);
const result = factory(undefined, ctx);
expect(result).toEqual(userWithOptionalFields);
expect(result.image).toBe("https://example.com/avatar.png");
expect(result.workspaceId).toBe("ws-123");
});
it("should throw UnauthorizedException when user is undefined", () => {
const ctx = createMockExecutionContext(undefined);
expect(() => factory(undefined, ctx)).toThrow(UnauthorizedException);
expect(() => factory(undefined, ctx)).toThrow("No authenticated user found on request");
});
it("should throw UnauthorizedException when request has no user property", () => {
// Request object without a user property at all
const ctx = {
switchToHttp: () => ({
getRequest: () => ({}),
}),
} as ExecutionContext;
expect(() => factory(undefined, ctx)).toThrow(UnauthorizedException);
});
it("should ignore the data parameter", () => {
const ctx = createMockExecutionContext(mockUser);
// The decorator doesn't use the data parameter, but ensure it doesn't break
const result = factory("some-data", ctx);
expect(result).toEqual(mockUser);
});
});

View File

@@ -1,5 +1,5 @@
import type { ExecutionContext } from "@nestjs/common";
import { createParamDecorator } from "@nestjs/common";
import { createParamDecorator, UnauthorizedException } from "@nestjs/common";
import type { AuthUser } from "@mosaic/shared";
interface RequestWithUser {
@@ -7,8 +7,11 @@ interface RequestWithUser {
}
export const CurrentUser = createParamDecorator(
(_data: unknown, ctx: ExecutionContext): AuthUser | undefined => {
(_data: unknown, ctx: ExecutionContext): AuthUser => {
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
if (!request.user) {
throw new UnauthorizedException("No authenticated user found on request");
}
return request.user;
}
);