Skip to content

Commit ad4484c

Browse files
start on application unit tests
1 parent fe19a1c commit ad4484c

2 files changed

Lines changed: 193 additions & 16 deletions

File tree

components/Application.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,62 @@ import { extract } from 'tar-fs';
1313
import gunzip from 'gunzip-maybe';
1414

1515
interface ApplicationConfig {
16+
// define known config properties
1617
package: string;
1718
install?: {
1819
command?: string;
1920
timeout?: number;
2021
};
22+
// an application config can have other arbitrary properties
23+
[key: string]: unknown;
24+
}
25+
26+
export class InvalidPackageIdentifierError extends TypeError {
27+
constructor(applicationName: string, packageIdentifier: unknown) {
28+
super(`Invalid 'package' property for application ${applicationName}: expected string, got ${typeof packageIdentifier}`)
29+
}
30+
}
31+
32+
export class InvalidInstallPropertyError extends TypeError {
33+
constructor(applicationName: string, installProperty: unknown) {
34+
super(`Invalid 'install' property for application ${applicationName}: expected object, got ${typeof installProperty}`)
35+
}
36+
}
37+
38+
export class InvalidInstallCommandError extends TypeError {
39+
constructor(applicationName: string, command: unknown) {
40+
super(`Invalid 'install.command' property for application ${applicationName}: expected string, got ${typeof command}`)
41+
}
42+
}
43+
44+
export class InvalidInstallTimeoutError extends TypeError {
45+
constructor(applicationName: string, timeout: unknown) {
46+
super(`Invalid 'install.timeout' property for application ${applicationName}: expected non-negative number, got ${typeof timeout}`)
47+
}
2148
}
2249

