From 43c9cbc863a37e54bf4896df46e2034cd26090f4 Mon Sep 17 00:00:00 2001 From: jianuo <32106500027@e.gzhu.edu.cn> Date: Fri, 19 Dec 2025 23:18:57 +0800 Subject: [PATCH] =?UTF-8?q?test=EF=BC=9A=E6=B7=BB=E5=8A=A0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=90=8E=E5=8F=B0=E7=9A=84=E5=8D=95=E5=85=83=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/business/admin/admin.service.spec.ts | 159 ++++++ .../admin_core/admin_core.service.spec.ts | 459 ++++++++++++++++++ src/core/guards/admin.guard.spec.ts | 81 ++++ 3 files changed, 699 insertions(+) create mode 100644 src/business/admin/admin.service.spec.ts create mode 100644 src/core/admin_core/admin_core.service.spec.ts create mode 100644 src/core/guards/admin.guard.spec.ts diff --git a/src/business/admin/admin.service.spec.ts b/src/business/admin/admin.service.spec.ts new file mode 100644 index 0000000..56f7e4b --- /dev/null +++ b/src/business/admin/admin.service.spec.ts @@ -0,0 +1,159 @@ +import { NotFoundException } from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { AdminCoreService } from '../../core/admin_core/admin_core.service'; +import { LogManagementService } from '../../core/utils/logger/log_management.service'; +import { Users } from '../../core/db/users/users.entity'; + +describe('AdminService', () => { + let service: AdminService; + + const adminCoreServiceMock: Pick = { + login: jest.fn(), + resetUserPassword: jest.fn(), + }; + + const usersServiceMock = { + findAll: jest.fn(), + findOne: jest.fn(), + }; + + const logManagementServiceMock: Pick = { + getRuntimeLogTail: jest.fn(), + getLogDirAbsolutePath: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + service = new AdminService( + adminCoreServiceMock as unknown as AdminCoreService, + usersServiceMock as any, + logManagementServiceMock as unknown as LogManagementService, + ); + }); + + it('should login admin successfully', async () => { + (adminCoreServiceMock.login as jest.Mock).mockResolvedValue({ + admin: { id: '1', username: 'admin', nickname: '管理员', role: 9 }, + access_token: 'token', + expires_at: 123, + }); + + const res = await service.login('admin', 'Admin123456'); + + expect(adminCoreServiceMock.login).toHaveBeenCalledWith({ identifier: 'admin', password: 'Admin123456' }); + expect(res.success).toBe(true); + expect(res.data?.admin?.role).toBe(9); + expect(res.message).toBe('管理员登录成功'); + }); + + it('should handle login failure', async () => { + (adminCoreServiceMock.login as jest.Mock).mockRejectedValue(new Error('密码错误')); + + const res = await service.login('admin', 'bad'); + + expect(res.success).toBe(false); + expect(res.error_code).toBe('ADMIN_LOGIN_FAILED'); + expect(res.message).toBe('密码错误'); + }); + + it('should handle non-Error login failure', async () => { + (adminCoreServiceMock.login as jest.Mock).mockRejectedValue('boom'); + + const res = await service.login('admin', 'bad'); + + expect(res.success).toBe(false); + expect(res.error_code).toBe('ADMIN_LOGIN_FAILED'); + expect(res.message).toBe('管理员登录失败'); + }); + + it('should list users with pagination', async () => { + const user = { + id: BigInt(1), + username: 'u1', + nickname: 'U1', + email: 'u1@test.com', + email_verified: true, + phone: null, + avatar_url: null, + role: 1, + created_at: new Date('2025-01-01T00:00:00Z'), + updated_at: new Date('2025-01-02T00:00:00Z'), + } as unknown as Users; + + usersServiceMock.findAll.mockResolvedValue([user]); + + const res = await service.listUsers(100, 0); + + expect(usersServiceMock.findAll).toHaveBeenCalledWith(100, 0); + expect(res.success).toBe(true); + expect(res.data?.users).toHaveLength(1); + expect(res.data?.users[0]).toMatchObject({ + id: '1', + username: 'u1', + nickname: 'U1', + role: 1, + }); + }); + + it('should get user by id', async () => { + const user = { + id: BigInt(3), + username: 'u3', + nickname: 'U3', + email: null, + email_verified: false, + phone: '123', + avatar_url: null, + role: 1, + created_at: new Date('2025-01-01T00:00:00Z'), + updated_at: new Date('2025-01-02T00:00:00Z'), + } as unknown as Users; + + usersServiceMock.findOne.mockResolvedValue(user); + + const res = await service.getUser(BigInt(3)); + + expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(3)); + expect(res.success).toBe(true); + expect(res.data?.user).toMatchObject({ id: '3', username: 'u3', nickname: 'U3' }); + }); + + it('should reset user password', async () => { + usersServiceMock.findOne.mockResolvedValue({ id: BigInt(2) } as unknown as Users); + (adminCoreServiceMock.resetUserPassword as jest.Mock).mockResolvedValue(undefined); + + const res = await service.resetPassword(BigInt(2), 'NewPass1234'); + + expect(usersServiceMock.findOne).toHaveBeenCalledWith(BigInt(2)); + expect(adminCoreServiceMock.resetUserPassword).toHaveBeenCalledWith(BigInt(2), 'NewPass1234'); + expect(res).toEqual({ success: true, message: '密码重置成功' }); + }); + + it('should throw NotFoundException when resetting password for missing user', async () => { + usersServiceMock.findOne.mockRejectedValue(new Error('not found')); + + await expect(service.resetPassword(BigInt(999), 'NewPass1234')).rejects.toBeInstanceOf(NotFoundException); + }); + + it('should get runtime logs', async () => { + (logManagementServiceMock.getRuntimeLogTail as jest.Mock).mockResolvedValue({ + file: 'dev.log', + updated_at: '2025-01-01T00:00:00.000Z', + lines: ['a', 'b'], + }); + + const res = await service.getRuntimeLogs(2); + + expect(logManagementServiceMock.getRuntimeLogTail).toHaveBeenCalledWith({ lines: 2 }); + expect(res.success).toBe(true); + expect(res.data?.file).toBe('dev.log'); + expect(res.data?.lines).toEqual(['a', 'b']); + }); + + it('should expose log dir absolute path', () => { + (logManagementServiceMock.getLogDirAbsolutePath as jest.Mock).mockReturnValue('/abs/logs'); + + expect(service.getLogDirAbsolutePath()).toBe('/abs/logs'); + expect(logManagementServiceMock.getLogDirAbsolutePath).toHaveBeenCalled(); + }); +}); diff --git a/src/core/admin_core/admin_core.service.spec.ts b/src/core/admin_core/admin_core.service.spec.ts new file mode 100644 index 0000000..8bd91a8 --- /dev/null +++ b/src/core/admin_core/admin_core.service.spec.ts @@ -0,0 +1,459 @@ +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as bcrypt from 'bcrypt'; +import * as crypto from 'crypto'; +import { AdminAuthPayload, AdminCoreService } from './admin_core.service'; +import { Users } from '../db/users/users.entity'; + +jest.mock('bcrypt', () => ({ + compare: jest.fn(), + hash: jest.fn(), +})); + +type UsersServiceLike = { + findByUsername: jest.Mock; + findByEmail: jest.Mock; + findAll: jest.Mock; + update: jest.Mock; + create: jest.Mock; +}; + +describe('AdminCoreService', () => { + let configService: Pick; + let usersService: UsersServiceLike; + let service: AdminCoreService; + + const secret = '0123456789abcdef'; + + const signToken = (payload: AdminAuthPayload, tokenSecret: string): string => { + const payloadJson = JSON.stringify(payload); + const payloadPart = Buffer.from(payloadJson, 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + + const signature = crypto + .createHmac('sha256', tokenSecret) + .update(payloadPart) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + + return `${payloadPart}.${signature}`; + }; + + beforeEach(() => { + jest.restoreAllMocks(); + + configService = { + get: jest.fn((key: string, defaultValue?: any) => { + if (key === 'ADMIN_TOKEN_SECRET') return secret; + if (key === 'ADMIN_TOKEN_TTL_SECONDS') return defaultValue ?? '28800'; + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'false'; + return defaultValue; + }), + }; + + usersService = { + findByUsername: jest.fn(), + findByEmail: jest.fn(), + findAll: jest.fn(), + update: jest.fn(), + create: jest.fn(), + }; + + service = new AdminCoreService(configService as ConfigService, usersService as any); + }); + + describe('login', () => { + it('should reject when admin does not exist', async () => { + usersService.findByUsername.mockResolvedValue(null); + + await expect(service.login({ identifier: 'admin', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('should reject non-admin user', async () => { + usersService.findByUsername.mockResolvedValue({ + id: BigInt(1), + username: 'user', + nickname: 'U', + role: 1, + password_hash: 'hash', + } as unknown as Users); + + await expect(service.login({ identifier: 'user', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('should reject admin without password_hash', async () => { + usersService.findByUsername.mockResolvedValue({ + id: BigInt(1), + username: 'admin', + nickname: '管理员', + role: 9, + password_hash: null, + } as unknown as Users); + + await expect(service.login({ identifier: 'admin', password: 'Admin123456' })).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('should reject wrong password', async () => { + usersService.findByUsername.mockResolvedValue({ + id: BigInt(1), + username: 'admin', + nickname: '管理员', + role: 9, + password_hash: 'hash', + } as unknown as Users); + + const bcryptAny = bcrypt as any; + bcryptAny.compare.mockResolvedValue(false); + + await expect(service.login({ identifier: 'admin', password: 'bad' })).rejects.toBeInstanceOf(UnauthorizedException); + }); + + it('should login with valid credentials and generate verifiable token', async () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + usersService.findByUsername.mockResolvedValue({ + id: BigInt(1), + username: 'admin', + nickname: '管理员', + role: 9, + password_hash: 'hash', + } as unknown as Users); + + const bcryptAny = bcrypt as any; + bcryptAny.compare.mockResolvedValue(true); + + const result = await service.login({ identifier: 'admin', password: 'Admin123456' }); + + expect(result.admin).toEqual({ id: '1', username: 'admin', nickname: '管理员', role: 9 }); + expect(result.access_token).toContain('.'); + expect(result.expires_at).toBeGreaterThan(now); + + const payload = service.verifyToken(result.access_token); + expect(payload).toMatchObject({ adminId: '1', username: 'admin', role: 9 }); + expect(payload.iat).toBe(now); + expect(payload.exp).toBe(result.expires_at); + }); + + it('should find admin by email identifier', async () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + usersService.findByUsername.mockResolvedValue(null); + usersService.findByEmail.mockResolvedValue({ + id: BigInt(1), + username: 'admin', + nickname: '管理员', + role: 9, + password_hash: 'hash', + } as unknown as Users); + + const bcryptAny = bcrypt as any; + bcryptAny.compare.mockResolvedValue(true); + + const result = await service.login({ identifier: 'admin@test.com', password: 'Admin123456' }); + expect(result.admin.role).toBe(9); + }); + + it('should find admin by phone identifier', async () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + usersService.findByUsername.mockResolvedValue(null); + usersService.findByEmail.mockResolvedValue(null); + usersService.findAll.mockResolvedValue([ + { + id: BigInt(2), + username: 'admin', + nickname: '管理员', + role: 9, + phone: '+86 13800000000', + password_hash: 'hash', + } as unknown as Users, + ]); + + const bcryptAny = bcrypt as any; + bcryptAny.compare.mockResolvedValue(true); + + const result = await service.login({ identifier: '+86 13800000000', password: 'Admin123456' }); + expect(result.admin.id).toBe('2'); + }); + + it('should return phone-matched user via findUserByIdentifier (coverage for line 168)', async () => { + usersService.findByUsername.mockResolvedValue(null); + usersService.findByEmail.mockResolvedValue(null); + usersService.findAll.mockResolvedValue([ + { + id: BigInt(10), + username: 'admin', + nickname: '管理员', + role: 9, + phone: '13800000000', + } as unknown as Users, + ]); + + const found = await (service as any).findUserByIdentifier('13800000000'); + expect(found?.id?.toString()).toBe('10'); + }); + + it('should fallback to default TTL when ADMIN_TOKEN_TTL_SECONDS is invalid', async () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_TOKEN_SECRET') return secret; + if (key === 'ADMIN_TOKEN_TTL_SECONDS') return 'not-a-number'; + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'false'; + return defaultValue; + }); + + usersService.findByUsername.mockResolvedValue({ + id: BigInt(1), + username: 'admin', + nickname: '管理员', + role: 9, + password_hash: 'hash', + } as unknown as Users); + + const bcryptAny = bcrypt as any; + bcryptAny.compare.mockResolvedValue(true); + + const result = await service.login({ identifier: 'admin', password: 'Admin123456' }); + expect(result.expires_at).toBe(now + 28800 * 1000); + }); + }); + + describe('verifyToken', () => { + it('should reject token when secret missing/too short', () => { + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_TOKEN_SECRET') return 'short'; + return defaultValue; + }); + + expect(() => service.verifyToken('a.b')).toThrow(BadRequestException); + }); + + it('should reject invalid token format', () => { + expect(() => service.verifyToken('no-dot')).toThrow(UnauthorizedException); + }); + + it('should reject token when payload JSON cannot be parsed (but signature valid)', () => { + const payloadPart = Buffer.from('not-json', 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + + const signature = crypto + .createHmac('sha256', secret) + .update(payloadPart) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + + expect(() => service.verifyToken(`${payloadPart}.${signature}`)).toThrow(UnauthorizedException); + }); + + it('should accept valid signed token and return payload', () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + const payload: AdminAuthPayload = { + adminId: '1', + username: 'admin', + role: 9, + iat: now, + exp: now + 1000, + }; + + const token = signToken(payload, secret); + expect(service.verifyToken(token)).toEqual(payload); + }); + + it('should reject expired token', () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + const payload: AdminAuthPayload = { + adminId: '1', + username: 'admin', + role: 9, + iat: now - 1000, + exp: now - 1, + }; + + const token = signToken(payload, secret); + expect(() => service.verifyToken(token)).toThrow(UnauthorizedException); + }); + + it('should reject token with invalid signature', () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + const payload: AdminAuthPayload = { + adminId: '1', + username: 'admin', + role: 9, + iat: now, + exp: now + 1000, + }; + + const token = signToken(payload, 'different_secret_012345'); + expect(() => service.verifyToken(token)).toThrow(UnauthorizedException); + }); + + it('should reject token with non-admin role', () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + const payload: AdminAuthPayload = { + adminId: '1', + username: 'user', + role: 1, + iat: now, + exp: now + 1000, + }; + + const token = signToken(payload, secret); + expect(() => service.verifyToken(token)).toThrow(UnauthorizedException); + }); + + it('should reject token when signature length mismatches expected', () => { + const now = 1735689600000; + jest.spyOn(Date, 'now').mockReturnValue(now); + + const payload: AdminAuthPayload = { + adminId: '1', + username: 'admin', + role: 9, + iat: now, + exp: now + 1000, + }; + + // Valid payloadPart, but deliberately wrong signature length + const payloadJson = JSON.stringify(payload); + const payloadPart = Buffer.from(payloadJson, 'utf8') + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/g, ''); + + expect(() => service.verifyToken(`${payloadPart}.x`)).toThrow(UnauthorizedException); + }); + }); + + describe('resetUserPassword', () => { + it('should update user password_hash when password is strong', async () => { + const bcryptAny = bcrypt as any; + bcryptAny.hash.mockResolvedValue('hashed'); + usersService.update.mockResolvedValue({} as any); + + await service.resetUserPassword(BigInt(5), 'NewPass1234'); + + expect(usersService.update).toHaveBeenCalledWith(BigInt(5), { password_hash: 'hashed' }); + }); + + it('should reject weak password', async () => { + await expect(service.resetUserPassword(BigInt(5), 'short')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should validate password strength directly (letters + numbers, 8+)', () => { + expect(() => (service as any).validatePasswordStrength('12345678')).toThrow(BadRequestException); + expect(() => (service as any).validatePasswordStrength('abcdefgh')).toThrow(BadRequestException); + expect(() => (service as any).validatePasswordStrength('Abcdef12')).not.toThrow(); + }); + + it('should reject too-long password (>128)', () => { + const long = `Abc1${'x'.repeat(200)}`; + expect(() => (service as any).validatePasswordStrength(long)).toThrow(BadRequestException); + }); + }); + + describe('bootstrapAdminIfEnabled', () => { + it('should do nothing when bootstrap disabled', async () => { + await service.onModuleInit(); + expect(usersService.findByUsername).not.toHaveBeenCalled(); + expect(usersService.create).not.toHaveBeenCalled(); + }); + + it('should skip when enabled but missing username/password', async () => { + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true'; + if (key === 'ADMIN_USERNAME') return undefined; + if (key === 'ADMIN_PASSWORD') return undefined; + if (key === 'ADMIN_NICKNAME') return defaultValue ?? '管理员'; + if (key === 'ADMIN_TOKEN_SECRET') return secret; + return defaultValue; + }); + + await service.onModuleInit(); + + expect(usersService.findByUsername).not.toHaveBeenCalled(); + expect(usersService.create).not.toHaveBeenCalled(); + }); + + it('should skip when existing user already present', async () => { + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true'; + if (key === 'ADMIN_USERNAME') return 'admin'; + if (key === 'ADMIN_PASSWORD') return 'Admin123456'; + if (key === 'ADMIN_NICKNAME') return '管理员'; + if (key === 'ADMIN_TOKEN_SECRET') return secret; + return defaultValue; + }); + + usersService.findByUsername.mockResolvedValue({ id: BigInt(1), username: 'admin', role: 9 } as any); + + await service.onModuleInit(); + + expect(usersService.create).not.toHaveBeenCalled(); + }); + + it('should skip and warn when existing user has same username but non-admin role', async () => { + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true'; + if (key === 'ADMIN_USERNAME') return 'admin'; + if (key === 'ADMIN_PASSWORD') return 'Admin123456'; + if (key === 'ADMIN_NICKNAME') return '管理员'; + if (key === 'ADMIN_TOKEN_SECRET') return secret; + return defaultValue; + }); + + usersService.findByUsername.mockResolvedValue({ id: BigInt(1), username: 'admin', role: 1 } as any); + + await service.onModuleInit(); + expect(usersService.create).not.toHaveBeenCalled(); + }); + + it('should create admin user when enabled and not existing', async () => { + (configService.get as jest.Mock).mockImplementation((key: string, defaultValue?: any) => { + if (key === 'ADMIN_BOOTSTRAP_ENABLED') return 'true'; + if (key === 'ADMIN_USERNAME') return 'admin'; + if (key === 'ADMIN_PASSWORD') return 'Admin123456'; + if (key === 'ADMIN_NICKNAME') return '管理员'; + if (key === 'ADMIN_TOKEN_SECRET') return secret; + return defaultValue; + }); + + usersService.findByUsername.mockResolvedValue(null); + const bcryptAny = bcrypt as any; + bcryptAny.hash.mockResolvedValue('hashed'); + + await service.onModuleInit(); + + expect(usersService.create).toHaveBeenCalledWith({ + username: 'admin', + password_hash: 'hashed', + nickname: '管理员', + role: 9, + email_verified: true, + }); + }); + }); +}); diff --git a/src/core/guards/admin.guard.spec.ts b/src/core/guards/admin.guard.spec.ts new file mode 100644 index 0000000..86df850 --- /dev/null +++ b/src/core/guards/admin.guard.spec.ts @@ -0,0 +1,81 @@ +import { ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { AdminCoreService, AdminAuthPayload } from '../admin_core/admin_core.service'; +import { AdminGuard } from './admin.guard'; + +describe('AdminGuard', () => { + const payload: AdminAuthPayload = { + adminId: '1', + username: 'admin', + role: 9, + iat: 1, + exp: 2, + }; + + const adminCoreServiceMock: Pick = { + verifyToken: jest.fn(), + }; + + const makeContext = (authorization?: any) => { + const req: any = { headers: {} }; + if (authorization !== undefined) { + req.headers['authorization'] = authorization; + } + + const ctx: Partial = { + switchToHttp: () => ({ + getRequest: () => req, + getResponse: () => ({} as any), + getNext: () => ({} as any), + }), + }; + + return { ctx: ctx as ExecutionContext, req }; + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should allow access with valid admin token', () => { + (adminCoreServiceMock.verifyToken as jest.Mock).mockReturnValue(payload); + + const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); + const { ctx, req } = makeContext('Bearer valid'); + + expect(guard.canActivate(ctx)).toBe(true); + expect(adminCoreServiceMock.verifyToken).toHaveBeenCalledWith('valid'); + expect(req.admin).toEqual(payload); + }); + + it('should deny access without token', () => { + const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); + const { ctx } = makeContext(undefined); + + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); + + it('should deny access with invalid Authorization format', () => { + const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); + const { ctx } = makeContext('InvalidFormat'); + + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); + + it('should deny access when verifyToken throws (invalid/expired)', () => { + (adminCoreServiceMock.verifyToken as jest.Mock).mockImplementation(() => { + throw new UnauthorizedException('Token已过期'); + }); + + const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); + const { ctx } = makeContext('Bearer bad'); + + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); + + it('should deny access when Authorization header is an array', () => { + const guard = new AdminGuard(adminCoreServiceMock as AdminCoreService); + const { ctx } = makeContext(['Bearer token']); + + expect(() => guard.canActivate(ctx)).toThrow(UnauthorizedException); + }); +});