chore: upgrade Node.js runtime to v24 across codebase #419
96
apps/api/src/auth/decorators/current-user.decorator.spec.ts
Normal file
96
apps/api/src/auth/decorators/current-user.decorator.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExecutionContext } from "@nestjs/common";
|
import type { ExecutionContext } from "@nestjs/common";
|
||||||
import { createParamDecorator } from "@nestjs/common";
|
import { createParamDecorator, UnauthorizedException } from "@nestjs/common";
|
||||||
import type { AuthUser } from "@mosaic/shared";
|
import type { AuthUser } from "@mosaic/shared";
|
||||||
|
|
||||||
interface RequestWithUser {
|
interface RequestWithUser {
|
||||||
@@ -7,8 +7,11 @@ interface RequestWithUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CurrentUser = createParamDecorator(
|
export const CurrentUser = createParamDecorator(
|
||||||
(_data: unknown, ctx: ExecutionContext): AuthUser | undefined => {
|
(_data: unknown, ctx: ExecutionContext): AuthUser => {
|
||||||
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
|
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
|
||||||
|
if (!request.user) {
|
||||||
|
throw new UnauthorizedException("No authenticated user found on request");
|
||||||
|
}
|
||||||
return request.user;
|
return request.user;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user