diff --git a/.changeset/response-types-integration-tests.md b/.changeset/response-types-integration-tests.md new file mode 100644 index 00000000..70c5126e --- /dev/null +++ b/.changeset/response-types-integration-tests.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +test(integration): verify response content-type across core HTTP paths diff --git a/test/integration/features/response-types/response-types.feature b/test/integration/features/response-types/response-types.feature new file mode 100644 index 00000000..30476579 --- /dev/null +++ b/test/integration/features/response-types/response-types.feature @@ -0,0 +1,37 @@ +@response-types +Feature: HTTP response types + Scenario Outline: GET path returns expected response Content-Type + When a client requests path "" with Accept header "" + Then the HTTP response status is + And the HTTP response Content-Type includes "" + + Examples: + | path | acceptHeader | statusCode | contentType | + | / | application/nostr+json | 200 | application/nostr+json | + | / | text/html | 200 | text/html | + | /healthz | */* | 200 | text/plain | + | /terms | */* | 200 | text/html | + | /.well-known/nodeinfo | */* | 200 | application/json | + | /nodeinfo/2.1 | */* | 200 | application/json | + | /nodeinfo/2.0 | */* | 200 | application/json | + + Scenario Outline: dynamic GET path returns expected response Content-Type + When a client requests dynamic path "" + Then the HTTP response status is + And the HTTP response Content-Type includes "" + + Examples: + | path | statusCode | contentType | + | /admissions/check/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef | 200 | application/json | + | /invoices/non-existent-invoice/status | 404 | application/json | + + Scenario Outline: POST path returns expected response Content-Type + Given payments are enabled with processor "" + When a client posts "" to path "" with Content-Type "" + Then the HTTP response status is + And the HTTP response Content-Type includes "" + + Examples: + | path | processor | contentTypeHeader | body | statusCode | contentType | + | /invoices | lnurl | application/x-www-form-urlencoded | | 400 | text/plain | + | /callbacks/lnbits | lnbits | application/json | {} | 403 | text/html | diff --git a/test/integration/features/response-types/response-types.feature.ts b/test/integration/features/response-types/response-types.feature.ts new file mode 100644 index 00000000..a973578e --- /dev/null +++ b/test/integration/features/response-types/response-types.feature.ts @@ -0,0 +1,112 @@ +import { After, Given, Then, When, World } from '@cucumber/cucumber' +import { expect } from 'chai' +import axios, { AxiosResponse } from 'axios' +import { assocPath, pipe } from 'ramda' +import { SettingsStatic } from '../../../../src/utils/settings' + +const BASE_URL = 'http://localhost:18808' + +Given( + 'payments are enabled with processor {string}', + function (this: World>, processor: string) { + const settings = SettingsStatic._settings as any + if (!this.parameters.previousResponseTypesSettings) { + this.parameters.previousResponseTypesSettings = structuredClone(settings) + } + + const baseSettings = pipe( + assocPath(['payments', 'enabled'], true), + assocPath(['payments', 'processor'], processor), + )(settings) as any + + if (processor === 'zebedee') { + SettingsStatic._settings = assocPath(['paymentsProcessors', 'zebedee', 'ipWhitelist'], [], baseSettings) as any + return + } + + if (processor === 'lnbits') { + this.parameters.lnbitsApiKeyModified = true + if (typeof this.parameters.previousLnbitsApiKey === 'undefined') { + this.parameters.previousLnbitsApiKey = process.env.LNBITS_API_KEY + } + process.env.LNBITS_API_KEY = 'integration-lnbits-api-key' + + SettingsStatic._settings = assocPath( + ['paymentsProcessors', 'lnbits', 'callbackBaseURL'], + 'http://localhost:18808/callbacks/lnbits', + baseSettings, + ) as any + return + } + + SettingsStatic._settings = baseSettings + }, +) + +When( + 'a client requests path {string} with Accept header {string}', + async function (this: World>, requestPath: string, acceptHeader: string) { + const response: AxiosResponse = await axios.get(`${BASE_URL}${requestPath}`, { + headers: { Accept: acceptHeader }, + validateStatus: () => true, + }) + + this.parameters.httpResponse = response + }, +) + +When('a client requests dynamic path {string}', async function (this: World>, requestPath: string) { + const response: AxiosResponse = await axios.get(`${BASE_URL}${requestPath}`, { + validateStatus: () => true, + }) + + this.parameters.httpResponse = response +}) + +When( + 'a client posts {string} to path {string} with Content-Type {string}', + async function ( + this: World>, + body: string, + requestPath: string, + contentTypeHeader: string, + ) { + const response: AxiosResponse = await axios.post(`${BASE_URL}${requestPath}`, body, { + headers: { 'content-type': contentTypeHeader }, + validateStatus: () => true, + }) + + this.parameters.httpResponse = response + }, +) + +Then('the HTTP response status is {int}', function (this: World>, statusCode: number) { + expect(this.parameters.httpResponse.status).to.equal(statusCode) +}) + +Then( + 'the HTTP response Content-Type includes {string}', + function (this: World>, contentType: string) { + const contentTypeHeader = this.parameters.httpResponse.headers['content-type'] + const headerValue = Array.isArray(contentTypeHeader) ? contentTypeHeader.join(';') : contentTypeHeader + const normalizedHeader = typeof headerValue === 'string' ? headerValue.toLowerCase() : '' + expect(normalizedHeader).to.include(contentType.toLowerCase()) + }, +) + +After({ tags: '@response-types' }, function (this: World>) { + if (this.parameters.previousResponseTypesSettings) { + SettingsStatic._settings = this.parameters.previousResponseTypesSettings + this.parameters.previousResponseTypesSettings = undefined + } + + if (this.parameters.lnbitsApiKeyModified) { + if (typeof this.parameters.previousLnbitsApiKey === 'undefined') { + delete process.env.LNBITS_API_KEY + } else { + process.env.LNBITS_API_KEY = this.parameters.previousLnbitsApiKey + } + this.parameters.previousLnbitsApiKey = undefined + this.parameters.lnbitsApiKeyModified = false + } +})