diff --git a/.changeset/breezy-scissors-speak.md b/.changeset/breezy-scissors-speak.md new file mode 100644 index 000000000..55627c10b --- /dev/null +++ b/.changeset/breezy-scissors-speak.md @@ -0,0 +1,5 @@ +--- +"@equinor/fusion-framework-module-http": patch +--- + +Added test for http client, to check if configured operators are not altered diff --git a/.changeset/loud-waves-laugh.md b/.changeset/loud-waves-laugh.md new file mode 100644 index 000000000..38a8a3e14 --- /dev/null +++ b/.changeset/loud-waves-laugh.md @@ -0,0 +1,48 @@ +--- +"@equinor/fusion-framework-module-http": patch +--- + +When adding operators to request and response handler to an http client instanstance, the values where added to the configured handlers permanently + +```ts +// create a new client from configuration +const fooClient = provider.createClient("foo"); +fooClient.requestHandler.setHeader("x-foo", "bar"); + +// generate a RequestInit object +const fooRequest = await lastValueFrom( + fooClient.requestHandler.process({ path: "/api", uri: fooClient.uri }), +); + +expect((fooRequest.headers as Headers)?.get("x-foo")).toBe("bar"); + +// create a new client from the same configuration +const barClient = provider.createClient("foo"); + +// generate a RequestInit object +const barRequest = await lastValueFrom( + barClient.requestHandler.process({ path: "/api", uri: barClient.uri }), +); + +// expect the request header to not been modified +// FAILED +expect((barRequest.headers as Headers)?.get("x-foo")).toBeUndefined(); +``` + +modified the `ProcessOperators` to accept operators on creation, which are clone to the instance. + +```diff +--- a/packages/modules/http/src/lib/client/client.ts ++++ a/packages/modules/http/src/lib/client/client.ts +constructor( + public uri: string, + options?: Partial>, +) { +- this.requestHandler = options?.requestHandler ?? new HttpRequestHandler(); ++ this.requestHandler = new HttpRequestHandler(options?.requestHandler); +- this.responseHandler = options?.responseHandler ?? new HttpResponseHandler(); ++ this.responseHandler = new HttpResponseHandler(options?.responseHandler); + this._init(); +} + +``` diff --git a/packages/modules/http/package.json b/packages/modules/http/package.json index 74c9574c2..73765e745 100644 --- a/packages/modules/http/package.json +++ b/packages/modules/http/package.json @@ -4,6 +4,7 @@ "description": "", "main": "dist/esm/index.js", "types": "index.d.ts", + "type": "module", "exports": { ".": { "import": "./dist/esm/index.js", @@ -44,7 +45,8 @@ }, "scripts": { "build": "tsc -b", - "prepack": "pnpm build" + "prepack": "pnpm build", + "test": "vitest" }, "keywords": [], "author": "", @@ -63,6 +65,7 @@ "rxjs": "^7.8.1" }, "devDependencies": { - "typescript": "^5.4.2" + "typescript": "^5.4.2", + "vitest": "^1.2.2" } } diff --git a/packages/modules/http/src/lib/client/client.ts b/packages/modules/http/src/lib/client/client.ts index 793078937..a95ce4bad 100644 --- a/packages/modules/http/src/lib/client/client.ts +++ b/packages/modules/http/src/lib/client/client.ts @@ -55,8 +55,8 @@ export class HttpClient< public uri: string, options?: Partial>, ) { - this.requestHandler = options?.requestHandler ?? new HttpRequestHandler(); - this.responseHandler = options?.responseHandler ?? new HttpResponseHandler(); + this.requestHandler = new HttpRequestHandler(options?.requestHandler); + this.responseHandler = new HttpResponseHandler(options?.responseHandler); this._init(); } diff --git a/packages/modules/http/src/lib/operators/process-operators.ts b/packages/modules/http/src/lib/operators/process-operators.ts index cc6eab612..991711a36 100644 --- a/packages/modules/http/src/lib/operators/process-operators.ts +++ b/packages/modules/http/src/lib/operators/process-operators.ts @@ -3,24 +3,75 @@ import type { Observable } from 'rxjs'; import { last, mergeScan } from 'rxjs/operators'; import { IProcessOperators, ProcessOperator } from './types'; +/** + * ProcessOperators class manages a collection of process operators + * and provides methods to add, set, get, and process these operators. + */ export class ProcessOperators implements IProcessOperators { - protected _operators: Record> = {}; + /** + * A record of process operators keyed by a string. + */ + protected _operators: Record>; + /** + * Accessor for the operators. + * @returns The record of process operators. + */ + get operators(): Record> { + return this._operators; + } + + /** + * Constructs a new instance of the ProcessOperators class. + * @param operators - An optional object containing process operators. + * It can be either an instance of IProcessOperators or a record of string keys and ProcessOperator values. + */ + constructor(operators?: IProcessOperators | Record>) { + if (operators && 'operators' in operators) { + this._operators = { ...operators.operators }; + } else { + this._operators = operators ?? {}; + } + } + + /** + * Adds a new operator to the collection. + * @param key The key under which the operator is stored. + * @param operator The operator to be added. + * @returns The instance of ProcessOperators for chaining. + * @throws Error if an operator with the same key already exists. + */ add(key: string, operator: ProcessOperator): ProcessOperators { if (Object.keys(this._operators).includes(key)) throw Error(`Operator [${key}] already defined`); return this.set(key, operator); } + /** + * Sets or updates an operator in the collection. + * @param key The key under which the operator is stored. + * @param operator The operator to be set. + * @returns The instance of ProcessOperators for chaining. + */ set(key: string, operator: ProcessOperator): ProcessOperators { this._operators[key] = operator; return this; } + /** + * Retrieves an operator from the collection by its key. + * @param key The key of the operator to retrieve. + * @returns The retrieved operator. + */ get(key: string): ProcessOperator { return this._operators[key]; } + /** + * Processes an input request through the chain of operators. + * @param request The request to be processed. + * @returns An Observable of the processed request. + */ process(request: T): Observable { const operators = Object.values(this._operators); /** if no operators registered, just return the observable value */ diff --git a/packages/modules/http/src/lib/operators/types.ts b/packages/modules/http/src/lib/operators/types.ts index b754f2268..f26a6c821 100644 --- a/packages/modules/http/src/lib/operators/types.ts +++ b/packages/modules/http/src/lib/operators/types.ts @@ -7,24 +7,44 @@ export type ProcessOperator = (request: T) => R | void | Promise { /** - * Add a new operator (throw error if already defined) + * Gets the operators registered in the collection. + */ + get operators(): Record>; + + /** + * Adds a new operator to the collection. + * @param key The key to identify the operator. + * @param operator The process operator to add. + * @returns The updated collection of process operators. + * @throws An error if the operator is already defined. */ add(key: string, operator: ProcessOperator): IProcessOperators; /** - * Add or sets a operator + * Adds or sets a process operator in the collection. + * @param key The key to identify the operator. + * @param operator The process operator to add or set. + * @returns The updated collection of process operators. */ set(key: string, operator: ProcessOperator): IProcessOperators; /** - * Get a operator, will return undefined on invalid key. + * Gets a process operator from the collection. + * @param key The key of the operator to retrieve. + * @returns The process operator associated with the key, or undefined if the key is invalid. */ get(key: string): ProcessOperator; /** - * Process registered processors. + * Processes the registered process operators. + * @param request The request to process. + * @returns An observable that emits the processed request. */ process(request: T): Observable; } diff --git a/packages/modules/http/tests/HttpClient.test.ts b/packages/modules/http/tests/HttpClient.test.ts new file mode 100644 index 000000000..dc11dd7dd --- /dev/null +++ b/packages/modules/http/tests/HttpClient.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import { HttpClientConfigurator } from '../src/configurator'; +import { HttpClient } from '../src/lib'; +import { HttpClientProvider } from '../src'; +import { lastValueFrom } from 'rxjs'; + +let provider: HttpClientProvider; + +describe('HttpClient', () => { + beforeEach(() => { + const config = new HttpClientConfigurator(HttpClient); + config.configureClient('foo', 'http://localhost:3000'); + provider = new HttpClientProvider(config); + }); + + it('should create instance', () => { + const client = provider.createClient('foo'); + expect(client).toBeDefined(); + expect(client.uri).toBe('http://localhost:3000'); + }); + + it('should allow providing headers in request', async () => {}); + + it('should not modify configured headers', async () => { + // create a new client from configuration + const fooClient = provider.createClient('foo'); + fooClient.requestHandler.setHeader('x-foo', 'bar'); + + // generate a RequestInit object + const fooRequest = await lastValueFrom( + fooClient.requestHandler.process({ path: '/api', uri: fooClient.uri }), + ); + + expect((fooRequest.headers as Headers)?.get('x-foo')).toBe('bar'); + + // create a new client from the same configuration + const barClient = provider.createClient('foo'); + + // generate a RequestInit object + const barRequest = await lastValueFrom( + barClient.requestHandler.process({ path: '/api', uri: barClient.uri }), + ); + + // expect the request header to not been modified + expect((barRequest.headers as Headers)?.get('x-foo')).toBeUndefined(); + }); +}); diff --git a/packages/modules/http/tsconfig.json b/packages/modules/http/tsconfig.json index ee338ad4c..1529b2d0a 100644 --- a/packages/modules/http/tsconfig.json +++ b/packages/modules/http/tsconfig.json @@ -15,7 +15,7 @@ } ], "include": [ - "src/**/*", + "src/**/*" ], "exclude": [ "node_modules", diff --git a/packages/modules/http/vitest.config.ts b/packages/modules/http/vitest.config.ts new file mode 100644 index 000000000..39aa69cfb --- /dev/null +++ b/packages/modules/http/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineProject } from 'vitest/config'; + +import { name, version } from './package.json'; + +export default defineProject({ + test: { + // TODO remove after __tests__ are deleted! + include: ['tests/**'], + name: `${name}@${version}`, + environment: 'happy-dom', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12b1de564..c01853d5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -898,6 +898,9 @@ importers: typescript: specifier: ^5.4.2 version: 5.4.2 + vitest: + specifier: ^1.2.2 + version: 1.3.1(@types/node@20.11.26)(happy-dom@13.8.2) packages/modules/module: dependencies: @@ -13135,7 +13138,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.2.6(@types/node@20.11.24) + vite: 5.2.7(@types/node@20.11.24) transitivePeerDependencies: - '@types/node' - less @@ -13309,6 +13312,42 @@ packages: fsevents: 2.3.3 dev: true + /vite@5.2.7(@types/node@20.11.24): + resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.24 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.13.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vite@5.2.7(@types/node@20.11.26): resolution: {integrity: sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==} engines: {node: ^18.0.0 || >=20.0.0}