Files
goodgo-platform/docs/audits/ADMIN_MODULE_TEST_ANALYSIS.md
Ho Ngoc Hai b8512ebff4 docs: consolidate audit and analysis reports into docs/audits/
Move 36 root-level audit/analysis documents and 7 web app audit documents
into docs/audits/ directory to declutter the project root. Remove stale
EXPLORATION_SUMMARY.txt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:37:50 +07:00

24 KiB
Raw Permalink Blame History

Admin Module - Untested Handlers/Source Files Analysis

Executive Summary

This document provides a comprehensive analysis of untested source files in the admin module (apps/api/src/modules/admin/), with detailed code listings and testing patterns for reference.

Analysis Date: 2026-04-11


1. UNTESTED HANDLER FILES

1.1 reject-listing.handler.ts (Commands)

Path: apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts

Status: NO TEST FILE FOUND

Source Code:

import { Inject } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings';
import { NotFoundException, ValidationException } from '@modules/shared';
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
import { RejectListingCommand } from './reject-listing.command';

export interface RejectListingResult {
  listingId: string;
  status: string;
  message: string;
}

@CommandHandler(RejectListingCommand)
export class RejectListingHandler implements ICommandHandler<RejectListingCommand> {
  constructor(
    @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
    private readonly eventBus: EventBus,
  ) {}

  async execute(command: RejectListingCommand): Promise<RejectListingResult> {
    const listing = await this.listingRepo.findById(command.listingId);
    if (!listing) {
      throw new NotFoundException('Listing không tồn tại');
    }

    if (listing.status !== 'PENDING_REVIEW') {
      throw new ValidationException(
        `Listing đang ở trạng thái ${listing.status}, chỉ có thể từ chối listing đang chờ duyệt`,
        { currentStatus: listing.status },
      );
    }

    listing.reject(command.reason);
    await this.listingRepo.update(listing);

    this.eventBus.publish(
      new ListingRejectedEvent(listing.id, command.adminId, command.reason),
    );

    return {
      listingId: listing.id,
      status: 'REJECTED',
      message: 'Listing đã bị từ chối',
    };
  }
}

Associated Command Class:

export class RejectListingCommand {
  constructor(
    public readonly listingId: string,
    public readonly adminId: string,
    public readonly reason: string,
  ) {}
}

Key Testing Points:

  • Successfully rejects a PENDING_REVIEW listing
  • Throws NotFoundException when listing doesn't exist
  • Throws ValidationException when listing is not in PENDING_REVIEW status
  • Calls listingRepo.update() exactly once
  • Publishes ListingRejectedEvent with correct data
  • Returns correct RejectListingResult with status 'REJECTED'

1.2 get-revenue-stats.handler.ts (Queries)

Path: apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts

Status: NO TEST FILE FOUND

Source Code:

import { Inject } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type RevenueStatsItem } from '../../../domain/repositories/admin-query.repository';
import { GetRevenueStatsQuery } from './get-revenue-stats.query';

@QueryHandler(GetRevenueStatsQuery)
export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuery> {
  constructor(
    @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
  ) {}

  async execute(query: GetRevenueStatsQuery): Promise<RevenueStatsItem[]> {
    return this.adminQueryRepo.getRevenueStats(query.startDate, query.endDate, query.groupBy);
  }
}

Associated Query Class:

export class GetRevenueStatsQuery {
  constructor(
    public readonly startDate: Date,
    public readonly endDate: Date,
    public readonly groupBy: 'day' | 'month' = 'month',
  ) {}
}

RevenueStatsItem Interface:

export interface RevenueStatsItem {
  period: string;
  totalRevenue: bigint;
  subscriptionRevenue: bigint;
  listingFeeRevenue: bigint;
  featuredListingRevenue: bigint;
  transactionCount: number;
}

Key Testing Points:

  • Returns RevenueStatsItem[] from repository
  • Passes correct startDate, endDate, and groupBy parameters
  • Calls adminQueryRepo.getRevenueStats() exactly once
  • Supports both 'day' and 'month' groupBy values
  • Default groupBy is 'month'
  • Handles empty results (empty array)

