Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,55 @@ import { Category } from '../domain/category';
import { CategoryDefinition } from '../domain/category-definition';
import { CategoryRepository } from '../domain/ports/category.repository';

const FOOD: CategoryDefinition = {
slug: Category.Food,
labelEs: 'Alimentos',
labelEn: 'Food',
parentSlug: null,
vertical: 'general',
sort: 10,
codePrefix: 'FOD',
archivedAt: null,
translations: [
{ locale: 'es', label: 'Alimentos' },
{ locale: 'en', label: 'Food' },
],
};

function makeRepo(listCategoriesFn: jest.Mock): CategoryRepository {
return {
loadAliasMap: () => Promise.resolve(new Map()),
listCategories: listCategoriesFn,
findBySlug: () => Promise.resolve(null),
createCategory: () => Promise.resolve(FOOD),
updateCategory: () => Promise.resolve(FOOD),
};
}

describe('ListCategories', () => {
it('returns the category taxonomy from the repository', async () => {
const categories: CategoryDefinition[] = [
{
slug: Category.Food,
labelEs: 'Alimentos',
labelEn: 'Food',
parentSlug: null,
vertical: 'general',
sort: 10,
codePrefix: 'FOD',
archivedAt: null,
translations: [
{ locale: 'es', label: 'Alimentos' },
{ locale: 'en', label: 'Food' },
],
},
];
const repo: CategoryRepository = {
loadAliasMap: () => Promise.resolve(new Map()),
listCategories: () => Promise.resolve(categories),
findBySlug: () => Promise.resolve(categories[0] ?? null),
createCategory: () => Promise.resolve(),
updateCategory: () => Promise.resolve(),
};

const result = await new ListCategories(repo).execute();

expect(result).toEqual(categories);
it('devuelve la taxonomía de categorías del repositorio', async () => {
const listCategories = jest.fn().mockResolvedValue([FOOD]);
const result = await new ListCategories(makeRepo(listCategories)).execute();

expect(result).toEqual([FOOD]);
});

it('llama al repositorio sin includeArchived por defecto (cara pública)', async () => {
const listCategories = jest.fn().mockResolvedValue([FOOD]);

await new ListCategories(makeRepo(listCategories)).execute();

expect(listCategories).toHaveBeenCalledWith(undefined);
});

it('pasa includeArchived: true cuando se solicita (cara admin)', async () => {
const archived: CategoryDefinition = { ...FOOD, archivedAt: new Date() };
const listCategories = jest.fn().mockResolvedValue([FOOD, archived]);

await new ListCategories(makeRepo(listCategories)).execute({
includeArchived: true,
});

expect(listCategories).toHaveBeenCalledWith({ includeArchived: true });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { UpdateCategory } from './update-category';
import { CategoryValidationError } from './category-admin.errors';
import { Category } from '../domain/category';
import { CategoryDefinition } from '../domain/category-definition';
import { CategoryRepository } from '../domain/ports/category.repository';

const BASE: CategoryDefinition = {
slug: 'baby_food',
labelEs: 'Alimentos para bebé',
labelEn: 'Baby food',
parentSlug: 'food',
vertical: 'general',
sort: 140,
archivedAt: null,
translations: [],
};

function makeRepo(
override: Partial<CategoryRepository> = {},
): CategoryRepository {
return {
loadAliasMap: () => Promise.resolve(new Map()),
listCategories: () => Promise.resolve([]),
findBySlug: () => Promise.resolve(BASE),
createCategory: () => Promise.resolve(BASE),
updateCategory: (_slug, input) =>
Promise.resolve({ ...BASE, archivedAt: input.archivedAt ?? null }),
...override,
};
}

describe('UpdateCategory — archive / restore', () => {
it('archiva una categoría no-núcleo fijando archivedAt', async () => {
const repo = makeRepo();
const result = await new UpdateCategory(repo).execute('baby_food', {
archived: true,
});
expect(result.archivedAt).toBeInstanceOf(Date);
});

it('restaura una categoría archivada pasando archived: false', async () => {
const archived: CategoryDefinition = { ...BASE, archivedAt: new Date() };
const repo = makeRepo({
findBySlug: () => Promise.resolve(archived),
updateCategory: (_slug, input) =>
Promise.resolve({ ...archived, archivedAt: input.archivedAt ?? null }),
});

const result = await new UpdateCategory(repo).execute('baby_food', {
archived: false,
});
expect(result.archivedAt).toBeNull();
});

it('preserva archivedAt cuando archived no se pasa', async () => {
const ts = new Date('2026-01-01T00:00:00Z');
const archived: CategoryDefinition = { ...BASE, archivedAt: ts };
const repo = makeRepo({
findBySlug: () => Promise.resolve(archived),
updateCategory: (_slug, input) =>
Promise.resolve({ ...archived, archivedAt: input.archivedAt ?? null }),
});

const result = await new UpdateCategory(repo).execute('baby_food', {
labelEs: 'Otro nombre',
});
expect(result.archivedAt).toEqual(ts);
});

it('rechaza archivar una categoría núcleo', async () => {
const core: CategoryDefinition = {
...BASE,
slug: Category.Food,
parentSlug: null,
};
const repo = makeRepo({ findBySlug: () => Promise.resolve(core) });

await expect(
new UpdateCategory(repo).execute(Category.Food, { archived: true }),
).rejects.toBeInstanceOf(CategoryValidationError);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,43 @@ describe('CategoriesAdminController', () => {
BadRequestException,
);
});

it('archiva una categoría no-núcleo vía DELETE', async () => {
const archived = { ...category, archivedAt: new Date().toISOString() };
const updateCategory = { execute: jest.fn().mockResolvedValue(archived) };
const controller = new CategoriesAdminController(
{ execute: jest.fn() } as never,
{ execute: jest.fn() } as never,
updateCategory as never,
);

await controller.delete('baby_food');

expect(updateCategory.execute).toHaveBeenCalledWith('baby_food', {
archived: true,
});
});

it('restaura una categoría archivada vía PATCH con archived: false', async () => {
const restored: CategoryDefinition = { ...category, archivedAt: null };
const updateCategory = { execute: jest.fn().mockResolvedValue(restored) };
const controller = new CategoriesAdminController(
{ execute: jest.fn() } as never,
{ execute: jest.fn() } as never,
updateCategory as never,
);

const result = await controller.update('baby_food', { archived: false });

expect(updateCategory.execute).toHaveBeenCalledWith('baby_food', {
labelEs: undefined,
labelEn: undefined,
parentSlug: undefined,
vertical: undefined,
sort: undefined,
archived: false,
translations: undefined,
});
expect(result.archivedAt).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,23 @@ describe('CategoriesController', () => {
expect(es[0]?.label).toBe('Alimentos');
expect(fr[0]?.label).toBe('Nourriture');
});

it('no expone archivedAt ni campos internos en la proyección pública', async () => {
const controller = new CategoriesController({
execute: () => Promise.resolve(categories),
});

const result = await controller.list('es', {});

expect(result[0]).not.toHaveProperty('archivedAt');
expect(result[0]).toEqual({
slug: 'food',
label: 'Alimentos',
labelEs: 'Alimentos',
labelEn: 'Food',
parentSlug: null,
vertical: 'general',
sort: 1,
});
});
});
Loading