test(reference): add unit tests for VnAdmin service and controller (GOO-59)

Add 29 unit tests covering VnAdminService (listProvinces, listDistricts,
listWards, resolveDistrict, isKnownDistrictName) and VnAdminController
(all endpoints + public access verification). 100% coverage on all metrics.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-23 10:49:30 +07:00
parent 0924f0cb9b
commit e706f4ddf2
2 changed files with 418 additions and 0 deletions

View File

@@ -0,0 +1,267 @@
import { VnAdminService } from '../vn-admin.service';
describe('VnAdminService', () => {
let service: VnAdminService;
let mockPrisma: {
vnProvince: { findMany: ReturnType<typeof vi.fn> };
vnDistrict: { findMany: ReturnType<typeof vi.fn>; findFirst: ReturnType<typeof vi.fn> };
vnWard: { findMany: ReturnType<typeof vi.fn> };
vnAdministrativeAlias: { findFirst: ReturnType<typeof vi.fn> };
};
beforeEach(() => {
mockPrisma = {
vnProvince: { findMany: vi.fn() },
vnDistrict: { findMany: vi.fn(), findFirst: vi.fn() },
vnWard: { findMany: vi.fn() },
vnAdministrativeAlias: { findFirst: vi.fn() },
};
service = new VnAdminService(mockPrisma as any);
});
// ── listProvinces ──────────────────────────────────────────────────
describe('listProvinces', () => {
it('returns all provinces ordered by name ascending', async () => {
const provinces = [
{ code: '01', name: 'Hà Nội', type: 'thanh_pho', codename: 'thanh_pho_ha_noi' },
{ code: '79', name: 'TP Hồ Chí Minh', type: 'thanh_pho', codename: 'thanh_pho_ho_chi_minh' },
];
mockPrisma.vnProvince.findMany.mockResolvedValue(provinces);
const result = await service.listProvinces();
expect(result).toEqual(provinces);
expect(mockPrisma.vnProvince.findMany).toHaveBeenCalledWith({
orderBy: [{ name: 'asc' }],
select: { code: true, name: true, type: true, codename: true },
});
});
it('returns an empty array when no provinces exist', async () => {
mockPrisma.vnProvince.findMany.mockResolvedValue([]);
const result = await service.listProvinces();
expect(result).toEqual([]);
});
});
// ── listDistricts ──────────────────────────────────────────────────
describe('listDistricts', () => {
it('returns districts filtered by provinceCode', async () => {
const districts = [
{ code: '760', provinceCode: '79', name: 'Quận 1', type: 'quan', codename: 'quan_1' },
{ code: '769', provinceCode: '79', name: 'Thành phố Thủ Đức', type: 'thanh_pho', codename: 'thanh_pho_thu_duc' },
];
mockPrisma.vnDistrict.findMany.mockResolvedValue(districts);
const result = await service.listDistricts('79');
expect(result).toEqual(districts);
expect(mockPrisma.vnDistrict.findMany).toHaveBeenCalledWith({
where: { provinceCode: '79' },
orderBy: [{ name: 'asc' }],
select: { code: true, provinceCode: true, name: true, type: true, codename: true },
});
});
it('returns an empty array when province has no districts', async () => {
mockPrisma.vnDistrict.findMany.mockResolvedValue([]);
const result = await service.listDistricts('99');
expect(result).toEqual([]);
});
});
// ── listWards ──────────────────────────────────────────────────────
describe('listWards', () => {
it('returns wards filtered by districtCode', async () => {
const wards = [
{ code: '26734', districtCode: '760', name: 'Phường Bến Nghé', type: 'phuong', codename: 'phuong_ben_nghe' },
{ code: '26737', districtCode: '760', name: 'Phường Bến Thành', type: 'phuong', codename: 'phuong_ben_thanh' },
];
mockPrisma.vnWard.findMany.mockResolvedValue(wards);
const result = await service.listWards('760');
expect(result).toEqual(wards);
expect(mockPrisma.vnWard.findMany).toHaveBeenCalledWith({
where: { districtCode: '760' },
orderBy: [{ name: 'asc' }],
select: { code: true, districtCode: true, name: true, type: true, codename: true },
});
});
it('returns an empty array when district has no wards', async () => {
mockPrisma.vnWard.findMany.mockResolvedValue([]);
const result = await service.listWards('999');
expect(result).toEqual([]);
});
});
// ── resolveDistrict ────────────────────────────────────────────────
describe('resolveDistrict', () => {
it('returns null resolution for empty string input', async () => {
const result = await service.resolveDistrict('');
expect(result).toEqual({
districtCode: null,
isCurrent: false,
resolvedViaAlias: false,
aliasReason: null,
});
expect(mockPrisma.vnDistrict.findFirst).not.toHaveBeenCalled();
expect(mockPrisma.vnAdministrativeAlias.findFirst).not.toHaveBeenCalled();
});
it('returns null resolution for whitespace-only input', async () => {
const result = await service.resolveDistrict(' ');
expect(result).toEqual({
districtCode: null,
isCurrent: false,
resolvedViaAlias: false,
aliasReason: null,
});
});
it('resolves a current district name directly', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue({ code: '760' });
const result = await service.resolveDistrict('Quận 1');
expect(result).toEqual({
districtCode: '760',
isCurrent: true,
resolvedViaAlias: false,
aliasReason: null,
});
expect(mockPrisma.vnDistrict.findFirst).toHaveBeenCalledWith({
where: { name: 'Quận 1' },
select: { code: true },
});
});
it('filters by provinceCode when provided', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue({ code: '760' });
await service.resolveDistrict('Quận 1', '79');
expect(mockPrisma.vnDistrict.findFirst).toHaveBeenCalledWith({
where: { name: 'Quận 1', provinceCode: '79' },
select: { code: true },
});
});
it('resolves a historical alias when no direct match is found', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue(null);
mockPrisma.vnAdministrativeAlias.findFirst.mockResolvedValue({
newDistrictCode: '769',
reason: 'Sáp nhập vào Thủ Đức',
});
const result = await service.resolveDistrict('Quận 2');
expect(result).toEqual({
districtCode: '769',
isCurrent: false,
resolvedViaAlias: true,
aliasReason: 'Sáp nhập vào Thủ Đức',
});
expect(mockPrisma.vnAdministrativeAlias.findFirst).toHaveBeenCalledWith({
where: { oldName: 'Quận 2', level: 'district' },
select: { newDistrictCode: true, reason: true },
});
});
it('returns null resolution when neither direct nor alias match', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue(null);
mockPrisma.vnAdministrativeAlias.findFirst.mockResolvedValue(null);
const result = await service.resolveDistrict('Nonexistent District');
expect(result).toEqual({
districtCode: null,
isCurrent: false,
resolvedViaAlias: false,
aliasReason: null,
});
});
it('returns null resolution when alias exists but has no newDistrictCode', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue(null);
mockPrisma.vnAdministrativeAlias.findFirst.mockResolvedValue({
newDistrictCode: null,
reason: 'Removed',
});
const result = await service.resolveDistrict('Old Name');
expect(result).toEqual({
districtCode: null,
isCurrent: false,
resolvedViaAlias: false,
aliasReason: null,
});
});
it('trims whitespace from input before matching', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue({ code: '760' });
await service.resolveDistrict(' Quận 1 ');
expect(mockPrisma.vnDistrict.findFirst).toHaveBeenCalledWith({
where: { name: 'Quận 1' },
select: { code: true },
});
});
it('skips alias lookup when direct match succeeds', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue({ code: '760' });
await service.resolveDistrict('Quận 1');
expect(mockPrisma.vnAdministrativeAlias.findFirst).not.toHaveBeenCalled();
});
});
// ── isKnownDistrictName ────────────────────────────────────────────
describe('isKnownDistrictName', () => {
it('returns true when resolveDistrict finds a districtCode', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue({ code: '760' });
const result = await service.isKnownDistrictName('Quận 1');
expect(result).toBe(true);
});
it('returns false when resolveDistrict finds no districtCode', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue(null);
mockPrisma.vnAdministrativeAlias.findFirst.mockResolvedValue(null);
const result = await service.isKnownDistrictName('Unknown District');
expect(result).toBe(false);
});
it('returns true when district is resolved via alias', async () => {
mockPrisma.vnDistrict.findFirst.mockResolvedValue(null);
mockPrisma.vnAdministrativeAlias.findFirst.mockResolvedValue({
newDistrictCode: '769',
reason: 'Merged',
});
const result = await service.isKnownDistrictName('Quận 9');
expect(result).toBe(true);
});
});
});

