feat : add testing
This commit is contained in:
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"orta.vscode-jest",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"vue.volar"
|
||||
]
|
||||
}
|
||||
Vendored
+6
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
{
|
||||
Generated
+1513
-39
File diff suppressed because it is too large
Load Diff
+7
-2
@@ -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"
|
||||
|
||||
@@ -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!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
Vendored
+12
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +2,8 @@ import { Entity, Column, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class User {
|
||||
@PrimaryColumn()
|
||||
userId: string;
|
||||
@PrimaryColumn({})
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
avatar: string;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user