@larkiny/astro-github-loader 0.11.3 → 0.12.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.
- package/README.md +28 -55
- package/dist/github.assets.d.ts +70 -0
- package/dist/github.assets.js +253 -0
- package/dist/github.auth.js +13 -9
- package/dist/github.cleanup.d.ts +3 -2
- package/dist/github.cleanup.js +30 -23
- package/dist/github.constants.d.ts +0 -16
- package/dist/github.constants.js +0 -16
- package/dist/github.content.d.ts +5 -131
- package/dist/github.content.js +152 -794
- package/dist/github.dryrun.d.ts +9 -5
- package/dist/github.dryrun.js +46 -25
- package/dist/github.link-transform.d.ts +2 -2
- package/dist/github.link-transform.js +65 -57
- package/dist/github.loader.js +30 -46
- package/dist/github.logger.d.ts +2 -2
- package/dist/github.logger.js +33 -24
- package/dist/github.paths.d.ts +76 -0
- package/dist/github.paths.js +190 -0
- package/dist/github.storage.d.ts +15 -0
- package/dist/github.storage.js +109 -0
- package/dist/github.types.d.ts +34 -4
- package/dist/index.d.ts +8 -6
- package/dist/index.js +3 -6
- package/dist/test-helpers.d.ts +130 -0
- package/dist/test-helpers.js +194 -0
- package/package.json +3 -1
- package/src/github.assets.spec.ts +717 -0
- package/src/github.assets.ts +365 -0
- package/src/github.auth.spec.ts +245 -0
- package/src/github.auth.ts +24 -10
- package/src/github.cleanup.spec.ts +380 -0
- package/src/github.cleanup.ts +91 -47
- package/src/github.constants.ts +0 -17
- package/src/github.content.spec.ts +305 -454
- package/src/github.content.ts +259 -957
- package/src/github.dryrun.spec.ts +586 -0
- package/src/github.dryrun.ts +105 -54
- package/src/github.link-transform.spec.ts +1345 -0
- package/src/github.link-transform.ts +174 -95
- package/src/github.loader.spec.ts +75 -50
- package/src/github.loader.ts +101 -76
- package/src/github.logger.spec.ts +795 -0
- package/src/github.logger.ts +77 -35
- package/src/github.paths.spec.ts +523 -0
- package/src/github.paths.ts +259 -0
- package/src/github.storage.spec.ts +367 -0
- package/src/github.storage.ts +127 -0
- package/src/github.types.ts +48 -9
- package/src/index.ts +43 -6
- package/src/test-helpers.ts +215 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { beforeEach, describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createConfigId,
|
|
4
|
+
loadImportState,
|
|
5
|
+
updateImportState,
|
|
6
|
+
getLatestCommitInfo,
|
|
7
|
+
} from "./github.dryrun.js";
|
|
8
|
+
import type { ImportOptions, StateFile } from "./github.dryrun.js";
|
|
9
|
+
import { createMockOctokit, MOCK_COMMIT } from "./test-helpers.js";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
// Mock the filesystem modules
|
|
14
|
+
vi.mock("node:fs/promises");
|
|
15
|
+
vi.mock("node:fs");
|
|
16
|
+
|
|
17
|
+
describe("github.dryrun", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("createConfigId", () => {
|
|
27
|
+
it("should generate a stable string from config (owner/repo@ref)", () => {
|
|
28
|
+
const config: ImportOptions = {
|
|
29
|
+
name: "Test Repo",
|
|
30
|
+
owner: "test-owner",
|
|
31
|
+
repo: "test-repo",
|
|
32
|
+
ref: "main",
|
|
33
|
+
includes: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const id = createConfigId(config);
|
|
37
|
+
|
|
38
|
+
expect(id).toBe("test-owner/test-repo@main");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should default to main branch when ref is not specified", () => {
|
|
42
|
+
const config: ImportOptions = {
|
|
43
|
+
name: "Test Repo",
|
|
44
|
+
owner: "algorand",
|
|
45
|
+
repo: "docs",
|
|
46
|
+
includes: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const id = createConfigId(config);
|
|
50
|
+
|
|
51
|
+
expect(id).toBe("algorand/docs@main");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should handle different refs correctly", () => {
|
|
55
|
+
const config: ImportOptions = {
|
|
56
|
+
name: "Test Repo",
|
|
57
|
+
owner: "user",
|
|
58
|
+
repo: "project",
|
|
59
|
+
ref: "develop",
|
|
60
|
+
includes: [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const id = createConfigId(config);
|
|
64
|
+
|
|
65
|
+
expect(id).toBe("user/project@develop");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("loadImportState", () => {
|
|
70
|
+
it("should return default state when file doesn't exist", async () => {
|
|
71
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
72
|
+
|
|
73
|
+
const state = await loadImportState("/test-dir");
|
|
74
|
+
|
|
75
|
+
expect(state).toEqual({
|
|
76
|
+
imports: {},
|
|
77
|
+
lastChecked: expect.any(String),
|
|
78
|
+
});
|
|
79
|
+
expect(vi.mocked(fs.readFile)).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should return parsed content when file has valid JSON", async () => {
|
|
83
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
84
|
+
|
|
85
|
+
const validState: StateFile = {
|
|
86
|
+
imports: {
|
|
87
|
+
"owner/repo@main": {
|
|
88
|
+
name: "Test Repo",
|
|
89
|
+
repoId: "owner/repo@main",
|
|
90
|
+
lastCommitSha: "abc123",
|
|
91
|
+
lastImported: "2024-01-01T00:00:00Z",
|
|
92
|
+
ref: "main",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
lastChecked: "2024-01-01T00:00:00Z",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(validState));
|
|
99
|
+
|
|
100
|
+
const state = await loadImportState("/test-dir");
|
|
101
|
+
|
|
102
|
+
expect(state).toEqual(validState);
|
|
103
|
+
expect(vi.mocked(fs.readFile)).toHaveBeenCalledWith(
|
|
104
|
+
"/test-dir/.github-import-state.json",
|
|
105
|
+
"utf-8",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should return default state when file has malformed JSON", async () => {
|
|
110
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
111
|
+
vi.mocked(fs.readFile).mockResolvedValue("{ invalid json }");
|
|
112
|
+
|
|
113
|
+
const mockLogger = {
|
|
114
|
+
warn: vi.fn(),
|
|
115
|
+
info: vi.fn(),
|
|
116
|
+
error: vi.fn(),
|
|
117
|
+
debug: vi.fn(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const state = await loadImportState("/test-dir", mockLogger as any);
|
|
121
|
+
|
|
122
|
+
expect(state).toEqual({
|
|
123
|
+
imports: {},
|
|
124
|
+
lastChecked: expect.any(String),
|
|
125
|
+
});
|
|
126
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
127
|
+
expect.stringContaining("Failed to load import state"),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should return default state when file has valid JSON but wrong shape (no imports key)", async () => {
|
|
132
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
133
|
+
|
|
134
|
+
// Valid JSON but missing the imports key
|
|
135
|
+
const invalidShape = {
|
|
136
|
+
lastChecked: "2024-01-01T00:00:00Z",
|
|
137
|
+
// missing imports key
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidShape));
|
|
141
|
+
|
|
142
|
+
const mockLogger = {
|
|
143
|
+
warn: vi.fn(),
|
|
144
|
+
info: vi.fn(),
|
|
145
|
+
error: vi.fn(),
|
|
146
|
+
debug: vi.fn(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const state = await loadImportState("/test-dir", mockLogger as any);
|
|
150
|
+
|
|
151
|
+
expect(state).toEqual({
|
|
152
|
+
imports: {},
|
|
153
|
+
lastChecked: expect.any(String),
|
|
154
|
+
});
|
|
155
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
156
|
+
expect.stringContaining("Malformed state file"),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should return default state when parsed value is null", async () => {
|
|
161
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
162
|
+
vi.mocked(fs.readFile).mockResolvedValue("null");
|
|
163
|
+
|
|
164
|
+
const mockLogger = {
|
|
165
|
+
warn: vi.fn(),
|
|
166
|
+
info: vi.fn(),
|
|
167
|
+
error: vi.fn(),
|
|
168
|
+
debug: vi.fn(),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const state = await loadImportState("/test-dir", mockLogger as any);
|
|
172
|
+
|
|
173
|
+
expect(state).toEqual({
|
|
174
|
+
imports: {},
|
|
175
|
+
lastChecked: expect.any(String),
|
|
176
|
+
});
|
|
177
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
178
|
+
expect.stringContaining("Malformed state file"),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should return default state when imports is not an object", async () => {
|
|
183
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
184
|
+
|
|
185
|
+
const invalidImports = {
|
|
186
|
+
imports: "not an object",
|
|
187
|
+
lastChecked: "2024-01-01T00:00:00Z",
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(invalidImports));
|
|
191
|
+
|
|
192
|
+
const mockLogger = {
|
|
193
|
+
warn: vi.fn(),
|
|
194
|
+
info: vi.fn(),
|
|
195
|
+
error: vi.fn(),
|
|
196
|
+
debug: vi.fn(),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const state = await loadImportState("/test-dir", mockLogger as any);
|
|
200
|
+
|
|
201
|
+
expect(state).toEqual({
|
|
202
|
+
imports: {},
|
|
203
|
+
lastChecked: expect.any(String),
|
|
204
|
+
});
|
|
205
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(
|
|
206
|
+
expect.stringContaining("Malformed state file"),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should use console.warn when logger is not provided", async () => {
|
|
211
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
212
|
+
vi.mocked(fs.readFile).mockResolvedValue("{ invalid json }");
|
|
213
|
+
|
|
214
|
+
const consoleWarnSpy = vi
|
|
215
|
+
.spyOn(console, "warn")
|
|
216
|
+
.mockImplementation(() => {});
|
|
217
|
+
|
|
218
|
+
await loadImportState("/test-dir");
|
|
219
|
+
|
|
220
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
221
|
+
expect.stringContaining("Failed to load import state"),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
consoleWarnSpy.mockRestore();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("updateImportState", () => {
|
|
229
|
+
it("should write state file with updated config", async () => {
|
|
230
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
231
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
232
|
+
|
|
233
|
+
const config: ImportOptions = {
|
|
234
|
+
name: "Test Repo",
|
|
235
|
+
owner: "test-owner",
|
|
236
|
+
repo: "test-repo",
|
|
237
|
+
ref: "main",
|
|
238
|
+
includes: [],
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
await updateImportState("/test-dir", config, "abc123");
|
|
242
|
+
|
|
243
|
+
expect(vi.mocked(fs.writeFile)).toHaveBeenCalledWith(
|
|
244
|
+
"/test-dir/.github-import-state.json",
|
|
245
|
+
expect.stringContaining("test-owner/test-repo@main"),
|
|
246
|
+
"utf-8",
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Verify the written content structure
|
|
250
|
+
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
|
251
|
+
const parsedContent = JSON.parse(writtenContent);
|
|
252
|
+
|
|
253
|
+
expect(parsedContent.imports["test-owner/test-repo@main"]).toEqual({
|
|
254
|
+
name: "Test Repo",
|
|
255
|
+
repoId: "test-owner/test-repo@main",
|
|
256
|
+
lastCommitSha: "abc123",
|
|
257
|
+
lastImported: expect.any(String),
|
|
258
|
+
ref: "main",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("should merge with existing state when updating", async () => {
|
|
263
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
264
|
+
|
|
265
|
+
const existingState: StateFile = {
|
|
266
|
+
imports: {
|
|
267
|
+
"other/repo@main": {
|
|
268
|
+
name: "Other Repo",
|
|
269
|
+
repoId: "other/repo@main",
|
|
270
|
+
lastCommitSha: "xyz789",
|
|
271
|
+
lastImported: "2024-01-01T00:00:00Z",
|
|
272
|
+
ref: "main",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
lastChecked: "2024-01-01T00:00:00Z",
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingState));
|
|
279
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
280
|
+
|
|
281
|
+
const config: ImportOptions = {
|
|
282
|
+
name: "New Repo",
|
|
283
|
+
owner: "new-owner",
|
|
284
|
+
repo: "new-repo",
|
|
285
|
+
ref: "develop",
|
|
286
|
+
includes: [],
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
await updateImportState("/test-dir", config, "newsha456");
|
|
290
|
+
|
|
291
|
+
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
|
292
|
+
const parsedContent = JSON.parse(writtenContent);
|
|
293
|
+
|
|
294
|
+
// Should have both the existing and new entries
|
|
295
|
+
expect(parsedContent.imports["other/repo@main"]).toBeDefined();
|
|
296
|
+
expect(parsedContent.imports["new-owner/new-repo@develop"]).toEqual({
|
|
297
|
+
name: "New Repo",
|
|
298
|
+
repoId: "new-owner/new-repo@develop",
|
|
299
|
+
lastCommitSha: "newsha456",
|
|
300
|
+
lastImported: expect.any(String),
|
|
301
|
+
ref: "develop",
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("should handle undefined commitSha", async () => {
|
|
306
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
307
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
308
|
+
|
|
309
|
+
const config: ImportOptions = {
|
|
310
|
+
name: "Test Repo",
|
|
311
|
+
owner: "test-owner",
|
|
312
|
+
repo: "test-repo",
|
|
313
|
+
ref: "main",
|
|
314
|
+
includes: [],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
await updateImportState("/test-dir", config);
|
|
318
|
+
|
|
319
|
+
const writtenContent = vi.mocked(fs.writeFile).mock.calls[0][1] as string;
|
|
320
|
+
const parsedContent = JSON.parse(writtenContent);
|
|
321
|
+
|
|
322
|
+
expect(
|
|
323
|
+
parsedContent.imports["test-owner/test-repo@main"].lastCommitSha,
|
|
324
|
+
).toBeUndefined();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should use console.warn when logger is not provided and write fails", async () => {
|
|
328
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
329
|
+
vi.mocked(fs.writeFile).mockRejectedValue(
|
|
330
|
+
new Error("Permission denied"),
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const consoleWarnSpy = vi
|
|
334
|
+
.spyOn(console, "warn")
|
|
335
|
+
.mockImplementation(() => {});
|
|
336
|
+
|
|
337
|
+
const config: ImportOptions = {
|
|
338
|
+
name: "Test Repo",
|
|
339
|
+
owner: "test-owner",
|
|
340
|
+
repo: "test-repo",
|
|
341
|
+
ref: "main",
|
|
342
|
+
includes: [],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
await updateImportState("/test-dir", config, "abc123");
|
|
346
|
+
|
|
347
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
348
|
+
expect.stringContaining("Failed to save import state"),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
consoleWarnSpy.mockRestore();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("getLatestCommitInfo", () => {
|
|
356
|
+
it("should fetch latest commit with mocked Octokit", async () => {
|
|
357
|
+
const { octokit, spies } = createMockOctokit();
|
|
358
|
+
|
|
359
|
+
const config: ImportOptions = {
|
|
360
|
+
name: "Test Repo",
|
|
361
|
+
owner: "test-owner",
|
|
362
|
+
repo: "test-repo",
|
|
363
|
+
ref: "main",
|
|
364
|
+
includes: [],
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const commitInfo = await getLatestCommitInfo(octokit, config);
|
|
368
|
+
|
|
369
|
+
expect(commitInfo).toEqual({
|
|
370
|
+
sha: MOCK_COMMIT.sha,
|
|
371
|
+
message: "Test commit",
|
|
372
|
+
date: "2024-01-01T00:00:00Z",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(spies.listCommitsSpy).toHaveBeenCalledWith({
|
|
376
|
+
owner: "test-owner",
|
|
377
|
+
repo: "test-repo",
|
|
378
|
+
sha: "main",
|
|
379
|
+
per_page: 1,
|
|
380
|
+
request: { signal: undefined },
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should return null when no commits are found", async () => {
|
|
385
|
+
const { octokit, spies } = createMockOctokit();
|
|
386
|
+
|
|
387
|
+
// Mock empty commits array
|
|
388
|
+
spies.listCommitsSpy.mockResolvedValue({
|
|
389
|
+
data: [],
|
|
390
|
+
status: 200,
|
|
391
|
+
url: "",
|
|
392
|
+
headers: {},
|
|
393
|
+
} as any);
|
|
394
|
+
|
|
395
|
+
const config: ImportOptions = {
|
|
396
|
+
name: "Test Repo",
|
|
397
|
+
owner: "test-owner",
|
|
398
|
+
repo: "test-repo",
|
|
399
|
+
ref: "main",
|
|
400
|
+
includes: [],
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const commitInfo = await getLatestCommitInfo(octokit, config);
|
|
404
|
+
|
|
405
|
+
expect(commitInfo).toBeNull();
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("should throw error when repository is not found (404)", async () => {
|
|
409
|
+
const { octokit, spies } = createMockOctokit();
|
|
410
|
+
|
|
411
|
+
// Mock 404 error
|
|
412
|
+
const error = new Error("Not found");
|
|
413
|
+
(error as any).status = 404;
|
|
414
|
+
spies.listCommitsSpy.mockRejectedValue(error);
|
|
415
|
+
|
|
416
|
+
const config: ImportOptions = {
|
|
417
|
+
name: "Test Repo",
|
|
418
|
+
owner: "test-owner",
|
|
419
|
+
repo: "nonexistent-repo",
|
|
420
|
+
ref: "main",
|
|
421
|
+
includes: [],
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
await expect(getLatestCommitInfo(octokit, config)).rejects.toThrow(
|
|
425
|
+
"Repository not found: test-owner/nonexistent-repo",
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it("should rethrow other API errors", async () => {
|
|
430
|
+
const { octokit, spies } = createMockOctokit();
|
|
431
|
+
|
|
432
|
+
// Mock generic error
|
|
433
|
+
const error = new Error("API rate limit exceeded");
|
|
434
|
+
(error as any).status = 429;
|
|
435
|
+
spies.listCommitsSpy.mockRejectedValue(error);
|
|
436
|
+
|
|
437
|
+
const config: ImportOptions = {
|
|
438
|
+
name: "Test Repo",
|
|
439
|
+
owner: "test-owner",
|
|
440
|
+
repo: "test-repo",
|
|
441
|
+
ref: "main",
|
|
442
|
+
includes: [],
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
await expect(getLatestCommitInfo(octokit, config)).rejects.toThrow(
|
|
446
|
+
"API rate limit exceeded",
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("should use default ref when not specified", async () => {
|
|
451
|
+
const { octokit, spies } = createMockOctokit();
|
|
452
|
+
|
|
453
|
+
const config: ImportOptions = {
|
|
454
|
+
name: "Test Repo",
|
|
455
|
+
owner: "test-owner",
|
|
456
|
+
repo: "test-repo",
|
|
457
|
+
includes: [],
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
await getLatestCommitInfo(octokit, config);
|
|
461
|
+
|
|
462
|
+
expect(spies.listCommitsSpy).toHaveBeenCalledWith(
|
|
463
|
+
expect.objectContaining({
|
|
464
|
+
sha: "main",
|
|
465
|
+
}),
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("should extract only first line of commit message", async () => {
|
|
470
|
+
const { octokit, spies } = createMockOctokit({
|
|
471
|
+
commitData: {
|
|
472
|
+
...MOCK_COMMIT,
|
|
473
|
+
commit: {
|
|
474
|
+
...MOCK_COMMIT.commit,
|
|
475
|
+
message: "First line of commit\n\nAdditional details\nMore details",
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const config: ImportOptions = {
|
|
481
|
+
name: "Test Repo",
|
|
482
|
+
owner: "test-owner",
|
|
483
|
+
repo: "test-repo",
|
|
484
|
+
ref: "main",
|
|
485
|
+
includes: [],
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const commitInfo = await getLatestCommitInfo(octokit, config);
|
|
489
|
+
|
|
490
|
+
expect(commitInfo?.message).toBe("First line of commit");
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("should handle AbortSignal", async () => {
|
|
494
|
+
const { octokit, spies } = createMockOctokit();
|
|
495
|
+
const abortController = new AbortController();
|
|
496
|
+
|
|
497
|
+
const config: ImportOptions = {
|
|
498
|
+
name: "Test Repo",
|
|
499
|
+
owner: "test-owner",
|
|
500
|
+
repo: "test-repo",
|
|
501
|
+
ref: "main",
|
|
502
|
+
includes: [],
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const abortError = new Error("Aborted");
|
|
506
|
+
(abortError as any).name = "AbortError";
|
|
507
|
+
spies.listCommitsSpy.mockRejectedValue(abortError);
|
|
508
|
+
|
|
509
|
+
abortController.abort();
|
|
510
|
+
|
|
511
|
+
await expect(
|
|
512
|
+
getLatestCommitInfo(octokit, config, abortController.signal),
|
|
513
|
+
).rejects.toThrow("Aborted");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("should use committer date if available, otherwise author date", async () => {
|
|
517
|
+
const { octokit } = createMockOctokit({
|
|
518
|
+
commitData: {
|
|
519
|
+
...MOCK_COMMIT,
|
|
520
|
+
commit: {
|
|
521
|
+
...MOCK_COMMIT.commit,
|
|
522
|
+
committer: {
|
|
523
|
+
name: "Committer",
|
|
524
|
+
email: "committer@example.com",
|
|
525
|
+
date: "2024-02-01T00:00:00Z",
|
|
526
|
+
},
|
|
527
|
+
author: {
|
|
528
|
+
name: "Author",
|
|
529
|
+
email: "author@example.com",
|
|
530
|
+
date: "2024-01-01T00:00:00Z",
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const config: ImportOptions = {
|
|
537
|
+
name: "Test Repo",
|
|
538
|
+
owner: "test-owner",
|
|
539
|
+
repo: "test-repo",
|
|
540
|
+
ref: "main",
|
|
541
|
+
includes: [],
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const commitInfo = await getLatestCommitInfo(octokit, config);
|
|
545
|
+
|
|
546
|
+
// Should use committer date
|
|
547
|
+
expect(commitInfo?.date).toBe("2024-02-01T00:00:00Z");
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("should fallback to current date when no dates available", async () => {
|
|
551
|
+
const { octokit } = createMockOctokit({
|
|
552
|
+
commitData: {
|
|
553
|
+
...MOCK_COMMIT,
|
|
554
|
+
commit: {
|
|
555
|
+
...MOCK_COMMIT.commit,
|
|
556
|
+
committer: {
|
|
557
|
+
name: "Committer",
|
|
558
|
+
email: "committer@example.com",
|
|
559
|
+
date: undefined as any,
|
|
560
|
+
},
|
|
561
|
+
author: {
|
|
562
|
+
name: "Author",
|
|
563
|
+
email: "author@example.com",
|
|
564
|
+
date: undefined as any,
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
const config: ImportOptions = {
|
|
571
|
+
name: "Test Repo",
|
|
572
|
+
owner: "test-owner",
|
|
573
|
+
repo: "test-repo",
|
|
574
|
+
ref: "main",
|
|
575
|
+
includes: [],
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
const commitInfo = await getLatestCommitInfo(octokit, config);
|
|
579
|
+
|
|
580
|
+
// Should use current date (ISO string format)
|
|
581
|
+
expect(commitInfo?.date).toMatch(
|
|
582
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
583
|
+
);
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|