@openally/github.sdk 1.0.0 → 1.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.
@@ -107,9 +107,15 @@ describe("GithubClient", () => {
107
107
  it("should return ApiEndpoints for repo sub-resources", () => {
108
108
  const client = new GithubClient();
109
109
 
110
- assert.ok(client.repos.owner.myrepo.tags() instanceof ApiEndpoint);
111
- assert.ok(client.repos.owner.myrepo.pulls() instanceof ApiEndpoint);
112
- assert.ok(client.repos.owner.myrepo.workflows() instanceof ApiEndpoint);
110
+ assert.ok(
111
+ client.repos.owner.myrepo.tags() instanceof ApiEndpoint
112
+ );
113
+ assert.ok(
114
+ client.repos.owner.myrepo.pulls() instanceof ApiEndpoint
115
+ );
116
+ assert.ok(
117
+ client.repos.owner.myrepo.workflows() instanceof ApiEndpoint
118
+ );
113
119
  });
114
120
 
115
121
  it("should use the configured token when fetching repo data", async() => {
@@ -126,7 +132,9 @@ describe("GithubClient", () => {
126
132
  headers: { "content-type": "application/json" }
127
133
  });
128
134
 
129
- const result = await client.repos.octocat["hello-world"].tags().all();
135
+ const result = await client.repos.octocat["hello-world"]
136
+ .tags()
137
+ .all();
130
138
 
131
139
  assert.deepEqual(result, [{ name: "v1.0.0" }]);
132
140
  });
@@ -145,22 +153,33 @@ describe("GithubClient", () => {
145
153
  headers: { "content-type": "application/json" }
146
154
  });
147
155
 
148
- await assert.doesNotReject(client.repos.octocat["hello-world"].tags().all());
156
+ await assert.doesNotReject(
157
+ client.repos.octocat["hello-world"].tags().all()
158
+ );
149
159
  });
150
160
 
151
161
  it("should fetch workflows with the envelope extractor", async() => {
152
162
  const client = new GithubClient();
163
+ const workflows = [{ id: 7, name: "CI" }];
153
164
 
154
165
  mockAgent
155
166
  .get(kGithubOrigin)
156
- .intercept({ path: "/repos/octocat/hello-world/actions/workflows", method: "GET" })
157
- .reply(200, JSON.stringify({ total_count: 1, workflows: [{ id: 7, name: "CI" }] }), {
167
+ .intercept({
168
+ path: "/repos/octocat/hello-world/actions/workflows",
169
+ method: "GET"
170
+ })
171
+ .reply(200, JSON.stringify({ total_count: 1, workflows }), {
158
172
  headers: { "content-type": "application/json" }
159
173
  });
160
174
 
161
- const result = await client.repos.octocat["hello-world"].workflows().all();
175
+ const result = await client.repos.octocat["hello-world"]
176
+ .workflows()
177
+ .all();
162
178
 
163
- assert.deepEqual(result, [{ id: 7, name: "CI" }]);
179
+ assert.deepEqual(
180
+ result,
181
+ workflows
182
+ );
164
183
  });
165
184
  });
166
185
  });
@@ -8,7 +8,7 @@ import { createApiProxy } from "../src/class/createApiProxy.ts";
8
8
 
