Skip to content

Commit 8699af8

Browse files
Copilotneilime
authored andcommitted
refactor: add reader adapter with complete fs abstraction
1 parent 81eeffa commit 8699af8

File tree

80 files changed

+1754
-1246
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+1754
-1246
lines changed

packages/cicd/github-actions/src/github-actions-generator.adapter.e2e.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import {
99
} from '@ci-dokumentor/core';
1010
import { initTestContainer } from './container.js';
1111
import mockFs from 'mock-fs';
12-
import { existsSync, readFileSync } from 'fs';
12+
import { existsSync, readFileSync } from 'node:fs';
1313
import { GitRepositoryProvider } from '@ci-dokumentor/repository-git';
1414
import { sanitizeSnapshotContent } from '@ci-dokumentor/core/tests';
15-
import { join } from 'path';
15+
import { join } from 'node:path';
1616

1717
const rootPath = join(__dirname, '../../../..');
1818

@@ -406,7 +406,7 @@ runs:
406406
rendererAdapter,
407407
repositoryProvider,
408408
})
409-
).rejects.toThrow('ENOENT, no such file or directory \'/test/action.yml\'');
409+
).rejects.toThrow('Source file does not exist: "/test/action.yml"');
410410

411411
await rendererAdapter.finalize();
412412
});

