feat : add testing

This commit is contained in:
2024-12-29 00:06:57 +01:00
parent b97f380131
commit 3a513042f4
27 changed files with 1755 additions and 319 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"orta.vscode-jest",
"dbaeumer.vscode-eslint",
"vue.volar"
]
}
+6 -1
View File
@@ -4,5 +4,10 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint" "editor.defaultFormatter": "dbaeumer.vscode-eslint"
}, },
"javascript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single",
"typescript.preferences.quoteStyle": "single" "typescript.preferences.quoteStyle": "single",
"eslint.format.enable": true,
"eslint.useFlatConfig": true,
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
}
} }
+3 -2
View File
@@ -19,7 +19,7 @@ export default [
ignores: ['**/.eslintrc.js'], ignores: ['**/.eslintrc.js'],
}, },
...compat.extends( ...compat.extends(
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-type-checked',
'plugin:prettier/recommended', 'plugin:prettier/recommended',
), ),
{ {
@@ -48,7 +48,8 @@ export default [
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'require-await': 'off',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/no-unused-vars': [ '@typescript-eslint/no-unused-vars': [
'warn', 'warn',
{ {
+1513 -39
View File
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -65,12 +65,14 @@
"jest": "^29.7.0", "jest": "^29.7.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"sqlite3": "^5.1.7",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"typescript-eslint": "^8.18.2"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [
@@ -78,7 +80,10 @@
"json", "json",
"ts" "ts"
], ],
"rootDir": "src", "rootDir": "./",
"modulePaths": [
"<rootDir>"
],
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
-21
View File
@@ -1,21 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let app: TestingModule;
beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
});
describe('getHello', () => {
it('should return "Hello World!"', () => {
const appController = app.get(AppController);
expect(appController.getHello()).toBe('Hello World!');
});
});
});
-9
View File
@@ -1,9 +0,0 @@
import { Controller } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiTags } from '@nestjs/swagger';
@ApiTags('admin')
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
}
@@ -1,103 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Migration1735396726613 implements MigrationInterface {
name = 'Migration1735396726613';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user" ("userId" character varying NOT NULL, "avatar" character varying NOT NULL, "username" character varying NOT NULL, CONSTRAINT "PK_d72ea127f30e21753c9e229891e" PRIMARY KEY ("userId"))`,
);
await queryRunner.query(
`CREATE TABLE "topic" ("id" SERIAL NOT NULL, CONSTRAINT "PK_33aa4ecb4e4f20aa0157ea7ef61" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "word" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, "ownerUserId" character varying, CONSTRAINT "PK_ad026d65e30f80b7056ca31f666" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "concept" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_3275d953a4dc4dadeef5a8aeed4" UNIQUE ("value"), CONSTRAINT "PK_83c1330866b9866ac2d0ed2b36b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "concept_in_turn" ("id" SERIAL NOT NULL, "order" integer NOT NULL DEFAULT '0', "subconcept" integer NOT NULL DEFAULT '0', "markers" integer NOT NULL DEFAULT '1', "conceptId" integer, "gameId" integer, CONSTRAINT "PK_c44692f972ab31a6245c8599b4b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "game" ("id" SERIAL NOT NULL, "seed" character varying NOT NULL, "currentWord" character varying NOT NULL, "started" boolean NOT NULL, "ownerUserId" character varying, CONSTRAINT "PK_352a30652cd352f552fef73dec5" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "player" ("id" SERIAL NOT NULL, "score" integer NOT NULL, "userUserId" character varying, "gameId" integer, CONSTRAINT "PK_65edadc946a7faf4b638d5e8885" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "word_topics_topic" ("wordId" integer NOT NULL, "topicId" integer NOT NULL, CONSTRAINT "PK_cc5d13565912abf7d0e1dcfc3a7" PRIMARY KEY ("wordId", "topicId"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_66e5a60dcdb89fc3be327eb139" ON "word_topics_topic" ("wordId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_3346a237bd507b395347a64ddc" ON "word_topics_topic" ("topicId") `,
);
await queryRunner.query(
`ALTER TABLE "word" ADD CONSTRAINT "FK_4472fdeaf534031039106aa04de" FOREIGN KEY ("ownerUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a" FOREIGN KEY ("conceptId") REFERENCES "concept"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "game" ADD CONSTRAINT "FK_9b45658e1fea65880fd35c9bacc" FOREIGN KEY ("ownerUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "player" ADD CONSTRAINT "FK_2b8a672d52e840e6c4e3bf79f58" FOREIGN KEY ("userUserId") REFERENCES "user"("userId") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "player" ADD CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a" FOREIGN KEY ("wordId") REFERENCES "word"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_3346a237bd507b395347a64ddcb" FOREIGN KEY ("topicId") REFERENCES "topic"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_3346a237bd507b395347a64ddcb"`,
);
await queryRunner.query(
`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a"`,
);
await queryRunner.query(
`ALTER TABLE "player" DROP CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a"`,
);
await queryRunner.query(
`ALTER TABLE "player" DROP CONSTRAINT "FK_2b8a672d52e840e6c4e3bf79f58"`,
);
await queryRunner.query(
`ALTER TABLE "game" DROP CONSTRAINT "FK_9b45658e1fea65880fd35c9bacc"`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9"`,
);
await queryRunner.query(
`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a"`,
);
await queryRunner.query(
`ALTER TABLE "word" DROP CONSTRAINT "FK_4472fdeaf534031039106aa04de"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_3346a237bd507b395347a64ddc"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_66e5a60dcdb89fc3be327eb139"`,
);
await queryRunner.query(`DROP TABLE "word_topics_topic"`);
await queryRunner.query(`DROP TABLE "player"`);
await queryRunner.query(`DROP TABLE "game"`);
await queryRunner.query(`DROP TABLE "concept_in_turn"`);
await queryRunner.query(`DROP TABLE "concept"`);
await queryRunner.query(`DROP TABLE "word"`);
await queryRunner.query(`DROP TABLE "topic"`);
await queryRunner.query(`DROP TABLE "user"`);
}
}
@@ -0,0 +1,48 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migration1735422446844 implements MigrationInterface {
name = 'Migration1735422446844'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "user" ("id" character varying NOT NULL, "avatar" character varying NOT NULL, "username" character varying NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "topic" ("id" SERIAL NOT NULL, CONSTRAINT "PK_33aa4ecb4e4f20aa0157ea7ef61" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "word" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, "ownerId" character varying, CONSTRAINT "PK_ad026d65e30f80b7056ca31f666" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "concept" ("id" SERIAL NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_3275d953a4dc4dadeef5a8aeed4" UNIQUE ("value"), CONSTRAINT "PK_83c1330866b9866ac2d0ed2b36b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "concept_in_turn" ("id" SERIAL NOT NULL, "order" integer NOT NULL DEFAULT '0', "subconcept" integer NOT NULL DEFAULT '0', "markers" integer NOT NULL DEFAULT '1', "conceptId" integer, "gameId" integer, CONSTRAINT "PK_c44692f972ab31a6245c8599b4b" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "game" ("id" SERIAL NOT NULL, "seed" character varying NOT NULL, "currentWord" character varying, "started" boolean NOT NULL DEFAULT false, "ownerId" character varying, CONSTRAINT "PK_352a30652cd352f552fef73dec5" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "player" ("id" SERIAL NOT NULL, "score" integer NOT NULL DEFAULT '0', "userId" character varying, "gameId" integer, CONSTRAINT "PK_65edadc946a7faf4b638d5e8885" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "word_topics_topic" ("wordId" integer NOT NULL, "topicId" integer NOT NULL, CONSTRAINT "PK_cc5d13565912abf7d0e1dcfc3a7" PRIMARY KEY ("wordId", "topicId"))`);
await queryRunner.query(`CREATE INDEX "IDX_66e5a60dcdb89fc3be327eb139" ON "word_topics_topic" ("wordId") `);
await queryRunner.query(`CREATE INDEX "IDX_3346a237bd507b395347a64ddc" ON "word_topics_topic" ("topicId") `);
await queryRunner.query(`ALTER TABLE "word" ADD CONSTRAINT "FK_e0edff82d908ba54dcd44d5cec6" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a" FOREIGN KEY ("conceptId") REFERENCES "concept"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "concept_in_turn" ADD CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "game" ADD CONSTRAINT "FK_d05575b5a28ec6dad65c2aef301" FOREIGN KEY ("ownerId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "player" ADD CONSTRAINT "FK_7687919bf054bf262c669d3ae21" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "player" ADD CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a" FOREIGN KEY ("gameId") REFERENCES "game"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a" FOREIGN KEY ("wordId") REFERENCES "word"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE "word_topics_topic" ADD CONSTRAINT "FK_3346a237bd507b395347a64ddcb" FOREIGN KEY ("topicId") REFERENCES "topic"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_3346a237bd507b395347a64ddcb"`);
await queryRunner.query(`ALTER TABLE "word_topics_topic" DROP CONSTRAINT "FK_66e5a60dcdb89fc3be327eb139a"`);
await queryRunner.query(`ALTER TABLE "player" DROP CONSTRAINT "FK_7dfdd31fcd2b5aa3b08ed15fe8a"`);
await queryRunner.query(`ALTER TABLE "player" DROP CONSTRAINT "FK_7687919bf054bf262c669d3ae21"`);
await queryRunner.query(`ALTER TABLE "game" DROP CONSTRAINT "FK_d05575b5a28ec6dad65c2aef301"`);
await queryRunner.query(`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_f1c8614584bd8afcac818bacfd9"`);
await queryRunner.query(`ALTER TABLE "concept_in_turn" DROP CONSTRAINT "FK_6dfaa966f86139a62aba3b2114a"`);
await queryRunner.query(`ALTER TABLE "word" DROP CONSTRAINT "FK_e0edff82d908ba54dcd44d5cec6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_3346a237bd507b395347a64ddc"`);
await queryRunner.query(`DROP INDEX "public"."IDX_66e5a60dcdb89fc3be327eb139"`);
await queryRunner.query(`DROP TABLE "word_topics_topic"`);
await queryRunner.query(`DROP TABLE "player"`);
await queryRunner.query(`DROP TABLE "game"`);
await queryRunner.query(`DROP TABLE "concept_in_turn"`);
await queryRunner.query(`DROP TABLE "concept"`);
await queryRunner.query(`DROP TABLE "word"`);
await queryRunner.query(`DROP TABLE "topic"`);
await queryRunner.query(`DROP TABLE "user"`);
}
}
+1
View File
@@ -15,6 +15,7 @@ const AppDataSource = new DataSource({
password: configService.get('PG_PASSWORD'), password: configService.get('PG_PASSWORD'),
username: configService.get('PG_USER'), username: configService.get('PG_USER'),
database: 'concept', database: 'concept',
applicationName: 'concept',
synchronize: false, synchronize: false,
entities: ['**/*.entity.ts'], entities: ['**/*.entity.ts'],
migrations: ['src/db/migrations/*-migration.ts'], migrations: ['src/db/migrations/*-migration.ts'],
+12
View File
@@ -0,0 +1,12 @@
import { User } from './users/entities/user.entity';
declare module 'express-session' {
interface SessionData {
user: User;
tokenData: {
access_token: string;
token_type: string;
refresh_token: string;
expires: Date;
};
}
}
+10 -9
View File
@@ -19,6 +19,7 @@ import { Observable, fromEvent, map } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { GameService } from '../services/games.service'; import { GameService } from '../services/games.service';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
@ApiTags('game') @ApiTags('game')
@Controller('games/:id') @Controller('games/:id')
@@ -47,7 +48,7 @@ export class GamesController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async start( async start(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
) { ) {
if (game.started) { if (game.started) {
@@ -59,7 +60,7 @@ export class GamesController {
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
if (game.owner.userId != session.user.userId) { if (game.owner.id != session.user.id) {
throw new HttpException( throw new HttpException(
'You can only start games you own', 'You can only start games you own',
HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN,
@@ -72,33 +73,33 @@ export class GamesController {
@Post('surrender') @Post('surrender')
@ApiOperation({ summary: 'Leave a game that has already started' }) @ApiOperation({ summary: 'Leave a game that has already started' })
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async surrender( surrender(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
) { ) {
if (!game.started) { if (!game.started) {
throw new HttpException('Game has not started', HttpStatus.BAD_REQUEST); throw new HttpException('Game has not started', HttpStatus.BAD_REQUEST);
} }
return await this.lobbyService.leaveGame(game, session.user); return this.lobbyService.leaveGame(game, session.user);
} }
@Post('kick/:playerId') @Post('kick/:playerId')
@ApiOperation({ summary: 'Kick a player' }) @ApiOperation({ summary: 'Kick a player' })
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async kick( kick(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
@Param('playerId') playerId: string, @Param('playerId') playerId: string,
) { ) {
if (session.user.userId != game.owner.userId) { if (session.user.id != game.owner.id) {
throw new HttpException( throw new HttpException(
'You do do not own this game', 'You do do not own this game',
HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN,
); );
} }
return await this.lobbyService.kickPlayer(game, session.user, playerId); return this.lobbyService.kickPlayer(game, session.user, playerId);
} }
@Sse('/subscribe') @Sse('/subscribe')
@@ -1,20 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LobbyController } from './lobby.controller';
import { LobbyService } from '../services/lobby.service';
describe('GamesController', () => {
let controller: LobbyController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LobbyController],
providers: [LobbyService],
}).compile();
controller = module.get<LobbyController>(LobbyController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
@@ -0,0 +1,42 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LobbyController } from './lobby.controller';
import { LobbyService } from '../services/lobby.service';
import { Game } from '../entities/game.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Player } from '../entities/player.entity';
import { GameService } from '../services/games.service';
import { User } from 'src/users/entities/user.entity';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { Concept } from '../entities/concept.entity';
import { ConceptInTurn } from '../entities/concept_in_turn.entity';
describe('LobbyController', () => {
let controller: LobbyController;
let service: LobbyService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [LobbyController],
providers: [LobbyService, GameService],
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [Game, Player, User, Concept, ConceptInTurn],
synchronize: true,
}),
TypeOrmModule.forFeature([Game, Player, User, Concept, ConceptInTurn]),
EventEmitterModule.forRoot(),
],
}).compile();
controller = module.get<LobbyController>(LobbyController);
service = module.get<LobbyService>(LobbyService);
});
describe('create', () => {
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
});
+14 -12
View File
@@ -18,6 +18,7 @@ import { Game } from '../entities/game.entity';
import { Observable, fromEvent, map } from 'rxjs'; import { Observable, fromEvent, map } from 'rxjs';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { GameResponseDTO } from '../dto/gameresponse.dto'; import { GameResponseDTO } from '../dto/gameresponse.dto';
import { SessionData } from 'express-session';
@ApiTags('lobby') @ApiTags('lobby')
@Controller('games') @Controller('games')
@@ -40,15 +41,16 @@ export class LobbyController {
}) })
@ApiResponse({ status: 403, description: 'Forbidden.' }) @ApiResponse({ status: 403, description: 'Forbidden.' })
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async create(@Session() session: Record<string, any>) { async create(@Session() session: SessionData) {
const games = await this.lobbyService.findByOwner(session.user); const games = await this.lobbyService.findByOwner(session.user);
if (games.length > 0) { if (games) {
throw new HttpException( throw new HttpException(
'You can only create one game at a time', 'You can only create one game at a time',
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
); );
} }
return await this.lobbyService.create(session.user); const createdGame = await this.lobbyService.create(session.user);
return new GameResponseDTO(createdGame);
} }
@Get() @Get()
@@ -71,10 +73,10 @@ export class LobbyController {
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get game information' }) @ApiOperation({ summary: 'Get game information' })
async findOne( findOne(
@Param('id') id: string, @Param('id') id: string,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
): Promise<GameResponseDTO> { ): GameResponseDTO {
return new GameResponseDTO(game); return new GameResponseDTO(game);
} }
@@ -83,10 +85,10 @@ export class LobbyController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async remove( async remove(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
) { ) {
if (game.owner.userId == session.user.userId) { if (game.owner.id != session.user.id) {
throw new HttpException( throw new HttpException(
'You can only delete games you own', 'You can only delete games you own',
HttpStatus.FORBIDDEN, HttpStatus.FORBIDDEN,
@@ -100,14 +102,14 @@ export class LobbyController {
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async join( async join(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
) { ) {
if (game.started) { if (game.started) {
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
} }
if (game.players.some((p) => p.user.userId == session.user.userId)) { if (game.players.some((p) => p.user.id == session.user.id)) {
throw new HttpException( throw new HttpException(
'You are already in this game', 'You are already in this game',
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@@ -119,14 +121,14 @@ export class LobbyController {
@Post(':id/leave') @Post(':id/leave')
@ApiOperation({ summary: "Leave a game that hasn't already started" }) @ApiOperation({ summary: "Leave a game that hasn't already started" })
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
async leave( leave(
@Param('id') id: string, @Param('id') id: string,
@Session() session: Record<string, any>, @Session() session: SessionData,
@Param('id', GamePipe) game: Game, @Param('id', GamePipe) game: Game,
) { ) {
if (game.started) { if (game.started) {
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
} }
return await this.lobbyService.leaveGame(game, session.user); return this.lobbyService.leaveGame(game, session.user);
} }
} }
+1 -6
View File
@@ -1,4 +1,4 @@
import { Expose, Transform, Type } from 'class-transformer'; import { Expose, Type } from 'class-transformer';
import { UserResponseDTO } from 'src/users/dto/userresponse.dto'; import { UserResponseDTO } from 'src/users/dto/userresponse.dto';
export class PlayerResponseDTO { export class PlayerResponseDTO {
@@ -11,11 +11,6 @@ export class PlayerResponseDTO {
@Expose() @Expose()
user: UserResponseDTO; user: UserResponseDTO;
@Expose()
get userId(): string {
return this.user.userId;
}
@Expose() @Expose()
score: number; score: number;
} }
+4 -2
View File
@@ -22,7 +22,9 @@ export class Game {
@ManyToOne(() => User, { eager: true }) @ManyToOne(() => User, { eager: true })
owner: User; owner: User;
@OneToMany(() => Player, (player) => player.game, { eager: true }) @OneToMany(() => Player, (player) => player.game, {
eager: true,
})
@JoinTable() @JoinTable()
players: Player[]; players: Player[];
@@ -35,6 +37,6 @@ export class Game {
@Column({ default: false }) @Column({ default: false })
started: boolean; started: boolean;
@OneToMany(() => ConceptInTurn, (cit) => cit.game) @OneToMany(() => ConceptInTurn, (cit) => cit.game, { onDelete: 'CASCADE' })
currentConcepts: ConceptInTurn[]; currentConcepts: ConceptInTurn[];
} }
+3 -8
View File
@@ -1,4 +1,4 @@
import { User } from 'src/users/entities/user.entity'; import { User } from '../../users/entities/user.entity';
import { import {
Column, Column,
Entity, Entity,
@@ -12,21 +12,16 @@ import {
import { Game } from './game.entity'; import { Game } from './game.entity';
@Entity() @Entity()
// @Unique(['user, game'])
export class Player { export class Player {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ManyToOne(() => User, { eager: true }) @ManyToOne(() => User, { eager: true, onDelete: 'CASCADE' })
user: User; user: User;
@ManyToOne(() => Game) @ManyToOne(() => Game, { onDelete: 'CASCADE' })
game: Game; game: Game;
@Column({ default: 0 }) @Column({ default: 0 })
score: number; score: number;
get userId(): string {
return this.user.userId;
}
} }
+22
View File
@@ -0,0 +1,22 @@
import { GameResponseDTO } from 'src/games/dto/gameresponse.dto';
import { PlayerResponseDTO } from 'src/games/dto/playerresponse.dto';
import { Game } from 'src/games/entities/game.entity';
export class GameCreateEvent extends GameResponseDTO {
type: 'create';
}
export class GameDeleteEvent {
type: 'delete';
constructor(public id?: number) {}
}
export class GameJoinEvent {
type: 'joingame';
gameId: number;
constructor(
public player: PlayerResponseDTO,
game?: Game,
) {
this.gameId = game.id;
}
}
+20 -2
View File
@@ -1,12 +1,30 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { LobbyService } from './lobby.service'; import { LobbyService } from './lobby.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { User } from 'src/users/entities/user.entity';
import { Concept } from '../entities/concept.entity';
import { ConceptInTurn } from '../entities/concept_in_turn.entity';
import { Game } from '../entities/game.entity';
import { Player } from '../entities/player.entity';
import { GameService } from './games.service';
describe('GamesService', () => { describe('LobbyService', () => {
let service: LobbyService; let service: LobbyService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [LobbyService], providers: [LobbyService, GameService],
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [Game, Player, User, Concept, ConceptInTurn],
synchronize: true,
}),
TypeOrmModule.forFeature([Game, Player, User, Concept, ConceptInTurn]),
EventEmitterModule.forRoot(),
],
}).compile(); }).compile();
service = module.get<LobbyService>(LobbyService); service = module.get<LobbyService>(LobbyService);
+16 -23
View File
@@ -7,6 +7,11 @@ import { Game } from '../entities/game.entity';
import { User } from 'src/users/entities/user.entity'; import { User } from 'src/users/entities/user.entity';
import { Player } from '../entities/player.entity'; import { Player } from '../entities/player.entity';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import {
GameCreateEvent,
GameDeleteEvent,
GameJoinEvent,
} from 'src/games/events/lobby';
@Injectable() @Injectable()
export class LobbyService { export class LobbyService {
@@ -21,9 +26,9 @@ export class LobbyService {
const createdGame = this.gameRepository.create(); const createdGame = this.gameRepository.create();
createdGame.owner = owner; createdGame.owner = owner;
createdGame.seed = randomUUID(); createdGame.seed = randomUUID();
this.gameRepository.save(createdGame); await this.gameRepository.save(createdGame);
await this.joinGame(createdGame, owner); await this.joinGame(createdGame, owner);
this.eventEmitter.emit('sse.lobby', { type: 'create', ...createdGame }); this.eventEmitter.emit('sse.lobby', new GameCreateEvent(createdGame));
return createdGame; return createdGame;
} }
@@ -32,27 +37,20 @@ export class LobbyService {
player.user = user; player.user = user;
player.game = game; player.game = game;
this.eventEmitter.emit('sse.lobby', { this.eventEmitter.emit('sse.lobby', new GameJoinEvent(player, game));
type: 'joinGame', this.eventEmitter.emit('sse.game.' + game.id, new GameJoinEvent(player));
game: game.id, await this.playerRepository.save(player);
player: player,
});
this.eventEmitter.emit('sse.game.' + game.id, {
type: 'joinGame',
player: player,
});
this.playerRepository.save(player);
} }
findByOwner(user: User) { findByOwner(user: User) {
return this.gameRepository.findBy({ owner: user }); return this.gameRepository.findOneBy({ owner: user });
} }
async leaveGame(game: Game, user: User) { leaveGame(game: Game, user: User) {
throw Error('not implemented'); throw Error('not implemented');
} }
async kickPlayer(game: Game, user: User, kickedPlayerId: string) { kickPlayer(game: Game, user: User, kickedPlayerId: string) {
throw Error('not implemented'); throw Error('not implemented');
} }
@@ -61,13 +59,8 @@ export class LobbyService {
} }
async remove(game: Game) { async remove(game: Game) {
this.eventEmitter.emit('sse.lobby', { this.eventEmitter.emit('sse.lobby', new GameDeleteEvent(game.id));
type: 'delete', this.eventEmitter.emit('sse.game.' + game.id, new GameDeleteEvent());
_id: game.id, await this.gameRepository.remove(game);
});
this.eventEmitter.emit('sse.game.' + game.id, {
type: 'delete',
});
this.gameRepository.remove(game);
} }
} }
+2 -1
View File
@@ -3,9 +3,10 @@ import { Expose } from 'class-transformer';
export class UserResponseDTO { export class UserResponseDTO {
constructor(partial: Partial<UserResponseDTO>) { constructor(partial: Partial<UserResponseDTO>) {
Object.assign(this, partial); Object.assign(this, partial);
this.id = partial.id;
} }
@Expose() @Expose()
userId: string; id: string;
@Expose() @Expose()
username: string; username: string;
+2 -2
View File
@@ -2,8 +2,8 @@ import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity() @Entity()
export class User { export class User {
@PrimaryColumn() @PrimaryColumn({})
userId: string; id: string;
@Column() @Column()
avatar: string; avatar: string;
-20
View File
@@ -1,20 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
+6 -10
View File
@@ -12,6 +12,7 @@ import { UserResponseDTO } from './dto/userresponse.dto';
import { ApiTags } from '@nestjs/swagger'; import { ApiTags } from '@nestjs/swagger';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
const configService = new ConfigService(); const configService = new ConfigService();
@@ -22,26 +23,21 @@ export class UsersController {
@Get('/login') @Get('/login')
@Redirect() @Redirect()
async login( async login(@Query('code') code: string, @Session() session: SessionData) {
@Query('code') code: string,
@Session() session: Record<string, any>,
) {
await this.usersService.createDiscordToken({ code }, session); await this.usersService.createDiscordToken({ code }, session);
await this.usersService.discordLogin(session); await this.usersService.discordLogin(session);
return { url: configService.get('CLIENT_ENDPOINT') }; return { url: configService.get<string>('CLIENT_ENDPOINT') };
} }
@Get('/logout') @Get('/logout')
@Redirect() @Redirect()
async logout(@Session() session: Record<string, any>) { async logout(@Session() session: SessionData) {
await this.usersService.discordLogout(session); await this.usersService.discordLogout(session);
return { url: configService.get('CLIENT_ENDPOINT') }; return { url: configService.get<string>('CLIENT_ENDPOINT') };
} }
@Get('/whoami') @Get('/whoami')
async remove( async remove(@Session() session: SessionData): Promise<UserResponseDTO> {
@Session() session: Record<string, any>,
): Promise<UserResponseDTO> {
if (!session.tokenData) { if (!session.tokenData) {
throw new HttpException( throw new HttpException(
"Session doesn't contains auth information", "Session doesn't contains auth information",
-18
View File
@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
+16 -9
View File
@@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { UserResponseDTO } from './dto/userresponse.dto'; import { UserResponseDTO } from './dto/userresponse.dto';
import { URLSearchParams } from 'url'; import { URLSearchParams } from 'url';
@@ -5,6 +8,7 @@ import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
@Injectable() @Injectable()
export class UsersService { export class UsersService {
@@ -21,7 +25,7 @@ export class UsersService {
code?: string; code?: string;
refresh_token?: string; refresh_token?: string;
}, },
session: Record<string, any>, session: SessionData,
): Promise<void> { ): Promise<void> {
let body; let body;
if (code) { if (code) {
@@ -106,7 +110,7 @@ export class UsersService {
} }
async discordLogin( async discordLogin(
session: Record<string, any>, session: SessionData,
recursive: boolean = false, recursive: boolean = false,
): Promise<User> { ): Promise<User> {
const req2 = await fetch('https://discord.com/api/users/@me', { const req2 = await fetch('https://discord.com/api/users/@me', {
@@ -135,24 +139,27 @@ export class UsersService {
} }
} }
const data2 = await req2.json(); const data2 = await req2.json();
const user = await this.getOrCreateUser(data2.id, data2); const user = await this.getOrCreateUser(
data2.id,
new UserResponseDTO(data2),
);
session.user = user; session.user = user;
return user; return user;
} }
async getOrCreateUser(userId: string, data?: UserResponseDTO) { async getOrCreateUser(id: string, data?: UserResponseDTO) {
let user = await this.usersRepository.findOneBy({ userId }); let user = await this.usersRepository.findOneBy({ id });
if (!user) { if (!user) {
user = this.usersRepository.create({ ...data, userId }); user = this.usersRepository.create({ ...data, id });
} else { } else {
Object.assign(user, data); Object.assign(user, data);
} }
this.usersRepository.save(user); await this.usersRepository.save(user);
return user; return user;
} }
async discordLogout(session: Record<string, any>) { async discordLogout(session: SessionData) {
this.revokeDiscordToken(session.tokenData.access_token); await this.revokeDiscordToken(session.tokenData.access_token);
delete session.user; delete session.user;
delete session.tokenData; delete session.tokenData;
} }