9
9
  describe("createApiProxy", () => {
10
10
  it("should call the factory with the accessed property key", () => {
11
- const proxy = createApiProxy((key: string) => key.toUpperCase());
11
+ const proxy = createApiProxy((key) => key.toUpperCase());
12
12
 
13
13
  assert.equal(proxy.hello, "HELLO");
14
14
  assert.equal(proxy.world, "WORLD");
@@ -16,7 +16,7 @@ describe("createApiProxy", () => {
16
16
 
17
17
  it("should call the factory on each property access", () => {
18
18
  let callCount = 0;
19
- const proxy = createApiProxy((key: string) => {
19
+ const proxy = createApiProxy((key) => {
20
20
  callCount++;
21
21
 
22
22
  return key;
@@ -30,7 +30,7 @@ describe("createApiProxy", () => {
30
30
  });
31
31
 
32
32
  it("should return different values for different keys", () => {
33
- const proxy = createApiProxy((key: string) => {
33
+ const proxy = createApiProxy((key) => {
34
34
  return { name: key };
35
35
  });
36
36
 
@@ -51,7 +51,7 @@ describe("createApiProxy", () => {
51
51
  });
52
52
 
53
53
  it("should return a plain object (no prototype)", () => {
54
- const proxy = createApiProxy((key: string) => key);
54
+ const proxy = createApiProxy((key) => key);
55
55
 
56
56
  assert.equal(Object.getPrototypeOf(proxy), null);
57
57
  });
@@ -0,0 +1,382 @@
1
+ // Import Node.js Dependencies
2
+ import { describe, it, beforeEach, afterEach } from "node:test";
3
+ import assert from "node:assert/strict";
4
+
5
+ // Import Third-party Dependencies
6
+ import { MockAgent, setGlobalDispatcher, getGlobalDispatcher, type Dispatcher } from "undici";
7
+
8
+ // Import Internal Dependencies
9
+ import { fetchRawFile } from "../src/api/rawFile.ts";
10
+ import { GithubClient } from "../src/class/GithubClient.ts";
11
+
12
+ // CONSTANTS
13
+ const kRawGithubOrigin = "https://raw.githubusercontent.com";
14
+
15
+ describe("fetchRawFile()", () => {
16
+ let mockAgent: MockAgent;
17
+ let originalDispatcher: Dispatcher;
18
+
19
+ beforeEach(() => {
20
+ originalDispatcher = getGlobalDispatcher();
21
+ mockAgent = new MockAgent();
22
+ mockAgent.disableNetConnect();
23
+ setGlobalDispatcher(mockAgent);
24
+ });
25
+
26
+ afterEach(async() => {
27
+ await mockAgent.close();
28
+ setGlobalDispatcher(originalDispatcher);
29
+ });
30
+
31
+ describe("raw text (no parser)", () => {
32
+ it("should return the raw file content as a string", async() => {
33
+ mockAgent
34
+ .get(kRawGithubOrigin)
35
+ .intercept({ path: "/octocat/hello-world/HEAD/README.md", method: "GET" })
36
+ .reply(200, "# Hello World\n");
37
+
38
+ const result = await fetchRawFile("octocat/hello-world", "README.md");
39
+
40
+ assert.equal(result, "# Hello World\n");
41
+ });
42
+
43
+ it("should default to ref HEAD", async() => {
44
+ mockAgent
45
+ .get(kRawGithubOrigin)
46
+ .intercept({ path: "/octocat/hello-world/HEAD/README.md", method: "GET" })
47
+ .reply(200, "content");
48
+
49
+ await assert.doesNotReject(
50
+ fetchRawFile("octocat/hello-world", "README.md")
51
+ );
52
+ });
53
+
54
+ it("should use the provided ref", async() => {
55
+ mockAgent
56
+ .get(kRawGithubOrigin)
57
+ .intercept({ path: "/octocat/hello-world/main/README.md", method: "GET" })
58
+ .reply(200, "content on main");
59
+
60
+ const result = await fetchRawFile("octocat/hello-world", "README.md", { ref: "main" });
61
+
62
+ assert.equal(result, "content on main");
63
+ });
64
+
65
+ it("should support file paths with directory segments", async() => {
66
+ mockAgent
67
+ .get(kRawGithubOrigin)
68
+ .intercept({ path: "/octocat/hello-world/HEAD/src/index.ts", method: "GET" })
69
+ .reply(200, "export {};\n");
70
+
71
+ const result = await fetchRawFile("octocat/hello-world", "src/index.ts");
72
+
73
+ assert.equal(result, "export {};\n");
74
+ });
75
+ });
76
+
77
+ describe("parser: \"json\"", () => {
78
+ it("should parse and return the file content as a JSON object", async() => {
79
+ const pkg = { name: "hello-world", version: "1.0.0" };
80
+
81
+ mockAgent
82
+ .get(kRawGithubOrigin)
83
+ .intercept({ path: "/octocat/hello-world/HEAD/package.json", method: "GET" })
84
+ .reply(200, JSON.stringify(pkg));
85
+
86
+ const result = await fetchRawFile<{ name: string; version: string; }>(
87
+ "octocat/hello-world",
88
+ "package.json",
89
+ { parser: "json" }
90
+ );
91
+
92
+ assert.deepEqual(result, pkg);
93
+ });
94
+
95
+ it("should return unknown by default when parser is \"json\"", async() => {
96
+ mockAgent
97
+ .get(kRawGithubOrigin)
98
+ .intercept({ path: "/octocat/hello-world/HEAD/data.json", method: "GET" })
99
+ .reply(200, JSON.stringify([1, 2, 3]));
100
+
101
+ const result = await fetchRawFile("octocat/hello-world", "data.json", { parser: "json" });
102
+
103
+ assert.deepEqual(result, [1, 2, 3]);
104
+ });
105
+ });
106
+
107
+ describe("custom parser function", () => {
108
+ it("should apply a custom parser to the raw content", async() => {
109
+ mockAgent
110
+ .get(kRawGithubOrigin)
111
+ .intercept({ path: "/octocat/hello-world/HEAD/VERSION", method: "GET" })
112
+ .reply(200, "2.3.1\n");
113
+
114
+ const result = await fetchRawFile(
115
+ "octocat/hello-world",
116
+ "VERSION",
117
+ { parser: (content) => content.trim() }
118
+ );
119
+
120
+ assert.equal(result, "2.3.1");
121
+ });
122
+
123
+ it("should pass the raw string content to the parser", async() => {
124
+ const rawContent = "key=value\nfoo=bar\n";
125
+ const captured: string[] = [];
126
+
127
+ mockAgent
128
+ .get(kRawGithubOrigin)
129
+ .intercept({ path: "/octocat/hello-world/HEAD/.env", method: "GET" })
130
+ .reply(200, rawContent);
131
+
132
+ await fetchRawFile(
133
+ "octocat/hello-world",
134
+ ".env",
135
+ {
136
+ parser: (content) => {
137
+ captured.push(content);
138
+
139
+ return content;
140
+ }
141
+ }
142
+ );
143
+
144
+ assert.equal(captured[0], rawContent);
145
+ });
146
+ });
147
+
148
+ describe("headers", () => {
149
+ it("should send the default User-Agent header", async() => {
150
+ mockAgent
151
+ .get(kRawGithubOrigin)
152
+ .intercept({
153
+ path: "/octocat/hello-world/HEAD/README.md",
154
+ method: "GET",
155
+ headers: { "user-agent": "@openally/github.sdk/1.0.0" }
156
+ })
157
+ .reply(200, "content");
158
+
159
+ await assert.doesNotReject(
160
+ fetchRawFile("octocat/hello-world", "README.md")
161
+ );
162
+ });
163
+
164
+ it("should send a custom User-Agent when provided", async() => {
165
+ mockAgent
166
+ .get(kRawGithubOrigin)
167
+ .intercept({
168
+ path: "/octocat/hello-world/HEAD/README.md",
169
+ method: "GET",
170
+ headers: { "user-agent": "my-app/2.0" }
171
+ })
172
+ .reply(200, "content");
173
+
174
+ await assert.doesNotReject(
175
+ fetchRawFile("octocat/hello-world", "README.md", { userAgent: "my-app/2.0" })
176
+ );
177
+ });
178
+
179
+ it("should send the Authorization header when a token is provided", async() => {
180
+ mockAgent
181
+ .get(kRawGithubOrigin)
182
+ .intercept({
183
+ path: "/octocat/hello-world/HEAD/README.md",
184
+ method: "GET",
185
+ headers: { authorization: "token secret123" }
186
+ })
187
+ .reply(200, "content");
188
+
189
+ await assert.doesNotReject(
190
+ fetchRawFile("octocat/hello-world", "README.md", { token: "secret123" })
191
+ );
192
+ });
193
+
194
+ it("should succeed without an Authorization header when no token is provided", async() => {
195
+ // undici's MockAgent will reject the request if the intercepted headers
196
+ // do not match — here we assert no `authorization` key is present by
197
+ // confirming the request succeeds against an interceptor that has no
198
+ // header constraint at all (the positive case with a token is separately tested).
199
+ mockAgent
200
+ .get(kRawGithubOrigin)
201
+ .intercept({ path: "/octocat/hello-world/HEAD/README.md", method: "GET" })
202
+ .reply(200, "content");
203
+
204
+ await assert.doesNotReject(
205
+ fetchRawFile("octocat/hello-world", "README.md")
206
+ );
207
+ });
208
+ });
209
+
210
+ describe("error handling", () => {
211
+ it("should throw on a 404 response", async() => {
212
+ mockAgent
213
+ .get(kRawGithubOrigin)
214
+ .intercept({ path: "/octocat/hello-world/HEAD/missing.txt", method: "GET" })
215
+ .reply(404, "Not Found");
216
+
217
+ await assert.rejects(
218
+ fetchRawFile("octocat/hello-world", "missing.txt"),
219
+ (err: Error) => {
220
+ assert.ok(err.message.includes("404"));
221
+
222
+ return true;
223
+ }
224
+ );
225
+ });
226
+
227
+ it("should throw on a 500 response", async() => {
228
+ mockAgent
229
+ .get(kRawGithubOrigin)
230
+ .intercept({ path: "/octocat/hello-world/HEAD/README.md", method: "GET" })
231
+ .reply(500, "Internal Server Error");
232
+
233
+ await assert.rejects(
234
+ fetchRawFile("octocat/hello-world", "README.md"),
235
+ (err: Error) => {
236
+ assert.ok(err.message.includes("500"));
237
+
238
+ return true;
239
+ }
240
+ );
241
+ });
242
+
243
+ it("should include the repository, ref, and file path in the error message", async() => {
244
+ mockAgent
245
+ .get(kRawGithubOrigin)
246
+ .intercept({ path: "/octocat/hello-world/HEAD/missing.txt", method: "GET" })
247
+ .reply(404, "Not Found");
248
+
249
+ await assert.rejects(
250
+ fetchRawFile("octocat/hello-world", "missing.txt"),
251
+ (err: Error) => {
252
+ assert.ok(err.message.includes("missing.txt"));
253
+ assert.ok(err.message.includes("octocat/hello-world"));
254
+ assert.ok(err.message.includes("HEAD"));
255
+
256
+ return true;
257
+ }
258
+ );
259
+ });
260
+ });
261
+ });
262
+
263
+ describe("GithubClient.fetchRawFile()", () => {
264
+ let mockAgent: MockAgent;
265
+ let originalDispatcher: Dispatcher;
266
+
267
+ beforeEach(() => {
268
+ originalDispatcher = getGlobalDispatcher();
269
+ mockAgent = new MockAgent();
270
+ mockAgent.disableNetConnect();
271
+ setGlobalDispatcher(mockAgent);
272
+ });
273
+
274
+ afterEach(async() => {
275
+ await mockAgent.close();
276
+ setGlobalDispatcher(originalDispatcher);
277
+ });
278
+
279
+ it("should fetch raw content using the client's token", async() => {
280
+ const client = new GithubClient({ token: "clienttoken" });
281
+
282
+ mockAgent
283
+ .get(kRawGithubOrigin)
284
+ .intercept({
285
+ path: "/octocat/hello-world/HEAD/README.md",
286
+ method: "GET",
287
+ headers: { authorization: "token clienttoken" }
288
+ })
289
+ .reply(200, "# Hello");
290
+
291
+ await assert.doesNotReject(
292
+ client.fetchRawFile("octocat/hello-world", "README.md")
293
+ );
294
+ });
295
+
296
+ it("should fetch raw content using the client's userAgent", async() => {
297
+ const client = new GithubClient({ userAgent: "my-client/1.0" });
298
+
299
+ mockAgent
300
+ .get(kRawGithubOrigin)
301
+ .intercept({
302
+ path: "/octocat/hello-world/HEAD/README.md",
303
+ method: "GET",
304
+ headers: { "user-agent": "my-client/1.0" }
305
+ })
306
+ .reply(200, "# Hello");
307
+
308
+ await assert.doesNotReject(
309
+ client.fetchRawFile("octocat/hello-world", "README.md")
310
+ );
311
+ });
312
+
313
+ it("should parse JSON when parser is \"json\"", async() => {
314
+ const client = new GithubClient();
315
+ const pkg = { name: "hello-world", version: "1.0.0" };
316
+
317
+ mockAgent
318
+ .get(kRawGithubOrigin)
319
+ .intercept({ path: "/octocat/hello-world/HEAD/package.json", method: "GET" })
320
+ .reply(200, JSON.stringify(pkg));
321
+
322
+ const result = await client.fetchRawFile<{ name: string; version: string; }>(
323
+ "octocat/hello-world",
324
+ "package.json",
325
+ { parser: "json" }
326
+ );
327
+
328
+ assert.deepEqual(result, pkg);
329
+ });
330
+
331
+ it("should apply a custom parser function", async() => {
332
+ const client = new GithubClient();
333
+
334
+ mockAgent
335
+ .get(kRawGithubOrigin)
336
+ .intercept({ path: "/octocat/hello-world/main/VERSION", method: "GET" })
337
+ .reply(200, "3.0.0\n");
338
+
339
+ const result = await client.fetchRawFile(
340
+ "octocat/hello-world",
341
+ "VERSION",
342
+ { ref: "main", parser: (s) => s.trim() }
343
+ );
344
+
345
+ assert.equal(result, "3.0.0");
346
+ });
347
+
348
+ it("should use the ref option when provided", async() => {
349
+ const client = new GithubClient();
350
+
351
+ mockAgent
352
+ .get(kRawGithubOrigin)
353
+ .intercept({ path: "/octocat/hello-world/v2.0.0/CHANGELOG.md", method: "GET" })
354
+ .reply(200, "## v2.0.0\n");
355
+
356
+ const result = await client.fetchRawFile(
357
+ "octocat/hello-world",
358
+ "CHANGELOG.md",
359
+ { ref: "v2.0.0" }
360
+ );
361
+
362
+ assert.equal(result, "## v2.0.0\n");
363
+ });
364
+
365
+ it("should throw when the file is not found", async() => {
366
+ const client = new GithubClient();
367
+
368
+ mockAgent
369
+ .get(kRawGithubOrigin)
370
+ .intercept({ path: "/octocat/hello-world/HEAD/missing.txt", method: "GET" })
371
+ .reply(404, "Not Found");
372
+
373
+ await assert.rejects(
374
+ client.fetchRawFile("octocat/hello-world", "missing.txt"),
375
+ (err: Error) => {
376
+ assert.ok(err.message.includes("404"));
377
+
378
+ return true;
379
+ }
380
+ );
381
+ });
382
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "rootDir": "..",
5
+ "types": ["node"],
6
+ "lib": ["DOM", "ES2022", "ES2023", "ES2024", "ESNext"]
7
+ },
8
+ "include": [
9
+ "."
10
+ ]
11
+ }