Skip to content
Merged
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
@@ -0,0 +1,218 @@
import testRule from './__helpers__/testRule';
import { DiagnosticSeverity } from '@stoplight/types';

testRule('xgen-IPA-118-no-additional-properties-false', [
{
name: 'valid without additionalProperties',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'integer' },
},
},
},
},
},
errors: [],
},
{
name: 'valid with additionalProperties: true',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: true,
},
},
},
},
errors: [],
},
{
name: 'valid with additionalProperties as schema',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: {
type: 'string',
},
},
},
},
},
errors: [],
},
{
name: 'invalid with additionalProperties: false',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
ReferencedSchema: {
type: 'object',
properties: {
property: { $ref: '#/components/schemas/ExampleSchema' },
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ExampleSchema'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid with nested additionalProperties: false',
document: {
components: {
schemas: {
ParentSchema: {
type: 'object',
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
},
},
},
errors: [
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ParentSchema', 'properties', 'child'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'invalid with multiple nested additionalProperties: false',
document: {
components: {
schemas: {
ParentSchema: {
type: 'object',
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
additionalProperties: false,
},
},
},
},
errors: [
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ParentSchema'],
severity: DiagnosticSeverity.Warning,
},
{
code: 'xgen-IPA-118-no-additional-properties-false',
message:
"Schema must not use 'additionalProperties: false'. Consider using 'additionalProperties: true' or omitting the property.",
path: ['components', 'schemas', 'ParentSchema', 'properties', 'child'],
severity: DiagnosticSeverity.Warning,
},
],
},
{
name: 'with exception',
document: {
components: {
schemas: {
ExampleSchema: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
},
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
ParentSchema: {
type: 'object',
'x-xgen-IPA-exception': {
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
},
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
},
},
},
},
errors: [],
},
{
name: 'invalid with multiple nested additionalProperties: false - exceptions',
document: {
components: {
schemas: {
ParentSchema: {
type: 'object',
properties: {
child: {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
},
},
additionalProperties: false,
'x-xgen-IPA-exception': {
'xgen-IPA-118-no-additional-properties-false': 'Exception reason',
},
},
},
},
},
errors: [],
},
]);
155 changes: 155 additions & 0 deletions tools/spectral/ipa/__tests__/utils/compareUtils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from '@jest/globals';
import {
findAdditionalPropertiesFalsePaths,
isDeepEqual,
removePropertiesByFlag,
removePropertyKeys,
Expand Down Expand Up @@ -329,3 +330,157 @@ describe('removeRequestProperties', () => {
expect(removeRequestProperties(input)).toEqual(expected);
});
});

describe('findAdditionalPropertiesFalsePaths', () => {
it('finds additionalProperties:false at root level', () => {
const schema = {
type: 'object',
properties: {
name: { type: 'string' },
},
additionalProperties: false,
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root']]);
});

it('finds additionalProperties:false in nested properties', () => {
const schema = {
type: 'object',
properties: {
user: {
type: 'object',
properties: {
address: {
type: 'object',
additionalProperties: false,
},
},
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'properties', 'user', 'properties', 'address']]);
});

it('finds additionalProperties:false in array items', () => {
const schema = {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'items']]);
});

it('finds additionalProperties:false in composition keywords', () => {
const schema = {
oneOf: [
{
type: 'object',
additionalProperties: false,
},
{
type: 'object',
properties: {
name: { type: 'string' },
},
},
],
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([['root', 'oneOf', '0']]);
});

it('finds multiple additionalProperties:false occurrences', () => {
const schema = {
type: 'object',
additionalProperties: false,
properties: {
user: {
type: 'object',
additionalProperties: false,
properties: {
addresses: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
},
},
},
},
metadata: {
allOf: [
{
type: 'object',
additionalProperties: false,
},
],
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([
['root'],
['root', 'properties', 'user'],
['root', 'properties', 'user', 'properties', 'addresses', 'items'],
['root', 'properties', 'metadata', 'allOf', '0'],
]);
});

it('handles deeply nested structures', () => {
const schema = {
type: 'object',
properties: {
level1: {
type: 'object',
properties: {
level2: {
type: 'object',
properties: {
level3: {
type: 'object',
properties: {
level4: {
type: 'object',
additionalProperties: false,
},
},
},
},
},
},
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([
['root', 'properties', 'level1', 'properties', 'level2', 'properties', 'level3', 'properties', 'level4'],
]);
});

it('does not find additionalProperties:true or schema objects', () => {
const schema = {
type: 'object',
additionalProperties: true,
properties: {
user: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
};

const results = findAdditionalPropertiesFalsePaths(schema, ['root']);
expect(results).toEqual([]);
});
});
Loading
Loading