Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ It was written by our latest trainee, `Jay Paltan`, and is expected to define on
This route is expected to have the following behaviour:
- `name` should be the exact name of a first-generation Pokemon (id from 1 to 151), and nothing else. If that is not the case, the API should return a 404 error.
- `name` should not be empty. If that is the case, the API should return a 400 error.
- it should use a robust authentication method and return a status 401 error if the user is not authenticated.
- Pokemon data should be retrieved from the [PokeAPI v2](https://pokeapi.co/docs/v2).

In addition, you might also be asked to look at the current authentication logic, but this will be specified by the inverviewer.
- ideally, we want to use a robust and standard authentication method and return a status 401 error if the user is not authenticated.

## Your mission - Part 1: Code review

Unfortunately, it seems that the current state of this repository does not meet our expectations and code quality standards.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.3.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"got": "^11.8.6",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
Expand All @@ -31,6 +35,7 @@
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/got": "^9.6.12",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
Expand Down
6,301 changes: 6,301 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

22 changes: 0 additions & 22 deletions src/app.controller.spec.ts

This file was deleted.

8 changes: 7 additions & 1 deletion src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiResponse, ApiTags } from '@nestjs/swagger';

@Controller()
@Controller('hello')
@ApiTags('hello')
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
@ApiResponse({
status: 200,
description: 'Simply returns the "Hello World!" string',
})
getHello(): string {
return this.appService.getHello();
}
Expand Down
31 changes: 31 additions & 0 deletions src/auth/auth.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';

const authSecret = 'password123+';

// Look at this file only if asked by the interviewer
@Injectable()
export class AuthInterceptor implements NestInterceptor {
constructor() {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
const ctx = context.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();

if (request.headers.authorization !== authSecret) {
response.status(401);
}

return data;
}),
);
}
}
11 changes: 11 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

const config = new DocumentBuilder()
.setTitle('A great example of NestJS API for Pokemon trainers')
.setDescription('The best way to get infos about Pokemon')
.setVersion('1.0')
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('swagger', app, document);

await app.listen(3000);
}
bootstrap();
8 changes: 8 additions & 0 deletions src/pokemon/dtos/get-pokemon-by-name.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';

export class GetPokemonByNameQuery {
@ApiProperty({ type: 'string' })
@IsString()
name: string;
}
66 changes: 66 additions & 0 deletions src/pokemon/pokemon.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
export var pokemonApiUrl = 'http://pokeapi.co/api/v2';

import {
Controller,
Get,
HttpException,
Inject,
NotFoundException,
Param,
Post,
Query,
UseInterceptors,
} from '@nestjs/common';
import { PokemonService } from './pokemon.service';
import { GetPokemonByNameQuery } from './dtos/get-pokemon-by-name.query';
import { Pokemon } from './types/pokemon';
import { AuthInterceptor } from '../auth/auth.interceptor';
import {
ApiInternalServerErrorResponse,
ApiQuery,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';

@Controller('pokemon')
@ApiTags('pokemon')
@UseInterceptors(AuthInterceptor)
export class PokemonController {
constructor(private pokemonService: PokemonService) {}

@Post('pokemon')
@ApiQuery({
type: GetPokemonByNameQuery,
description: 'Lookup string for pokemons (match against pokemon name)',
})
@ApiResponse({
status: 200,
description: 'Returns pokemons whose name matches a string query param',
})
@ApiInternalServerErrorResponse({
description: 'This will never happen, trust me',
})
async GetPokemonByNameController(
@Query() name: any,
): Promise<Pokemon | null> {
if (name === null) return;

name == null
? name.trim() != ''
? ((name = name),
(pokemonApiUrl = pokemonApiUrl + '/'),
(pokemonApiUrl = pokemonApiUrl + name))
: ((pokemonApiUrl = pokemonApiUrl + '"?offset=20"'),
(pokemonApiUrl = pokemonApiUrl + '&limit=20'))
: ((pokemonApiUrl = pokemonApiUrl + '"?offset=20"'),
(pokemonApiUrl = pokemonApiUrl + '&limit=20'));

console.log('Printing name for debug : ', name);

const myPokemon = await this.pokemonService.findPokemonByNameOrFail(name);

console.log('Printing name for debug : ', myPokemon);

return myPokemon;
}
}
9 changes: 9 additions & 0 deletions src/pokemon/pokemon.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PokemonController } from './pokemon.controller';
import { PokemonService } from './pokemon.service';

