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.
This commit is contained in:
Bastian Wagner
2026-08-19 10:50:30 +02:00
parent 2e7b280270
commit d09476ba41

View File

@@ -152,5 +152,104 @@ describe('Visible vertical slice smoke (e2e)', () => {
}) })
.expect(400); .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<App>,
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;
}
}); });
}); });