Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#type',
Expand Down Expand Up @@ -104,6 +105,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#type',
Expand Down Expand Up @@ -134,6 +136,7 @@ describe('validator', () => {
property: '_key',
},
],
warnings: null,
skippedSchemas: [
{
schemaId: '#type',
Expand All @@ -155,6 +158,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand All @@ -180,6 +184,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand Down Expand Up @@ -208,6 +213,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: null,
});
});
Expand All @@ -229,6 +235,7 @@ describe('validator', () => {
property: '_key',
},
],
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand Down Expand Up @@ -257,6 +264,7 @@ describe('validator', () => {
property: '_key',
},
],
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand All @@ -279,6 +287,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand All @@ -301,6 +310,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand All @@ -323,6 +333,7 @@ describe('validator', () => {
).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand Down Expand Up @@ -352,6 +363,7 @@ describe('validator', () => {
validation: 'format',
},
],
warnings: null,
skippedSchemas: [
{
schemaId: '#GraphObject',
Expand All @@ -362,3 +374,209 @@ describe('validator', () => {
});
});
});

describe('validator — permissive enum properties', () => {
// In-memory NHI-shaped schema. The shape mirrors the data-model's NHI class
// additions in M001/S02: `nhiType`, `nhiOwnerStatus`, `aiConfidence` are
// enum-constrained; `isAi` is a bare boolean (type, not enum).
const NHI_SCHEMA = {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: '#NHI',
type: 'object',
properties: {
_key: { type: 'string', minLength: 10 },
_class: {
oneOf: [
{ type: 'string', minLength: 2 },
{
type: 'array',
minItems: 1,
items: { type: 'string', minLength: 2 },
},
],
},
_type: { type: 'string', minLength: 3 },
nhiType: {
type: 'string',
enum: ['service_account', 'api_key', 'workload_identity'],
},
nhiOwnerStatus: {
type: 'string',
enum: ['active', 'inactive', 'orphaned'],
},
aiConfidence: {
type: 'string',
enum: ['high', 'medium', 'low'],
},
isAi: { type: 'boolean' },
},
required: ['_key', '_class', '_type'],
};

const validNhiEntity = {
_class: ['NHI'],
_type: 'NHI',
_key: '0123456789',
nhiType: 'service_account',
nhiOwnerStatus: 'active',
aiConfidence: 'high',
isAi: true,
};

test('valid NHI entity produces no errors and no warnings', () => {
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
expect(validator.validateEntity(validNhiEntity)).toEqual({
isValid: true,
errors: null,
warnings: null,
skippedSchemas: [
{ schemaId: '#NHI', reason: 'type-already-validated', type: 'class' },
],
});
});

test('unknown nhiType becomes a warning, not an error', () => {
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const result = validator.validateEntity({
...validNhiEntity,
nhiType: 'unknown_subtype',
});
expect(result.isValid).toBe(true);
expect(result.errors).toBeNull();
expect(result.warnings).toEqual([
{
schemaId: '#NHI',
property: 'nhiType',
message: expect.stringContaining('must be equal to one of'),
validation: 'enum',
},
]);
});

test('unknown nhiOwnerStatus becomes a warning', () => {
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const result = validator.validateEntity({
...validNhiEntity,
nhiOwnerStatus: 'mystery',
});
expect(result.isValid).toBe(true);
expect(result.errors).toBeNull();
expect(result.warnings).toHaveLength(1);
expect(result.warnings![0].property).toBe('nhiOwnerStatus');
expect(result.warnings![0].validation).toBe('enum');
});

test('unknown aiConfidence becomes a warning', () => {
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const result = validator.validateEntity({
...validNhiEntity,
aiConfidence: 'extremely high',
});
expect(result.isValid).toBe(true);
expect(result.warnings).toHaveLength(1);
expect(result.warnings![0].property).toBe('aiConfidence');
});

test('type mismatch on a permissive property still hard-errors', () => {
// isAi: 'yes' is a string where boolean is required. The property is not
// in the permissive set anyway, but the point is: keyword === 'type' is
// not 'enum', so even a permissive property would still hard-fail here.
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const result = validator.validateEntity({
...validNhiEntity,
isAi: 'yes',
});
expect(result.isValid).toBe(false);
expect(result.warnings).toBeNull();
expect(result.errors).toEqual([
{
schemaId: '#NHI',
property: 'isAi',
message: expect.stringContaining('must be boolean'),
validation: 'type',
},
]);
});

test('type mismatch on a permissive enum property is still an error', () => {
// nhiType is in the permissive set, but a number-shaped value triggers
// keyword === 'type' (not 'enum'), so it must hard-error.
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const result = validator.validateEntity({
...validNhiEntity,
nhiType: 42,
});
expect(result.isValid).toBe(false);
expect(result.errors).toEqual([
{
schemaId: '#NHI',
property: 'nhiType',
message: expect.stringContaining('must be string'),
validation: 'type',
},
]);
});

test('mixed: permissive enum violation + missing _key produces both warnings and errors', () => {
const validator = new EntityValidator({ schemas: [NHI_SCHEMA] });
const { _key, ...entityWithoutKey } = validNhiEntity;
const result = validator.validateEntity({
...entityWithoutKey,
nhiType: 'unknown_subtype',
});
expect(result.isValid).toBe(false);
expect(result.errors).toEqual([
{
schemaId: '#NHI',
property: '_key',
message: "must have required property '_key'",
validation: 'required',
},
]);
expect(result.warnings).toEqual([
{
schemaId: '#NHI',
property: 'nhiType',
message: expect.stringContaining('must be equal to one of'),
validation: 'enum',
},
]);
});

test('permissiveEnumProperties: [] makes enum violations hard-error again', () => {
const validator = new EntityValidator({
schemas: [NHI_SCHEMA],
permissiveEnumProperties: [],
});
const result = validator.validateEntity({
...validNhiEntity,
nhiType: 'unknown_subtype',
});
expect(result.isValid).toBe(false);
expect(result.warnings).toBeNull();
expect(result.errors).toEqual([
{
schemaId: '#NHI',
property: 'nhiType',
message: expect.stringContaining('must be equal to one of'),
validation: 'enum',
},
]);
});

test('permissiveEnumProperties can be extended with additional properties', () => {
const validator = new EntityValidator({
schemas: [NHI_SCHEMA],
permissiveEnumProperties: ['nhiType', 'aiConfidence', 'customField'],
});
// nhiOwnerStatus is no longer permissive in this configuration.
const result = validator.validateEntity({
...validNhiEntity,
nhiOwnerStatus: 'mystery',
});
expect(result.isValid).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors![0].property).toBe('nhiOwnerStatus');
expect(result.errors![0].validation).toBe('enum');
});
});
6 changes: 5 additions & 1 deletion packages/integration-sdk-entity-validator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ export {
isEntityValidationError,
type EntityValidationError,
} from './entityValidationError';
export { getValidator, getValidatorSync } from './singleton';
export {
getValidator,
getValidatorSync,
setSchemaSingleton,
} from './singleton';
Loading
Loading