docs: add visible slice local workflow
Document the migration/seed/API/web workflow in a root README, add an API e2e smoke test that always checks /api/health and additionally exercises the real DatabaseModule/seeded data plus the arrivesAt validation rejection when DATABASE_URL is available, and add a `test:e2e` root script. Also fix `.env` loading so the documented root-level `.env` is actually found: `main.ts` never loaded dotenv at all, and `data-source.ts` loaded it relative to `process.cwd()`, which is `apps/api` (not the repo root) whenever npm runs a `--workspace` script. Both now resolve the repo-root `.env` explicitly. Also narrowed the CLI migrations glob to numeric-prefixed files so it no longer tries to load the colocated `*.migration.spec.ts` as a migration. Verified end-to-end against a real PostgreSQL instance: migrate, seed twice (idempotent), start the API, and exercise every documented route including a rejected POST /api/travel body containing arrivesAt. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,5 +5,6 @@
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
},
|
||||
"testTimeout": 15000
|
||||
}
|
||||
|
||||
156
apps/api/test/visible-slice.e2e-spec.ts
Normal file
156
apps/api/test/visible-slice.e2e-spec.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
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<App>;
|
||||
|
||||
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<App>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user