@missioncontrolio/client 0.0.3
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.
- package/.env.local.example +9 -0
- package/README.md +226 -0
- package/package.json +25 -0
- package/src/client/autoCancelOrderHandler.ts +135 -0
- package/src/client/clientBuilder.ts +139 -0
- package/src/client/generated/client.ts +130 -0
- package/src/client/generated/kiota-lock.json +34 -0
- package/src/client/generated/models/index.ts +1723 -0
- package/src/client/generated/order/cancelOrder/index.ts +53 -0
- package/src/client/generated/order/claimReservation/index.ts +34 -0
- package/src/client/generated/order/claimReservation/item/index.ts +53 -0
- package/src/client/generated/order/createOrder/index.ts +53 -0
- package/src/client/generated/order/index.ts +112 -0
- package/src/client/generated/order/reportOrder/index.ts +53 -0
- package/src/client/generated/partner/index.ts +55 -0
- package/src/client/generated/priceList/index.ts +55 -0
- package/src/client/generated/product/hasStock/index.ts +53 -0
- package/src/client/generated/product/index.ts +119 -0
- package/src/client/generated/publisher/index.ts +55 -0
- package/src/client/generated/region/index.ts +63 -0
- package/src/client/generated/reservation/index.ts +71 -0
- package/src/client/generated/reservation/item/cancel/index.ts +48 -0
- package/src/client/generated/reservation/item/index.ts +62 -0
- package/src/client/missionControlEnvironment.ts +32 -0
- package/src/client/missionControlHandlers.ts +61 -0
- package/src/client/missionControlHeaders.ts +10 -0
- package/src/client/nonIdempotentPaths.ts +9 -0
- package/src/client/partnerIdHandler.ts +33 -0
- package/tests/autoCancelOrderHandler.test.ts +263 -0
- package/tests/clientBuilder.test.ts +154 -0
- package/tests/integration/fixture.ts +50 -0
- package/tests/integration/partner.test.ts +32 -0
- package/tests/integration/priceList.test.ts +28 -0
- package/tests/integration/product.test.ts +30 -0
- package/tests/integration/publisher.test.ts +30 -0
- package/tests/integration/region.test.ts +36 -0
- package/tests/integration/setup.ts +22 -0
- package/tests/missionControlEnvironment.test.ts +66 -0
- package/tests/missionControlHeaders.test.ts +20 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import type { RequestOption } from "@microsoft/kiota-abstractions";
|
|
3
|
+
import type { Middleware } from "@microsoft/kiota-http-fetchlibrary";
|
|
4
|
+
import { AutoCancelOrderHandler } from "../src/client/autoCancelOrderHandler";
|
|
5
|
+
import { MissionControlHeaders } from "../src/client/missionControlHeaders";
|
|
6
|
+
|
|
7
|
+
const TEST_REQUEST_ID = "aaaabbbb-cccc-dddd-eeee-ffffffffffff";
|
|
8
|
+
const BASE = "https://api.test.com";
|
|
9
|
+
|
|
10
|
+
// -- Recording middleware (equivalent to C# RecordingHandler) ----------------
|
|
11
|
+
|
|
12
|
+
interface CapturedRequest {
|
|
13
|
+
url: string;
|
|
14
|
+
init: RequestInit;
|
|
15
|
+
body: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class RecordingMiddleware implements Middleware {
|
|
19
|
+
next: Middleware | undefined;
|
|
20
|
+
sentRequests: CapturedRequest[] = [];
|
|
21
|
+
private queue: Array<Response | Error> = [];
|
|
22
|
+
|
|
23
|
+
enqueueResponse(status: number, body?: string): void {
|
|
24
|
+
this.queue.push(new Response(body ?? null, { status }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enqueueError(error: Error): void {
|
|
28
|
+
this.queue.push(error);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async execute(
|
|
32
|
+
url: string,
|
|
33
|
+
requestInit: RequestInit,
|
|
34
|
+
_requestOptions?: Record<string, RequestOption>,
|
|
35
|
+
): Promise<Response> {
|
|
36
|
+
const body = requestInit.body != null ? String(requestInit.body) : null;
|
|
37
|
+
this.sentRequests.push({ url, init: { ...requestInit }, body });
|
|
38
|
+
|
|
39
|
+
const item = this.queue.shift();
|
|
40
|
+
if (!item) return new Response(null, { status: 200 });
|
|
41
|
+
if (item instanceof Error) throw item;
|
|
42
|
+
return item;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -- Helpers -----------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
function createPipeline(): {
|
|
49
|
+
handler: AutoCancelOrderHandler;
|
|
50
|
+
inner: RecordingMiddleware;
|
|
51
|
+
} {
|
|
52
|
+
const inner = new RecordingMiddleware();
|
|
53
|
+
const handler = new AutoCancelOrderHandler();
|
|
54
|
+
handler.next = inner;
|
|
55
|
+
return { handler, inner };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createOrderBody(requestId: string): string {
|
|
59
|
+
return JSON.stringify({ requestId });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createOrderInit(path: string, requestId: string): [string, RequestInit] {
|
|
63
|
+
return [
|
|
64
|
+
`${BASE}${path}`,
|
|
65
|
+
{
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: new Headers({ "Content-Type": "application/json" }),
|
|
68
|
+
body: createOrderBody(requestId),
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertIsCancelRequest(
|
|
74
|
+
captured: CapturedRequest,
|
|
75
|
+
expectedRequestId: string,
|
|
76
|
+
): void {
|
|
77
|
+
expect(new URL(captured.url).pathname).toContain("/Order/cancelOrder");
|
|
78
|
+
expect(captured.body).not.toBeNull();
|
|
79
|
+
const parsed = JSON.parse(captured.body!);
|
|
80
|
+
expect(parsed.originalRequestId).toBe(expectedRequestId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// -- Tests -------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe("AutoCancelOrderHandler", () => {
|
|
86
|
+
let handler: AutoCancelOrderHandler;
|
|
87
|
+
let inner: RecordingMiddleware;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
({ handler, inner } = createPipeline());
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("successful request does not send cancel request", async () => {
|
|
94
|
+
inner.enqueueResponse(200);
|
|
95
|
+
const [url, init] = createOrderInit("/Order/createOrder", TEST_REQUEST_ID);
|
|
96
|
+
|
|
97
|
+
const response = await handler.execute(url, init);
|
|
98
|
+
|
|
99
|
+
expect(response.status).toBe(200);
|
|
100
|
+
expect(inner.sentRequests).toHaveLength(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it.each([500, 502, 503, 504])(
|
|
104
|
+
"server error %i sends cancel request",
|
|
105
|
+
async (status) => {
|
|
106
|
+
inner.enqueueResponse(status);
|
|
107
|
+
inner.enqueueResponse(200); // cancel response
|
|
108
|
+
const [url, init] = createOrderInit(
|
|
109
|
+
"/Order/createOrder",
|
|
110
|
+
TEST_REQUEST_ID,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const response = await handler.execute(url, init);
|
|
114
|
+
|
|
115
|
+
expect(response.status).toBe(status);
|
|
116
|
+
expect(inner.sentRequests).toHaveLength(2);
|
|
117
|
+
assertIsCancelRequest(inner.sentRequests[1], TEST_REQUEST_ID);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
it("client error does not send cancel request", async () => {
|
|
122
|
+
inner.enqueueResponse(400);
|
|
123
|
+
const [url, init] = createOrderInit("/Order/createOrder", TEST_REQUEST_ID);
|
|
124
|
+
|
|
125
|
+
const response = await handler.execute(url, init);
|
|
126
|
+
|
|
127
|
+
expect(response.status).toBe(400);
|
|
128
|
+
expect(inner.sentRequests).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("connection error sends cancel request and rethrows", async () => {
|
|
132
|
+
inner.enqueueError(new TypeError("fetch failed"));
|
|
133
|
+
inner.enqueueResponse(200); // cancel response
|
|
134
|
+
const [url, init] = createOrderInit("/Order/createOrder", TEST_REQUEST_ID);
|
|
135
|
+
|
|
136
|
+
await expect(handler.execute(url, init)).rejects.toThrow("fetch failed");
|
|
137
|
+
|
|
138
|
+
expect(inner.sentRequests).toHaveLength(2);
|
|
139
|
+
assertIsCancelRequest(inner.sentRequests[1], TEST_REQUEST_ID);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("non-order endpoint does not send cancel request on error", async () => {
|
|
143
|
+
inner.enqueueResponse(500);
|
|
144
|
+
const response = await handler.execute(`${BASE}/Product`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: new Headers({ "Content-Type": "application/json" }),
|
|
147
|
+
body: "{}",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(response.status).toBe(500);
|
|
151
|
+
expect(inner.sentRequests).toHaveLength(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("claimReservation sends cancel request on error", async () => {
|
|
155
|
+
inner.enqueueResponse(500);
|
|
156
|
+
inner.enqueueResponse(200);
|
|
157
|
+
const [url, init] = createOrderInit(
|
|
158
|
+
"/Order/claimReservation/some-id",
|
|
159
|
+
TEST_REQUEST_ID,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await handler.execute(url, init);
|
|
163
|
+
|
|
164
|
+
expect(inner.sentRequests).toHaveLength(2);
|
|
165
|
+
assertIsCancelRequest(inner.sentRequests[1], TEST_REQUEST_ID);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("reportOrder sends cancel request on error", async () => {
|
|
169
|
+
inner.enqueueResponse(500);
|
|
170
|
+
inner.enqueueResponse(200);
|
|
171
|
+
const [url, init] = createOrderInit(
|
|
172
|
+
"/Order/reportOrder",
|
|
173
|
+
TEST_REQUEST_ID,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await handler.execute(url, init);
|
|
177
|
+
|
|
178
|
+
expect(inner.sentRequests).toHaveLength(2);
|
|
179
|
+
assertIsCancelRequest(inner.sentRequests[1], TEST_REQUEST_ID);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("missing requestId does not send cancel request", async () => {
|
|
183
|
+
inner.enqueueResponse(500);
|
|
184
|
+
|
|
185
|
+
const response = await handler.execute(`${BASE}/Order/createOrder`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: new Headers({ "Content-Type": "application/json" }),
|
|
188
|
+
body: JSON.stringify({ productId: "aaaabbbb-cccc-dddd-eeee-ffffffffffff" }),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(response.status).toBe(500);
|
|
192
|
+
expect(inner.sentRequests).toHaveLength(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("cancel request fails — original error still returned", async () => {
|
|
196
|
+
inner.enqueueResponse(500); // original 5xx
|
|
197
|
+
inner.enqueueError(new Error("Cancel failed")); // cancel blows up
|
|
198
|
+
const [url, init] = createOrderInit("/Order/createOrder", TEST_REQUEST_ID);
|
|
199
|
+
|
|
200
|
+
const response = await handler.execute(url, init);
|
|
201
|
+
|
|
202
|
+
expect(response.status).toBe(500);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("copies auth and partner-id headers to cancel request", async () => {
|
|
206
|
+
inner.enqueueResponse(500);
|
|
207
|
+
inner.enqueueResponse(200);
|
|
208
|
+
|
|
209
|
+
const headers = new Headers({
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
Authorization: "Bearer test-token",
|
|
212
|
+
[MissionControlHeaders.PartnerId]: "partner-123",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const response = await handler.execute(`${BASE}/Order/createOrder`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers,
|
|
218
|
+
body: createOrderBody(TEST_REQUEST_ID),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(response.status).toBe(500);
|
|
222
|
+
expect(inner.sentRequests).toHaveLength(2);
|
|
223
|
+
|
|
224
|
+
const cancelInit = inner.sentRequests[1].init;
|
|
225
|
+
const cancelHeaders = new Headers(cancelInit.headers);
|
|
226
|
+
expect(cancelHeaders.get("Authorization")).toBe("Bearer test-token");
|
|
227
|
+
expect(cancelHeaders.get(MissionControlHeaders.PartnerId)).toBe(
|
|
228
|
+
"partner-123",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("cancel URI preserves path prefix", async () => {
|
|
233
|
+
inner.enqueueResponse(500);
|
|
234
|
+
inner.enqueueResponse(200);
|
|
235
|
+
const [url, init] = createOrderInit(
|
|
236
|
+
"/partnerApi/v2/Order/createOrder",
|
|
237
|
+
TEST_REQUEST_ID,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
await handler.execute(url, init);
|
|
241
|
+
|
|
242
|
+
const cancelUrl = new URL(inner.sentRequests[1].url);
|
|
243
|
+
expect(cancelUrl.pathname).toBe("/partnerApi/v2/Order/cancelOrder");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("cancel URI strips query string", async () => {
|
|
247
|
+
inner.enqueueResponse(500);
|
|
248
|
+
inner.enqueueResponse(200);
|
|
249
|
+
|
|
250
|
+
const headers = new Headers({ "Content-Type": "application/json" });
|
|
251
|
+
await handler.execute(
|
|
252
|
+
`${BASE}/Order/createOrder?foo=bar`,
|
|
253
|
+
{
|
|
254
|
+
method: "POST",
|
|
255
|
+
headers,
|
|
256
|
+
body: createOrderBody(TEST_REQUEST_ID),
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const cancelUrl = new URL(inner.sentRequests[1].url);
|
|
261
|
+
expect(cancelUrl.search).toBe("");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { ClientBuilder } from "../src/client/clientBuilder";
|
|
3
|
+
import { MissionControlEnvironment } from "../src/client/missionControlEnvironment";
|
|
4
|
+
|
|
5
|
+
describe("ClientBuilder", () => {
|
|
6
|
+
describe("withDefaultPartnerId", () => {
|
|
7
|
+
it("throws when partnerId is an empty string", () => {
|
|
8
|
+
expect(() => new ClientBuilder().withDefaultPartnerId("")).toThrow(
|
|
9
|
+
"partnerId must not be null or whitespace.",
|
|
10
|
+
);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("throws when partnerId is whitespace only", () => {
|
|
14
|
+
expect(() => new ClientBuilder().withDefaultPartnerId(" ")).toThrow(
|
|
15
|
+
"partnerId must not be null or whitespace.",
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns the builder instance for fluent chaining", () => {
|
|
20
|
+
const builder = new ClientBuilder();
|
|
21
|
+
expect(builder.withDefaultPartnerId("partner-123")).toBe(builder);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("withBaseUrl", () => {
|
|
26
|
+
it("throws when baseUrl is an empty string", () => {
|
|
27
|
+
expect(() => new ClientBuilder().withBaseUrl("")).toThrow(
|
|
28
|
+
"baseUrl must not be null or whitespace.",
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("throws when baseUrl is whitespace only", () => {
|
|
33
|
+
expect(() => new ClientBuilder().withBaseUrl(" ")).toThrow(
|
|
34
|
+
"baseUrl must not be null or whitespace.",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns the builder instance for fluent chaining", () => {
|
|
39
|
+
const builder = new ClientBuilder();
|
|
40
|
+
expect(builder.withBaseUrl("http://localhost:5000")).toBe(builder);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("withScope", () => {
|
|
45
|
+
it("throws when scope is an empty string", () => {
|
|
46
|
+
expect(() => new ClientBuilder().withScope("")).toThrow(
|
|
47
|
+
"scope must not be null or whitespace.",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("throws when scope is whitespace only", () => {
|
|
52
|
+
expect(() => new ClientBuilder().withScope(" ")).toThrow(
|
|
53
|
+
"scope must not be null or whitespace.",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the builder instance for fluent chaining", () => {
|
|
58
|
+
const builder = new ClientBuilder();
|
|
59
|
+
expect(builder.withScope("api://app-id/.default")).toBe(builder);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("forEnvironment", () => {
|
|
64
|
+
it("returns the builder instance for the Sandbox environment", () => {
|
|
65
|
+
const builder = new ClientBuilder();
|
|
66
|
+
expect(builder.forEnvironment(MissionControlEnvironment.Sandbox)).toBe(
|
|
67
|
+
builder,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns the builder instance for the Production environment", () => {
|
|
72
|
+
const builder = new ClientBuilder();
|
|
73
|
+
expect(builder.forEnvironment(MissionControlEnvironment.Production)).toBe(
|
|
74
|
+
builder,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("can be chained after withAzureAdClientCredentials", () => {
|
|
79
|
+
const builder = new ClientBuilder();
|
|
80
|
+
const result = builder
|
|
81
|
+
.withAzureAdClientCredentials("id", "secret")
|
|
82
|
+
.forEnvironment(MissionControlEnvironment.Sandbox);
|
|
83
|
+
expect(result).toBe(builder);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("withAzureAdClientCredentials", () => {
|
|
88
|
+
it("returns the builder instance for fluent chaining", () => {
|
|
89
|
+
const builder = new ClientBuilder();
|
|
90
|
+
expect(
|
|
91
|
+
builder.withAzureAdClientCredentials("client-id", "client-secret"),
|
|
92
|
+
).toBe(builder);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns the builder instance when a custom tenantId is provided", () => {
|
|
96
|
+
const builder = new ClientBuilder();
|
|
97
|
+
expect(
|
|
98
|
+
builder.withAzureAdClientCredentials(
|
|
99
|
+
"client-id",
|
|
100
|
+
"client-secret",
|
|
101
|
+
"tenant-id",
|
|
102
|
+
),
|
|
103
|
+
).toBe(builder);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("build", () => {
|
|
108
|
+
it("throws when no credentials have been configured", () => {
|
|
109
|
+
expect(() => new ClientBuilder().build()).toThrow(
|
|
110
|
+
"No credentials configured. Call withAzureAdClientCredentials() before calling build().",
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("throws even after forEnvironment is called without credentials", () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
new ClientBuilder()
|
|
117
|
+
.forEnvironment(MissionControlEnvironment.Sandbox)
|
|
118
|
+
.build(),
|
|
119
|
+
).toThrow("No credentials configured.");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns a client when credentials are configured", () => {
|
|
123
|
+
const client = new ClientBuilder()
|
|
124
|
+
.withAzureAdClientCredentials("fake-client-id", "fake-client-secret")
|
|
125
|
+
.build();
|
|
126
|
+
expect(client).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns a client for the Sandbox environment", () => {
|
|
130
|
+
const client = new ClientBuilder()
|
|
131
|
+
.forEnvironment(MissionControlEnvironment.Sandbox)
|
|
132
|
+
.withAzureAdClientCredentials("fake-client-id", "fake-client-secret")
|
|
133
|
+
.build();
|
|
134
|
+
expect(client).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("returns a client when a default partner ID is set", () => {
|
|
138
|
+
const client = new ClientBuilder()
|
|
139
|
+
.withDefaultPartnerId("partner-123")
|
|
140
|
+
.withAzureAdClientCredentials("fake-client-id", "fake-client-secret")
|
|
141
|
+
.build();
|
|
142
|
+
expect(client).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("returns a client when a custom base URL and scope override are set", () => {
|
|
146
|
+
const client = new ClientBuilder()
|
|
147
|
+
.withBaseUrl("http://localhost:5000")
|
|
148
|
+
.withScope("api://local-app-id/.default")
|
|
149
|
+
.withAzureAdClientCredentials("fake-client-id", "fake-client-secret")
|
|
150
|
+
.build();
|
|
151
|
+
expect(client).toBeDefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ClientBuilder } from "../../src/client/clientBuilder";
|
|
2
|
+
import type { Client } from "../../src/client/generated/client";
|
|
3
|
+
|
|
4
|
+
// Credentials are read from process.env, which Vitest populates from .env.local
|
|
5
|
+
// (placed next to vitest.integration.config.ts, never committed).
|
|
6
|
+
// Copy .env.local.example to .env.local and fill in your values to run locally.
|
|
7
|
+
// In CI, set the environment variables directly.
|
|
8
|
+
const REQUIRED_KEYS = [
|
|
9
|
+
"MC_TENANT_ID",
|
|
10
|
+
"MC_CLIENT_ID",
|
|
11
|
+
"MC_CLIENT_SECRET",
|
|
12
|
+
"MC_PARTNER_ID",
|
|
13
|
+
"MC_BASE_URL",
|
|
14
|
+
"MC_SCOPE",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
const missingKeys = REQUIRED_KEYS.filter((key) => !process.env[key]);
|
|
18
|
+
/**
|
|
19
|
+
* When non-empty, one or more required credentials are absent.
|
|
20
|
+
* Tests use this to skip gracefully rather than failing.
|
|
21
|
+
*
|
|
22
|
+
* Copy .env.local.example to .env.local and fill in your values to run integration tests locally.
|
|
23
|
+
* In CI, set the environment variables directly.
|
|
24
|
+
*/
|
|
25
|
+
export const missingCredentials: string =
|
|
26
|
+
missingKeys.length > 0
|
|
27
|
+
? `Missing required credentials: ${missingKeys.join(", ")}. ` +
|
|
28
|
+
"Copy .env.local.example to .env.local and fill in your values."
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
export const productId: string = process.env["MC_PRODUCT_ID"] ?? "";
|
|
32
|
+
export const regionId: string = process.env["MC_REGION_ID"] ?? "";
|
|
33
|
+
export const publisherId: string = process.env["MC_PUBLISHER_ID"] ?? "";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates a fully configured integration-test client from the loaded credentials.
|
|
37
|
+
* Only call this inside a test that is guarded by {@link missingCredentials}.
|
|
38
|
+
*/
|
|
39
|
+
export function createClient(): Client {
|
|
40
|
+
return new ClientBuilder()
|
|
41
|
+
.withAzureAdClientCredentials(
|
|
42
|
+
process.env["MC_CLIENT_ID"]!,
|
|
43
|
+
process.env["MC_CLIENT_SECRET"]!,
|
|
44
|
+
process.env["MC_TENANT_ID"],
|
|
45
|
+
)
|
|
46
|
+
.withDefaultPartnerId(process.env["MC_PARTNER_ID"]!)
|
|
47
|
+
.withBaseUrl(process.env["MC_BASE_URL"]!)
|
|
48
|
+
.withScope(process.env["MC_SCOPE"]!)
|
|
49
|
+
.build();
|
|
50
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient, missingCredentials } from "./fixture";
|
|
3
|
+
|
|
4
|
+
describe.skipIf(!!missingCredentials)("Partner API (integration)", () => {
|
|
5
|
+
it("get returns a result", async () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
|
|
8
|
+
const result = await client.partner.get();
|
|
9
|
+
|
|
10
|
+
expect(result).toBeDefined();
|
|
11
|
+
expect(result?.result).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("get returns an array of partners", async () => {
|
|
15
|
+
const client = createClient();
|
|
16
|
+
|
|
17
|
+
const result = await client.partner.get();
|
|
18
|
+
|
|
19
|
+
expect(Array.isArray(result?.result)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("get includes pagination metadata", async () => {
|
|
23
|
+
const client = createClient();
|
|
24
|
+
|
|
25
|
+
const result = await client.partner.get();
|
|
26
|
+
|
|
27
|
+
expect(result?.totalCount).toBeDefined();
|
|
28
|
+
expect(typeof result?.totalCount).toBe("number");
|
|
29
|
+
expect(result?.pageCount).toBeDefined();
|
|
30
|
+
expect(typeof result?.pageCount).toBe("number");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient, missingCredentials } from "./fixture";
|
|
3
|
+
|
|
4
|
+
describe.skipIf(!!missingCredentials)("PriceList API (integration)", () => {
|
|
5
|
+
it("get returns a price list for the first available product", async () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
|
|
8
|
+
// Discover the first available product so no extra env var is needed.
|
|
9
|
+
const products = await client.product.get();
|
|
10
|
+
const product = products?.result?.[0];
|
|
11
|
+
|
|
12
|
+
if (!product) {
|
|
13
|
+
console.log(
|
|
14
|
+
"No products found in the configured environment — skipping.",
|
|
15
|
+
);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await client.priceList.get({
|
|
20
|
+
queryParameters: {
|
|
21
|
+
productId: product.id!,
|
|
22
|
+
publisherId: product.publisherId!,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient, missingCredentials } from "./fixture";
|
|
3
|
+
|
|
4
|
+
describe.skipIf(!!missingCredentials)("Product API (integration)", () => {
|
|
5
|
+
it("get returns a result", async () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
|
|
8
|
+
const result = await client.product.get();
|
|
9
|
+
|
|
10
|
+
expect(result).toBeDefined();
|
|
11
|
+
expect(result?.result).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("get returns an array of products", async () => {
|
|
15
|
+
const client = createClient();
|
|
16
|
+
|
|
17
|
+
const result = await client.product.get();
|
|
18
|
+
|
|
19
|
+
expect(Array.isArray(result?.result)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("get includes pagination metadata", async () => {
|
|
23
|
+
const client = createClient();
|
|
24
|
+
|
|
25
|
+
const result = await client.product.get();
|
|
26
|
+
|
|
27
|
+
expect(typeof result?.totalCount).toBe("number");
|
|
28
|
+
expect(typeof result?.pageCount).toBe("number");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient, missingCredentials } from "./fixture";
|
|
3
|
+
|
|
4
|
+
describe.skipIf(!!missingCredentials)("Publisher API (integration)", () => {
|
|
5
|
+
it("get returns a result", async () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
|
|
8
|
+
const result = await client.publisher.get();
|
|
9
|
+
|
|
10
|
+
expect(result).toBeDefined();
|
|
11
|
+
expect(result?.result).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("get returns an array of publishers", async () => {
|
|
15
|
+
const client = createClient();
|
|
16
|
+
|
|
17
|
+
const result = await client.publisher.get();
|
|
18
|
+
|
|
19
|
+
expect(Array.isArray(result?.result)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("get includes pagination metadata", async () => {
|
|
23
|
+
const client = createClient();
|
|
24
|
+
|
|
25
|
+
const result = await client.publisher.get();
|
|
26
|
+
|
|
27
|
+
expect(typeof result?.totalCount).toBe("number");
|
|
28
|
+
expect(typeof result?.pageCount).toBe("number");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createClient, missingCredentials, publisherId } from "./fixture";
|
|
3
|
+
|
|
4
|
+
describe.skipIf(!!missingCredentials)("Region API (integration)", () => {
|
|
5
|
+
it("get returns a result", async () => {
|
|
6
|
+
const client = createClient();
|
|
7
|
+
|
|
8
|
+
const result = await client.region.get({
|
|
9
|
+
queryParameters: { publisherId },
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
expect(result).toBeDefined();
|
|
13
|
+
expect(result?.result).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("get returns an array of regions", async () => {
|
|
17
|
+
const client = createClient();
|
|
18
|
+
|
|
19
|
+
const result = await client.region.get({
|
|
20
|
+
queryParameters: { publisherId },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(Array.isArray(result?.result)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("get includes pagination metadata", async () => {
|
|
27
|
+
const client = createClient();
|
|
28
|
+
|
|
29
|
+
const result = await client.region.get({
|
|
30
|
+
queryParameters: { publisherId },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(typeof result?.totalCount).toBe("number");
|
|
34
|
+
expect(typeof result?.pageCount).toBe("number");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Load .env.local from the project root (next to vitest.integration.config.ts).
|
|
5
|
+
// This ensures credentials are available via process.env when running integration tests.
|
|
6
|
+
// In CI, set the environment variables directly instead of using this file.
|
|
7
|
+
const envFile = resolve(import.meta.dirname, "../../.env.local");
|
|
8
|
+
if (existsSync(envFile)) {
|
|
9
|
+
process.loadEnvFile(envFile);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Allow self-signed certificates when running against a local development server.
|
|
13
|
+
// This is scoped to the integration test process and must never be used in production code.
|
|
14
|
+
const baseUrl = process.env["MC_BASE_URL"] ?? "";
|
|
15
|
+
if (
|
|
16
|
+
baseUrl.startsWith("http://localhost") ||
|
|
17
|
+
baseUrl.startsWith("https://localhost") ||
|
|
18
|
+
baseUrl.startsWith("http://127.0.0.1") ||
|
|
19
|
+
baseUrl.startsWith("https://127.0.0.1")
|
|
20
|
+
) {
|
|
21
|
+
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0";
|
|
22
|
+
}
|