import { config } from 'dotenv'; import { resolve } from 'path'; import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, Module } from '@nestjs/common'; import request from 'supertest'; import { App } from 'supertest/types'; import { AppModule } from './../src/app.module'; import { CharactersModule } from './../src/characters/characters.module'; import { DatabaseModule } from './../src/database/database.module'; import { configureApplication } from './../src/app.config'; import { TravelModule } from './../src/travel/travel.module'; import { WorldModule } from './../src/world/world.module'; import { DEMO_CHARACTER_ID } from './../src/demo/demo-character.constants'; import { BURNED_ROAD_ID } from './../src/database/seeds/vertical-slice.constants'; // `npm run test:e2e` runs from the `apps/api` workspace, so process.cwd() // is `apps/api`, not the repo root where the documented `.env` lives. // Load it the same way `main.ts`/`data-source.ts` do, without overriding a // `DATABASE_URL` a developer or CI already exported. config({ path: resolve(__dirname, '../../../.env') }); @Module({}) class TestDatabaseModule {} @Module({}) class TestCharactersModule {} @Module({}) class TestTravelModule {} @Module({}) class TestWorldModule {} describe('Visible vertical slice smoke (e2e)', () => { describe('without a developer database', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }) .overrideModule(DatabaseModule) .useModule(TestDatabaseModule) .overrideModule(CharactersModule) .useModule(TestCharactersModule) .overrideModule(TravelModule) .useModule(TestTravelModule) .overrideModule(WorldModule) .useModule(TestWorldModule) .compile(); app = moduleFixture.createNestApplication(); configureApplication(app); await app.init(); }); afterEach(async () => { await app?.close(); }); it('GET /api/health returns ok', () => { return request(app.getHttpServer()) .get('/api/health') .expect(200) .expect({ status: 'ok' }); }); }); // These assertions exercise the real DatabaseModule, TypeORM entities and // the seeded demo character/world data, so they only run when a reachable // PostgreSQL database is configured (see README.md for local setup). const describeWithDatabase = process.env.DATABASE_URL ? describe : describe.skip; describeWithDatabase('against a migrated and seeded database', () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); configureApplication(app); await app.init(); }); afterAll(async () => { await app?.close(); }); it('GET /api/health returns ok', () => { return request(app.getHttpServer()) .get('/api/health') .expect(200) .expect({ status: 'ok' }); }); it('GET /api/characters/me returns the seeded demo character', async () => { const response = await request(app.getHttpServer()) .get('/api/characters/me') .expect(200); const body = response.body as { id: string; name: string; currentLocation: { id: string; key: string; name: string }; }; expect(body).toMatchObject({ id: DEMO_CHARACTER_ID, name: 'Aric Duskwalker', }); expect(typeof body.currentLocation.id).toBe('string'); expect(typeof body.currentLocation.key).toBe('string'); expect(typeof body.currentLocation.name).toBe('string'); }); it('GET /api/world/current-location returns the character location with its connections', async () => { const response = await request(app.getHttpServer()) .get('/api/world/current-location') .expect(200); const body = response.body as { id: string; key: string; name: string; connections: unknown[]; }; expect(typeof body.id).toBe('string'); expect(typeof body.key).toBe('string'); expect(typeof body.name).toBe('string'); expect(Array.isArray(body.connections)).toBe(true); expect(body.connections.length).toBeGreaterThan(0); }); it('GET /api/travel/current returns a known travel status', async () => { const response = await request(app.getHttpServer()) .get('/api/travel/current') .expect(200); const body = response.body as { status: string }; expect(['IDLE', 'TRAVELLING', 'COMPLETED']).toContain(body.status); }); it('POST /api/travel rejects a body carrying a non-whitelisted arrivesAt field', () => { return request(app.getHttpServer()) .post('/api/travel') .send({ targetLocationId: BURNED_ROAD_ID, arrivesAt: new Date().toISOString(), }) .expect(400); }); }); });