Skip to content

Commit 141db8d

Browse files
committed
test(desktop): validate release arguments against the Electron Builder schema
The 0.2.1 Windows build died at schema validation on an option Electron Builder 26 had removed, and the suite was green because it compared the generated arguments to a hand-written array — the same mistake written twice. That shape of test cannot tell that arguments are invalid, only that they changed. Merge the generated `--config.*` arguments onto the packaged build configuration and validate the result with Ajv against the installed `app-builder-lib/scheme.json`, under the same Ajv settings Electron Builder uses. Both resolve through the existing electron-builder dependency, so this adds nothing to the lockfile and follows whatever schema the installed version ships — including a future major that moves these options again. It catches more than the original defect. Verified by mutation: restoring `--config.win.publisherName` fails three tests with "rejects unknown options: /win.publisherName", and dropping a required `azureSignOptions` field fails with "must have required property 'certificateProfileName'" — a class the previous test could never detect.
1 parent 78462b0 commit 141db8d

1 file changed

Lines changed: 71 additions & 44 deletions

File tree

apps/desktop/tests/package-win.spec.ts

Lines changed: 71 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,72 @@ import {
66
windowsSigningArgs,
77
} from '../scripts/package-win'
88

9-
interface SchemaNode {
10-
readonly $ref?: string
11-
readonly anyOf?: readonly SchemaNode[]
12-
readonly properties?: Readonly<Record<string, SchemaNode>>
13-
}
9+
type JsonObject = Record<string, unknown>
1410