1.3 user-deactivated.listener.ts (Listeners)

Path: apps/api/src/modules/admin/application/listeners/user-deactivated.listener.ts

Status: NO TEST FILE FOUND

Source Code:

import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { type UserDeactivatedEvent } from '@modules/auth';
import { type LoggerService, type PrismaService } from '@modules/shared';

@Injectable()
export class UserDeactivatedListener {
  constructor(
    private readonly prisma: PrismaService,
    private readonly logger: LoggerService,
  ) {}

  @OnEvent('user.deactivated', { async: true })
  async handle(event: UserDeactivatedEvent): Promise<void> {
    this.logger.log(`Handling user.deactivated for user ${event.aggregateId}`, 'UserDeactivatedListener');

    const deactivated = await this.prisma.listing.updateMany({
      where: {
        sellerId: event.aggregateId,
        status: { in: ['ACTIVE', 'PENDING_REVIEW'] },
      },
      data: { status: 'EXPIRED' },
    });

    this.logger.log(
      `Expired ${deactivated.count} listings for deactivated user ${event.aggregateId}`,
      'UserDeactivatedListener',
    );
  }
}

Key Testing Points:

  • Handles 'user.deactivated' event (async: true)
  • Updates listings with status ACTIVE or PENDING_REVIEW to EXPIRED
  • Only updates listings for the deactivated seller
  • Logs initial event handling
  • Logs number of expired listings after operation
  • Returns void (Promise)
  • Handles case with 0 listings updated
  • Handles case with multiple listings updated

2. EXISTING TEST FILES STRUCTURE & PATTERNS

2.1 Test File for Reference: approve-listing.handler.spec.ts

Path: apps/api/src/modules/admin/application/__tests__/approve-listing.handler.spec.ts

Complete Source Code:

import { ListingEntity } from '@modules/listings/domain/entities/listing.entity';
import { type IListingRepository } from '@modules/listings/domain/repositories/listing.repository';
import { Price } from '@modules/listings/domain/value-objects/price.vo';
import { ApproveListingCommand } from '../commands/approve-listing/approve-listing.command';
import { ApproveListingHandler } from '../commands/approve-listing/approve-listing.handler';

function createPendingListing(id = 'listing-1'): ListingEntity {
  const price = Price.create(1_000_000_000n).unwrap();
  const listing = ListingEntity.createNew(
    id, 'prop-1', 'seller-1', 'SALE', price, 100, 'agent-1',
  );
  listing.submitForReview();
  listing.clearDomainEvents();
  return listing;
}

