|
1 | 1 | import * as core from '@actions/core' |
2 | | -import { |
3 | | - DevcontainerContent, |
4 | | - validateExtensions, |
5 | | - validateTasks, |
6 | | - validateFeatures, |
7 | | - run |
8 | | -} from '../src/main' |
9 | | - |
10 | | -// Mock fs module with constants and promises |
| 2 | +import * as fs from 'fs' |
| 3 | +import { run } from '../src/main' |
| 4 | + |
| 5 | +jest.mock('@actions/core') |
11 | 6 | jest.mock('fs', () => ({ |
12 | | - existsSync: jest.fn(), |
13 | | - readFileSync: jest.fn(), |
14 | 7 | promises: { |
15 | | - access: jest.fn().mockResolvedValue(undefined), |
16 | | - readFile: jest.fn().mockResolvedValue( |
17 | | - JSON.stringify({ |
18 | | - customizations: { |
19 | | - vscode: { |
20 | | - extensions: ['ext1', 'ext2'] |
21 | | - } |
22 | | - }, |
23 | | - tasks: { |
24 | | - build: 'npm run build', |
25 | | - test: 'npm test', |
26 | | - run: 'npm start' |
27 | | - }, |
28 | | - features: { |
29 | | - 'ghcr.io/devcontainers/features/github-cli:1': {}, |
30 | | - 'ghcr.io/devcontainers-contrib/features/prettier:1': {} |
31 | | - } |
32 | | - }) |
33 | | - ), |
34 | | - appendFile: jest.fn(), |
35 | | - writeFile: jest.fn() |
| 8 | + access: jest.fn(), |
| 9 | + readFile: jest.fn() |
36 | 10 | }, |
37 | 11 | constants: { |
38 | | - O_RDONLY: 0, |
39 | | - O_WRONLY: 1, |
40 | | - O_RDWR: 2, |
41 | | - S_IFMT: 0o170000, |
42 | | - S_IFREG: 0o100000, |
43 | | - S_IFDIR: 0o040000, |
44 | | - S_IFCHR: 0o020000, |
45 | | - S_IFBLK: 0o060000, |
46 | | - S_IFIFO: 0o010000, |
47 | | - S_IFLNK: 0o120000, |
48 | | - S_IFSOCK: 0o140000, |
49 | | - F_OK: 0, |
50 | | - R_OK: 4, |
51 | | - W_OK: 2, |
52 | | - X_OK: 1 |
| 12 | + O_RDONLY: 0 |
53 | 13 | } |
54 | 14 | })) |
55 | 15 |
|
56 | | -const getInputMock = jest.spyOn(core, 'getInput').mockImplementation() |
57 | | -const setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation() |
58 | | -const infoMock = jest.spyOn(core, 'info').mockImplementation() |
59 | | - |
60 | | -const mockDevcontainer: DevcontainerContent = { |
61 | | - customizations: { |
62 | | - vscode: { |
63 | | - extensions: ['ext1', 'ext2'] |
64 | | - } |
65 | | - }, |
66 | | - tasks: { |
67 | | - build: 'npm run build', |
68 | | - test: 'npm test', |
69 | | - run: 'npm start' |
70 | | - }, |
71 | | - features: { |
72 | | - 'ghcr.io/devcontainers/features/github-cli:1': {}, |
73 | | - 'ghcr.io/devcontainers-contrib/features/prettier:1': {} |
74 | | - } |
75 | | -} |
76 | | - |
77 | | -describe('validate-devcontainer', () => { |
| 16 | +describe('run', () => { |
78 | 17 | beforeEach(() => { |
79 | 18 | jest.clearAllMocks() |
80 | 19 | }) |
81 | 20 |
|
82 | | - test('validates extensions', () => { |
83 | | - const requiredExtensions = ['ext1', 'ext3'] |
84 | | - const result = validateExtensions(mockDevcontainer, requiredExtensions) |
85 | | - expect(result).toBeDefined() |
86 | | - }) |
87 | | - |
88 | | - test('validates tasks', () => { |
89 | | - const result = validateTasks(mockDevcontainer) |
90 | | - expect(result).toBeDefined() |
91 | | - }) |
92 | | - |
93 | | - test('validates features', () => { |
94 | | - const requiredFeatures = [ |
95 | | - 'ghcr.io/devcontainers/features/github-cli:1', |
96 | | - 'ghcr.io/devcontainers-contrib/features/prettier:1', |
97 | | - 'ghcr.io/devcontainers/features/docker-in-docker:1' |
98 | | - ] |
99 | | - const result = validateFeatures(mockDevcontainer, requiredFeatures) |
100 | | - expect(result).toBeDefined() |
101 | | - }) |
| 21 | + test('should validate successfully with valid devcontainer.json', async () => { |
| 22 | + const mockContent = { |
| 23 | + customizations: { |
| 24 | + vscode: { |
| 25 | + extensions: ['required-ext'] |
| 26 | + } |
| 27 | + }, |
| 28 | + tasks: { |
| 29 | + build: 'npm run build', |
| 30 | + test: 'npm test', |
| 31 | + run: 'npm start' |
| 32 | + } |
| 33 | + } |
102 | 34 |
|
103 | | - test('runs validation', async () => { |
104 | | - getInputMock.mockImplementation(name => { |
| 35 | + // Mock inputs and file operations |
| 36 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
105 | 37 | switch (name) { |
106 | 38 | case 'required-extensions': |
107 | | - return 'ext1,ext2' |
108 | | - case 'devcontainer-path': |
109 | | - return 'path/to/devcontainer.json' |
| 39 | + return 'required-ext' |
110 | 40 | case 'validate-tasks': |
111 | 41 | return 'true' |
112 | | - case 'required-features': |
113 | | - return 'ghcr.io/devcontainers/features/github-cli:1,ghcr.io/devcontainers-contrib/features/prettier:1' |
114 | 42 | default: |
115 | 43 | return '' |
116 | 44 | } |
117 | 45 | }) |
| 46 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 47 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 48 | + JSON.stringify(mockContent) |
| 49 | + ) |
118 | 50 |
|
119 | 51 | await run() |
120 | | - expect(setFailedMock).not.toHaveBeenCalled() |
121 | | - expect(infoMock).toHaveBeenCalledWith('All validations passed successfully') |
| 52 | + expect(core.setFailed).not.toHaveBeenCalled() |
122 | 53 | }) |
123 | 54 |
|
124 | | - test('does not call validateFeatures when required-features is an empty string', async () => { |
125 | | - getInputMock.mockImplementation(name => { |
126 | | - switch (name) { |
127 | | - case 'required-extensions': |
128 | | - return 'ext1,ext2' |
129 | | - case 'devcontainer-path': |
130 | | - return 'path/to/devcontainer.json' |
131 | | - case 'validate-tasks': |
132 | | - return 'true' |
133 | | - case 'required-features': |
134 | | - return '' |
135 | | - default: |
136 | | - return '' |
| 55 | + test('should throw error when devcontainer.json is not found', async () => { |
| 56 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
| 57 | + if (name === 'required-extensions') return 'ext1' |
| 58 | + return '' |
| 59 | + }) |
| 60 | + ;(fs.promises.access as jest.Mock).mockRejectedValue( |
| 61 | + new Error('File not found') |
| 62 | + ) |
| 63 | + |
| 64 | + await run() |
| 65 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 66 | + expect.stringContaining('devcontainer.json not found') |
| 67 | + ) |
| 68 | + }) |
| 69 | + |
| 70 | + test('should throw error when JSON is invalid', async () => { |
| 71 | + jest.spyOn(core, 'getInput').mockReturnValue('ext1') |
| 72 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 73 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue('invalid json') |
| 74 | + |
| 75 | + await run() |
| 76 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 77 | + expect.stringContaining('Invalid JSON') |
| 78 | + ) |
| 79 | + }) |
| 80 | + |
| 81 | + test('should throw error when devcontainer structure is invalid', async () => { |
| 82 | + const invalidContent = { |
| 83 | + customizations: { |
| 84 | + vscode: { |
| 85 | + extensions: 'not-an-array' |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + jest.spyOn(core, 'getInput').mockReturnValue('ext1') |
| 91 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 92 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 93 | + JSON.stringify(invalidContent) |
| 94 | + ) |
| 95 | + |
| 96 | + await run() |
| 97 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 98 | + 'Invalid devcontainer.json structure' |
| 99 | + ) |
| 100 | + }) |
| 101 | + |
| 102 | + test('should throw error when required extensions are missing', async () => { |
| 103 | + const content = { |
| 104 | + customizations: { |
| 105 | + vscode: { |
| 106 | + extensions: ['ext1'] |
| 107 | + } |
137 | 108 | } |
| 109 | + } |
| 110 | + |
| 111 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
| 112 | + if (name === 'required-extensions') return 'ext1,ext2' |
| 113 | + return '' |
138 | 114 | }) |
| 115 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 116 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 117 | + JSON.stringify(content) |
| 118 | + ) |
139 | 119 |
|
140 | 120 | await run() |
141 | | - expect(setFailedMock).not.toHaveBeenCalled() |
142 | | - expect(infoMock).toHaveBeenCalledWith('All validations passed successfully') |
| 121 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 122 | + expect.stringContaining('Missing required extensions') |
| 123 | + ) |
143 | 124 | }) |
144 | 125 |
|
145 | | - test('does not call validateFeatures when required-features is not present', async () => { |
146 | | - getInputMock.mockImplementation(name => { |
147 | | - switch (name) { |
148 | | - case 'required-extensions': |
149 | | - return 'ext1,ext2' |
150 | | - case 'devcontainer-path': |
151 | | - return 'path/to/devcontainer.json' |
152 | | - case 'validate-tasks': |
153 | | - return 'true' |
154 | | - default: |
155 | | - return '' |
| 126 | + test('should validate features when required-features input is provided', async () => { |
| 127 | + const content = { |
| 128 | + customizations: { |
| 129 | + vscode: { |
| 130 | + extensions: ['ext1'] |
| 131 | + } |
| 132 | + }, |
| 133 | + features: { |
| 134 | + feature1: {} |
156 | 135 | } |
| 136 | + } |
| 137 | + |
| 138 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
| 139 | + if (name === 'required-extensions') return 'ext1' |
| 140 | + if (name === 'required-features') return 'feature1,feature2' |
| 141 | + return '' |
157 | 142 | }) |
| 143 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 144 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 145 | + JSON.stringify(content) |
| 146 | + ) |
158 | 147 |
|
159 | 148 | await run() |
160 | | - expect(setFailedMock).not.toHaveBeenCalled() |
161 | | - expect(infoMock).toHaveBeenCalledWith('All validations passed successfully') |
| 149 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 150 | + expect.stringContaining('Missing required features') |
| 151 | + ) |
| 152 | + }) |
| 153 | + |
| 154 | + test('should handle task validation when enabled', async () => { |
| 155 | + const content = { |
| 156 | + customizations: { |
| 157 | + vscode: { |
| 158 | + extensions: ['ext1'] |
| 159 | + } |
| 160 | + }, |
| 161 | + tasks: { |
| 162 | + build: 'npm build' |
| 163 | + // missing test and run tasks |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
| 168 | + if (name === 'required-extensions') return 'ext1' |
| 169 | + if (name === 'validate-tasks') return 'true' |
| 170 | + return '' |
| 171 | + }) |
| 172 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 173 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 174 | + JSON.stringify(content) |
| 175 | + ) |
| 176 | + |
| 177 | + await run() |
| 178 | + expect(core.setFailed).toHaveBeenCalledWith( |
| 179 | + expect.stringContaining('Missing or invalid required tasks') |
| 180 | + ) |
| 181 | + }) |
| 182 | + |
| 183 | + test('should skip feature validation when required-features input is empty', async () => { |
| 184 | + const content = { |
| 185 | + customizations: { |
| 186 | + vscode: { |
| 187 | + extensions: ['ext1'] |
| 188 | + } |
| 189 | + } |
| 190 | + } |
| 191 | + |
| 192 | + jest.spyOn(core, 'getInput').mockImplementation(name => { |
| 193 | + if (name === 'required-extensions') return 'ext1' |
| 194 | + if (name === 'required-features') return '' |
| 195 | + return '' |
| 196 | + }) |
| 197 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 198 | + ;(fs.promises.readFile as jest.Mock).mockResolvedValue( |
| 199 | + JSON.stringify(content) |
| 200 | + ) |
| 201 | + |
| 202 | + await run() |
| 203 | + expect(core.setFailed).not.toHaveBeenCalled() |
| 204 | + }) |
| 205 | + |
| 206 | + test('should handle unknown errors correctly', async () => { |
| 207 | + // Create an object that matches Error interface but isn't an Error instance |
| 208 | + const errorLike = { |
| 209 | + name: 'CustomError', |
| 210 | + message: 'Custom error message', |
| 211 | + toString(): string { |
| 212 | + return this.message |
| 213 | + } |
| 214 | + } as Error |
| 215 | + |
| 216 | + jest.spyOn(core, 'getInput').mockImplementation(() => { |
| 217 | + throw errorLike |
| 218 | + }) |
| 219 | + |
| 220 | + await run() |
| 221 | + expect(core.setFailed).toHaveBeenCalledWith('An unknown error occurred') |
| 222 | + }) |
| 223 | + |
| 224 | + test('should handle string errors correctly', async () => { |
| 225 | + jest.spyOn(core, 'getInput').mockReturnValue('ext1') |
| 226 | + ;(fs.promises.access as jest.Mock).mockResolvedValue(undefined) |
| 227 | + ;(fs.promises.readFile as jest.Mock).mockRejectedValue( |
| 228 | + 'String error message' |
| 229 | + ) |
| 230 | + |
| 231 | + await run() |
| 232 | + |
| 233 | + expect(core.setFailed).toHaveBeenCalledWith('String error message') |
162 | 234 | }) |
163 | 235 | }) |
0 commit comments