1511
const requireFromTests = createRequire(import.meta.url)
16-
const schema = requireFromTests(
17-
requireFromTests.resolve('app-builder-lib/scheme.json', {
18-
paths: [requireFromTests.resolve('electron-builder')],
19-
}),
20-
) as { readonly definitions: Readonly<Record<string, SchemaNode>> }
21-
22-
function referencedDefinition(node: SchemaNode): SchemaNode | undefined {
23-
const reference = node.$ref ?? node.anyOf?.find(branch => branch.$ref !== undefined)?.$ref
24-
return reference === undefined ? undefined : schema.definitions[reference.replace('#/definitions/', '')]
12+
const electronBuilderPath = requireFromTests.resolve('electron-builder')
13+
const schema: unknown = requireFromTests(
14+
requireFromTests.resolve('app-builder-lib/scheme.json', { paths: [electronBuilderPath] }),
15+
)
16+
const Ajv = requireFromTests(requireFromTests.resolve('ajv', { paths: [electronBuilderPath] })) as {
17+
readonly default: new (options: JsonObject) => {
18+
compile: (schema: unknown) => ((data: unknown) => boolean) & { errors?: readonly { instancePath: string, keyword: string, message?: string, params: JsonObject }[] }
19+
}
2520
}
21+
// The same Ajv settings app-builder-lib validates a release configuration with,
22+
// so a configuration this accepts is one Electron Builder accepts.
23+
const validateConfiguration = new Ajv.default({
24+
allErrors: true,
25+
verbose: true,
26+
coerceTypes: true,
27+
strict: false,
28+
}).compile(schema)
29+
30+
const baseConfiguration = (requireFromTests('../package.json') as { readonly build: JsonObject }).build
2631

2732
/**
28-
* Assert that a `--config.<path>` option exists in the installed Electron Builder schema.
29-
*
30-
* `WindowsConfiguration` sets `additionalProperties: false`, so an option that
31-
* the schema does not declare fails validation before any build work and takes
32-
* the whole `win` object down with it. Comparing against the real schema keeps
33-
* these arguments honest across Electron Builder upgrades, which is what an
34-
* expected-array assertion cannot do.
35-
* @param path - Dotted option path with the `--config.` prefix removed.
33+
* Apply generated `--config.<path> <value>` arguments to the packaged build configuration.
34+
* @param args - Arguments as they reach Electron Builder.
35+
* @returns The configuration Electron Builder would validate.
3636
*/
37-
function assertSchemaOption(path: string): void {
38-
const [root, ...rest] = path.split('.')
39-
expect(root).toBe('win')
40-
let definition = schema.definitions['WindowsConfiguration']!
41-
rest.forEach((segment, index) => {
42-
const property = definition.properties?.[segment]
43-
if (property === undefined) {
44-
throw new Error(`Electron Builder has no option '${rest.slice(0, index + 1).join('.')}' under win`)
37+
function configurationFrom(args: readonly string[]): JsonObject {
38+
const configuration = structuredClone(baseConfiguration)
39+
for (let index = 0; index < args.length; index += 1) {
40+
const argument = args[index]!
41+
if (!argument.startsWith('--config.')) continue
42+
const path = argument.slice('--config.'.length).split('.')
43+
let node = configuration
44+
for (const key of path.slice(0, -1)) {
45+
node[key] ??= {}
46+
node = node[key] as JsonObject
4547
}
46-
const next = referencedDefinition(property)
47-
if (next !== undefined) definition = next
48-
else if (index !== rest.length - 1) {
49-
throw new Error(`Electron Builder option 'win.${rest.slice(0, index + 1).join('.')}' has no nested options`)
50-
}
51-
})
48+
node[path.at(-1)!] = args[index + 1]
49+
index += 1
50+
}
51+
return configuration
52+
}
53+
54+
/**
55+
* Assert Electron Builder would accept the configuration these arguments produce.
56+
*
57+
* Comparing generated arguments against a hand-written array cannot tell that
58+
* the arguments are invalid — both copies carry the same mistake. Electron
59+
* Builder validates the merged configuration before it packages anything, so
60+
* running that same validation here fails in the suite instead of at the tag.
61+
* @param args - Arguments as they reach Electron Builder.
62+
*/
63+
function assertConfigurationAccepted(args: readonly string[]): void {
64+
if (validateConfiguration(configurationFrom(args))) return
65+
const errors = validateConfiguration.errors ?? []
66+
const unknown = errors
67+
.filter(error => error.keyword === 'additionalProperties')
68+
.map(error => `${error.instancePath}.${String(error.params['additionalProperty'])}`)
69+
if (unknown.length > 0) throw new Error(`Electron Builder rejects unknown options: ${unknown.join(', ')}`)
70+
// anyOf/type noise follows every real error; the specific keywords name the cause.
71+
const specific = errors.filter(error => error.keyword !== 'anyOf' && error.keyword !== 'type')
72+
const reported = (specific.length > 0 ? specific : errors)
73+
.map(error => `${error.instancePath === '' ? 'configuration' : error.instancePath} ${error.message ?? 'is invalid'}`)
74+
throw new Error(`Electron Builder rejects the configuration: ${[...new Set(reported)].join('; ')}`)
5275
}
5376

5477
const signingEnvironment: NodeJS.ProcessEnv = {
@@ -101,24 +124,28 @@ describe('Windows Azure signing configuration', () => {
101124
})
102125
})
103126

104-
describe('Electron Builder option names', () => {
127+
describe('Electron Builder configuration', () => {
105128
const certificateEnvironment: NodeJS.ProcessEnv = {
106129
WIN_CSC_LINK: 'certificate.p12',
107130
WIN_CSC_KEY_PASSWORD: 'password',
108131
WINDOWS_SIGNING_PUBLISHER_NAME: 'CN=Example Publisher, O=Example Publisher',
109132
}
110133

111-
it('emits only options the installed Electron Builder schema declares', () => {
134+
it('accepts the packaged configuration on its own', () => {
135+
expect(() => { assertConfigurationAccepted([]) }).not.toThrow()
136+
})
137+
138+
it('accepts the configuration every signing method produces', () => {
112139
for (const environment of [signingEnvironment, certificateEnvironment]) {
113-
const options = windowsSigningArgs(environment).filter(argument => argument.startsWith('--config.'))
114-
expect(options.length).toBeGreaterThan(0)
115-
for (const option of options) assertSchemaOption(option.slice('--config.'.length))
140+
const args = windowsSigningArgs(environment)
141+
expect(args.length).toBeGreaterThan(0)
142+
expect(() => { assertConfigurationAccepted(args) }).not.toThrow()
116143
}
117144
})
118145

119-
it('rejects an option the schema does not declare', () => {
120-
expect(() => { assertSchemaOption('win.publisherName') })
121-
.toThrow("Electron Builder has no option 'publisherName' under win")
146+
it('rejects an option Electron Builder has removed', () => {
147+
expect(() => { assertConfigurationAccepted(['--config.win.publisherName', 'CN=Example Publisher']) })
148+
.toThrow('Electron Builder rejects unknown options: /win.publisherName')
122149
})
123150
})
124151

0 commit comments

Comments
 (0)