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