From d09476ba41f8393bd55a34e9849a6debf1a7e5a0 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Wed, 19 Aug 2026 10:50:30 +0200 Subject: [PATCH] test: cover the successful travel start happy path in the e2e smoke suite The DB-gated e2e suite covered health, seeded reads, and the arrivesAt whitelist rejection, but never asserted that a valid POST /api/travel actually transitions to TRAVELLING, that a concurrent second start is rejected with 409/TRAVEL_ALREADY_ACTIVE, or that a travel completes and moves the character. Added one test covering all three plus GET /api/world/current-location reflecting the move, and restores the demo character to its original location afterward so the suite stays safely re-runnable. --- apps/api/test/visible-slice.e2e-spec.ts | 99 +++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/apps/api/test/visible-slice.e2e-spec.ts b/apps/api/test/visible-slice.e2e-spec.ts index dfed8c6..28a4473 100644 --- a/apps/api/test/visible-slice.e2e-spec.ts +++ b/apps/api/test/visible-slice.e2e-spec.ts @@ -152,5 +152,104 @@ describe('Visible vertical slice smoke (e2e)', () => { }) .expect(400); }); + + it( + 'POST /api/travel starts a travel, rejects a concurrent start, and completes into the moved character (full happy path)', + async () => { + // `GET current-location` lazily completes any overdue travel left + // behind by a previous run before we read the starting point, so + // this test is safe to re-run without a fresh seed. + const origin = await request(app.getHttpServer()) + .get('/api/world/current-location') + .expect(200); + const originLocationId: string = origin.body.id; + const outbound = origin.body.connections[0]; + const targetLocationId: string = outbound.targetLocation.id; + const travelDurationSeconds: number = outbound.travelDurationSeconds; + + const started = await request(app.getHttpServer()) + .post('/api/travel') + .send({ targetLocationId }) + .expect(201); + + expect(started.body).toMatchObject({ + status: 'TRAVELLING', + targetLocation: { id: targetLocationId }, + }); + expect(typeof started.body.arrivesAt).toBe('string'); + expect(Number.isNaN(Date.parse(started.body.arrivesAt))).toBe(false); + + const concurrentStart = await request(app.getHttpServer()) + .post('/api/travel') + .send({ targetLocationId }) + .expect(409); + expect(concurrentStart.body).toMatchObject({ + code: 'TRAVEL_ALREADY_ACTIVE', + }); + + const arrivedTravel = await pollUntilTravelCompletes( + app, + travelDurationSeconds, + ); + expect(arrivedTravel).toMatchObject({ + status: 'COMPLETED', + targetLocation: { id: targetLocationId }, + }); + + const arrivedLocation = await request(app.getHttpServer()) + .get('/api/world/current-location') + .expect(200); + expect(arrivedLocation.body.id).toBe(targetLocationId); + + // Restore the demo character to its original location so the suite + // (and this test) stays safely re-runnable. + const returnConnection = arrivedLocation.body.connections.find( + (connection: { targetLocation: { id: string } }) => + connection.targetLocation.id === originLocationId, + ); + expect(returnConnection).toBeDefined(); + + await request(app.getHttpServer()) + .post('/api/travel') + .send({ targetLocationId: originLocationId }) + .expect(201); + + const returnedTravel = await pollUntilTravelCompletes( + app, + returnConnection.travelDurationSeconds, + ); + expect(returnedTravel).toMatchObject({ + status: 'COMPLETED', + targetLocation: { id: originLocationId }, + }); + + const restoredLocation = await request(app.getHttpServer()) + .get('/api/world/current-location') + .expect(200); + expect(restoredLocation.body.id).toBe(originLocationId); + }, + 30_000, + ); + + async function pollUntilTravelCompletes( + application: INestApplication, + travelDurationSeconds: number, + ): Promise<{ status: string; targetLocation?: { id: string } }> { + const deadline = Date.now() + travelDurationSeconds * 1000 + 5_000; + let body: { status: string; targetLocation?: { id: string } }; + + do { + const response = await request(application.getHttpServer()) + .get('/api/travel/current') + .expect(200); + body = response.body; + if (body.status === 'COMPLETED') { + return body; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } while (Date.now() < deadline); + + return body; + } }); });