feat: expose health and demo character APIs

This commit is contained in:
Bastian Wagner
2026-08-18 20:00:40 +02:00
parent 0615bb1841
commit e9b111daf8
14 changed files with 208 additions and 51 deletions

View File

@@ -1,22 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

View File

@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@@ -1,11 +1,9 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CharactersModule } from './characters/characters.module';
import { DatabaseModule } from './database/database.module';
import { HealthModule } from './health/health.module';
@Module({
imports: [DatabaseModule],
controllers: [AppController],
providers: [AppService],
imports: [DatabaseModule, HealthModule, CharactersModule],
})
export class AppModule {}

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -0,0 +1,12 @@
import { Controller, Get } from '@nestjs/common';
import { CharactersService } from './characters.service';
@Controller('characters')
export class CharactersController {
constructor(private readonly charactersService: CharactersService) {}
@Get('me')
getDemoCharacter() {
return this.charactersService.getDemoCharacter();
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CharactersController } from './characters.controller';
import { CharactersService } from './characters.service';
import { Character } from './entities/character.entity';
@Module({
imports: [TypeOrmModule.forFeature([Character])],
controllers: [CharactersController],
providers: [CharactersService],
})
export class CharactersModule {}

View File

@@ -0,0 +1,58 @@
import { NotFoundException } from '@nestjs/common';
import { Repository } from 'typeorm';
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
import { SOUTH_GATE_ID } from '../database/seeds/vertical-slice.constants';
import { Character } from './entities/character.entity';
import { CharactersService } from './characters.service';
describe('CharactersService', () => {
it('returns the demo character with its current location summary', async () => {
const repository = {
findOne: jest.fn().mockResolvedValue({
id: DEMO_CHARACTER_ID,
name: 'Aric Duskwalker',
level: 1,
experience: 0,
currentHp: 100,
baseHp: 100,
baseAttack: 6,
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
},
}),
} as unknown as Repository<Character>;
const service = new CharactersService(repository);
await expect(service.getDemoCharacter()).resolves.toEqual({
id: DEMO_CHARACTER_ID,
name: 'Aric Duskwalker',
level: 1,
experience: 0,
currentHp: 100,
maxHp: 100,
attack: 6,
currentLocation: {
id: SOUTH_GATE_ID,
key: 'south-gate',
name: 'S\u00fcdtor von Graufurt',
},
});
expect(repository.findOne).toHaveBeenCalledWith({
where: { id: DEMO_CHARACTER_ID },
relations: { currentLocation: true },
});
});
it('reports a missing demo seed as not found', async () => {
const repository = {
findOne: jest.fn().mockResolvedValue(null),
} as unknown as Repository<Character>;
const service = new CharactersService(repository);
await expect(service.getDemoCharacter()).rejects.toBeInstanceOf(
NotFoundException,
);
});
});

View File

@@ -0,0 +1,39 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DEMO_CHARACTER_ID } from '../demo/demo-character.constants';
import { Character } from './entities/character.entity';
@Injectable()
export class CharactersService {
constructor(
@InjectRepository(Character)
private readonly characters: Repository<Character>,
) {}
async getDemoCharacter() {
const character = await this.characters.findOne({
where: { id: DEMO_CHARACTER_ID },
relations: { currentLocation: true },
});
if (!character) {
throw new NotFoundException('Demo character has not been seeded');
}
return {
id: character.id,
name: character.name,
level: character.level,
experience: character.experience,
currentHp: character.currentHp,
maxHp: character.baseHp,
attack: character.baseAttack,
currentLocation: {
id: character.currentLocation.id,
key: character.currentLocation.key,
name: character.currentLocation.name,
},
};
}
}

View File

@@ -0,0 +1,7 @@
import { HealthController } from './health.controller';
describe('HealthController', () => {
it('returns an ok status for liveness checks', () => {
expect(new HealthController().getHealth()).toEqual({ status: 'ok' });
});
});

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
getHealth() {
return { status: 'ok' };
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -5,6 +5,7 @@ import { configureApplication } from './app.config';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
configureApplication(app);
app.enableShutdownHooks();
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@@ -3,13 +3,17 @@ 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';
@Module({})
class TestDatabaseModule {}
describe('AppController (e2e)', () => {
@Module({})
class TestCharactersModule {}
describe('API (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
@@ -18,6 +22,8 @@ describe('AppController (e2e)', () => {
})
.overrideModule(DatabaseModule)
.useModule(TestDatabaseModule)
.overrideModule(CharactersModule)
.useModule(TestCharactersModule)
.compile();
app = moduleFixture.createNestApplication();
@@ -25,11 +31,11 @@ describe('AppController (e2e)', () => {
await app.init();
});
it('/api (GET)', () => {
it('/api/health (GET)', () => {
return request(app.getHttpServer())
.get('/api')
.get('/api/health')
.expect(200)
.expect('Hello World!');
.expect({ status: 'ok' });
});
it('/ (GET) is not an API route', () => {