NODEDC_PLATFORM/packages/external-provider-contract/test/provider-package.test.mjs

465 lines
20 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import {
L2_CONNECTION_INSTANCE_SCHEMA_VERSION,
instantiateL2Connection,
validateDataProductPublish,
validateProviderPackage,
} from "../src/index.mjs";
import {
GELIOS_POSITIONS_DATA_PRODUCT_ID,
GELIOS_POSITIONS_DATA_PRODUCT_VERSION,
GELIOS_POSITIONS_ONTOLOGY_REVISION,
GELIOS_UNIT_SOURCE_ID_PREFIX,
geliosProviderPackageV1,
geliosUnitsCurrentFixtureV1,
} from "../providers/gelios/v1/index.mjs";
import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs";
const expectedFields = [
"course_degrees",
"display_name",
"elevation_meters",
"geometry",
"hdop",
"horizontal_accuracy_meters",
"object_kind",
"operational_status",
"position_source",
"position_valid",
"quality_flags",
"satellite_count",
"speed_kph",
];
assert.deepEqual(validateProviderPackage(geliosProviderPackageV1), { ok: true, errors: [] });
const geliosAuth = geliosProviderPackageV1.authModes[0];
const geliosUnitsRead = geliosProviderPackageV1.capabilities[0];
assert.equal(geliosAuth.id, "gelios.rest-bearer.v1");
assert.deepEqual(geliosAuth.transport, { placement: "header", name: "Authorization" });
assert.deepEqual(geliosAuth.tokenLifecycle, {
artifacts: ["access", "refresh"],
requestArtifact: "access",
refreshMode: "operator_managed",
});
assert.equal(geliosAuth.tokenLifecycle.artifacts.includes("writer"), false);
assert.equal(geliosUnitsRead.request.baseUrl, "https://api.geliospro.com");
assert.equal(geliosUnitsRead.request.path, "/api/v1/units");
assert.deepEqual(geliosUnitsRead.request.query, {});
const product = geliosProviderPackageV1.dataProducts[0];
const registeredProduct = JSON.parse(await readFile(new URL(
"../../../services/external-data-plane/definitions/fleet.positions.current.v1.json",
import.meta.url,
), "utf8"));
assert.equal(product.id, GELIOS_POSITIONS_DATA_PRODUCT_ID);
assert.equal(product.version, GELIOS_POSITIONS_DATA_PRODUCT_VERSION);
assert.equal(product.ontologyRevision, GELIOS_POSITIONS_ONTOLOGY_REVISION);
assert.equal(product.id, "fleet.positions.current.v1");
assert.equal(product.version, "1.0.0");
assert.equal(product.ontologyRevision, "ontology.map.moving_object.v1");
assert.deepEqual(product.fields, expectedFields);
assert.deepEqual(product, registeredProduct);
assert.doesNotThrow(() => normalizeDataProductDefinition(product));
assert.equal(product.fields.every((field) => /^[a-z][a-z0-9_]*$/.test(field)), true);
const mapping = geliosProviderPackageV1.mappingContracts[0];
assert.equal(mapping.target.dataProductId, product.id);
assert.equal(mapping.target.version, product.version);
assert.equal(mapping.target.ontologyRevision, product.ontologyRevision);
assert.deepEqual(
[...Object.keys(mapping.fact.attributes), "geometry"].sort(),
[...product.fields].sort(),
);
assert.equal(mapping.derivations.operational_status.parameters.staleAfterMs, 60000);
assert.deepEqual(mapping.derivations.quality_flags.default, []);
assert.equal(mapping.fact.observedAt.fallback, "collection_received_at");
assert.equal(GELIOS_UNIT_SOURCE_ID_PREFIX, "gelios-unit-");
assert.equal(mapping.fact.sourceId.prefix, GELIOS_UNIT_SOURCE_ID_PREFIX);
assert.equal(mapping.fact.sourceId.coerce, "string");
assert.equal(mapping.fact.geometry.longitude.paths.includes("lastMsg.lon"), true);
assert.equal(mapping.fact.geometry.latitude.paths.includes("lastMsg.lat"), true);
assert.equal(mapping.fact.attributes.speed_kph.paths.includes("lastMsg.speed"), true);
assert.equal(validateDataProductPublish(geliosUnitsCurrentFixtureV1.expectedPublish).ok, true);
assert.equal(
geliosUnitsCurrentFixtureV1.sourceResponse.units.length,
geliosUnitsCurrentFixtureV1.expectedPublish.facts.length,
"every entity returned for the bound credential remains in the publish fixture",
);
assert.deepEqual(
geliosUnitsCurrentFixtureV1.expectedPublish.facts.map((fact) => fact.sourceId),
geliosUnitsCurrentFixtureV1.sourceResponse.units.map(
(unit) => `${GELIOS_UNIT_SOURCE_ID_PREFIX}${String(unit.id)}`,
),
);
assert.equal(
geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].sourceId,
"gelios-unit-001002",
"provider ID strings must remain byte-for-byte stable after the canonical prefix",
);
assert.equal(geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].geometry, undefined);
assert.equal(geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].attributes.operational_status, "no_position");
assert.equal(geliosUnitsCurrentFixtureV1.sourceResponse.units[1].lastMsg, undefined);
assert.equal(
geliosUnitsCurrentFixtureV1.expectedPublish.facts[1].observedAt,
geliosUnitsCurrentFixtureV1.collectionContext.receivedAt,
);
const realtimeProfileId = "gelios.positions.current.realtime.v1";
assert.equal(
geliosProviderPackageV1.collectionProfiles[0].cardinality.onExceed,
"require_partitioned_data_product",
);
const accountA = instantiateL2Connection(geliosProviderPackageV1, {
tenantId: "tenant-alpha",
connectionId: "gelios-account-alpha",
collectionProfileId: realtimeProfileId,
providerCredentialRef: "ndc-credref:provider-alpha-0001",
});
const accountB = instantiateL2Connection(geliosProviderPackageV1, {
tenantId: "tenant-beta",
connectionId: "gelios-account-beta",
collectionProfileId: realtimeProfileId,
providerCredentialRef: "ndc-credref:provider-beta-0001",
});
assert.equal(accountA.schemaVersion, L2_CONNECTION_INSTANCE_SCHEMA_VERSION);
assert.equal(accountA.package.id, accountB.package.id);
assert.equal(accountA.package.version, accountB.package.version);
assert.equal(accountA.l2TemplateId, accountB.l2TemplateId);
assert.notEqual(accountA.connectionId, accountB.connectionId);
assert.notEqual(accountA.credentialRefs.provider.reference, accountB.credentialRefs.provider.reference);
assert.equal(accountA.credentialRefs.provider.owner, "ndc_l2_credentials");
assert.deepEqual(Object.keys(accountA.credentialRefs), ["provider"]);
assert.deepEqual(accountA.systemBindings.publisher, {
role: "publisher",
owner: "ndc_l2_credentials",
management: "control_plane_managed",
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
desiredState: "bound",
status: "unresolved",
});
assert.equal(accountA.scope.mode, "all_visible_to_credential");
assert.equal(accountA.scope.refresh, "each_collection_run");
assert.equal(Object.isFrozen(accountA), true);
assert.equal(Object.isFrozen(accountA.credentialRefs), true);
assert.equal(Object.isFrozen(accountA.systemBindings), true);
const withEntityAllowlist = structuredClone(geliosProviderPackageV1);
withEntityAllowlist.capabilities[0].entityScope.unitIds = ["gelios-unit-1001"];
assert.equal(
validateProviderPackage(withEntityAllowlist).errors.includes("capabilities[0].entityScope.unitIds_not_allowed"),
true,
);
const withGroupFilter = structuredClone(geliosProviderPackageV1);
withGroupFilter.collectionProfiles[0].entityScope.businessEntityFilter = "provider_group";
assert.equal(
validateProviderPackage(withGroupFilter).errors.includes("collectionProfiles[0].entityScope.businessEntityFilter_must_be_forbidden"),
true,
);
const withWrongRevision = structuredClone(geliosProviderPackageV1);
withWrongRevision.mappingContracts[0].target.ontologyRevision = "gelios.positions.v1";
assert.equal(
validateProviderPackage(withWrongRevision).errors.includes("mappingContract.gelios.units.to.fleet.positions.current.v1.target.ontologyRevision_must_match_data_product"),
true,
);
const withMissingDerivation = structuredClone(geliosProviderPackageV1);
delete withMissingDerivation.mappingContracts[0].derivations.position_valid;
assert.equal(
validateProviderPackage(withMissingDerivation).errors.includes("mappingContracts[0].derive_position_valid_missing_definition"),
true,
);
const withWrongFactSemanticType = structuredClone(geliosProviderPackageV1);
withWrongFactSemanticType.mappingContracts[0].fact.semanticType.constant = "map.other";
assert.equal(
validateProviderPackage(withWrongFactSemanticType).errors.includes(
"mappingContract.gelios.units.to.fleet.positions.current.v1.fact.semanticType_must_equal_target_constant",
),
true,
);
const nonGeospatialPackage = structuredClone(geliosProviderPackageV1);
nonGeospatialPackage.dataProducts[0].fields = ["display_name"];
nonGeospatialPackage.fieldPolicies[0].targetFields = ["display_name"];
delete nonGeospatialPackage.mappingContracts[0].fact.geometry;
nonGeospatialPackage.mappingContracts[0].fact.attributes = {
display_name: { strategy: "first_non_empty", paths: ["name"], coerce: "string" },
};
delete nonGeospatialPackage.mappingContracts[0].derivations;
assert.deepEqual(validateProviderPackage(nonGeospatialPackage), { ok: true, errors: [] });
for (const restrictedPath of ["phone", "phone.number", "lmsg"]) {
const withRestrictedMappingPath = structuredClone(geliosProviderPackageV1);
withRestrictedMappingPath.mappingContracts[0].fact.attributes.display_name.paths = [restrictedPath];
assert.equal(
validateProviderPackage(withRestrictedMappingPath).errors.includes(
"mappingContract.gelios.units.to.fleet.positions.current.v1.fact_source_path_restricted",
),
true,
);
}
const withFieldPolicyProductMismatch = structuredClone(geliosProviderPackageV1);
const secondProduct = structuredClone(withFieldPolicyProductMismatch.dataProducts[0]);
secondProduct.id = "fleet.positions.alternate.v1";
withFieldPolicyProductMismatch.dataProducts.push(secondProduct);
withFieldPolicyProductMismatch.manifest.dataProductIds.push(secondProduct.id);
withFieldPolicyProductMismatch.fieldPolicies[0].dataProductId = secondProduct.id;
assert.equal(
validateProviderPackage(withFieldPolicyProductMismatch).errors.includes(
"mappingContract.gelios.units.to.fleet.positions.current.v1.field_policy_data_product_mismatch",
),
true,
);
const withCamelCaseField = structuredClone(geliosProviderPackageV1);
withCamelCaseField.dataProducts[0].fields[12] = "speedKph";
assert.equal(validateProviderPackage(withCamelCaseField).ok, false);
const withSecretField = structuredClone(geliosProviderPackageV1);
withSecretField.dataProducts[0].fields[12] = "api_token";
withSecretField.fieldPolicies[0].targetFields[12] = "api_token";
withSecretField.mappingContracts[0].fact.attributes.api_token = { constant: "metadata-only" };
delete withSecretField.mappingContracts[0].fact.attributes.speed_kph;
const secretFieldValidation = validateProviderPackage(withSecretField);
assert.equal(secretFieldValidation.errors.includes("dataProducts[0].fields_must_not_contain_secret_fields"), true);
assert.equal(secretFieldValidation.errors.includes("fieldPolicies[0].targetFields_must_not_contain_secret_fields"), true);
assert.equal(secretFieldValidation.errors.includes("mappingContracts[0].fact.attributes.api_token_secret_field_not_allowed"), true);
const withRuntimeAccountState = structuredClone(geliosProviderPackageV1);
withRuntimeAccountState.tenantId = "must-not-live-in-package";
assert.equal(
validateProviderPackage(withRuntimeAccountState).errors.includes("providerPackage_must_not_contain_connection_instance_state"),
true,
);
const withPrimitiveCapability = structuredClone(geliosProviderPackageV1);
withPrimitiveCapability.capabilities.push(null);
assert.doesNotThrow(() => validateProviderPackage(withPrimitiveCapability));
assert.equal(validateProviderPackage(withPrimitiveCapability).ok, false);
const withMalformedNestedArrays = structuredClone(geliosProviderPackageV1);
withMalformedNestedArrays.collectionProfiles[0].capabilityIds = {};
withMalformedNestedArrays.l2Templates[0].credentialBindings = {};
withMalformedNestedArrays.l2Templates[0].steps = {};
assert.doesNotThrow(() => validateProviderPackage(withMalformedNestedArrays));
assert.equal(validateProviderPackage(withMalformedNestedArrays).ok, false);
const withCredentialInStaticQuery = structuredClone(geliosProviderPackageV1);
withCredentialInStaticQuery.authModes[0].transport = { placement: "query", name: "token" };
withCredentialInStaticQuery.capabilities[0].request.query.token = "plain-provider-key";
assert.equal(
validateProviderPackage(withCredentialInStaticQuery).errors.includes("capabilities[0].request.query.token_credential_parameter_not_allowed"),
true,
);
assert.equal(
validateProviderPackage(withCredentialInStaticQuery).errors.includes("capability.gelios.units.current.read.request.query_must_not_embed_credential_parameter"),
true,
);
const withCredentialInBaseUrl = structuredClone(geliosProviderPackageV1);
withCredentialInBaseUrl.capabilities[0].request.baseUrl = "https://user:pass@api.geliospro.com/?token=forbidden";
assert.equal(validateProviderPackage(withCredentialInBaseUrl).ok, false);
for (const unsafePath of ["/ok\nX-Evil: yes", "/back\\slash", "/query?x=1"]) {
const withUnsafeRequestPath = structuredClone(geliosProviderPackageV1);
withUnsafeRequestPath.capabilities[0].request.path = unsafePath;
assert.equal(validateProviderPackage(withUnsafeRequestPath).ok, false);
}
for (const unsafeCollectionPath of ["__proto__.polluted", "data.items[0]", "constructor.prototype"]) {
const withUnsafeCollectionPath = structuredClone(geliosProviderPackageV1);
withUnsafeCollectionPath.capabilities[0].request.response.collectionPaths = [unsafeCollectionPath];
assert.equal(validateProviderPackage(withUnsafeCollectionPath).ok, false);
}
const withUnsafeMappingPath = structuredClone(geliosProviderPackageV1);
withUnsafeMappingPath.mappingContracts[0].fact.attributes.display_name.paths = ["__proto__.polluted"];
assert.equal(validateProviderPackage(withUnsafeMappingPath).ok, false);
const withUnsafeCollectionCapability = structuredClone(geliosProviderPackageV1);
withUnsafeCollectionCapability.capabilities[0].classification = "write";
assert.equal(
validateProviderPackage(withUnsafeCollectionCapability).errors.includes("collectionProfile.gelios.positions.current.realtime.v1.capabilityIds_must_reference_implemented_safe_read"),
true,
);
const withUnexecutedSecondCapability = structuredClone(geliosProviderPackageV1);
const secondCapability = structuredClone(withUnexecutedSecondCapability.capabilities[0]);
secondCapability.id = "gelios.units.metadata.read";
withUnexecutedSecondCapability.capabilities.push(secondCapability);
withUnexecutedSecondCapability.manifest.capabilityIds.push(secondCapability.id);
for (const profile of withUnexecutedSecondCapability.collectionProfiles) {
profile.capabilityIds.push(secondCapability.id);
}
assert.equal(
validateProviderPackage(withUnexecutedSecondCapability).errors.includes(
"collectionProfile.gelios.positions.current.realtime.v1.capabilityIds_must_select_one_v1_read",
),
true,
);
const withReorderedSteps = structuredClone(geliosProviderPackageV1);
[withReorderedSteps.l2Templates[0].steps[0], withReorderedSteps.l2Templates[0].steps[1]] = [
withReorderedSteps.l2Templates[0].steps[1],
withReorderedSteps.l2Templates[0].steps[0],
];
assert.equal(
validateProviderPackage(withReorderedSteps).errors.includes("l2Templates[0].steps_sequence_invalid"),
true,
);
const withUnknownConnectionParameters = structuredClone(geliosProviderPackageV1);
withUnknownConnectionParameters.l2Templates[0].connectionParameters = ["foo"];
assert.equal(
validateProviderPackage(withUnknownConnectionParameters).errors.includes(
"l2Templates[0].connectionParameters_must_match_v1_contract",
),
true,
);
const withCredentialAuthMismatch = structuredClone(geliosProviderPackageV1);
const secondAuthMode = structuredClone(withCredentialAuthMismatch.authModes[0]);
secondAuthMode.id = "gelios.rest-bearer.alternate.v1";
withCredentialAuthMismatch.authModes.push(secondAuthMode);
withCredentialAuthMismatch.manifest.authModeIds.push(secondAuthMode.id);
withCredentialAuthMismatch.l2Templates[0].credentialBindings[0].authModeId = secondAuthMode.id;
assert.equal(
validateProviderPackage(withCredentialAuthMismatch).errors.includes(
"l2Template.gelios.positions.current.l2.v1.provider_credential_auth_mode_mismatch",
),
true,
);
const withCallerManagedPublisher = structuredClone(geliosProviderPackageV1);
withCallerManagedPublisher.l2Templates[0].credentialBindings[1].management = "connection_input";
assert.equal(
validateProviderPackage(withCallerManagedPublisher).errors.includes(
"l2Templates[0].credentialBindings[1].publisher_binding_shape_invalid",
),
true,
);
const withUnknownTokenArtifact = structuredClone(geliosProviderPackageV1);
withUnknownTokenArtifact.authModes[0].tokenLifecycle.artifacts.push("writer");
assert.equal(
validateProviderPackage(withUnknownTokenArtifact).errors.includes(
"authModes[0].tokenLifecycle.artifacts_invalid",
),
true,
);
const withRefreshUsedForRequest = structuredClone(geliosProviderPackageV1);
withRefreshUsedForRequest.authModes[0].tokenLifecycle.requestArtifact = "refresh";
assert.equal(
validateProviderPackage(withRefreshUsedForRequest).errors.includes(
"authModes[0].tokenLifecycle.requestArtifact_must_be_access",
),
true,
);
const withUnmanagedRefreshArtifact = structuredClone(geliosProviderPackageV1);
withUnmanagedRefreshArtifact.authModes[0].tokenLifecycle.refreshMode = "not_applicable";
assert.equal(
validateProviderPackage(withUnmanagedRefreshArtifact).errors.includes(
"authModes[0].tokenLifecycle.refreshMode_required_for_refresh_artifact",
),
true,
);
for (const history of [{
mode: "none",
intervalMs: 60000,
strategy: "latest-per-entity-per-bucket",
retentionDays: 90,
}, {
mode: "sampled",
intervalMs: 24 * 60 * 60 * 1000 + 1,
strategy: "latest-per-entity-per-bucket",
retentionDays: 90,
}, {
mode: "all",
retentionDays: 3651,
}]) {
const withInvalidHistory = structuredClone(geliosProviderPackageV1);
withInvalidHistory.dataProducts[0].history = history;
assert.equal(validateProviderPackage(withInvalidHistory).ok, false);
assert.throws(() => normalizeDataProductDefinition(withInvalidHistory.dataProducts[0]));
}
const withBrokenArtifactChain = structuredClone(geliosProviderPackageV1);
withBrokenArtifactChain.l2Templates[0].steps[4].dataProductId = "unrelated.product.v1";
assert.equal(
validateProviderPackage(withBrokenArtifactChain).errors.includes("collectionProfile.gelios.positions.current.realtime.v1.template_data_product_mismatch"),
true,
);
const withSecretNamedDerivationParameter = structuredClone(geliosProviderPackageV1);
withSecretNamedDerivationParameter.mappingContracts[0].derivations.position_valid.parameters = {
accessToken: "opaque-provider-value",
};
assert.equal(
validateProviderPackage(withSecretNamedDerivationParameter).errors.includes(
"mappingContracts[0].derivations.position_valid.parameters.accessToken_secret_field_not_allowed",
),
true,
);
const withObjectConstant = structuredClone(geliosProviderPackageV1);
withObjectConstant.mappingContracts[0].fact.attributes.object_kind.constant = {
accessToken: "opaque-provider-value",
};
assert.equal(
validateProviderPackage(withObjectConstant).errors.includes(
"mappingContracts[0].fact.attributes.object_kind.constant_must_be_scalar_or_scalar_array",
),
true,
);
assert.throws(() => instantiateL2Connection(geliosProviderPackageV1, {
tenantId: "tenant-alpha",
connectionId: "gelios-account-alpha",
collectionProfileId: realtimeProfileId,
providerCredentialRef: "Bearer plaintext-must-not-cross-boundary",
}), /l2_connection_invalid/);
assert.throws(() => instantiateL2Connection(geliosProviderPackageV1, {
tenantId: "tenant-alpha",
connectionId: "gelios-account-alpha",
collectionProfileId: realtimeProfileId,
providerCredentialRef: "ndc-credref:provider-alpha-0001",
writerCredentialRef: "ndc-credref:caller-writer-forbidden",
}), /options\.writerCredentialRef_not_allowed/);
const serializedPackage = JSON.stringify(geliosProviderPackageV1);
assert.equal(serializedPackage.includes('"prefix":"unit-"'), false);
assert.equal(serializedPackage.includes("unitIds"), false);
assert.equal(serializedPackage.includes("tenant-alpha"), false);
assert.equal(serializedPackage.includes("gelios-gateway"), false);
assert.equal(serializedPackage.includes("writer_credential_ref"), false);
assert.equal(serializedPackage.includes("writerCapabilityByReferenceOnly"), false);
assert.equal(serializedPackage.includes("publisherBindingControlPlaneManaged\":true"), true);
assert.equal(serializedPackage.includes("providerSpecificServiceForbidden\":true"), true);
assert.deepEqual(
geliosProviderPackageV1.l2Templates[0].steps.map((step) => step.kind),
[
"collection_trigger",
"provider_request",
"extract_items",
"semantic_mapping",
"data_product_publish",
],
);
assert.equal(
geliosProviderPackageV1.l2Templates[0].steps.at(-1).nodeType,
"n8n-nodes-ndc.ndcDataProductPublish",
);
console.log("external-provider-contract provider-package: ok");