@ranwhenparked/trustap-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,193 @@
1
+ import type { MinimalHttpClient } from "../../core.ts";
2
+
3
+ /**
4
+ * Middleware for intercepting and transforming HTTP requests and responses in tests.
5
+ *
6
+ * Contract:
7
+ * - `onRequest` MUST return a Request object (either the input request or a new one).
8
+ * It MUST NOT return null/undefined or mutate the input request.
9
+ * If middleware throws, the error bubbles up and fails the test.
10
+ *
11
+ * - `onResponse` MUST return a Response object (either the input response or a new one).
12
+ * It MUST NOT return null/undefined or mutate the input response.
13
+ * If middleware throws, the error bubbles up and fails the test.
14
+ *
15
+ * - Middleware can be synchronous or asynchronous (return Promise).
16
+ *
17
+ * - To skip processing, return the input request/response unchanged.
18
+ *
19
+ * Example:
20
+ * ```typescript
21
+ * const authMiddleware: MockMiddleware = {
22
+ * onRequest: ({ request }) => {
23
+ * const headers = new Headers(request.headers);
24
+ * headers.set('Authorization', 'Bearer token');
25
+ * return new Request(request, { headers });
26
+ * }
27
+ * };
28
+ * ```
29
+ */
30
+ export interface MockMiddleware {
31
+ onRequest?: (options: { request: Request }) => Request | Promise<Request>;
32
+ onResponse?: (options: {
33
+ request: Request;
34
+ response: Response;
35
+ }) => Response | Promise<Response>;
36
+ }
37
+
38
+ interface MockRequestRecord {
39
+ path: string;
40
+ method: string;
41
+ options?: unknown;
42
+ request: Request;
43
+ response: Response;
44
+ }
45
+
46
+ interface MockResult {
47
+ data?: unknown;
48
+ error?: unknown;
49
+ status?: number;
50
+ headers?: Record<string, string>;
51
+ }
52
+
53
+ const defaultResponse = (): Response =>
54
+ Response.json({ success: true }, {
55
+ status: 200,
56
+ headers: { "content-type": "application/json" },
57
+ });
58
+
59
+ export class MockHttpClient implements MinimalHttpClient<MockMiddleware> {
60
+ private middleware: MockMiddleware[] = [];
61
+ private responses = new Map<string, MockResult>();
62
+ public requests: MockRequestRecord[] = [];
63
+
64
+ use(middleware: MockMiddleware): void {
65
+ this.middleware.push(middleware);
66
+ }
67
+
68
+ setResponse(path: string, method: string, result: MockResult): void {
69
+ this.responses.set(`${method.toUpperCase()}:${path}`, result);
70
+ }
71
+
72
+ private createRequestInit(method: string, options?: unknown): RequestInit {
73
+ const init: RequestInit = { method: method.toUpperCase() };
74
+ if (
75
+ options &&
76
+ typeof options === "object" &&
77
+ "headers" in (options as Record<string, unknown>)
78
+ ) {
79
+ const provided = (options as { headers?: HeadersInit }).headers;
80
+ if (provided) {
81
+ init.headers = new Headers(provided);
82
+ }
83
+ }
84
+ return init;
85
+ }
86
+
87
+ private async processRequestMiddleware(
88
+ request: Request,
89
+ ): Promise<Request> {
90
+ let processed = request;
91
+ for (const middleware of this.middleware) {
92
+ if (middleware.onRequest) {
93
+ const result = middleware.onRequest({ request: processed });
94
+ processed = result instanceof Promise ? await result : result;
95
+ }
96
+ }
97
+ return processed;
98
+ }
99
+
100
+ private buildMockResponse(
101
+ preset: MockResult | undefined,
102
+ ): Response {
103
+ if (!preset) {
104
+ return defaultResponse();
105
+ }
106
+ return Response.json(
107
+ preset.data ?? preset.error ?? { success: true },
108
+ {
109
+ status: preset.status ?? (preset.error ? 400 : 200),
110
+ headers: {
111
+ "content-type": "application/json",
112
+ ...preset.headers,
113
+ },
114
+ },
115
+ );
116
+ }
117
+
118
+ private async processResponseMiddleware(
119
+ request: Request,
120
+ response: Response,
121
+ ): Promise<Response> {
122
+ let processed = response;
123
+ for (const middleware of this.middleware) {
124
+ if (middleware.onResponse) {
125
+ const result = middleware.onResponse({
126
+ request,
127
+ response: processed,
128
+ });
129
+ processed = result instanceof Promise ? await result : result;
130
+ }
131
+ }
132
+ return processed;
133
+ }
134
+
135
+ private async dispatch(
136
+ path: string,
137
+ method: string,
138
+ options?: unknown,
139
+ ): Promise<{ data?: unknown; error?: unknown; response: Response }> {
140
+ const init = this.createRequestInit(method, options);
141
+ let request = new Request(`https://mock.local${path}`, init);
142
+ request = await this.processRequestMiddleware(request);
143
+
144
+ const key = `${method.toUpperCase()}:${path}`;
145
+ const preset = this.responses.get(key);
146
+ const response = this.buildMockResponse(preset);
147
+ const finalResponse = await this.processResponseMiddleware(request, response);
148
+
149
+ const record: MockRequestRecord = {
150
+ path,
151
+ method: method.toUpperCase(),
152
+ options,
153
+ request,
154
+ response: finalResponse,
155
+ };
156
+ this.requests.push(record);
157
+
158
+ // Return either data OR error, never both (matches real fetch client behavior)
159
+ if (preset?.error !== undefined) {
160
+ return {
161
+ error: preset.error,
162
+ response: finalResponse,
163
+ };
164
+ }
165
+
166
+ return {
167
+ data: preset?.data,
168
+ response: finalResponse,
169
+ };
170
+ }
171
+
172
+ GET(path: string, options?: unknown) {
173
+ return this.dispatch(path, "GET", options);
174
+ }
175
+ POST(path: string, options?: unknown) {
176
+ return this.dispatch(path, "POST", options);
177
+ }
178
+ PUT(path: string, options?: unknown) {
179
+ return this.dispatch(path, "PUT", options);
180
+ }
181
+ PATCH(path: string, options?: unknown) {
182
+ return this.dispatch(path, "PATCH", options);
183
+ }
184
+ DELETE(path: string, options?: unknown) {
185
+ return this.dispatch(path, "DELETE", options);
186
+ }
187
+ HEAD(path: string, options?: unknown) {
188
+ return this.dispatch(path, "HEAD", options);
189
+ }
190
+ OPTIONS(path: string, options?: unknown) {
191
+ return this.dispatch(path, "OPTIONS", options);
192
+ }
193
+ }
@@ -0,0 +1,24 @@
1
+ import { describe } from "vitest";
2
+
3
+ function containsNodeModules(url: string): boolean {
4
+ return url.includes("/node_modules/") || url.includes("\\node_modules\\");
5
+ }
6
+
7
+ export function runTrustapSuite(
8
+ moduleUrl: string,
9
+ suiteName: string,
10
+ callback: () => void,
11
+ ): void {
12
+ const shouldRun = !containsNodeModules(moduleUrl);
13
+
14
+ if (shouldRun) {
15
+ callback();
16
+ } else {
17
+ describe.skip(
18
+ suiteName,
19
+ () => {
20
+ // skipped when executed from a dependency context
21
+ },
22
+ );
23
+ }
24
+ }
@@ -0,0 +1,82 @@
1
+ /* eslint-disable sonarjs/no-hardcoded-passwords */
2
+ import type { CreateTrustapClientOptions } from "../../client-factory.ts";
3
+ import type { operations } from "../../schema.d.ts";
4
+
5
+ export const mockBasicAuth = {
6
+ username: "test_api_key",
7
+ password: "test_secret",
8
+ } as const;
9
+
10
+ export const mockOAuthToken = "test_oauth_token_abc123";
11
+
12
+ export const sampleChargeQuery: operations["basic.getCharge"]["parameters"]["query"] =
13
+ {
14
+ price: 1234,
15
+ currency: "usd",
16
+ };
17
+
18
+ export const sampleTransactionBody: operations["basic.createTransaction"]["requestBody"]["content"]["application/json"] =
19
+ {
20
+ charge: 250,
21
+ charge_calculator_version: 1,
22
+ charge_seller: 120,
23
+ currency: "usd",
24
+ description: "Test transaction",
25
+ price: 1234,
26
+ role: "buyer",
27
+ };
28
+
29
+ export const sampleCarrierFacilityRequest: operations["basic.getCarrierFacilityOptions"]["requestBody"]["content"]["application/json"] =
30
+ {
31
+ country_code: "us",
32
+ delivery_type: "parcel_locker",
33
+ search_text: "Austin",
34
+ };
35
+
36
+ export function createTestOptions(
37
+ overrides: Partial<CreateTrustapClientOptions> = {},
38
+ ): CreateTrustapClientOptions {
39
+ const hasBasicOverride = Object.prototype.hasOwnProperty.call(
40
+ overrides,
41
+ "basicAuth",
42
+ );
43
+ const hasTokenOverride = Object.prototype.hasOwnProperty.call(
44
+ overrides,
45
+ "getAccessToken",
46
+ );
47
+
48
+ return {
49
+ apiUrl: overrides.apiUrl ?? "https://test.trustap.com",
50
+ basicAuth: hasBasicOverride ? overrides.basicAuth : mockBasicAuth,
51
+ getAccessToken: hasTokenOverride
52
+ ? overrides.getAccessToken
53
+ : () => Promise.resolve(mockOAuthToken),
54
+ authOverrides: overrides.authOverrides,
55
+ basePath: overrides.basePath,
56
+ };
57
+ }
58
+
59
+ export function encodeBasicAuth(username: string, password: string): string {
60
+ const credentials = `${username}:${password}`;
61
+
62
+ // Use btoa if available (browser, Deno), otherwise use Buffer (Node)
63
+ if (typeof btoa !== "undefined") {
64
+ return btoa(credentials);
65
+ }
66
+
67
+ const bufferGlobal = (
68
+ globalThis as {
69
+ Buffer?: {
70
+ from: (input: string) => {
71
+ toString: (encoding: "base64") => string;
72
+ };
73
+ };
74
+ }
75
+ ).Buffer;
76
+
77
+ if (bufferGlobal) {
78
+ return bufferGlobal.from(credentials).toString("base64");
79
+ }
80
+
81
+ throw new Error("No base64 encoder available in this environment");
82
+ }
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createTrustapClient } from "../index.ts";
3
+ import type { TrustapClient } from "../index.ts";
4
+ import { runTrustapSuite } from "./helpers/run-guard.ts";
5
+
6
+ const TEST_USERNAME = "test_key";
7
+ const TEST_PASSWORD = Buffer.from("test_secret").toString("base64");
8
+ const TEST_ALT_PASSWORD = Buffer.from("secret").toString("base64");
9
+ const TEST_URL = "https://test.trustap.com";
10
+ const TEST_TOKEN = "test_token";
11
+
12
+ // Helper functions for type validation tests
13
+ function makeValidCall(client: TrustapClient) {
14
+ return client["basic.getCharge"]({
15
+ params: {
16
+ query: {
17
+ price: 1000,
18
+ currency: "usd",
19
+ },
20
+ },
21
+ });
22
+ }
23
+
24
+ function makeLegacyCall(client: TrustapClient) {
25
+ return client["basic.getCharge"]({
26
+ query: {
27
+ price: 2000,
28
+ currency: "eur",
29
+ },
30
+ });
31
+ }
32
+
33
+ function makeLegacyCallCompat(client: TrustapClient) {
34
+ return client["basic.getCharge"]({
35
+ query: { price: 1000, currency: "usd" },
36
+ });
37
+ }
38
+
39
+ function makeModernCallCompat(client: TrustapClient) {
40
+ return client["basic.getCharge"]({
41
+ params: {
42
+ query: { price: 1000, currency: "usd" },
43
+ },
44
+ });
45
+ }
46
+
47
+ runTrustapSuite(import.meta.url, "Node Client Integration", () => {
48
+ describe("Node Client Integration", () => {
49
+ let client: TrustapClient;
50
+
51
+ beforeEach(() => {
52
+ client = createTrustapClient({
53
+ apiUrl: TEST_URL,
54
+ basicAuth: {
55
+ username: TEST_USERNAME,
56
+ password: TEST_PASSWORD,
57
+ },
58
+ });
59
+ });
60
+
61
+ describe("Client Creation", () => {
62
+ it("should create a client with basic auth", () => {
63
+ expect(client).toBeDefined();
64
+ expect(typeof client["basic.getCharge"]).toBe("function");
65
+ });
66
+
67
+ it("should create a client with OAuth", () => {
68
+ const oauthClient = createTrustapClient({
69
+ apiUrl: TEST_URL,
70
+ getAccessToken: () => Promise.resolve(TEST_TOKEN),
71
+ });
72
+
73
+ expect(oauthClient).toBeDefined();
74
+ });
75
+
76
+ it("should create a client with auth overrides", () => {
77
+ const clientWithOverrides = createTrustapClient({
78
+ apiUrl: TEST_URL,
79
+ basicAuth: {
80
+ username: TEST_USERNAME,
81
+ password: TEST_PASSWORD,
82
+ },
83
+ authOverrides: {
84
+ "/custom": "basic",
85
+ },
86
+ });
87
+
88
+ expect(clientWithOverrides).toBeDefined();
89
+ });
90
+
91
+ it("should expose raw HTTP client", () => {
92
+ const rawClient = (client as { raw: unknown }).raw;
93
+ expect(rawClient).toBeDefined();
94
+ expect(typeof (rawClient as { GET: unknown }).GET).toBe("function");
95
+ });
96
+ });
97
+
98
+ describe("Operation Methods", () => {
99
+ it("should have operation methods from operationIdToPath", () => {
100
+ expect(typeof client["basic.getCharge"]).toBe("function");
101
+ expect(typeof client["basic.createTransaction"]).toBe("function");
102
+ expect(typeof client["oauth.getUser"]).toBe("function");
103
+ expect(typeof client["oauth.updateUser"]).toBe("function");
104
+ });
105
+
106
+ it("should return undefined for non-existent operations", () => {
107
+ const nonExistent = (client as Record<string, unknown>)[
108
+ "nonexistent.op"
109
+ ];
110
+ expect(nonExistent).toBeUndefined();
111
+ });
112
+ });
113
+
114
+ describe("TypeScript Types", () => {
115
+ it("should enforce correct parameter types at compile time", () => {
116
+ // This test verifies type safety through compilation
117
+ // If these type assertions pass TypeScript checking, types are correct
118
+
119
+ // Valid call with correct types
120
+ expect(makeValidCall).toBeDefined();
121
+ expect(() => makeValidCall(client)).not.toThrow();
122
+
123
+ // Legacy format should also be valid
124
+ expect(makeLegacyCall).toBeDefined();
125
+ expect(() => makeLegacyCall(client)).not.toThrow();
126
+ });
127
+
128
+ it("should return properly typed responses", () => {
129
+ // Mock the response at runtime since we don't have a real server
130
+ type ResponseType = Awaited<
131
+ ReturnType<(typeof client)["basic.getCharge"]>
132
+ >;
133
+ const mockResponse: ResponseType = {
134
+ data: {
135
+ charge: 250,
136
+ charge_calculator_version: 1,
137
+ charge_seller: 120,
138
+ currency: "usd",
139
+ price: 1234,
140
+ },
141
+ error: undefined,
142
+ response: Response.json({ id: "charge_123" }),
143
+ };
144
+
145
+ const response = mockResponse;
146
+
147
+ expect(response).toBeDefined();
148
+ expect(response.data).toBeDefined();
149
+ expect(response.response).toBeInstanceOf(Response);
150
+ });
151
+ });
152
+
153
+ describe("OpenAPI-Fetch Integration", () => {
154
+ it("should use openapi-fetch for HTTP requests", () => {
155
+ // The raw client should be an openapi-fetch client
156
+ const rawClient = (client as { raw: { GET: unknown } }).raw;
157
+ expect(typeof rawClient.GET).toBe("function");
158
+ });
159
+
160
+ it("should wrap client as path-based client", () => {
161
+ // Path-based access should work (from wrapAsPathBasedClient)
162
+ expect(client).toBeDefined();
163
+ });
164
+ });
165
+
166
+ describe("Middleware Application", () => {
167
+ it("should apply auth middleware for basic auth", () => {
168
+ const clientWithBasic = createTrustapClient({
169
+ apiUrl: TEST_URL,
170
+ basicAuth: {
171
+ username: "key",
172
+ password: TEST_ALT_PASSWORD,
173
+ },
174
+ });
175
+
176
+ // Middleware should be applied (verifiable through raw client)
177
+ const raw = (clientWithBasic as { raw: { use: unknown } }).raw;
178
+ expect(typeof raw.use).toBe("function");
179
+ });
180
+
181
+ it("should apply auth middleware for OAuth", () => {
182
+ const clientWithOAuth = createTrustapClient({
183
+ apiUrl: TEST_URL,
184
+ getAccessToken: () => Promise.resolve(TEST_TOKEN),
185
+ });
186
+
187
+ const raw = (clientWithOAuth as { raw: { use: unknown } }).raw;
188
+ expect(typeof raw.use).toBe("function");
189
+ });
190
+
191
+ it("should not apply middleware when no auth configured", () => {
192
+ const clientNoAuth = createTrustapClient({
193
+ apiUrl: TEST_URL,
194
+ });
195
+
196
+ expect(clientNoAuth).toBeDefined();
197
+ });
198
+ });
199
+
200
+ describe("Error Handling", () => {
201
+ it("should handle invalid API URLs gracefully", () => {
202
+ expect(() => {
203
+ createTrustapClient({
204
+ apiUrl: "",
205
+ basicAuth: { username: "test" },
206
+ });
207
+ }).not.toThrow();
208
+ });
209
+
210
+ it("should handle missing credentials gracefully", () => {
211
+ expect(() => {
212
+ createTrustapClient({
213
+ apiUrl: "https://test.trustap.com",
214
+ });
215
+ }).not.toThrow();
216
+ });
217
+ });
218
+
219
+ describe("Backward Compatibility", () => {
220
+ it("should support legacy query parameter format", () => {
221
+ expect(makeLegacyCallCompat).toBeDefined();
222
+ expect(() => makeLegacyCallCompat(client)).not.toThrow();
223
+ });
224
+
225
+ it("should support new params format", () => {
226
+ expect(makeModernCallCompat).toBeDefined();
227
+ expect(() => makeModernCallCompat(client)).not.toThrow();
228
+ });
229
+
230
+ it("should handle both formats interchangeably", () => {
231
+ // Both should be valid at runtime
232
+ const legacy = client["basic.getCharge"]({
233
+ query: { price: 1000, currency: "usd" },
234
+ });
235
+ const modern = client["basic.getCharge"]({
236
+ params: { query: { price: 1000, currency: "usd" } },
237
+ });
238
+
239
+ expect(legacy).toBeDefined();
240
+ expect(modern).toBeDefined();
241
+ });
242
+ });
243
+ });
244
+ });