Skip to content

Commit 8b082a7

Browse files
jbl428imdudu1
andcommitted
test: add node fetch injector url test
Co-authored-by: imdudu1 <[email protected]>
1 parent f1b8547 commit 8b082a7

8 files changed

+204
-9
lines changed

lib/decorators/http-exchange.decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface HttpExchangeMetadata {
1414
url: string;
1515
}
1616

17-
type AsyncFunction = (...args: unknown[]) => Promise<unknown>;
17+
type AsyncFunction = (...args: any[]) => Promise<unknown>;
1818

1919
export function HttpExchange(method: HttpMethod, url: string) {
2020
return function <P extends string>(

lib/decorators/http-interface.decorator.spec.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ describe("HttpInterface", () => {
99
class TestService {}
1010

1111
// when
12-
const result = Reflect.getMetadata(HTTP_INTERFACE_METADATA, TestService);
12+
const result = Reflect.getMetadata(
13+
HTTP_INTERFACE_METADATA,
14+
TestService.prototype
15+
);
1316

1417
// then
1518
expect(result).toBe("");
@@ -21,7 +24,10 @@ describe("HttpInterface", () => {
2124
class TestService {}
2225

2326
// when
24-
const result = Reflect.getMetadata(HTTP_INTERFACE_METADATA, TestService);
27+
const result = Reflect.getMetadata(
28+
HTTP_INTERFACE_METADATA,
29+
TestService.prototype
30+
);
2531

2632
// then
2733
expect(result).toBe("/api/v1/sample");

lib/decorators/http-interface.decorator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { HTTP_INTERFACE_METADATA } from "./constants";
33

44
export function HttpInterface(url = ""): ClassDecorator {
55
const decorator: ClassDecorator = (target) => {
6-
Reflect.defineMetadata(HTTP_INTERFACE_METADATA, url, target);
6+
Reflect.defineMetadata(HTTP_INTERFACE_METADATA, url, target.prototype);
77
};
88

99
return applyDecorators(Injectable, decorator);

lib/fixture/stub-discovery.service.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
type DiscoveryOptions,
3+
DiscoveryService,
4+
ModulesContainer,
5+
} from "@nestjs/core";
6+
import { type InstanceWrapper } from "@nestjs/core/injector/instance-wrapper";
7+
import { type Module } from "@nestjs/core/injector/module";
8+
9+
export class StubDiscoveryService extends DiscoveryService {
10+
#providers: InstanceWrapper[] = [];
11+
12+
constructor() {
13+
super(new ModulesContainer());
14+
}
15+
16+
addProvider<T>(Provide: new (...args: unknown[]) => T): T {
17+
const instance = new Provide();
18+
const instanceWrapper = {
19+
instance,
20+
metatype: { prototype: Provide.prototype },
21+
};
22+
this.#providers.push(instanceWrapper as any);
23+
24+
return instance;
25+
}
26+
27+
clear(): void {
28+
this.#providers = [];
29+
}
30+
31+
override getProviders(
32+
options?: DiscoveryOptions,
33+
modules?: Module[]
34+
): InstanceWrapper[] {
35+
return this.#providers;
36+
}
37+
}

lib/fixture/stub-http-client.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { type HttpClient } from "../types/http-client.interface";
2+
3+
export class StubHttpClient implements HttpClient {
4+
#requestInfo: Request[] = [];
5+
#responses: Response[] = [];
6+
7+
async request(request: Request): Promise<Response> {
8+
this.#requestInfo.push(request);
9+
10+
const response = this.#responses.shift();
11+
12+
if (typeof response === "undefined") {
13+
throw new Error("empty response array");
14+
}
15+
16+
return response;
17+
}
18+
19+
get requestInfo(): Request[] {
20+
return this.#requestInfo;
21+
}
22+
23+
addResponse(body: Record<string, unknown> | string): void {
24+
const response = new Response(JSON.stringify(body));
25+
this.#responses.push(response);
26+
}
27+
28+
clear(): void {
29+
this.#requestInfo = [];
30+
this.#responses = [];
31+
}
32+
}

lib/node-fetch.injector.spec.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { MetadataScanner } from "@nestjs/core";
2+
import { beforeEach, describe, test, expect } from "vitest";
3+
import { GetExchange, HttpInterface, PathVariable } from "./decorators";
4+
import { StubDiscoveryService } from "./fixture/stub-discovery.service";
5+
import { StubHttpClient } from "./fixture/stub-http-client";
6+
import { NodeFetchInjector } from "./node-fetch.injector";
7+
8+
describe("NodeFetchInjector", () => {
9+
const metadataScanner = new MetadataScanner();
10+
const httpClient = new StubHttpClient();
11+
const discoveryService = new StubDiscoveryService();
12+
const nodeFetchInjector = new NodeFetchInjector(
13+
metadataScanner,
14+
discoveryService,
15+
httpClient
16+
);
17+
18+
beforeEach(() => {
19+
httpClient.clear();
20+
discoveryService.clear();
21+
});
22+
23+
test("should not wrap method if there is no http exchange", async () => {
24+
// given
25+
@HttpInterface()
26+
class SampleClient {
27+
async request(): Promise<string> {
28+
return "request";
29+
}
30+
}
31+
const instance = discoveryService.addProvider(SampleClient);
32+
nodeFetchInjector.onModuleInit();
33+
34+
// when
35+
const actual = await instance.request();
36+
37+
// then
38+
expect(actual).toBe("request");
39+
expect(httpClient.requestInfo).toHaveLength(0);
40+
});
41+
42+
test("should request to given url", async () => {
43+
// given
44+
@HttpInterface("https://example.com")
45+
class SampleClient {
46+
@GetExchange("/api")
47+
async request(): Promise<string> {
48+
return "request";
49+
}
50+
}
51+
const instance = discoveryService.addProvider(SampleClient);
52+
httpClient.addResponse({ status: "ok" });
53+
nodeFetchInjector.onModuleInit();
54+
55+
// when
56+
const actual = await instance.request();
57+
58+
// then
59+
expect(actual).toEqual({ status: "ok" });
60+
expect(httpClient.requestInfo).toHaveLength(1);
61+
expect(httpClient.requestInfo[0].url).toBe("https://example.com/api");
62+
});
63+
64+
test("should request to path parm replaced url", async () => {
65+
// given
66+
@HttpInterface("https://example.com")
67+
class SampleClient {
68+
@GetExchange("/api/users/{id}/{id}")
69+
async request(@PathVariable("id") id: string): Promise<string> {
70+
return "request";
71+
}
72+
}
73+
const instance = discoveryService.addProvider(SampleClient);
74+
httpClient.addResponse({ status: "ok" });
75+
nodeFetchInjector.onModuleInit();
76+
77+
// when
78+
const actual = await instance.request("1");
79+
80+
// then
81+
expect(actual).toEqual({ status: "ok" });
82+
expect(httpClient.requestInfo).toHaveLength(1);
83+
expect(httpClient.requestInfo[0].url).toBe(
84+
"https://example.com/api/users/1/1"
85+
);
86+
});
87+
88+
test("should request to multiple path parm replaced url", async () => {
89+
// given
90+
@HttpInterface("https://example.com")
91+
class SampleClient {
92+
@GetExchange("/api/users/{id}/{status}")
93+
async request(
94+
@PathVariable("id") id: string,
95+
@PathVariable("status") status: string
96+
): Promise<string> {
97+
return "request";
98+
}
99+
}
100+
const instance = discoveryService.addProvider(SampleClient);
101+
httpClient.addResponse({ status: "ok" });
102+
nodeFetchInjector.onModuleInit();
103+
104+
// when
105+
const actual = await instance.request("123", "active");
106+
107+
// then
108+
expect(actual).toEqual({ status: "ok" });
109+
expect(httpClient.requestInfo).toHaveLength(1);
110+
expect(httpClient.requestInfo[0].url).toBe(
111+
"https://example.com/api/users/123/active"
112+
);
113+
});
114+
});

lib/node-fetch.injector.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import {
88
PATH_VARIABLE_METADATA,
99
type PathVariableMetadata,
1010
} from "./decorators";
11+
import { HttpClient } from "./types/http-client.interface";
1112

1213
@Injectable()
1314
export class NodeFetchInjector implements OnModuleInit {
1415
constructor(
1516
private readonly metadataScanner: MetadataScanner,
16-
private readonly discoveryService: DiscoveryService
17+
private readonly discoveryService: DiscoveryService,
18+
private readonly httpClient: HttpClient
1719
) {}
1820

1921
onModuleInit(): void {
@@ -43,14 +45,15 @@ export class NodeFetchInjector implements OnModuleInit {
4345

4446
wrapper.instance[methodName] = async (...args: any[]) => {
4547
const url = [...(pathMetadata?.entries() ?? [])].reduce(
46-
(url, [index, value]) => url.replace(value, args[index]),
48+
(url, [index, value]) =>
49+
url.replace(new RegExp(`{${value}}`, "g"), args[index]),
4750
`${baseUrl}${httpExchangeMetadata.url}`
4851
);
4952
const request = new Request(url);
5053

51-
return await fetch(request).then(
52-
async (response) => await response.json()
53-
);
54+
return await this.httpClient
55+
.request(request)
56+
.then(async (response) => await response.json());
5457
};
5558
});
5659
});

lib/types/http-client.interface.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface HttpClient {
2+
request: (request: Request) => Promise<Response>;
3+
}

0 commit comments

Comments
 (0)