describe('ApproveListingHandler', () => {
  let handler: ApproveListingHandler;
  let mockListingRepo: { [K in keyof IListingRepository]: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockListingRepo = {
      findById: vi.fn(),
      findByIdWithProperty: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
      search: vi.fn(),
      findByStatus: vi.fn(),
      findBySellerId: vi.fn(),
    };

    mockEventBus = { publish: vi.fn() };

    handler = new ApproveListingHandler(
      mockListingRepo as any,
      mockEventBus as any,
    );
  });

  it('approves a pending listing successfully', async () => {
    const listing = createPendingListing();
    mockListingRepo.findById.mockResolvedValue(listing);
    mockListingRepo.update.mockResolvedValue(undefined);

    const command = new ApproveListingCommand('listing-1', 'admin-1', 'Looks good');
    const result = await handler.execute(command);

    expect(result.status).toBe('ACTIVE');
    expect(result.listingId).toBe('listing-1');
    expect(mockListingRepo.update).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('throws NotFoundException when listing does not exist', async () => {
    mockListingRepo.findById.mockResolvedValue(null);

    const command = new ApproveListingCommand('nonexistent', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow('Listing không tồn tại');
  });

  it('throws ValidationException when listing is not pending review', async () => {
    const price = Price.create(500_000_000n).unwrap();
    const listing = ListingEntity.createNew(
      'listing-1', 'prop-1', 'seller-1', 'SALE', price, 80,
    );
    listing.clearDomainEvents();
    mockListingRepo.findById.mockResolvedValue(listing);

    const command = new ApproveListingCommand('listing-1', 'admin-1');

    await expect(handler.execute(command)).rejects.toThrow(/trạng thái/);
  });
});

Key Patterns Used:

  1. Factory Function: createPendingListing() creates test entities
  2. Mock Setup: Typed mocks using ReturnType<typeof vi.fn>
  3. Initialization: beforeEach() sets up fresh mocks for each test
  4. Entity Testing: Uses actual domain entities with state transitions
  5. Domain Events: Clears domain events after setup with .clearDomainEvents()
  6. Test Structure:
    • Happy path (success case)
    • Error cases (NotFoundException, ValidationException)
    • Side effect verification (repository calls, events published)

2.2 Listener Test Pattern: user-banned.listener.spec.ts

Path: apps/api/src/modules/admin/application/__tests__/user-banned.listener.spec.ts

Complete Source Code:

import { UserBannedListener } from '../listeners/user-banned.listener';

describe('UserBannedListener', () => {
  let listener: UserBannedListener;
  let mockCommandBus: { execute: ReturnType<typeof vi.fn> };
  let mockPrisma: {
    listing: { updateMany: ReturnType<typeof vi.fn> };
    user: { findUnique: ReturnType<typeof vi.fn> };
  };
  let mockLogger: { log: ReturnType<typeof vi.fn>; warn: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockCommandBus = { execute: vi.fn().mockResolvedValue(undefined) };
    mockPrisma = {
      listing: { updateMany: vi.fn().mockResolvedValue({ count: 3 }) },
      user: { findUnique: vi.fn() },
    };
    mockLogger = { log: vi.fn(), warn: vi.fn() };

    listener = new UserBannedListener(
      mockCommandBus as any,
      mockPrisma as any,
      mockLogger as any,
    );
  });

  it('deactivates all user listings when banned', async () => {
    mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });

    await listener.handle({
      aggregateId: 'user-1',
      adminId: 'admin-1',
      reason: 'Vi phạm chính sách',
      eventName: 'user.banned',
      occurredAt: new Date(),
    });

    expect(mockPrisma.listing.updateMany).toHaveBeenCalledWith({
      where: {
        sellerId: 'user-1',
        status: { in: ['ACTIVE', 'PENDING_REVIEW', 'DRAFT'] },
      },
      data: { status: 'EXPIRED' },
    });
  });

  it('notifies banned user via email', async () => {
    mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: 'user@example.com' });

    await listener.handle({
      aggregateId: 'user-1',
      adminId: 'admin-1',
      reason: 'Spam',
      eventName: 'user.banned',
      occurredAt: new Date(),
    });

    expect(mockCommandBus.execute).toHaveBeenCalledWith(
      expect.objectContaining({
        userId: 'user-1',
        channel: 'EMAIL',
        templateKey: 'user.banned',
        templateData: { reason: 'Spam' },
      }),
    );
  });

  it('skips email notification when user has no email', async () => {
    mockPrisma.user.findUnique.mockResolvedValue({ id: 'user-1', email: null });

    await listener.handle({
      aggregateId: 'user-1',
      adminId: 'admin-1',
      reason: 'Vi phạm',
      eventName: 'user.banned',
      occurredAt: new Date(),
    });

    expect(mockPrisma.listing.updateMany).toHaveBeenCalled();
    expect(mockCommandBus.execute).not.toHaveBeenCalled();
  });
});

Listener Test Patterns:

  1. Event Object Construction: Manually create event with all properties
  2. Multiple Dependencies: Mock CommandBus, Prisma, Logger
  3. Side Effects Testing: Verify database calls and command execution
  4. Conditional Logic: Test both happy path and edge cases (no email)
  5. Chained Operations: Test interactions between multiple services

2.3 Query Handler Pattern: get-dashboard-stats.handler.spec.ts

Path: apps/api/src/modules/admin/application/__tests__/get-dashboard-stats.handler.spec.ts

Complete Source Code:

import { type IAdminQueryRepository } from '../../domain/repositories/admin-query.repository';
import { GetDashboardStatsHandler } from '../queries/get-dashboard-stats/get-dashboard-stats.handler';
import { GetDashboardStatsQuery } from '../queries/get-dashboard-stats/get-dashboard-stats.query';

describe('GetDashboardStatsHandler', () => {
  let handler: GetDashboardStatsHandler;
  let mockAdminQueryRepo: { [K in keyof IAdminQueryRepository]: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockAdminQueryRepo = {
      getModerationQueue: vi.fn(),
      getDashboardStats: vi.fn(),
      getRevenueStats: vi.fn(),
      getUsers: vi.fn(),
    };

    handler = new GetDashboardStatsHandler(mockAdminQueryRepo as any);
  });

  it('returns dashboard stats', async () => {
    const stats = {
      totalUsers: 150,
      totalListings: 500,
      activeListings: 200,
      pendingModerationCount: 15,
      totalAgents: 30,
      verifiedAgents: 20,
      totalTransactions: 50,
      newUsersLast30Days: 25,
      newListingsLast30Days: 40,
    };

    mockAdminQueryRepo.getDashboardStats.mockResolvedValue(stats);

    const result = await handler.execute(new GetDashboardStatsQuery());

    expect(result).toEqual(stats);
    expect(mockAdminQueryRepo.getDashboardStats).toHaveBeenCalledTimes(1);
  });
});

Query Handler Test Patterns:

  1. Simple Delegation: Query handlers typically just delegate to repository
  2. Minimal Setup: Mock only the repository
  3. Result Verification: Check the returned data matches expectations
  4. Call Count: Verify the repository method was called exactly once

3. HANDLER CODE FOR REFERENCE

3.1 Similar Handler: ban-user.handler.spec.ts

Path: apps/api/src/modules/admin/application/__tests__/ban-user.handler.spec.ts

Complete Source Code (for comparison to reject-listing):

import { UserEntity } from '@modules/auth/domain/entities/user.entity';
import { type IUserRepository } from '@modules/auth/domain/repositories/user.repository';
import { HashedPassword } from '@modules/auth/domain/value-objects/hashed-password.vo';
import { Phone } from '@modules/auth/domain/value-objects/phone.vo';
import { BanUserCommand } from '../commands/ban-user/ban-user.command';
import { BanUserHandler } from '../commands/ban-user/ban-user.handler';

async function createUser(role = 'BUYER' as any, isActive = true): Promise<UserEntity> {
  const phone = Phone.create('0901234567').unwrap();
  const hash = await HashedPassword.fromPlain('Password123');
  const passwordHash = hash.isOk ? hash.unwrap() : null;
  const user = new UserEntity('user-1', {
    email: null,
    phone,
    passwordHash,
    fullName: 'Test User',
    avatarUrl: null,
    role,
    kycStatus: 'NONE',
    kycData: null,
    isActive,
  });
  return user;
}