packages/cicd/github-actions/src/github-actions-generator.adapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export class GitHubActionsGeneratorAdapter implements GeneratorAdapter {
103103
rendererAdapter: RendererAdapter;
104104
repositoryProvider: RepositoryProvider;
105105
}): Promise<void> {
106-
const gitHubActionOrWorkflow = this.gitHubActionsParser.parseFile(
106+
const gitHubActionOrWorkflow = await this.gitHubActionsParser.parseFile(
107107
source,
108108
await repositoryProvider.getRepositoryInfo()
109109
);

packages/cicd/github-actions/src/github-actions-parser.spec.ts

Lines changed: 70 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
1-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2-
import mockFs from 'mock-fs';
1+
import { describe, it, expect, beforeEach, afterEach, Mocked, vi } from 'vitest';
32
import {
43
GitHubActionsParser,
54
GitHubAction,
65
GitHubWorkflow,
76
GitHubActionInput,
87
GitHubActionOutput,
98
} from './github-actions-parser.js';
10-
import { RepositoryInfo } from '@ci-dokumentor/core';
11-
import { RepositoryInfoMockFactory } from '@ci-dokumentor/core/tests';
9+
import { ReaderAdapter, RepositoryInfo } from '@ci-dokumentor/core';
10+
import { ReaderAdapterMockFactory, RepositoryInfoMockFactory } from '@ci-dokumentor/core/tests';
1211

1312
describe('GitHubActionsParser', () => {
14-
let repositoryInfo: RepositoryInfo;
13+
let mockReaderAdapter: Mocked<ReaderAdapter>;
14+
let mockRepositoryInfo: Mocked<RepositoryInfo>;
1515
let parser: GitHubActionsParser;
1616

1717
beforeEach(() => {
18-
repositoryInfo = RepositoryInfoMockFactory.create();
18+
vi.resetAllMocks();
1919

20-
parser = new GitHubActionsParser();
20+
mockReaderAdapter = ReaderAdapterMockFactory.create();
21+
mockRepositoryInfo = RepositoryInfoMockFactory.create();
22+
23+
parser = new GitHubActionsParser(mockReaderAdapter);
2124
});
2225

2326
afterEach(() => {
24-
mockFs.restore();
27+
vi.resetAllMocks();
2528
});
2629

2730
describe('parseFile', () => {
2831
describe('GitHub Actions', () => {
2932
it('should parse an action file', async () => {
3033
// Arrange
31-
mockFs({
32-
'/test': {
33-
'action.yml': `name: Test Action
34+
const filePath = '/test/action.yml';
35+
const fileContent = `name: Test Action
3436
description: A test GitHub Action
3537
author: Test Author
3638
branding:
@@ -62,15 +64,13 @@ outputs:
6264
value: \${{ steps.step-id.outputs.value }}
6365
runs:
6466
using: composite
65-
`,
66-
},
67-
});
67+
`;
68+
69+
mockReaderAdapter.resourceExists.mockReturnValue(true);
70+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
6871

6972
// Act
70-
const result = parser.parseFile(
71-
'/test/action.yml',
72-
repositoryInfo
73-
) as GitHubAction;
73+
const result = await parser.parseFile(filePath, mockRepositoryInfo) as GitHubAction;
7474

7575
// Assert
7676
expect(result).toBeDefined();
@@ -117,29 +117,22 @@ runs:
117117
describe('GitHub Workflows', () => {
118118
it('should parse a complete workflow file', async () => {
119119
// Arrange
120-
mockFs({
121-
'/test': {
122-
'.github': {
123-
workflows: {
124-
'workflow.yml': `name: Test Workflow
120+
const filePath = '/test/.github/workflows/workflow.yml';
121+
const fileContent = `name: Test Workflow
125122
on: push
126123
jobs:
127124
build:
128125
runs-on: ubuntu-latest
129126
steps:
130127
- name: Checkout code
131128
uses: actions/checkout@v2
132-
`,
133-
},
134-
},
135-
},
136-
});
129+
`;
130+
131+
mockReaderAdapter.resourceExists.mockReturnValue(true);
132+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
137133

138134
// Act
139-
const result = parser.parseFile(
140-
'/test/.github/workflows/workflow.yml',
141-
repositoryInfo
142-
) as GitHubWorkflow;
135+
const result = await parser.parseFile(filePath, mockRepositoryInfo) as GitHubWorkflow;
143136

144137
// Assert
145138
expect(result).toBeDefined();
@@ -151,28 +144,21 @@ jobs:
151144

152145
it('should parse a workflow without defined name', async () => {
153146
// Arrange
154-
mockFs({
155-
'/test': {
156-
'.github': {
157-
workflows: {
158-
'workflow-test.yml': `on: push
147+
const filePath = '/test/.github/workflows/workflow-test.yml';
148+
const fileContent = `on: push
159149
jobs:
160150
build:
161151
runs-on: ubuntu-latest
162152
steps:
163153
- name: Checkout code
164154
uses: actions/checkout@v2
165-
`,
166-
},
167-
},
168-
},
169-
});
155+
`;
156+
157+
mockReaderAdapter.resourceExists.mockReturnValue(true);
158+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
170159

171160
// Act
172-
const result = parser.parseFile(
173-
'/test/.github/workflows/workflow-test.yml',
174-
repositoryInfo
175-
) as GitHubWorkflow;
161+
const result = await parser.parseFile(filePath, mockRepositoryInfo) as GitHubWorkflow;
176162

177163
// Assert
178164
expect(result).toBeDefined();
@@ -183,101 +169,69 @@ jobs:
183169
describe('Error handling', () => {
184170
it('should throw error for invalid YAML', async () => {
185171
// Arrange
186-
mockFs({
187-
'/test': {
188-
'.github': {
189-
workflows: {
190-
'invalid.yml': `invalid: yaml: content`,
191-
},
192-
},
193-
},
194-
});
172+
const filePath = '/test/.github/workflows/invalid.yml';
173+
const fileContent = `invalid: yaml: content`;
174+
175+
mockReaderAdapter.resourceExists.mockReturnValue(true);
176+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
195177

196178
// Act & Assert
197-
expect(() =>
198-
parser.parseFile('/test/.github/workflows/invalid.yml', repositoryInfo)
199-
).toThrow('Nested mappings are not allowed in compact mappings at line 1, column 10');
179+
await expect(parser.parseFile(filePath, mockRepositoryInfo)).rejects.toThrow();
200180
});
201181

202182
it('should throw error for empty file', async () => {
203183
// Arrange
204-
mockFs({
205-
'/test': {
206-
'.github': {
207-
workflows: {
208-
'empty.yml': '',
209-
},
210-
},
211-
},
212-
});
184+
const filePath = '/test/.github/workflows/empty.yml';
185+
186+
mockReaderAdapter.resourceExists.mockReturnValue(true);
187+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(''));
213188

214189
// Act & Assert
215-
expect(() =>
216-
parser.parseFile('/test/.github/workflows/empty.yml', repositoryInfo)
217-
).toThrow('Unsupported source file');
190+
await expect(parser.parseFile(filePath, mockRepositoryInfo)).rejects.toThrow();
218191
});
219192

220193
it('should throw error for plain text when parseable as YAML', async () => {
221194
// Arrange
222-
mockFs({
223-
'/test': {
224-
'.github': {
225-
workflows: {
226-
'plain-text.yml': `This is not a YAML file`,
227-
},
228-
},
229-
},
230-
});
195+
const filePath = '/test/.github/workflows/plain-text.yml';
196+
const fileContent = `This is not a YAML file`;
197+
198+
mockReaderAdapter.resourceExists.mockReturnValue(true);
199+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
231200

232201
// Act & Assert
233-
expect(() =>
234-
parser.parseFile('/test/.github/workflows/plain-text.yml', repositoryInfo)
235-
).toThrow(
236-
'Unsupported GitHub Actions file format: /test/.github/workflows/plain-text.yml'
237-
);
202+
await expect(parser.parseFile(filePath, mockRepositoryInfo)).rejects.toThrow();
238203
});
239204

240205
it('should throw error for valid YAML but unsupported structure', async () => {
241206
// Arrange
242-
mockFs({
243-
'/test': {
244-
'.github': {
245-
workflows: {
246-
'object-without-required-fields.yml': `someField: value
207+
const filePath = '/test/.github/workflows/object-without-required-fields.yml';
208+
const fileContent = `someField: value
247209
anotherField: 123
248-
`,
249-
},
250-
},
251-
},
252-
});
210+
`;
211+
212+
mockReaderAdapter.resourceExists.mockReturnValue(true);
213+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
253214

254215
// Act & Assert
255-
expect(() =>
256-
parser.parseFile(
257-
'/test/.github/workflows/object-without-required-fields.yml',
258-
repositoryInfo
259-
)
260-
).toThrow(
261-
'Unsupported GitHub Actions file format: /test/.github/workflows/object-without-required-fields.yml'
262-
);
216+
await expect(parser.parseFile(filePath, mockRepositoryInfo)).rejects.toThrow();
263217
});
264218
});
265219

266220
describe('Type detection', () => {
267221
it('should detect GitHub Action when it has name but no on/jobs properties', async () => {
268222
// Arrange
269-
mockFs({
270-
'/test': {
271-
'action.yml': `name: Simple Action
223+
const filePath = '/test/action.yml';
224+
const fileContent = `name: Simple Action
272225
description: A simple GitHub Action
273226
runs:
274227
using: node20
275-
`,
276-
},
277-
});
228+
`;
229+
230+
mockReaderAdapter.resourceExists.mockReturnValue(true);
231+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
278232

279233
// Act
280-
const result = parser.parseFile('/test/action.yml', repositoryInfo);
234+
const result = await parser.parseFile(filePath, mockRepositoryInfo);
281235

282236
// Assert
283237
expect(result).toBeDefined();
@@ -288,29 +242,22 @@ runs:
288242

289243
it('should detect GitHub Workflow when it has on property', async () => {
290244
// Arrange
291-
mockFs({
292-
'/test': {
293-
'.github': {
294-
workflows: {
295-
'workflow.yml': `name: Test Workflow
245+
const filePath = '/test/.github/workflows/workflow.yml';
246+
const fileContent = `name: Test Workflow
296247
on: push
297248
jobs:
298249
build:
299250
runs-on: ubuntu-latest
300251
steps:
301252
- name: Checkout code
302253
uses: actions/checkout@v2
303-
`,
304-
},
305-
},
306-
},
307-
});
254+
`;
255+
256+
mockReaderAdapter.resourceExists.mockReturnValue(true);
257+
mockReaderAdapter.readResource.mockResolvedValue(Buffer.from(fileContent));
308258

309259
// Act
310-
const result = parser.parseFile(
311-
'/test/.github/workflows/workflow.yml',
312-
repositoryInfo
313-
);
260+
const result = await parser.parseFile(filePath, mockRepositoryInfo);
314261

315262
// Assert
316263
expect(result).toBeDefined();

packages/cicd/github-actions/src/github-actions-parser.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { readFileSync } from 'node:fs';
1+
import { FileReaderAdapter, RepositoryInfo } from '@ci-dokumentor/core';
2+
import type { ReaderAdapter } from '@ci-dokumentor/core';
3+
import { inject, injectable } from 'inversify';
24
import { basename, dirname, extname, join, relative } from 'node:path';
35
import { parse } from 'yaml';
4-
import { RepositoryInfo } from '@ci-dokumentor/core';
56

67
// See https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/github-action.json
78

@@ -101,7 +102,10 @@ export type GitHubWorkflowOutput = {
101102

102103
export type GitHubActionsManifest = GitHubAction | GitHubWorkflow;
103104

105+
@injectable()
104106
export class GitHubActionsParser {
107+
constructor(@inject(FileReaderAdapter) private readonly readerAdapter: ReaderAdapter) { }
108+
105109
isGitHubActionFile(source: string): boolean {
106110
// Check if the source is a GitHub Action by looking for action.yml or action.yaml
107111
return /action\.ya?ml$/i.test(source);
@@ -112,11 +116,16 @@ export class GitHubActionsParser {
112116
return source.includes('.github/workflows/');
113117
}
114118

115-
parseFile(
119+
async parseFile(
116120
source: string,
117121
repositoryInfo: RepositoryInfo
118-
): GitHubActionsManifest {
119-
const parsed = parse(readFileSync(source, 'utf8'));
122+
): Promise<GitHubActionsManifest> {
123+
if (!this.readerAdapter.resourceExists(source)) {
124+
throw new Error(`Source file does not exist: "${source}"`);
125+
}
126+
127+
const content = await this.readerAdapter.readResource(source);
128+
const parsed = parse(content.toString('utf8'));
120129
if (!parsed) {
121130
throw new Error(`Unsupported source file: ${source}`);
122131
}

0 commit comments

Comments
 (0)