2350
export function assertApplicationConfig(
2451
applicationName: string,
25-
applicationConfig: object & Record<'package', unknown>
52+
applicationConfig: Record<'package', unknown> & Record<string, unknown>
2653
): asserts applicationConfig is ApplicationConfig {
2754
if (typeof applicationConfig.package !== 'string') {
28-
throw new TypeError(
29-
`Invalid 'package' property for application ${applicationName}: expected string, got ${typeof applicationConfig.package}`
30-
);
55+
throw new InvalidPackageIdentifierError(applicationName, applicationConfig.package);
3156
}
3257

3358
if ('install' in applicationConfig) {
34-
if (typeof applicationConfig.install !== 'object' || applicationConfig.install === null) {
35-
throw new TypeError(
36-
`Invalid 'install' property for application ${applicationName}: expected object, got ${typeof applicationConfig.install}`
37-
);
59+
if (typeof applicationConfig.install !== 'object' || applicationConfig.install === null || Array.isArray(applicationConfig.install)) {
60+
throw new InvalidInstallPropertyError(applicationName, applicationConfig.install);
3861
}
3962

4063
if ('command' in applicationConfig.install && typeof applicationConfig.install.command !== 'string') {
41-
throw new TypeError(
42-
`Invalid 'install.command' property for application ${applicationName}: expected string, got ${typeof applicationConfig.install.command}`
43-
);
64+
throw new InvalidInstallCommandError(applicationName, applicationConfig.install.command);
4465
}
4566

4667
if (
4768
'timeout' in applicationConfig.install &&
4869
(typeof applicationConfig.install.timeout !== 'number' || applicationConfig.install.timeout < 0)
4970
) {
50-
throw new TypeError(
51-
`Invalid 'install.timeout' property for application ${applicationName}: expected non-negativenumber, got ${typeof applicationConfig.install.timeout}`
52-
);
71+
throw new InvalidInstallTimeoutError(applicationName, applicationConfig.install.timeout);
5372
}
5473
}
5574
}
@@ -343,7 +362,7 @@ export class Application {
343362
* during the installation process in order to actually resolve what the user specifies for a
344363
* component matching some of npm's package resolution rules.
345364
*/
346-
function derivePackageIdentifier(packageIdentifier: string) {
365+
export function derivePackageIdentifier(packageIdentifier: string) {
347366
if (packageIdentifier.includes(':')) {
348367
return packageIdentifier;
349368
}
@@ -477,7 +496,6 @@ export function nonInteractiveSpawn(
477496
env.GIT_SSH_COMMAND = gitSSHCommand;
478497
}
479498

480-
// eslint-disable-next-line sonarjs/os-command
481499
const childProcess = spawn(command, args, {
482500
shell: true,
483501
cwd,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { describe, it } from 'mocha';
2+
import {
3+
assertApplicationConfig,
4+
InvalidPackageIdentifierError,
5+
InvalidInstallPropertyError,
6+
InvalidInstallCommandError,
7+
InvalidInstallTimeoutError,
8+
} from '@/components/Application';
9+
import assert from 'node:assert/strict';
10+
11+
describe('Application', () => {
12+
describe('assertApplicationConfig', () => {
13+
const applicationName = 'test-application';
14+
15+
it('should pass for valid minimal config', () => {
16+
assert.doesNotThrow(() => {
17+
assertApplicationConfig(applicationName, { package: 'my-package' });
18+
});
19+
});
20+
21+
it('should pass for valid config with install options', () => {
22+
assert.doesNotThrow(() => {
23+
assertApplicationConfig(applicationName, {
24+
package: 'my-package',
25+
install: {
26+
command: 'npm ci',
27+
timeout: 60000,
28+
},
29+
});
30+
});
31+
});
32+
33+
it('should pass for valid config with partial install options', () => {
34+
assert.doesNotThrow(() => {
35+
assertApplicationConfig(applicationName, {
36+
package: 'my-package',
37+
install: { command: 'npm ci' },
38+
});
39+
});
40+
41+
assert.doesNotThrow(() => {
42+
assertApplicationConfig(applicationName, {
43+
package: 'my-package',
44+
install: { timeout: 60000 },
45+
});
46+
});
47+
48+
assert.doesNotThrow(() => {
49+
assertApplicationConfig(applicationName, {
50+
package: 'my-package',
51+
install: {},
52+
});
53+
});
54+
});
55+
56+
it('should pass for config with additional, arbitrary options', () => {
57+
assert.doesNotThrow(() => {
58+
assertApplicationConfig(applicationName, {
59+
package: 'my-package',
60+
foo: 'bar',
61+
baz: 42,
62+
fuzz: { buzz: true }
63+
});
64+
});
65+
});
66+
67+
it('should fail for invalid package identifiers', () => {
68+
const invalidValues = [null, undefined, 42, {}, [], true, false];
69+
70+
for (const invalidValue of invalidValues) {
71+
assert.throws(
72+
() => {
73+
assertApplicationConfig(applicationName, {
74+
package: invalidValue,
75+
});
76+
},
77+
(error: Error) => {
78+
return (
79+
error instanceof InvalidPackageIdentifierError &&
80+
error.message === `Invalid 'package' property for application ${applicationName}: expected string, got ${typeof invalidValue}`
81+
);
82+
}
83+
);
84+
}
85+
});
86+
87+
it('should fail for invalid install property', () => {
88+
const invalidValues = [null, 42, 'string', [], true, false];
89+
90+
for (const invalidValue of invalidValues) {
91+
assert.throws(
92+
() => {
93+
assertApplicationConfig(applicationName, {
94+
package: 'my-package',
95+
install: invalidValue,
96+
});
97+
},
98+
(error: Error) => {
99+
return (
100+
error instanceof InvalidInstallPropertyError &&
101+
error.message === `Invalid 'install' property for application ${applicationName}: expected object, got ${typeof invalidValue}`
102+
);
103+
}
104+
);
105+
}
106+
});
107+
108+
it('should fail for invalid install.command', () => {
109+
const invalidValues = [null, undefined, 42, {}, [], true, false];
110+
111+
for (const invalidValue of invalidValues) {
112+
assert.throws(
113+
() => {
114+
assertApplicationConfig(applicationName, {
115+
package: 'my-package',
116+
install: { command: invalidValue },
117+
});
118+
},
119+
(error: Error) => {
120+
return (
121+
error instanceof InvalidInstallCommandError &&
122+
error.message === `Invalid 'install.command' property for application ${applicationName}: expected string, got ${typeof invalidValue}`
123+
);
124+
}
125+
);
126+
}
127+
});
128+
129+
it('should fail for invalid install.timeout', () => {
130+
const invalidValues = [null, undefined, 'string', {}, [], true, false, -1, -100];
131+
132+
for (const invalidValue of invalidValues) {
133+
assert.throws(
134+
() => {
135+
assertApplicationConfig(applicationName, {
136+
package: 'my-package',
137+
install: { timeout: invalidValue },
138+
});
139+
},
140+
(error: Error) => {
141+
return (
142+
error instanceof InvalidInstallTimeoutError &&
143+
error.message === `Invalid 'install.timeout' property for application ${applicationName}: expected non-negative number, got ${typeof invalidValue}`
144+
);
145+
}
146+
);
147+
}
148+
});
149+
150+
it('should pass for valid timeout of 0', () => {
151+
assert.doesNotThrow(() => {
152+
assertApplicationConfig(applicationName, {
153+
package: 'my-package',
154+
install: { timeout: 0 },
155+
});
156+
});
157+
});
158+
});
159+
});

0 commit comments

Comments
 (0)