HFU Digital Docs
RoomKitBackend

Testing

Test patterns for RoomKit using in-memory storage mocks

Overview

RoomKit's domain services depend on abstract storage interfaces (BookingStorage, RoomStorage, LocationStorage, …), so the cleanest way to test them is to substitute in-memory implementations of those interfaces — no Prisma client, no database.

RoomKit does not currently export a public createInMemoryStorage() factory. The mocks under packages/backend/src/__tests__/mocks/ are package-internal and not part of the published surface — copy the pattern into your own test helpers, or write a thin in-memory class per interface you actually use.

Writing your own in-memory mocks

For each storage abstract class you depend on, extend it and store data in a Map keyed by id. The internal RoomKit test suite does this and is the canonical reference (packages/backend/src/__tests__/mocks/mock-booking.storage.ts, etc. — visible in the GitHub source tree).

import { BookingStorage } from '@hfu.digital/roomkit-nestjs';
import type { Booking } from '@hfu.digital/roomkit-nestjs';

export class InMemoryBookingStorage extends BookingStorage {
    private bookings = new Map<string, Booking>();

    async create(booking: Booking): Promise<Booking> {
        this.bookings.set(booking.id, booking);
        return booking;
    }

    async findById(id: string): Promise<Booking | null> {
        return this.bookings.get(id) ?? null;
    }

    // … implement the rest of the BookingStorage abstract methods …
}

Compose those into a storage map and pass it to RoomKitModule.register():

import { Test } from '@nestjs/testing';
import {
    RoomKitModule,
    BookingService,
    LocationService,
    RoomService,
} from '@hfu.digital/roomkit-nestjs';

describe('BookingService', () => {
    let bookingService: BookingService;
    let locationService: LocationService;
    let roomService: RoomService;

    beforeEach(async () => {
        const module = await Test.createTestingModule({
            imports: [
                RoomKitModule.register({
                    storage: {
                        location: new InMemoryLocationStorage(),
                        room: new InMemoryRoomStorage(),
                        booking: new InMemoryBookingStorage(),
                        recurrence: new InMemoryRecurrenceStorage(),
                        blackout: new InMemoryBlackoutStorage(),
                        conflict: new InMemoryConflictStorage(),
                        config: new InMemoryConfigStorage(),
                        audit: new InMemoryAuditStorage(),
                        priority: new InMemoryPriorityStorage(),
                    },
                }),
            ],
        }).compile();

        bookingService = module.get(BookingService);
        locationService = module.get(LocationService);
        roomService = module.get(RoomService);
    });
});

exam and bulkOperation storages are only required when you enable the corresponding features.exams / features.bulkOperations flag.

Booking lifecycle test

it('walks the full booking lifecycle', async () => {
    const location = await locationService.createNode({
        type: 'room',
        displayName: 'Lecture Hall A',
    });

    const room = await roomService.create({
        locationNodeId: location.id,
        seatedCapacity: 100,
        examCapacity: 50,
        standingCapacity: 150,
    });

    const booking = await bookingService.create({
        roomId: room.id,
        requesterId: 'user-1',
        title: 'CS 101 Lecture',
        startsAt: new Date('2026-03-02T09:00:00Z'),
        endsAt: new Date('2026-03-02T10:00:00Z'),
        purposeType: 'LECTURE',
    });
    expect(booking.status).toBe('REQUESTED');

    const confirmed = await bookingService.confirm(booking.id, 'admin');
    expect(confirmed.status).toBe('CONFIRMED');

    const started = await bookingService.checkIn(booking.id, 'user-1');
    expect(started.status).toBe('IN_PROGRESS');

    const completed = await bookingService.complete(booking.id, 'user-1');
    expect(completed.status).toBe('COMPLETED');
});

BookingStatus is the RecurrenceFrequency / BookingStatus union exported from ./types/enumsREQUESTED | CONFIRMED | IN_PROGRESS | COMPLETED | CANCELLED.

Conflict detection test

import { BookingConflictError } from '@hfu.digital/roomkit-nestjs';

it('detects overlapping bookings', async () => {
    const booking = await bookingService.create({
        roomId: room.id,
        requesterId: 'user-1',
        title: 'Morning Meeting',
        startsAt: new Date('2026-03-02T09:00:00Z'),
        endsAt: new Date('2026-03-02T10:00:00Z'),
        purposeType: 'SEMINAR',
    });

    await expect(
        bookingService.create({
            roomId: room.id,
            requesterId: 'user-2',
            title: 'Overlapping Meeting',
            startsAt: new Date('2026-03-02T09:30:00Z'),
            endsAt: new Date('2026-03-02T10:30:00Z'),
            purposeType: 'SEMINAR',
        }),
    ).rejects.toThrow(BookingConflictError);
});

Recurrence test

import { RecurrenceService } from '@hfu.digital/roomkit-nestjs';

it('expands a weekly series', async () => {
    const recurrenceService = module.get(RecurrenceService);

    const result = await recurrenceService.createRecurringBooking(
        {
            roomId: room.id,
            requesterId: 'user-1',
            title: 'Weekly Standup',
            startsAt: new Date('2026-03-02T09:00:00Z'),
            endsAt: new Date('2026-03-02T09:30:00Z'),
            purposeType: 'SEMINAR',
        },
        {
            frequency: 'WEEKLY',
            seriesStartsAt: new Date('2026-03-02'),
            seriesEndsAt: new Date('2026-03-30'),
            daysOfWeek: [1], // Monday
        },
    );

    // Mar 2, 9, 16, 23, 30 — five Mondays in the window
    expect(result.bookings).toHaveLength(5);
});

Note the public method is createRecurringBooking, not createRecurringSeries. See the RecurrenceService reference.

Testing emitted events

import { EventBus } from '@hfu.digital/roomkit-nestjs';

it('emits BookingRequested on create', async () => {
    const eventBus = module.get(EventBus);
    const events: any[] = [];

    eventBus.on('BookingRequested', (event) => events.push(event));

    const booking = await bookingService.create({
        roomId: room.id,
        requesterId: 'user-1',
        title: 'Event Test',
        startsAt: new Date('2026-03-02T14:00:00Z'),
        endsAt: new Date('2026-03-02T15:00:00Z'),
        purposeType: 'SEMINAR',
    });

    expect(events).toHaveLength(1);
    expect(events[0].payload.id).toBe(booking.id);
});

On this page