All files / src/Infrastructure/ExternalService/Pokemon HttpClient.ts

93.54% Statements 29/31
80% Branches 8/10
100% Functions 4/4
93.54% Lines 29/31

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111  2x   2x 2x 2x   2x         2x 5x   5x               14x   14x 1x       1x             13x 13x                         13x 3x           3x           10x   10x 10x 1x           1x         9x 9x   9x                         9x 13x   9x       9x                
import { ClientInterface } from '@Application/Shared/Pokemon/ClientInterface';
import { Pokemon } from '@Domain/ValueObjects/Pokemon';
import { Result } from '@Domain/Types/Result';
import { HttpError } from '@Domain/Errors/HttpError';
import { PokemonSpecies } from '@Infrastructure/ExternalService/Pokemon/types';
import { ValidationError } from '@Domain/Errors/ValidationError';
import { LoggerInterface } from '@Application/Shared/Monitoring/LoggerInterface';
import config from '@Infrastructure/Environments/config';
 
/**
 * HTTP client for communicating with the Pokemon API
 */
export class HttpClient implements ClientInterface {
    private readonly baseUrl = config.pokemonClient.baseUrl;
 
    constructor(private readonly logger: LoggerInterface) { }
 
    /**
     * Retrieves Pokemon data from the external API
     * @param name The name of the Pokemon to retrieve
     * @returns A promise resolving to a Result containing either the Pokemon data or an Error
     */
    async getPokemon(name: string): Promise<Result<Error, Pokemon>> {
        this.logger.info(`Fetching pokemon`, { name });
 
        if (!name) {
            this.logger.error(`Name is required`, {
                name,
            });
 
            return {
                success: false,
                error: new ValidationError('Name is required'),
            };
        }
 
        let response: Response;
        try {
            response = await fetch(`${this.baseUrl}/${name.toLowerCase()}`);
        } catch (error) {
            this.logger.error(`Failed to fetch pokemon`, {
                name,
                body: error,
            });
 
            return {
                success: false,
                error: new HttpError(500, `Failed to fetch pokemon '${name}'`),
            };
        }
 
        if (!response.ok) {
            this.logger.error(`Failed to fetch pokemon`, {
                name,
                status: response.status,
                body: await response.text(),
            });
 
            return {
                success: false,
                error: new HttpError(response.status, `Failed to fetch pokemon '${name}'`),
            };
        }
 
        const data = await response.json();
 
        const result = PokemonSpecies.safeParse(data);
        if (!result.success) {
            this.logger.error(`Failed to parse pokemon`, {
                name,
                body: data,
                error: result.error.message,
            });
 
            return {
                success: false,
                error: new ValidationError(result.error.message),
            };
        } else {
            this.logger.info(`Pokemon fetched successfully`, { name });
            this.logger.debug(`Pokemon fetched successfully: body`, { name, body: data });
 
            return {
                success: true,
                data: this.mapToPokemon(result.data),
            };
        }
    }
 
    /**
     * Maps the API response to the Pokemon domain object
     * @param data The raw data from the API
     * @returns A Pokemon domain object
     */
    private mapToPokemon(data: PokemonSpecies): Pokemon {
        const descriptionEntry = data.flavor_text_entries.find(
            (entry) => entry.language.name === 'en'
        );
        const description = descriptionEntry
            ? descriptionEntry.flavor_text.replace(/[\n\f\r]/g, ' ')
            : '';
 
        return new Pokemon(
            data.name,
            description,
            data.habitat ? data.habitat.name : 'unknown',
            data.is_legendary
        );
    }
}