Skip to content

Commit b1d2ce1

Browse files
author
Lennart Klose
committed
Add dvelopFetch-method and DvelopInternals for future signatures
1 parent 2ad9a02 commit b1d2ce1

4 files changed

Lines changed: 256 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { dvelopFetch } from "./dvelop-fetch";
2+
import { DvelopContext } from "../context/context";
3+
import { generateRequestId } from "../generate-uuid/generate-uudi-id";
4+
import { buildTraceparentHeader } from "../trace-context/traceparent-header/traceparent-header";
5+
import { deepMergeObjects } from "../util/deep-merge-objects";
6+
7+
jest.mock("../generate-uuid/generate-uudi-id");
8+
jest.mock("../trace-context/traceparent-header/traceparent-header");
9+
jest.mock("../util/deep-merge-objects");
10+
11+
const mockFetch = jest.fn();
12+
global.fetch = mockFetch as any;
13+
14+
describe("dvelopFetch", () => {
15+
beforeEach(() => {
16+
jest.clearAllMocks();
17+
(deepMergeObjects as jest.Mock).mockImplementation((defaultInit, customInit) => ({
18+
...defaultInit,
19+
...customInit,
20+
}));
21+
});
22+
23+
it("should call fetch with default headers and no context properties", async () => {
24+
const context: DvelopContext = {};
25+
const mockResponse = { status: 200 } as Response;
26+
mockFetch.mockResolvedValue(mockResponse);
27+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id-123");
28+
29+
const result = await dvelopFetch(context, "/api/endpoint");
30+
31+
expect(result).toBe(mockResponse);
32+
expect(mockFetch).toHaveBeenCalledTimes(1);
33+
expect(mockFetch).toHaveBeenCalledWith("/api/endpoint", expect.objectContaining({
34+
headers: expect.objectContaining({
35+
"ContentType": "application/json",
36+
"Accept": "application/hal+json, application/json",
37+
"x-dv-request-id": "test-request-id-123",
38+
}),
39+
}));
40+
});
41+
42+
it("should use provided requestId from context instead of generating one", async () => {
43+
const context: DvelopContext = { requestId: "provided-request-id" };
44+
const mockResponse = { status: 200 } as Response;
45+
mockFetch.mockResolvedValue(mockResponse);
46+
47+
await dvelopFetch(context, "/api/endpoint");
48+
49+
expect(generateRequestId).not.toHaveBeenCalled();
50+
expect(mockFetch).toHaveBeenCalledWith("/api/endpoint", expect.objectContaining({
51+
headers: expect.objectContaining({
52+
"x-dv-request-id": "provided-request-id",
53+
}),
54+
}));
55+
});
56+
57+
it("should add Authorization header when authSessionId is present", async () => {
58+
const context: DvelopContext = { authSessionId: "test-auth-token" };
59+
const mockResponse = { status: 200 } as Response;
60+
mockFetch.mockResolvedValue(mockResponse);
61+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
62+
63+
await dvelopFetch(context, "/api/endpoint");
64+
65+
expect(mockFetch).toHaveBeenCalledWith("/api/endpoint", expect.objectContaining({
66+
headers: expect.objectContaining({
67+
"Authorization": "Bearer test-auth-token",
68+
}),
69+
}));
70+
});
71+
72+
it("should add traceparent header when traceContext is present", async () => {
73+
const mockTraceContext = { traceId: "123", spanId: "456" };
74+
const context: DvelopContext = { traceContext: mockTraceContext as any };
75+
const mockResponse = { status: 200 } as Response;
76+
mockFetch.mockResolvedValue(mockResponse);
77+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
78+
(buildTraceparentHeader as jest.Mock).mockReturnValue("00-123-456-01");
79+
80+
await dvelopFetch(context, "/api/endpoint");
81+
82+
expect(buildTraceparentHeader).toHaveBeenCalledWith(mockTraceContext);
83+
expect(mockFetch).toHaveBeenCalledWith("/api/endpoint", expect.objectContaining({
84+
headers: expect.objectContaining({
85+
"traceparent": "00-123-456-01",
86+
}),
87+
}));
88+
});
89+
90+
it("should include both Authorization and traceparent headers", async () => {
91+
const mockTraceContext = { traceId: "123", spanId: "456" };
92+
const context: DvelopContext = {
93+
authSessionId: "test-auth-token",
94+
traceContext: mockTraceContext as any,
95+
};
96+
const mockResponse = { status: 200 } as Response;
97+
mockFetch.mockResolvedValue(mockResponse);
98+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
99+
(buildTraceparentHeader as jest.Mock).mockReturnValue("00-123-456-01");
100+
101+
await dvelopFetch(context, "/api/endpoint");
102+
103+
const callArgs = mockFetch.mock.calls[0];
104+
expect(callArgs[1].headers).toMatchObject({
105+
"Authorization": "Bearer test-auth-token",
106+
"traceparent": "00-123-456-01",
107+
"x-dv-request-id": "test-request-id",
108+
"ContentType": "application/json",
109+
"Accept": "application/hal+json, application/json",
110+
});
111+
});
112+
113+
it("should prepend systemBaseUri to input", async () => {
114+
const context: DvelopContext = { systemBaseUri: "https://api.example.com" };
115+
const mockResponse = { status: 200 } as Response;
116+
mockFetch.mockResolvedValue(mockResponse);
117+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
118+
(deepMergeObjects as jest.Mock).mockImplementation((defaultInit, customInit) => ({
119+
...defaultInit,
120+
...customInit,
121+
}));
122+
123+
await dvelopFetch(context, "/api/endpoint");
124+
125+
expect(mockFetch).toHaveBeenCalledWith("https://api.example.com/api/endpoint", expect.any(Object));
126+
});
127+
128+
it("should merge custom init with default init", async () => {
129+
const context: DvelopContext = {};
130+
const customInit: RequestInit = {
131+
method: "POST",
132+
body: JSON.stringify({ data: "test" }),
133+
};
134+
const mockResponse = { status: 200 } as Response;
135+
mockFetch.mockResolvedValue(mockResponse);
136+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
137+
138+
const mergedInit = {
139+
headers: {
140+
"ContentType": "application/json",
141+
"Accept": "application/hal+json, application/json",
142+
"x-dv-request-id": "test-request-id",
143+
},
144+
method: "POST",
145+
body: JSON.stringify({ data: "test" }),
146+
};
147+
(deepMergeObjects as jest.Mock).mockReturnValue(mergedInit);
148+
149+
await dvelopFetch(context, "/api/endpoint", customInit);
150+
151+
expect(deepMergeObjects).toHaveBeenCalled();
152+
const defaultInitArg = (deepMergeObjects as jest.Mock).mock.calls[0][0];
153+
const customInitArg = (deepMergeObjects as jest.Mock).mock.calls[0][1];
154+
155+
expect(defaultInitArg).toHaveProperty("headers");
156+
expect(customInitArg).toBe(customInit);
157+
158+
expect(mockFetch).toHaveBeenCalledWith("/api/endpoint", mergedInit);
159+
});
160+
161+
it("should handle empty custom init", async () => {
162+
const context: DvelopContext = {};
163+
const mockResponse = { status: 200 } as Response;
164+
mockFetch.mockResolvedValue(mockResponse);
165+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
166+
167+
await dvelopFetch(context, "/api/endpoint", {});
168+
169+
expect(deepMergeObjects).toHaveBeenCalled();
170+
expect(mockFetch).toHaveBeenCalledTimes(1);
171+
});
172+
173+
it("should handle URL object as input", async () => {
174+
const context: DvelopContext = {};
175+
const url = new URL("https://example.com/api/endpoint");
176+
const mockResponse = { status: 200 } as Response;
177+
mockFetch.mockResolvedValue(mockResponse);
178+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
179+
(deepMergeObjects as jest.Mock).mockImplementation((defaultInit, customInit) => ({
180+
...defaultInit,
181+
...customInit,
182+
}));
183+
184+
await dvelopFetch(context, url);
185+
186+
const callArgs = mockFetch.mock.calls[0];
187+
expect(callArgs[0]).toBe("" + url);
188+
});
189+
190+
it("should handle fetch errors", async () => {
191+
const context: DvelopContext = {};
192+
const mockError = new Error("Network error");
193+
mockFetch.mockRejectedValue(mockError);
194+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
195+
196+
await expect(dvelopFetch(context, "/api/endpoint")).rejects.toThrow("Network error");
197+
});
198+
199+
it("should call deepMergeObjects with default and custom init", async () => {
200+
const context: DvelopContext = {};
201+
const customInit: RequestInit = { method: "PUT" };
202+
const mockResponse = { status: 200 } as Response;
203+
mockFetch.mockResolvedValue(mockResponse);
204+
(generateRequestId as jest.Mock).mockReturnValue("test-request-id");
205+
(deepMergeObjects as jest.Mock).mockImplementation((defaultInit, customInit) => ({
206+
...defaultInit,
207+
...customInit,
208+
}));
209+
210+
await dvelopFetch(context, "/api/endpoint", customInit);
211+
212+
expect(deepMergeObjects).toHaveBeenCalledWith(
213+
expect.objectContaining({
214+
headers: expect.any(Object),
215+
}),
216+
customInit
217+
);
218+
});
219+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DvelopContext } from "../context/context";
2+
import { generateRequestId } from "../generate-uuid/generate-uudi-id";
3+
import { buildTraceparentHeader } from "../trace-context/traceparent-header/traceparent-header";
4+
import { deepMergeObjects } from "../util/deep-merge-objects";
5+
6+
export async function dvelopFetch(context: DvelopContext, input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
7+
8+
const defaultInit: RequestInit = {};
9+
10+
defaultInit.headers = {
11+
"ContentType": "application/json",
12+
"Accept": "application/hal+json, application/json",
13+
"x-dv-request-id": context.requestId || generateRequestId()
14+
};
15+
16+
if (context.authSessionId) {
17+
defaultInit.headers["Authorization"] = `Bearer ${context.authSessionId}`;
18+
}
19+
20+
if (context.traceContext) {
21+
defaultInit.headers["traceparent"] = buildTraceparentHeader(context.traceContext);
22+
}
23+
24+
const fetchInput: RequestInfo | URL = (context.systemBaseUri || "") + input
25+
const fetchInit: RequestInit = deepMergeObjects(defaultInit, init || {})
26+
27+
return fetch(fetchInput, fetchInit)
28+
}

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ export { TraceContext } from "./trace-context/trace-context";
3535
export { TraceContextError } from "./trace-context/trace-context-error";
3636
export { buildTraceparentHeader, parseTraceparentHeader } from "./trace-context/traceparent-header/traceparent-header";
3737
export { generateTraceContext, generateTraceId, generateSpanId } from "./trace-context/generate-trace-context/generate-trace-context";
38+
39+
export { dvelopFetch } from "./dvelop-fetch/dvelop-fetch";
40+
export { DvelopInternals } from "./internals/internals";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { DvelopContext } from "../context/context";
2+
3+
export interface DvelopInternals<T> {
4+
fetch?: (context: DvelopContext, input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
5+
transform?: (response: Response) => T
6+
}

0 commit comments

Comments
 (0)