diff --git a/src/contracts/mobileApplicationGraphExamples.ts b/src/contracts/mobileApplicationGraphExamples.ts index 10b27e1693..92fb913224 100644 --- a/src/contracts/mobileApplicationGraphExamples.ts +++ b/src/contracts/mobileApplicationGraphExamples.ts @@ -1,18 +1,28 @@ -import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import { createEvidence } from "../domain/evidence.js"; -import { jsonValueSchema } from "../domain/jsonValue.js"; +import { + artifactInventoryResultSchema, + type ArtifactNode, + type ArtifactOccurrence, +} from "../domain/artifactGraph.js"; +import { + artifactGraphDigest, + artifactIdForContent, + artifactManifestId, + occurrenceIdForLocation, +} from "../domain/artifactIdentity.js"; const inventory = ( format: "apk" | "ipa" | "directory" | "hap", digit: string, ) => { const sha = digit.repeat(64); - const artifactId = `art_${digestCanonicalValue({ sha256: sha }, "Artifact inventory")}`; - const occurrenceId = `occ_${digestCanonicalValue( - { root: artifactId }, - "Artifact inventory", - )}`; - const nodes = [ + const artifactId = artifactIdForContent(sha); + const occurrenceId = occurrenceIdForLocation({ + rootArtifactId: artifactId, + logicalPath: ".", + entryKind: "file", + }); + const nodes: ArtifactNode[] = [ { artifact_id: artifactId, format: format === "directory" ? "directory" : "zip", @@ -24,7 +34,7 @@ const inventory = ( limitations: [], }, ]; - const occurrences = [ + const occurrences: ArtifactOccurrence[] = [ { occurrence_id: occurrenceId, artifact_id: artifactId, @@ -42,24 +52,15 @@ const inventory = ( limitations: [], }, ]; - const graphSha256 = digestCanonicalValue( - { - nodes, - occurrences, - edges: [], - integrity_contradictions: [], - }, - "Artifact inventory", - ); - return jsonValueSchema.parse({ + const graphSha256 = artifactGraphDigest({ + nodes, + occurrences, + edges: [], + contradictions: [], + }); + return artifactInventoryResultSchema.parse({ manifest: { - manifest_id: `agm_${digestCanonicalValue( - { - root_artifact_id: artifactId, - graph_sha256: graphSha256, - }, - "Artifact inventory", - )}`, + manifest_id: artifactManifestId(artifactId, graphSha256), root_artifact_id: artifactId, root_sha256: sha, root_format: format, @@ -72,6 +73,8 @@ const inventory = ( occurrences, edges: [], provenance: [], + integrity_contradictions: [], + coverage: { scope: "root", status: "complete" }, limitations: [], }); }; diff --git a/tests/acceptance/tools/mobileGraphExamples.test.ts b/tests/acceptance/tools/mobileGraphExamples.test.ts new file mode 100644 index 0000000000..084495f691 --- /dev/null +++ b/tests/acceptance/tools/mobileGraphExamples.test.ts @@ -0,0 +1,105 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect } from "vitest"; + +import { + ANDROID_APPLICATION_GRAPH_EXAMPLE, + APPLE_APPLICATION_GRAPH_EXAMPLE, + HARMONY_APPLICATION_GRAPH_EXAMPLE, + MACOS_APPLICATION_GRAPH_EXAMPLE, +} from "../../../src/contracts/mobileApplicationGraphExamples.js"; +import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for([ + { + name: "Android", + command: "project-android-application-graph", + operation: "project_android_application_graph", + example: ANDROID_APPLICATION_GRAPH_EXAMPLE, + }, + { + name: "IPA", + command: "project-apple-application-graph", + operation: "project_apple_application_graph", + example: APPLE_APPLICATION_GRAPH_EXAMPLE, + }, + { + name: "macOS", + command: "project-apple-application-graph", + operation: "project_apple_application_graph", + example: MACOS_APPLICATION_GRAPH_EXAMPLE, + }, + { + name: "Harmony", + command: "project-harmony-application-graph", + operation: "project_harmony_application_graph", + example: HARMONY_APPLICATION_GRAPH_EXAMPLE, + }, +] as const)( + "CLI executes the canonical $name graph example without target files", + async ({ command, operation, example }, { cli }) => { + const root = await createTestTempDirectory("rea-mobile-graph-example-"); + const input = JSON.stringify(example); + const path = join(root, "example.json"); + await writeFile(path, input); + for (const argument of [input, path]) { + const result = await cli.run({ + arguments: [command, argument, "--json"], + cwd: root, + }); + expect(result.exitCode, result.stdout).toBe(0); + const evidence = parseEvidence(result.json); + expect(evidence.operation).toBe(operation); + expect(evidence.normalized_result).toMatchObject({ + coverage: { status: "partial", inventory_complete: true }, + }); + } + }, +); + +cliTest( + "CLI still rejects an unsupported inventory coverage status", + async ({ cli }) => { + const original = ANDROID_APPLICATION_GRAPH_EXAMPLE.inventory_evidence[0]!; + const inventory = artifactInventoryResultSchema.parse( + original.normalized_result, + ); + const evidence = createEvidence( + { path: "Fixture.apk", sha256: "a".repeat(64), format: "apk" }, + original.provider, + { + operation: "inventory_artifact", + parameters: {}, + result: { + ...inventory, + coverage: { scope: "root", status: "invalid" }, + }, + confidence: "observed", + authority: "shipped-artifact", + }, + ); + const result = await cli.run({ + arguments: [ + "project-android-application-graph", + JSON.stringify({ inventory_evidence: [evidence] }), + "--json", + ], + }); + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "project_android_application_graph", + issues: expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining("coverage.status"), + }), + ]), + }, + }); + }, +);