feat(api): implement /users/me/preferences endpoint
All checks were successful
ci/woodpecker/push/web Pipeline was successful
ci/woodpecker/push/api Pipeline was successful

Implements GET/PATCH/PUT /users/me/preferences. Fixes profile page 'Preferences unavailable' error by correcting the /api prefix in frontend calls and adding PATCH handler to controller.

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #539.
This commit is contained in:
2026-02-27 10:51:28 +00:00
committed by jason.woltje
parent b3922e1d5b
commit 833662a64f
5 changed files with 275 additions and 3 deletions

View File

@@ -0,0 +1,141 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { PreferencesService } from "./preferences.service";
import type { PrismaService } from "../prisma/prisma.service";
import type { UpdatePreferencesDto } from "./dto";
describe("PreferencesService", () => {
let service: PreferencesService;
const mockPrisma = {
userPreference: {
findUnique: vi.fn(),
create: vi.fn(),
update: vi.fn(),
},
};
const mockUserId = "user-uuid-123";
const mockDbPreference = {
id: "pref-uuid-456",
userId: mockUserId,
theme: "system",
locale: "en",
timezone: null,
settings: {},
updatedAt: new Date("2026-01-01T00:00:00Z"),
};
beforeEach(() => {
service = new PreferencesService(mockPrisma as unknown as PrismaService);
vi.clearAllMocks();
});
describe("getPreferences", () => {
it("should return existing preferences", async () => {
mockPrisma.userPreference.findUnique.mockResolvedValue(mockDbPreference);
const result = await service.getPreferences(mockUserId);
expect(result).toMatchObject({
id: mockDbPreference.id,
userId: mockUserId,
theme: "system",
locale: "en",
timezone: null,
settings: {},
});
expect(mockPrisma.userPreference.findUnique).toHaveBeenCalledWith({
where: { userId: mockUserId },
});
expect(mockPrisma.userPreference.create).not.toHaveBeenCalled();
});
it("should create default preferences when none exist", async () => {
mockPrisma.userPreference.findUnique.mockResolvedValue(null);
mockPrisma.userPreference.create.mockResolvedValue(mockDbPreference);
const result = await service.getPreferences(mockUserId);
expect(result).toMatchObject({
id: mockDbPreference.id,
userId: mockUserId,
theme: "system",
locale: "en",
});
expect(mockPrisma.userPreference.create).toHaveBeenCalledWith({
data: expect.objectContaining({
userId: mockUserId,
theme: "system",
locale: "en",
}),
});
});
});
describe("updatePreferences", () => {
it("should update existing preferences", async () => {
const updateDto: UpdatePreferencesDto = { theme: "dark", locale: "fr" };
const updatedPreference = { ...mockDbPreference, theme: "dark", locale: "fr" };
mockPrisma.userPreference.findUnique.mockResolvedValue(mockDbPreference);
mockPrisma.userPreference.update.mockResolvedValue(updatedPreference);
const result = await service.updatePreferences(mockUserId, updateDto);
expect(result).toMatchObject({ theme: "dark", locale: "fr" });
expect(mockPrisma.userPreference.update).toHaveBeenCalledWith({
where: { userId: mockUserId },
data: expect.objectContaining({ theme: "dark", locale: "fr" }),
});
expect(mockPrisma.userPreference.create).not.toHaveBeenCalled();
});
it("should create preferences when updating non-existent record", async () => {
const updateDto: UpdatePreferencesDto = { theme: "light" };
const createdPreference = { ...mockDbPreference, theme: "light" };
mockPrisma.userPreference.findUnique.mockResolvedValue(null);
mockPrisma.userPreference.create.mockResolvedValue(createdPreference);
const result = await service.updatePreferences(mockUserId, updateDto);
expect(result).toMatchObject({ theme: "light" });
expect(mockPrisma.userPreference.create).toHaveBeenCalledWith({
data: expect.objectContaining({
userId: mockUserId,
theme: "light",
}),
});
expect(mockPrisma.userPreference.update).not.toHaveBeenCalled();
});
it("should handle timezone update", async () => {
const updateDto: UpdatePreferencesDto = { timezone: "America/New_York" };
const updatedPreference = { ...mockDbPreference, timezone: "America/New_York" };
mockPrisma.userPreference.findUnique.mockResolvedValue(mockDbPreference);
mockPrisma.userPreference.update.mockResolvedValue(updatedPreference);
const result = await service.updatePreferences(mockUserId, updateDto);
expect(result.timezone).toBe("America/New_York");
expect(mockPrisma.userPreference.update).toHaveBeenCalledWith({
where: { userId: mockUserId },
data: expect.objectContaining({ timezone: "America/New_York" }),
});
});
it("should handle settings update", async () => {
const updateDto: UpdatePreferencesDto = { settings: { notifications: true } };
const updatedPreference = { ...mockDbPreference, settings: { notifications: true } };
mockPrisma.userPreference.findUnique.mockResolvedValue(mockDbPreference);
mockPrisma.userPreference.update.mockResolvedValue(updatedPreference);
const result = await service.updatePreferences(mockUserId, updateDto);
expect(result.settings).toEqual({ notifications: true });
});
});
});