diff --git a/src/notarytool.ts b/src/notarytool.ts index 4f48108..649aa57 100644 --- a/src/notarytool.ts +++ b/src/notarytool.ts @@ -64,6 +64,31 @@ async function getNotarizationLogs(opts: NotarizeOptions, id: string) { } } +export function parseNotarytoolOutput(output: string): any { + const rawOut = output.trim(); + + let jsonOut: string = ''; + + for (const line of rawOut.split('\n')) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('{') && trimmedLine.endsWith('}')) { + jsonOut = line; + break; + } + } + + d('notarytool produced output:\n', output); + + let parsed: any; + try { + parsed = JSON.parse(jsonOut); + } catch (err) { + throw new Error(`Could not parse notarytool output: \n\n${rawOut}`); + } + + return parsed; +} + export async function isNotaryToolAvailable(notarytoolPath?: string) { if (notarytoolPath !== undefined) { const result = await spawn(notarytoolPath, ['--version']); @@ -110,16 +135,8 @@ export async function notarizeAndWaitForNotaryTool(opts: NotarizeOptions) { ]; const result = await runNotaryTool(notarizeArgs, opts.notarytoolPath); - const rawOut = result.output.trim(); - - let parsed: any; - try { - parsed = JSON.parse(rawOut); - } catch (err) { - throw new Error( - `Failed to notarize via notarytool. Failed with unexpected result: \n\n${rawOut}`, - ); - } + + const parsed = parseNotarytoolOutput(result.output); let logOutput: undefined | string; if (typeof parsed.id === 'string') { diff --git a/test/notarytool.test.ts b/test/notarytool.test.ts new file mode 100644 index 0000000..fe3a8ce --- /dev/null +++ b/test/notarytool.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; +import { parseNotarytoolOutput } from '../src/notarytool.js'; + +describe('parseNotarytoolOutput', () => { + it('parses valid JSON output', () => { + const output = '{"status": "Accepted", "id": "123"}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '123', + }); + }); + + it('parses JSON with whitespace', () => { + const output = '\n\n {"status": "Accepted", "id": "456"} \n'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '456', + }); + }); + + it('parses JSON with warnings before it', () => { + const output = 'Warning: Some warning message\n{"status": "Accepted", "id": "789"}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: '789', + }); + }); + + it('parses JSON with warnings after it', () => { + const output = '{"status": "Accepted", "id": "abc"}\nWarning: Some warning message'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + id: 'abc', + }); + }); + + it('parses JSON with warnings before and after it', () => { + const output = + 'Warning: First warning\n{"status": "Invalid", "id": "def"}\nWarning: Second warning'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Invalid', + id: 'def', + }); + }); + + it('throws error for invalid JSON', () => { + const output = 'not json at all'; + expect(() => parseNotarytoolOutput(output)).toThrow( + 'Could not parse notarytool output: \n\nnot json at all', + ); + }); + + it('throws error for incomplete JSON', () => { + const output = '{"status": "Accepted"'; + expect(() => parseNotarytoolOutput(output)).toThrow('Could not parse notarytool output'); + }); + + it('parses nested JSON objects', () => { + const output = '{"status": "Accepted", "data": {"nested": "value"}}'; + expect(parseNotarytoolOutput(output)).toEqual({ + status: 'Accepted', + data: { nested: 'value' }, + }); + }); +});