View File

@@ -0,0 +1,151 @@
import { BadRequestException } from '@nestjs/common';
import { VnAdminController } from '../vn-admin.controller';
describe('VnAdminController', () => {
let controller: VnAdminController;
let mockVnAdmin: {
listProvinces: ReturnType<typeof vi.fn>;
listDistricts: ReturnType<typeof vi.fn>;
listWards: ReturnType<typeof vi.fn>;
resolveDistrict: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
mockVnAdmin = {
listProvinces: vi.fn(),
listDistricts: vi.fn(),
listWards: vi.fn(),
resolveDistrict: vi.fn(),
};
controller = new VnAdminController(mockVnAdmin as any);
});
// ── GET /reference/vn-admin/provinces ──────────────────────────────
describe('listProvinces', () => {
it('delegates to VnAdminService.listProvinces and returns result', async () => {
const provinces = [
{ code: '01', name: 'Hà Nội', type: 'thanh_pho', codename: 'thanh_pho_ha_noi' },
];
mockVnAdmin.listProvinces.mockResolvedValue(provinces);
const result = await controller.listProvinces();
expect(result).toEqual(provinces);
expect(mockVnAdmin.listProvinces).toHaveBeenCalledOnce();
});
});
// ── GET /reference/vn-admin/provinces/:code/districts ──────────────
describe('listDistricts', () => {
it('delegates to VnAdminService.listDistricts with the province code', async () => {
const districts = [
{ code: '760', provinceCode: '79', name: 'Quận 1', type: 'quan', codename: 'quan_1' },
];
mockVnAdmin.listDistricts.mockResolvedValue(districts);
const result = await controller.listDistricts('79');
expect(result).toEqual(districts);
expect(mockVnAdmin.listDistricts).toHaveBeenCalledWith('79');
});
});
// ── GET /reference/vn-admin/districts/:code/wards ──────────────────
describe('listWards', () => {
it('delegates to VnAdminService.listWards with the district code', async () => {
const wards = [
{ code: '26734', districtCode: '760', name: 'Phường Bến Nghé', type: 'phuong', codename: 'phuong_ben_nghe' },
];
mockVnAdmin.listWards.mockResolvedValue(wards);
const result = await controller.listWards('760');
expect(result).toEqual(wards);
expect(mockVnAdmin.listWards).toHaveBeenCalledWith('760');
});
});
// ── GET /reference/vn-admin/resolve-district ───────────────────────
describe('resolveDistrict', () => {
it('delegates to VnAdminService.resolveDistrict with name and provinceCode', async () => {
const resolution = {
districtCode: '760',
isCurrent: true,
resolvedViaAlias: false,
aliasReason: null,
};
mockVnAdmin.resolveDistrict.mockResolvedValue(resolution);
const result = await controller.resolveDistrict('Quận 1', '79');
expect(result).toEqual(resolution);
expect(mockVnAdmin.resolveDistrict).toHaveBeenCalledWith('Quận 1', '79');
});
it('delegates without provinceCode when not provided', async () => {
const resolution = {
districtCode: '769',
isCurrent: false,
resolvedViaAlias: true,
aliasReason: 'Sáp nhập',
};
mockVnAdmin.resolveDistrict.mockResolvedValue(resolution);
const result = await controller.resolveDistrict('Quận 2', undefined);
expect(result).toEqual(resolution);
expect(mockVnAdmin.resolveDistrict).toHaveBeenCalledWith('Quận 2', undefined);
});
it('throws BadRequestException when name is undefined', async () => {
await expect(controller.resolveDistrict(undefined, undefined)).rejects.toThrow(
BadRequestException,
);
expect(mockVnAdmin.resolveDistrict).not.toHaveBeenCalled();
});
it('throws BadRequestException when name is empty string', async () => {
await expect(controller.resolveDistrict('', undefined)).rejects.toThrow(
BadRequestException,
);
expect(mockVnAdmin.resolveDistrict).not.toHaveBeenCalled();
});
it('throws BadRequestException when name is whitespace only', async () => {
await expect(controller.resolveDistrict(' ', undefined)).rejects.toThrow(
BadRequestException,
);
expect(mockVnAdmin.resolveDistrict).not.toHaveBeenCalled();
});
it('throws BadRequestException with correct message', async () => {
await expect(controller.resolveDistrict(undefined, undefined)).rejects.toThrow(
'Query parameter "name" is required.',
);
});
});
// ── No auth guard (intentionally public) ───────────────────────────
describe('public access (no auth guard)', () => {
it('controller has no UseGuards decorator — all endpoints are public', () => {
// VnAdminController intentionally has no @UseGuards decorator.
// Verify by checking that the controller class metadata does not include
// the NestJS guards metadata key.
const guards = Reflect.getMetadata('__guards__', VnAdminController);
expect(guards).toBeUndefined();
});
it('individual route handlers have no method-level guards', () => {
const methods = ['listProvinces', 'listDistricts', 'listWards', 'resolveDistrict'] as const;
for (const method of methods) {
const guards = Reflect.getMetadata('__guards__', controller[method]);
expect(guards).toBeUndefined();
}
});
});
});