diff --git a/package.json b/package.json index cb4c1d24..cd4ad480 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,6 @@ "@nestjs/swagger": "12.0.1", "date-fns": "4.4.0", "helmet": "8.3.0", - "http-status-codes": "2.3.0", "logdown": "3.3.1", "package-json": "10.0.1", "reflect-metadata": "0.2.2", diff --git a/src/Server.ts b/src/Server.ts index 0497d9f6..7a9cc983 100644 --- a/src/Server.ts +++ b/src/Server.ts @@ -1,9 +1,9 @@ import {NestFactory} from '@nestjs/core'; +import {HttpStatus} from '@nestjs/common'; import {NestExpressApplication} from '@nestjs/platform-express'; import {DocumentBuilder, SwaggerModule} from '@nestjs/swagger'; import {NextFunction, Request, Response} from 'express'; import helmet from 'helmet'; -import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {AppModule} from './app.module.js'; import {ServerConfig} from './config.js'; @@ -89,8 +89,8 @@ function createRateLimitMiddleware(config: ServerConfig) { if (current.count >= config.RATE_LIMIT_MAX_REQUESTS) { const retryAfter = Math.ceil((current.resetAt - now) / rateLimitCleanupThreshold); response.setHeader('Retry-After', String(retryAfter)); - response.status(HTTP_STATUS.TOO_MANY_REQUESTS).json({ - code: HTTP_STATUS.TOO_MANY_REQUESTS, + response.status(HttpStatus.TOO_MANY_REQUESTS).json({ + code: HttpStatus.TOO_MANY_REQUESTS, message: 'Too many requests', }); return; @@ -100,5 +100,3 @@ function createRateLimitMiddleware(config: ServerConfig) { next(); }; } - -export {HTTP_STATUS}; diff --git a/src/controllers/info.controller.ts b/src/controllers/info.controller.ts index f2576b17..7926c04f 100644 --- a/src/controllers/info.controller.ts +++ b/src/controllers/info.controller.ts @@ -1,12 +1,11 @@ -import {Controller, Get} from '@nestjs/common'; +import {Controller, Get, HttpStatus} from '@nestjs/common'; import {ApiOperation, ApiResponse, ApiTags} from '@nestjs/swagger'; -import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {config} from '../config.js'; import {InfoResult} from '../swagger.js'; interface InfoRouteResponseBody { - code: HTTP_STATUS; + code: HttpStatus; commit?: string; version?: string; } @@ -15,11 +14,11 @@ interface InfoRouteResponseBody { @Controller() export class InfoController { @ApiOperation({description: 'Get information about the server', operationId: 'getServerInformation'}) - @ApiResponse({description: 'That worked', status: HTTP_STATUS.OK, type: InfoResult}) + @ApiResponse({description: 'That worked', status: HttpStatus.OK, type: InfoResult}) @Get('_info') info(): InfoRouteResponseBody { return { - code: HTTP_STATUS.OK, + code: HttpStatus.OK, commit: config.COMMIT, version: config.VERSION, }; diff --git a/src/controllers/main.controller.ts b/src/controllers/main.controller.ts index 5b1ef629..2c42a3b5 100644 --- a/src/controllers/main.controller.ts +++ b/src/controllers/main.controller.ts @@ -1,14 +1,13 @@ -import {Controller, Get, Query, Res} from '@nestjs/common'; +import {Controller, Get, HttpStatus, Query, Res} from '@nestjs/common'; import {ApiExcludeEndpoint, ApiOperation, ApiQuery, ApiResponse, ApiTags} from '@nestjs/swagger'; import {Response} from 'express'; -import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {RawResult} from '../swagger.js'; import {getLogger} from '../utils.js'; import {unpkgBase} from './packages.controller.js'; interface MainRouteResponseBody { - code: HTTP_STATUS; + code: HttpStatus; message?: string; url?: string; } @@ -22,8 +21,8 @@ export class MainController { @ApiExcludeEndpoint() @Get('favicon.ico') favicon(@Res() res: Response): void { - res.status(HTTP_STATUS.NOT_FOUND).json({ - code: HTTP_STATUS.NOT_FOUND, + res.status(HttpStatus.NOT_FOUND).json({ + code: HttpStatus.NOT_FOUND, message: 'Not found', } satisfies MainRouteResponseBody); } @@ -31,8 +30,8 @@ export class MainController { @ApiOperation({description: "Get the server's repository URL", operationId: 'getServerRepositoryUrl'}) @ApiQuery({description: 'Get the result as JSON', name: 'raw', required: false, type: Boolean}) @ApiQuery({description: 'Get a link to unpkg.com', name: 'unpkg', required: false, type: Boolean}) - @ApiResponse({description: 'That worked', status: HTTP_STATUS.OK, type: RawResult}) - @ApiResponse({description: 'Redirect to repository URL', status: HTTP_STATUS.MOVED_TEMPORARILY}) + @ApiResponse({description: 'That worked', status: HttpStatus.OK, type: RawResult}) + @ApiResponse({description: 'Redirect to repository URL', status: HttpStatus.FOUND}) @Get() main(@Query('raw') raw: string, @Query('unpkg') unpkg: string, @Res() res: Response): void { logger.info('Got request for main page'); @@ -42,20 +41,20 @@ export class MainController { if (raw !== undefined && raw !== 'false') { logger.info(`Returning raw unpkg info for main page: "${redirectUrl}" ...`); res.json({ - code: HTTP_STATUS.OK, + code: HttpStatus.OK, url: redirectUrl, } satisfies MainRouteResponseBody); return; } logger.info(`Redirecting main page to unpkg: "${redirectUrl}" ...`); - res.redirect(HTTP_STATUS.MOVED_TEMPORARILY, redirectUrl); + res.redirect(HttpStatus.FOUND, redirectUrl); return; } if (raw !== undefined && raw !== 'false') { logger.info(`Returning raw info for main page: "${repositoryUrl}" ...`); res.json({ - code: HTTP_STATUS.OK, + code: HttpStatus.OK, url: repositoryUrl, } satisfies MainRouteResponseBody); return; diff --git a/src/controllers/packages.controller.ts b/src/controllers/packages.controller.ts index f6d82367..9f43ed27 100644 --- a/src/controllers/packages.controller.ts +++ b/src/controllers/packages.controller.ts @@ -1,7 +1,6 @@ -import {Controller, Get, Param, Query, Res} from '@nestjs/common'; +import {Controller, Get, HttpStatus, Param, Query, Res} from '@nestjs/common'; import {ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags} from '@nestjs/swagger'; import {Response} from 'express'; -import {StatusCodes as HTTP_STATUS} from 'http-status-codes'; import {URL} from 'node:url'; import validatePackageName from 'validate-npm-package-name'; @@ -10,7 +9,7 @@ import {RawError, RawResult} from '../swagger.js'; import {getLogger, validateUrl} from '../utils.js'; interface PackagesRouteResponseBody { - code: HTTP_STATUS; + code: HttpStatus; message?: string; url?: string; } @@ -25,11 +24,11 @@ export class PackagesController { @ApiParam({name: 'packageName', required: true, type: String}) @ApiQuery({description: 'Get the result as JSON', name: 'raw', required: false, type: Boolean}) @ApiQuery({description: 'Get a link to unpkg.com', name: 'unpkg', required: false, type: Boolean}) - @ApiResponse({description: 'That worked', status: HTTP_STATUS.OK, type: RawResult}) - @ApiResponse({description: 'Redirect to repository URL', status: HTTP_STATUS.MOVED_TEMPORARILY}) - @ApiResponse({description: 'Version or package not found', status: HTTP_STATUS.NOT_FOUND, type: RawError}) - @ApiResponse({description: 'Invalid package name', status: HTTP_STATUS.UNPROCESSABLE_ENTITY, type: RawError}) - @ApiResponse({description: 'Internal server error', status: HTTP_STATUS.INTERNAL_SERVER_ERROR, type: RawError}) + @ApiResponse({description: 'That worked', status: HttpStatus.OK, type: RawResult}) + @ApiResponse({description: 'Redirect to repository URL', status: HttpStatus.FOUND}) + @ApiResponse({description: 'Version or package not found', status: HttpStatus.NOT_FOUND, type: RawError}) + @ApiResponse({description: 'Invalid package name', status: HttpStatus.UNPROCESSABLE_ENTITY, type: RawError}) + @ApiResponse({description: 'Internal server error', status: HttpStatus.INTERNAL_SERVER_ERROR, type: RawError}) @Get(':packageName') async getPackage( @Param('packageName') rawPackageName: string, @@ -48,7 +47,7 @@ export class PackagesController { @Res() res: Response ): Promise { if (!scope.trim().startsWith('@')) { - res.status(HTTP_STATUS.NOT_FOUND).json({code: HTTP_STATUS.NOT_FOUND, message: 'Not found'}); + res.status(HttpStatus.NOT_FOUND).json({code: HttpStatus.NOT_FOUND, message: 'Not found'}); return; } const {name: pkgPart, version} = parsePackageAndVersion(rawPackageName.trim()); @@ -66,8 +65,8 @@ async function handlePackageRequest( logger.info(`Got request for package "${packageName}" (version "${version}").`); if (!validatePackageName(packageName).validForNewPackages) { - response.status(HTTP_STATUS.UNPROCESSABLE_ENTITY).json({ - code: HTTP_STATUS.UNPROCESSABLE_ENTITY, + response.status(HttpStatus.UNPROCESSABLE_ENTITY).json({ + code: HttpStatus.UNPROCESSABLE_ENTITY, message: 'Invalid package name', } satisfies PackagesRouteResponseBody); return; @@ -77,8 +76,8 @@ async function handlePackageRequest( const redirectUrl = `${unpkgBase}/${packageName}@${version}/`; if (!validateUrl(redirectUrl)) { - response.status(HTTP_STATUS.BAD_REQUEST).json({ - code: HTTP_STATUS.BAD_REQUEST, + response.status(HttpStatus.BAD_REQUEST).json({ + code: HttpStatus.BAD_REQUEST, message: `Invalid URL: ${redirectUrl}`, } satisfies PackagesRouteResponseBody); return; @@ -86,36 +85,36 @@ async function handlePackageRequest( if (queryParamExists(query, 'raw')) { logger.info(`Returning raw unpkg info for "${packageName}": "${redirectUrl}" ...`); - response.json({code: HTTP_STATUS.OK, url: redirectUrl} satisfies PackagesRouteResponseBody); + response.json({code: HttpStatus.OK, url: redirectUrl} satisfies PackagesRouteResponseBody); return; } logger.info(`Redirecting package "${packageName}" to unpkg: "${redirectUrl}" ...`); - response.redirect(HTTP_STATUS.MOVED_TEMPORARILY, redirectUrl); + response.redirect(HttpStatus.FOUND, redirectUrl); return; } const parseResult = await getPackageUrl(packageName, version); - let errorCode: HTTP_STATUS; + let errorCode: HttpStatus; let errorMessage: string; switch (parseResult.status) { case ParseStatus.INVALID_PACKAGE_NAME: { - errorCode = HTTP_STATUS.UNPROCESSABLE_ENTITY; + errorCode = HttpStatus.UNPROCESSABLE_ENTITY; errorMessage = 'Invalid package name'; break; } case ParseStatus.INVALID_URL: case ParseStatus.NO_URL_FOUND: { - errorCode = HTTP_STATUS.NOT_FOUND; + errorCode = HttpStatus.NOT_FOUND; errorMessage = `No source URL found. Please visit https://www.npmjs.com/package/${packageName}.`; break; } case ParseStatus.PACKAGE_NOT_FOUND: { - errorCode = HTTP_STATUS.NOT_FOUND; + errorCode = HttpStatus.NOT_FOUND; errorMessage = 'Package not found'; break; } @@ -124,23 +123,23 @@ async function handlePackageRequest( const redirectSite = parseResult.url; if (queryParamExists(query, 'raw')) { logger.info(`Returning raw info for "${packageName}": "${redirectSite}" ...`); - response.json({code: HTTP_STATUS.OK, url: redirectSite} satisfies PackagesRouteResponseBody); + response.json({code: HttpStatus.OK, url: redirectSite} satisfies PackagesRouteResponseBody); return; } logger.info(`Redirecting package "${packageName}" to "${redirectSite}" ...`); - response.redirect(HTTP_STATUS.MOVED_TEMPORARILY, redirectSite); + response.redirect(HttpStatus.FOUND, redirectSite); return; } case ParseStatus.VERSION_NOT_FOUND: { - errorCode = HTTP_STATUS.NOT_FOUND; + errorCode = HttpStatus.NOT_FOUND; errorMessage = 'Version not found'; break; } case ParseStatus.SERVER_ERROR: default: { - errorCode = HTTP_STATUS.INTERNAL_SERVER_ERROR; + errorCode = HttpStatus.INTERNAL_SERVER_ERROR; errorMessage = 'Internal server error'; break; } diff --git a/test/Server.e2e.test.ts b/test/Server.e2e.test.ts index 92ce4ee5..4d9df8b9 100644 --- a/test/Server.e2e.test.ts +++ b/test/Server.e2e.test.ts @@ -1,11 +1,12 @@ import 'reflect-metadata'; +import {HttpStatus} from '@nestjs/common'; import {NestExpressApplication} from '@nestjs/platform-express'; import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {ServerConfig} from '../src/config.js'; import {ParseStatus} from '../src/RepositoryParser.js'; import * as repositoryParser from '../src/RepositoryParser.js'; -import {createApp, HTTP_STATUS} from '../src/Server.js'; +import {createApp} from '../src/Server.js'; const defaultConfig: ServerConfig = { CACHE_DURATION_SECONDS: 300, @@ -56,22 +57,22 @@ describe('server routes', () => { it('serves health endpoint', async () => { const response = await fetch(`${baseUrl}/_health`); - expect(response.status).toBe(HTTP_STATUS.OK); + expect(response.status).toBe(HttpStatus.OK); }); it('serves info endpoint', async () => { const response = await fetch(`${baseUrl}/_info`); const body = (await response.json()) as {code: number; commit: string; version?: string}; - expect(response.status).toBe(HTTP_STATUS.OK); - expect(body.code).toBe(HTTP_STATUS.OK); + expect(response.status).toBe(HttpStatus.OK); + expect(body.code).toBe(HttpStatus.OK); expect(body.commit).toBeDefined(); }); it('redirects main route to repository', async () => { const response = await fetch(`${baseUrl}/`, {redirect: 'manual'}); - expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY); + expect(response.status).toBe(HttpStatus.FOUND); expect(response.headers.get('location')).toBe('https://github.com/ffflorian/pkgsource'); }); @@ -79,9 +80,9 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/?raw=true`); const body = (await response.json()) as {code: number; url: string}; - expect(response.status).toBe(HTTP_STATUS.OK); + expect(response.status).toBe(HttpStatus.OK); expect(body).toEqual({ - code: HTTP_STATUS.OK, + code: HttpStatus.OK, url: 'https://github.com/ffflorian/pkgsource', }); }); @@ -89,7 +90,7 @@ describe('server routes', () => { it('supports unpkg mode on main route', async () => { const response = await fetch(`${baseUrl}/?unpkg=true`, {redirect: 'manual'}); - expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY); + expect(response.status).toBe(HttpStatus.FOUND); expect(response.headers.get('location')).toBe('https://unpkg.com/browse/pkgsource@latest/'); }); @@ -98,16 +99,16 @@ describe('server routes', () => { const favicon = await fetch(`${baseUrl}/favicon.ico`); expect(await robots.text()).toBe('User-agent: *\nDisallow: /'); - expect(favicon.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(favicon.status).toBe(HttpStatus.NOT_FOUND); }); it('returns json not found via global exception filter', async () => { const response = await fetch(`${baseUrl}/not-a-scope/unknown-package`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(response.status).toBe(HttpStatus.NOT_FOUND); expect(body).toEqual({ - code: HTTP_STATUS.NOT_FOUND, + code: HttpStatus.NOT_FOUND, message: 'Not found', }); }); @@ -116,14 +117,14 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/!invalid`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.UNPROCESSABLE_ENTITY); + expect(response.status).toBe(HttpStatus.UNPROCESSABLE_ENTITY); expect(body.message).toBe('Invalid package name'); }); it('supports unpkg mode for package endpoints', async () => { const response = await fetch(`${baseUrl}/lodash@4.17.21?unpkg=true`, {redirect: 'manual'}); - expect(response.status).toBe(HTTP_STATUS.MOVED_TEMPORARILY); + expect(response.status).toBe(HttpStatus.FOUND); expect(response.headers.get('location')).toBe('https://unpkg.com/browse/lodash@4.17.21/'); }); @@ -136,14 +137,14 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/lodash?raw=true`); const body = (await response.json()) as {code: number; url: string}; - expect(response.status).toBe(HTTP_STATUS.OK); + expect(response.status).toBe(HttpStatus.OK); expect(body).toEqual({ - code: HTTP_STATUS.OK, + code: HttpStatus.OK, url: 'https://github.com/lodash/lodash', }); }); - it('maps parser not found statuses to HTTP_STATUS.NOT_FOUND', async () => { + it('maps parser not found statuses to HttpStatus.NOT_FOUND', async () => { vi.spyOn(repositoryParser, 'getPackageUrl').mockResolvedValueOnce({ status: ParseStatus.NO_URL_FOUND, }); @@ -151,11 +152,11 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/left-pad`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(response.status).toBe(HttpStatus.NOT_FOUND); expect(body.message).toContain('No source URL found'); }); - it('maps parser package not found status to HTTP_STATUS.NOT_FOUND', async () => { + it('maps parser package not found status to HttpStatus.NOT_FOUND', async () => { vi.spyOn(repositoryParser, 'getPackageUrl').mockResolvedValueOnce({ status: ParseStatus.PACKAGE_NOT_FOUND, }); @@ -163,7 +164,7 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/definitely-missing-package`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(response.status).toBe(HttpStatus.NOT_FOUND); expect(body.message).toBe('Package not found'); }); @@ -175,7 +176,7 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/lodash@0.0.0-does-not-exist`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(response.status).toBe(HttpStatus.NOT_FOUND); expect(body.message).toBe('Version not found'); }); @@ -187,7 +188,7 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/problematic-package`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.INTERNAL_SERVER_ERROR); + expect(response.status).toBe(HttpStatus.INTERNAL_SERVER_ERROR); expect(body.message).toBe('Internal server error'); }); @@ -200,7 +201,7 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/%40scope/pkg@1.2.3?raw=true`); const body = (await response.json()) as {code: number; url: string}; - expect(response.status).toBe(HTTP_STATUS.OK); + expect(response.status).toBe(HttpStatus.OK); expect(body.url).toBe('https://github.com/example/pkg'); expect(getPackageUrlSpy).toHaveBeenCalledWith('@scope/pkg', '1.2.3'); }); @@ -209,9 +210,9 @@ describe('server routes', () => { const response = await fetch(`${baseUrl}/scope/pkg`); const body = (await response.json()) as {code: number; message: string}; - expect(response.status).toBe(HTTP_STATUS.NOT_FOUND); + expect(response.status).toBe(HttpStatus.NOT_FOUND); expect(body).toEqual({ - code: HTTP_STATUS.NOT_FOUND, + code: HttpStatus.NOT_FOUND, message: 'Not found', }); }); @@ -230,8 +231,8 @@ describe('rate limiting', () => { await started.app.close(); - expect([HTTP_STATUS.OK, HTTP_STATUS.TOO_MANY_REQUESTS]).toContain(firstResponse.status); - expect(secondResponse.status).toBe(HTTP_STATUS.TOO_MANY_REQUESTS); + expect([HttpStatus.OK, HttpStatus.TOO_MANY_REQUESTS]).toContain(firstResponse.status); + expect(secondResponse.status).toBe(HttpStatus.TOO_MANY_REQUESTS); expect(body).toEqual({ code: 429, message: 'Too many requests', diff --git a/yarn.lock b/yarn.lock index d7168967..bb5ef740 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2338,13 +2338,6 @@ __metadata: languageName: node linkType: hard -"http-status-codes@npm:2.3.0": - version: 2.3.0 - resolution: "http-status-codes@npm:2.3.0" - checksum: 10c0/c2412188929e8eed6623eef468c62d0c3c082919c03e9b74fd79cfd060d11783dba44603e38a3cee52d26563fe32005913eaf6120aa8ba907da1238f3eaad5fe - languageName: node - linkType: hard - "https-proxy-agent@npm:^7.0.1": version: 7.0.2 resolution: "https-proxy-agent@npm:7.0.2" @@ -3384,7 +3377,6 @@ __metadata: eslint: "npm:10.10.0" eslint-plugin-perfectionist: "npm:5.11.0" helmet: "npm:8.3.0" - http-status-codes: "npm:2.3.0" lefthook: "npm:2.1.12" logdown: "npm:3.3.1" oxlint: "npm:1.82.0"