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"
},
"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'],
},
...compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-type-checked',
'plugin:prettier/recommended',
),
{
@@ -48,7 +48,8 @@ export default [
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'require-await': 'off',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/no-unused-vars': [
'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",
"prettier": "^3.2.5",
"source-map-support": "^0.5.21",
"sqlite3": "^5.1.7",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"typescript-eslint": "^8.18.2"
},
"jest": {
"moduleFileExtensions": [
@@ -78,7 +80,10 @@
"json",
"ts"
],
"rootDir": "src",
"rootDir": "./",
"modulePaths": [
"<rootDir>"
],
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(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'),
username: configService.get('PG_USER'),
database: 'concept',
applicationName: 'concept',
synchronize: false,
entities: ['**/*.entity.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 { GameService } from '../services/games.service';
import { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
@ApiTags('game')
@Controller('games/:id')
@@ -47,7 +48,7 @@ export class GamesController {
@UseGuards(AuthGuard)
async start(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
) {
if (game.started) {
@@ -59,7 +60,7 @@ export class GamesController {
HttpStatus.BAD_REQUEST,
);
}
if (game.owner.userId != session.user.userId) {
if (game.owner.id != session.user.id) {
throw new HttpException(
'You can only start games you own',
HttpStatus.FORBIDDEN,
@@ -72,33 +73,33 @@ export class GamesController {
@Post('surrender')
@ApiOperation({ summary: 'Leave a game that has already started' })
@UseGuards(AuthGuard)
async surrender(
surrender(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
) {
if (!game.started) {
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')
@ApiOperation({ summary: 'Kick a player' })
@UseGuards(AuthGuard)
async kick(
kick(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
@Param('playerId') playerId: string,
) {
if (session.user.userId != game.owner.userId) {
if (session.user.id != game.owner.id) {
throw new HttpException(
'You do do not own this game',
HttpStatus.FORBIDDEN,
);
}
return await this.lobbyService.kickPlayer(game, session.user, playerId);
return this.lobbyService.kickPlayer(game, session.user, playerId);
}
@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 { EventEmitter2 } from '@nestjs/event-emitter';
import { GameResponseDTO } from '../dto/gameresponse.dto';
import { SessionData } from 'express-session';
@ApiTags('lobby')
@Controller('games')
@@ -40,15 +41,16 @@ export class LobbyController {
})
@ApiResponse({ status: 403, description: 'Forbidden.' })
@UseGuards(AuthGuard)
async create(@Session() session: Record<string, any>) {
async create(@Session() session: SessionData) {
const games = await this.lobbyService.findByOwner(session.user);
if (games.length > 0) {
if (games) {
throw new HttpException(
'You can only create one game at a time',
HttpStatus.BAD_REQUEST,
);
}
return await this.lobbyService.create(session.user);
const createdGame = await this.lobbyService.create(session.user);
return new GameResponseDTO(createdGame);
}
@Get()
@@ -71,10 +73,10 @@ export class LobbyController {
@Get(':id')
@ApiOperation({ summary: 'Get game information' })
async findOne(
findOne(
@Param('id') id: string,
@Param('id', GamePipe) game: Game,
): Promise<GameResponseDTO> {
): GameResponseDTO {
return new GameResponseDTO(game);
}
@@ -83,10 +85,10 @@ export class LobbyController {
@UseGuards(AuthGuard)
async remove(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
) {
if (game.owner.userId == session.user.userId) {
if (game.owner.id != session.user.id) {
throw new HttpException(
'You can only delete games you own',
HttpStatus.FORBIDDEN,
@@ -100,14 +102,14 @@ export class LobbyController {
@UseGuards(AuthGuard)
async join(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
) {
if (game.started) {
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(
'You are already in this game',
HttpStatus.BAD_REQUEST,
@@ -119,14 +121,14 @@ export class LobbyController {
@Post(':id/leave')
@ApiOperation({ summary: "Leave a game that hasn't already started" })
@UseGuards(AuthGuard)
async leave(
leave(
@Param('id') id: string,
@Session() session: Record<string, any>,
@Session() session: SessionData,
@Param('id', GamePipe) game: Game,
) {
if (game.started) {
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';
export class PlayerResponseDTO {
@@ -11,11 +11,6 @@ export class PlayerResponseDTO {
@Expose()
user: UserResponseDTO;
@Expose()
get userId(): string {
return this.user.userId;
}
@Expose()
score: number;
}
+4 -2
View File
@@ -22,7 +22,9 @@ export class Game {
@ManyToOne(() => User, { eager: true })
owner: User;
@OneToMany(() => Player, (player) => player.game, { eager: true })
@OneToMany(() => Player, (player) => player.game, {
eager: true,
})
@JoinTable()
players: Player[];
@@ -35,6 +37,6 @@ export class Game {
@Column({ default: false })
started: boolean;
@OneToMany(() => ConceptInTurn, (cit) => cit.game)
@OneToMany(() => ConceptInTurn, (cit) => cit.game, { onDelete: 'CASCADE' })
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 {
Column,
Entity,
@@ -12,21 +12,16 @@ import {
import { Game } from './game.entity';
@Entity()
// @Unique(['user, game'])
export class Player {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, { eager: true })
@ManyToOne(() => User, { eager: true, onDelete: 'CASCADE' })
user: User;
@ManyToOne(() => Game)
@ManyToOne(() => Game, { onDelete: 'CASCADE' })
game: Game;
@Column({ default: 0 })
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 { 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;
beforeEach(async () => {
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();
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 { Player } from '../entities/player.entity';
import { randomUUID } from 'node:crypto';
import {
GameCreateEvent,
GameDeleteEvent,
GameJoinEvent,
} from 'src/games/events/lobby';
@Injectable()
export class LobbyService {
@@ -21,9 +26,9 @@ export class LobbyService {
const createdGame = this.gameRepository.create();
createdGame.owner = owner;
createdGame.seed = randomUUID();
this.gameRepository.save(createdGame);
await this.gameRepository.save(createdGame);
await this.joinGame(createdGame, owner);
this.eventEmitter.emit('sse.lobby', { type: 'create', ...createdGame });
this.eventEmitter.emit('sse.lobby', new GameCreateEvent(createdGame));
return createdGame;
}
@@ -32,27 +37,20 @@ export class LobbyService {
player.user = user;
player.game = game;
this.eventEmitter.emit('sse.lobby', {
type: 'joinGame',
game: game.id,
player: player,
});
this.eventEmitter.emit('sse.game.' + game.id, {
type: 'joinGame',
player: player,
});
this.playerRepository.save(player);
this.eventEmitter.emit('sse.lobby', new GameJoinEvent(player, game));
this.eventEmitter.emit('sse.game.' + game.id, new GameJoinEvent(player));
await this.playerRepository.save(player);
}
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');
}
async kickPlayer(game: Game, user: User, kickedPlayerId: string) {
kickPlayer(game: Game, user: User, kickedPlayerId: string) {
throw Error('not implemented');
}
@@ -61,13 +59,8 @@ export class LobbyService {
}
async remove(game: Game) {
this.eventEmitter.emit('sse.lobby', {
type: 'delete',
_id: game.id,
});
this.eventEmitter.emit('sse.game.' + game.id, {
type: 'delete',
});
this.gameRepository.remove(game);
this.eventEmitter.emit('sse.lobby', new GameDeleteEvent(game.id));
this.eventEmitter.emit('sse.game.' + game.id, new GameDeleteEvent());
await this.gameRepository.remove(game);
}
}
+2 -1
View File
@@ -3,9 +3,10 @@ import { Expose } from 'class-transformer';
export class UserResponseDTO {
constructor(partial: Partial<UserResponseDTO>) {
Object.assign(this, partial);
this.id = partial.id;
}
@Expose()
userId: string;
id: string;
@Expose()
username: string;
+2 -2
View File
@@ -2,8 +2,8 @@ import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryColumn()
userId: string;
@PrimaryColumn({})
id: string;
@Column()
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 { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
const configService = new ConfigService();
@@ -22,26 +23,21 @@ export class UsersController {
@Get('/login')
@Redirect()
async login(
@Query('code') code: string,
@Session() session: Record<string, any>,
) {
async login(@Query('code') code: string, @Session() session: SessionData) {
await this.usersService.createDiscordToken({ code }, session);
await this.usersService.discordLogin(session);
return { url: configService.get('CLIENT_ENDPOINT') };
return { url: configService.get<string>('CLIENT_ENDPOINT') };
}
@Get('/logout')
@Redirect()
async logout(@Session() session: Record<string, any>) {
async logout(@Session() session: SessionData) {
await this.usersService.discordLogout(session);
return { url: configService.get('CLIENT_ENDPOINT') };
return { url: configService.get<string>('CLIENT_ENDPOINT') };
}
@Get('/whoami')
async remove(
@Session() session: Record<string, any>,
): Promise<UserResponseDTO> {
async remove(@Session() session: SessionData): Promise<UserResponseDTO> {
if (!session.tokenData) {
throw new HttpException(
"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 { UserResponseDTO } from './dto/userresponse.dto';
import { URLSearchParams } from 'url';
@@ -5,6 +8,7 @@ import { User } from './entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ConfigService } from '@nestjs/config';
import { SessionData } from 'express-session';
@Injectable()
export class UsersService {
@@ -21,7 +25,7 @@ export class UsersService {
code?: string;
refresh_token?: string;
},
session: Record<string, any>,
session: SessionData,
): Promise<void> {
let body;
if (code) {
@@ -106,7 +110,7 @@ export class UsersService {
}
async discordLogin(
session: Record<string, any>,
session: SessionData,
recursive: boolean = false,
): Promise<User> {
const req2 = await fetch('https://discord.com/api/users/@me', {
@@ -135,24 +139,27 @@ export class UsersService {
}
}
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;
return user;
}
async getOrCreateUser(userId: string, data?: UserResponseDTO) {
let user = await this.usersRepository.findOneBy({ userId });
async getOrCreateUser(id: string, data?: UserResponseDTO) {
let user = await this.usersRepository.findOneBy({ id });
if (!user) {
user = this.usersRepository.create({ ...data, userId });
user = this.usersRepository.create({ ...data, id });
} else {
Object.assign(user, data);
}
this.usersRepository.save(user);
await this.usersRepository.save(user);
return user;
}
async discordLogout(session: Record<string, any>) {
this.revokeDiscordToken(session.tokenData.access_token);
async discordLogout(session: SessionData) {
await this.revokeDiscordToken(session.tokenData.access_token);
delete session.user;
delete session.tokenData;
}