Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 208 additions & 113 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"nestjs-i18n": "^10.6.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
Expand Down
10 changes: 10 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { APP_INTERCEPTOR } from "@nestjs/core";
import { AppController } from "./app.controller";
import { AuthModule } from "./auth/auth.module";
import { TranslationInterceptor } from "./common/interceptors/translation.interceptor";
import { CoursesModule } from "./courses/courses.module";
import { DatabaseModule } from "./database/database.module";
import { EventsModule } from "./events/events.module";
import { FacultiesModule } from "./faculties/faculties.module";
import { I18nModule } from "./i18n/i18n.module";
import { ServicesModule } from "./services/services.module";
import { UsersModule } from "./users/users.module";

Expand All @@ -15,6 +18,7 @@ import { UsersModule } from "./users/users.module";
envFilePath: [".env.local"],
}),
DatabaseModule,
I18nModule,
FacultiesModule,
UsersModule,
AuthModule,
Expand All @@ -23,5 +27,11 @@ import { UsersModule } from "./users/users.module";
CoursesModule,
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TranslationInterceptor,
},
],
})
export class AppModule {}
84 changes: 84 additions & 0 deletions src/common/dto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Test, TestingModule } from "@nestjs/testing";
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

describe("DTO Tests", () => {
let module: TestingModule;

beforeAll(async () => {
module = await Test.createTestingModule({}).compile();
});

afterAll(async () => {
await module.close();
});

describe("CourseTranslationDto", () => {
it("should validate with correct data", async () => {
const { CourseTranslationDto } = await import(
"@/courses/dto/create-course.dto"
);
const dto = plainToInstance(CourseTranslationDto, {
languageCode: "en",
name: "Computer Science",
acronym: "CS",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
});

describe("CreateCourseDto", () => {
it("should be defined", async () => {
const { CreateCourseDto } = await import(
"@/courses/dto/create-course.dto"
);
expect(new CreateCourseDto()).toBeDefined();
});
});

describe("EventTranslationDto", () => {
it("should validate with correct data", async () => {
const { EventTranslationDto } = await import(
"@/events/dto/create-event.dto"
);
const dto = plainToInstance(EventTranslationDto, {
languageCode: "en",
name: "FEUP Week",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
});

describe("CreateEventDto", () => {
it("should be defined", async () => {
const { CreateEventDto } = await import("@/events/dto/create-event.dto");
expect(new CreateEventDto()).toBeDefined();
});
});

describe("FacultyTranslationDto", () => {
it("should validate with correct data", async () => {
const { FacultyTranslationDto } = await import(
"@/faculties/dto/create-faculty.dto"
);
const dto = plainToInstance(FacultyTranslationDto, {
languageCode: "en",
name: "Faculty of Engineering",
acronym: "FEUP",
});
const errors = await validate(dto);
expect(errors.length).toBe(0);
});
});

describe("CreateFacultyDto", () => {
it("should be defined", async () => {
const { CreateFacultyDto } = await import(
"@/faculties/dto/create-faculty.dto"
);
expect(new CreateFacultyDto()).toBeDefined();
});
});
});
215 changes: 215 additions & 0 deletions src/common/interceptors/translation.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { ExecutionContext } from "@nestjs/common";
import { of } from "rxjs";
import { I18nService } from "@/i18n/i18n.service";
import { TranslationInterceptor } from "./translation.interceptor";

const createMockRequest = (headers: Record<string, string> = {}) => ({
headers,
i18nLanguage: undefined as string | undefined,
});

describe("TranslationInterceptor", () => {
let interceptor: TranslationInterceptor;
let i18nService: I18nService;
let mockContext: ExecutionContext;
let mockRequest: { headers: Record<string, string>; i18nLanguage?: string };

beforeEach(() => {
i18nService = new I18nService();
interceptor = new TranslationInterceptor(i18nService);

mockRequest = createMockRequest();
mockContext = {
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as ExecutionContext;
});

describe("extractLanguage", () => {
it("should return en for undefined", () => {
const result = interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(result).toBeDefined();
});

it("should return en when accept-language is not provided", () => {
mockRequest = createMockRequest({});
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("en");
});

it("should return en when accept-language is empty", () => {
mockRequest = createMockRequest({ "accept-language": "" });
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("en");
});

it("should return pt when accept-language is pt", () => {
mockRequest = createMockRequest({ "accept-language": "pt" });
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("pt");
});

it("should return pt when accept-language has pt-BR", () => {
mockRequest = createMockRequest({ "accept-language": "pt-BR" });
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("pt");
});

it("should return en for unsupported language", () => {
mockRequest = createMockRequest({ "accept-language": "fr" });
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("en");
});

it("should handle accept-language with quality value", () => {
mockRequest = createMockRequest({
"accept-language": "pt;q=0.9, en;q=0.8",
});
interceptor.intercept(mockContext, {
handle: () => of({}),
} as any);
expect(mockRequest.i18nLanguage).toBe("pt");
});

it("should process response through pipe", async () => {
const mockData = {
translations: [{ languageCode: "en", name: "Test" }],
};

let result: any;
interceptor
.intercept(mockContext, {
handle: () => of(mockData),
} as any)
.subscribe((data) => {
result = data;
});

expect(result).toBeDefined();
expect(result.name).toBe("Test");
});
});

describe("transformResponse", () => {
it("should return data as is if null", () => {
const result = (interceptor as any).transformResponse(null, "en");
expect(result).toBeNull();
});

it("should return data as is if not object", () => {
const result = (interceptor as any).transformResponse("string", "en");
expect(result).toBe("string");
});

it("should transform array of items", () => {
const data = [
{
translations: [{ languageCode: "en", name: "Test", acronym: "T" }],
},
];
const result = (interceptor as any).transformResponse(data, "en");
expect(result[0].name).toBe("Test");
expect(result[0].translations).toBeUndefined();
});

it("should transform single item", () => {
const data = {
translations: [{ languageCode: "en", name: "Test", acronym: "T" }],
};
const result = (interceptor as any).transformResponse(data, "en");
expect(result.name).toBe("Test");
expect(result.translations).toBeUndefined();
});

it("should fallback to en if requested language not available", () => {
const data = {
translations: [{ languageCode: "pt", name: "Teste" }],
};
const result = (interceptor as any).transformResponse(data, "pt");
expect(result.name).toBe("Teste");
});

it("should return original item if no translations", () => {
const data = { id: 1, name: "Test" };
const result = (interceptor as any).transformResponse(data, "en");
expect(result).toEqual(data);
});

it("should return item with translations when no matching language and no en fallback", () => {
const data = {
translations: [{ languageCode: "es", name: "Prueba" }],
};
const result = (interceptor as any).transformResponse(data, "pt");
expect(result.translations).toBeDefined();
});

it("should include description if available in translation", () => {
const data = {
translations: [
{
languageCode: "en",
name: "Test",
description: "Test description",
},
],
};
const result = (interceptor as any).transformResponse(data, "en");
expect(result.name).toBe("Test");
expect(result.description).toBe("Test description");
});

it("should delete defaultLanguage from result", () => {
const data = {
defaultLanguage: "en",
translations: [{ languageCode: "en", name: "Test" }],
};
const result = (interceptor as any).transformResponse(data, "en");
expect(result.defaultLanguage).toBeUndefined();
});

it("should apply all translation fields when available", () => {
const data = {
translations: [
{
languageCode: "en",
name: "Test Name",
acronym: "TNA",
description: "Test Desc",
},
],
};
const result = (interceptor as any).transformResponse(data, "en");
expect(result.name).toBe("Test Name");
expect(result.acronym).toBe("TNA");
expect(result.description).toBe("Test Desc");
});

it("should handle translation without optional name", () => {
const data = {
translations: [
{
languageCode: "en",
acronym: "TNA",
description: "Test Desc",
},
],
};
const result = (interceptor as any).transformResponse(data, "en");
expect(result.acronym).toBe("TNA");
expect(result.name).toBeUndefined();
});
});
});
Loading
Loading