describe('BanUserHandler', () => {
  let handler: BanUserHandler;
  let mockUserRepo: { [K in keyof IUserRepository]: ReturnType<typeof vi.fn> };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  beforeEach(() => {
    mockUserRepo = {
      findById: vi.fn(),
      findByPhone: vi.fn(),
      findByEmail: vi.fn(),
      save: vi.fn(),
      update: vi.fn(),
    };

    mockEventBus = { publish: vi.fn() };

    handler = new BanUserHandler(
      mockUserRepo as any,
      mockEventBus as any,
    );
  });

  it('bans an active user successfully', async () => {
    const user = await createUser('BUYER', true);
    mockUserRepo.findById.mockResolvedValue(user);
    mockUserRepo.update.mockResolvedValue(undefined);

    const command = new BanUserCommand('user-1', 'admin-1', 'Spam activity');
    const result = await handler.execute(command);

    expect(result.isActive).toBe(false);
    expect(result.message).toContain('bị ban');
    expect(mockUserRepo.update).toHaveBeenCalledTimes(1);
    expect(mockEventBus.publish).toHaveBeenCalled();
  });

  it('unbans a banned user successfully', async () => {
    const user = await createUser('BUYER', false);
    mockUserRepo.findById.mockResolvedValue(user);
    mockUserRepo.update.mockResolvedValue(undefined);

    const command = new BanUserCommand('user-1', 'admin-1', 'Resolved', true);
    const result = await handler.execute(command);

    expect(result.isActive).toBe(true);
    expect(result.message).toContain('gỡ ban');
  });

  it('throws NotFoundException when user does not exist', async () => {
    mockUserRepo.findById.mockResolvedValue(null);

    const command = new BanUserCommand('nonexistent', 'admin-1', 'test');

    await expect(handler.execute(command)).rejects.toThrow('Người dùng không tồn tại');
  });

  it('prevents banning an admin user', async () => {
    const admin = await createUser('ADMIN', true);
    mockUserRepo.findById.mockResolvedValue(admin);

    const command = new BanUserCommand('user-1', 'admin-1', 'test');

    await expect(handler.execute(command)).rejects.toThrow(/admin/i);
  });

  it('throws when trying to ban already banned user', async () => {
    const user = await createUser('BUYER', false);
    mockUserRepo.findById.mockResolvedValue(user);

    const command = new BanUserCommand('user-1', 'admin-1', 'test');

    await expect(handler.execute(command)).rejects.toThrow('Người dùng đã bị ban');
  });
});

4. INFRASTRUCTURE & REPOSITORY FILES (No tests required but provided for context)

4.1 prisma-admin-query.repository.ts

Path: apps/api/src/modules/admin/infrastructure/repositories/prisma-admin-query.repository.ts

Status: Infrastructure layer - Integration tests may be appropriate instead

Note: This is an implementation detail that delegates to more granular query functions. Consider integration tests instead of unit tests.


5. PRESENTATION LAYER FILES (No tests typically required)

5.1 admin.controller.ts

Path: apps/api/src/modules/admin/presentation/controllers/admin.controller.ts

Status: Controllers typically use E2E tests, not unit tests

Note: Controllers are tested via E2E/integration tests, not unit tests in this architecture.


6. TEST WRITING RECOMMENDATIONS

For reject-listing.handler.spec.ts

Follow the pattern from approve-listing.handler.spec.ts:

  1. Create helper function to build listing entities
  2. Mock IListingRepository and EventBus
  3. Test 3 scenarios:
    • Successfully rejects pending listing → publishes event
    • Throws when listing doesn't exist
    • Throws when listing not in PENDING_REVIEW status

For get-revenue-stats.handler.spec.ts

Follow the pattern from get-dashboard-stats.handler.spec.ts:

  1. Mock IAdminQueryRepository
  2. Test single scenario:
    • Returns results from repository.getRevenueStats() call
  3. Verify correct parameters passed:
    • startDate, endDate, groupBy

For user-deactivated.listener.spec.ts

Follow the pattern from user-banned.listener.spec.ts:

  1. Mock PrismaService and LoggerService
  2. Test scenarios:
    • Successfully expires listings for deactivated user
    • Updates both ACTIVE and PENDING_REVIEW listings
    • Logs correct messages
    • Handles case with 0 listings updated
    • Handles case with multiple listings updated

7. COMPLETE FILE STRUCTURE

