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:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user