@Module({
controllers: [PokemonController],
providers: [PokemonService]
})
export class PokemonModule {}
47 changes: 47 additions & 0 deletions src/pokemon/pokemon.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PokemonService } from './pokemon.service';

describe('PokemonService (integration)', () => {
const pokemonService = new PokemonService();

it('findPokemonByNameOrFail - valid pokemon name', async () => {
// Given
const pokemonName = 'bulbasaur';

// When
const pokemonStats =
await pokemonService.findPokemonByNameOrFail(pokemonName);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce qu'on fait dans nos tests, c'est l'écriture Gherkin

On pourrait faire ça ici, ça donnerait pour ce test:

  it('findPokemonByNameOrFail - valid pokemon name', async () => {
    // Given
    const pokemonName = 'bulbasaur';

    // When
    const pokemonStats =
      await pokemonService.findPokemonByNameOrFail(pokemonName);

    // Then
    expect(pokemonStats).toMatchObject({
      id: 1,
      name: 'bulbasaur',
      species: {
        name: 'bulbasaur',
        url: 'https://pokeapi.co/api/v2/pokemon-species/1/',
      },
      types: ['grass', 'poison'],
      weight: 69,
      height: 7,
    });
  });

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appliqué aux tests unitaires

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bon, donc on a mit un message sur slack 😄

Tu disais que ce serait mieux aussi de l'appliquer aux e2e 👍


// Then
expect(pokemonStats).toMatchObject({
id: 1,
name: 'bulbasaur',
species: {
name: 'bulbasaur',
url: 'https://pokeapi.co/api/v2/pokemon-species/1/',
},
types: ['grass', 'poison'],
weight: 69,
height: 7,
});
});

it('findPokemonByNameOrFail - non-existing pokemon name', async () => {
// Given
const pokemonName = 'bulba';

// When + Then
await expect(() =>
pokemonService.findPokemonByNameOrFail(pokemonName),
).rejects.toThrow();
});

it('findPokemonByNameOrFail - 2nd generation pokemon name', async () => {
// Given
const pokemonName = 'togepi';

// When + Then
await expect(() =>
pokemonService.findPokemonByNameOrFail(pokemonName),
).rejects.toThrow();
});
});
56 changes: 56 additions & 0 deletions src/pokemon/pokemon.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import got, { Got } from 'got';
import { Pokemon } from './types/pokemon';
import { pokemonApiUrl } from './pokemon.controller';
import { PokemonSpecies } from './types/species';

@Injectable()
export class PokemonService {
private httpClient: Got;
constructor() {
this.httpClient = got.extend({
prefixUrl: pokemonApiUrl,
responseType: 'json',
throwHttpErrors: false,
});
}

async findPokemonByNameOrFail(pokemonName: string): Promise<Pokemon> {
type GetPokemonResponse = {
name: string;
id: number;
height: number;
weight: number;
types: { type: { name: string; url: string } }[];
species: PokemonSpecies;
};

return await this.httpClient
.get(`pokemon/${pokemonName}`)
.then((response) => response.body as unknown as GetPokemonResponse)
.then((body) => {
if (!body.id) {
return null;
}

const types = body.types
.map((type) => type.type)
.map((type) => type.name);

return {
name: body.name,
id: body.id,
height: body.height,
weight: body.weight,
types,
species: {
name: body.species.name,
url: body.species.url,
},
};
})
.catch((error) => {
throw error;
});
}
}
10 changes: 10 additions & 0 deletions src/pokemon/types/pokemon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PokemonSpecies } from './species';

export type Pokemon = {
name: string;
id: number;
height: number;
weight: number;
types: string[];
species: PokemonSpecies;
};
4 changes: 4 additions & 0 deletions src/pokemon/types/species.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type PokemonSpecies = {
name: string;
url: string;
};
Loading