admin/
├── application/
│   ├── __tests__/
│   │   ├── adjust-subscription.handler.spec.ts ✅
│   │   ├── admin-audit.listener.spec.ts ✅
│   │   ├── approve-kyc.handler.spec.ts ✅
│   │   ├── approve-listing.handler.spec.ts ✅ (REFERENCE)
│   │   ├── ban-user.handler.spec.ts ✅
│   │   ├── bulk-moderate-listings.handler.spec.ts ✅
│   │   ├── get-audit-logs.handler.spec.ts ✅
│   │   ├── get-dashboard-stats.handler.spec.ts ✅ (REFERENCE)
│   │   ├── get-kyc-queue.handler.spec.ts ✅
│   │   ├── get-moderation-queue.handler.spec.ts ✅
│   │   ├── get-user-detail.handler.spec.ts ✅
│   │   ├── get-users.handler.spec.ts ✅
│   │   ├── reject-kyc.handler.spec.ts ✅
│   │   ├── update-user-status.handler.spec.ts ✅
│   │   ├── user-banned.listener.spec.ts ✅ (REFERENCE)
│   │   └── [MISSING] reject-listing.handler.spec.ts ❌
│   │   └── [MISSING] get-revenue-stats.handler.spec.ts ❌
│   │   └── [MISSING] user-deactivated.listener.spec.ts ❌
│   │
│   ├── commands/
│   │   ├── adjust-subscription/
│   │   ├── approve-kyc/
│   │   ├── approve-listing/
│   │   ├── ban-user/
│   │   ├── bulk-moderate-listings/
│   │   ├── reject-kyc/
│   │   ├── reject-listing/ ❌ NO TEST
│   │   ├── update-user-status/
│   │   └── index.ts
│   │
│   ├── listeners/
│   │   ├── admin-audit.listener.ts ✅
│   │   ├── user-banned.listener.ts ✅
│   │   ├── user-deactivated.listener.ts ❌ NO TEST
│   │   └── (no index.ts)
│   │
│   ├── queries/
│   │   ├── get-audit-logs/
│   │   ├── get-dashboard-stats/
│   │   ├── get-kyc-queue/
│   │   ├── get-moderation-queue/
│   │   ├── get-revenue-stats/ ❌ NO TEST
│   │   ├── get-user-detail/
│   │   ├── get-users/
│   │   └── index.ts
│   │
│   ├── index.ts
│   └── application.module.ts
│
├── domain/
│   ├── __tests__/
│   │   └── admin-events.spec.ts ✅
│   │
│   ├── events/
│   │   ├── kyc-approved.event.ts
│   │   ├── kyc-rejected.event.ts
│   │   ├── listing-approved.event.ts
│   │   ├── listing-rejected.event.ts
│   │   ├── subscription-adjusted.event.ts
│   │   ├── user-banned.event.ts
│   │   ├── user-unbanned.event.ts
│   │   └── index.ts
│   │
│   ├── repositories/
│   │   ├── admin-query.repository.ts
│   │   ├── audit-log.repository.ts
│   │   └── index.ts
│   │
│   ├── domain.module.ts
│   └── index.ts
│
├── infrastructure/
│   ├── repositories/
│   │   ├── admin-stats.queries.ts
│   │   ├── admin-user.queries.ts
│   │   ├── prisma-admin-query.repository.ts ( Integration test candidate)
│   │   ├── prisma-audit-log.repository.ts ( Integration test candidate)
│   │   └── index.ts
│   │
│   ├── infrastructure.module.ts
│   └── index.ts
│
├── presentation/
│   ├── controllers/
│   │   ├── admin.controller.ts ( E2E test - not unit test)
│   │   ├── admin-moderation.controller.ts ( E2E test - not unit test)
│   │   └── index.ts
│   │
│   ├── dto/
│   │   ├── adjust-subscription.dto.ts
│   │   ├── approve-kyc.dto.ts
│   │   ├── approve-listing.dto.ts
│   │   ├── ban-user.dto.ts
│   │   ├── bulk-moderate.dto.ts
│   │   ├── get-audit-logs-query.dto.ts
│   │   ├── get-users-query.dto.ts
│   │   ├── reject-kyc.dto.ts
│   │   ├── reject-listing.dto.ts
│   │   ├── revenue-stats.dto.ts
│   │   ├── update-user-status.dto.ts
│   │   └── index.ts
│   │
│   ├── presentation.module.ts
│   └── index.ts
│
├── admin.module.ts
└── index.ts

Legend:

  • Has test file
  • Missing test file
  • Not a unit test candidate (integration/E2E)

Summary: Critical Files Requiring Tests

File Type Priority Pattern to Follow
reject-listing.handler.ts Command Handler HIGH approve-listing.handler.spec.ts
get-revenue-stats.handler.ts Query Handler HIGH get-dashboard-stats.handler.spec.ts
user-deactivated.listener.ts Event Listener HIGH user-banned.listener.spec.ts