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
44 changes: 42 additions & 2 deletions packages/ruleset/src/functions/integer-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ module.exports = function (schema, _opts, context) {
// These are the valid format values that can be used with integer schemas.
const intFormats = ['int32', 'int64'];

//These are the minimum and maximum values for integer schemas
const int32Minimum = -2147483648;
const int32Maximum = 2147483647;

// Constrained for interoperability per
// https://datatracker.ietf.org/doc/html/rfc7159#section-6
const int64Minimum = -9007199254740991; // -(2^53-1) or Number.MIN_SAFE_INTEGER
const int64Maximum = 9007199254740991; // 2^53 or Number.MAX_SAFE_INTEGER

/**
* This function performs various checks on an integer schema property to make sure it
* contains the "minimum", "maximum" attributes.
Expand All @@ -50,10 +59,13 @@ function integerBoundaryErrors(schema, path) {

const errors = [];

if (isDefined(format) && !intFormats.includes(format)) {
if (
!isDefined(format) ||
(isDefined(format) && !intFormats.includes(format))
) {
errors.push({
message: `Integer schemas should specify format as one of ${intFormats.join(
'.'
', '
)}`,
path,
});
Expand All @@ -63,12 +75,40 @@ function integerBoundaryErrors(schema, path) {
message: `Integer schemas should define property 'minimum'`,
path,
});
} else if (format === 'int32') {
if (minimum > int32Maximum || minimum < int32Minimum) {
errors.push({
message: `'minimum' value is out of safe range`,
path,
});
}
} else {
if (minimum > int64Maximum || minimum < int64Minimum) {
errors.push({
message: `'minimum' value is out of safe range`,
path,
});
}
}
if (!isDefined(maximum)) {
errors.push({
message: `Integer schemas should define property 'maximum'`,
path,
});
} else if (format === 'int32') {
if (maximum > int32Maximum || maximum < int32Minimum) {
errors.push({
message: `'maximum' value is out of safe range`,
path,
});
}
} else {
if (maximum > int64Maximum || maximum < int64Minimum) {
errors.push({
message: `'maximum' value is out of safe range`,
path,
});
}
}
if (isDefined(minimum) && isDefined(maximum) && minimum > maximum) {
errors.push({
Expand Down
101 changes: 101 additions & 0 deletions packages/ruleset/test/rules/integer-attributes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
testDocument.components.schemas['RuleTester2'] = {
description: 'Tests integer fields within allOf',
type: ['integer'],
format: 'int32',
allOf: [
{
minimum: 1,
Expand All @@ -142,6 +143,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: 'integer',
format: 'int32',
minimum: 1,
},
},
Expand All @@ -166,6 +168,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: 'integer',
format: 'int64',
maximum: 720,
},
},
Expand Down Expand Up @@ -216,6 +219,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: 'integer',
format: 'int32',
allOf: [{ maximum: 10 }, { maximum: 25 }],
},
},
Expand All @@ -240,6 +244,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: 'integer',
format: 'int32',
allOf: [
{
oneOf: [{ minimum: 0 }, { minimum: 3 }],
Expand Down Expand Up @@ -300,6 +305,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: 'integer',
format: 'int64',
allOf: [{ minimum: 10 }, { maximum: 9 }],
},
},
Expand Down Expand Up @@ -328,6 +334,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
in: 'query',
schema: {
type: ['integer'],
format: 'int32',
minimum: 16,
maximum: 15,
},
Expand Down Expand Up @@ -404,6 +411,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
properties: {
size: {
type: 'integer',
format: 'int32',
description: 'no validation',
},
},
Expand Down Expand Up @@ -437,6 +445,7 @@ describe(`Spectral rule: ${ruleId}`, () => {
] = {
schema: {
type: 'integer',
format: 'int32',
description: 'no validation',
},
};
Expand Down Expand Up @@ -464,5 +473,97 @@ describe(`Spectral rule: ${ruleId}`, () => {
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
it('Integer schema int32 minimum is out of safe range', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
format: 'int32',
minimum: -9007199254740991,
maximum: 2147483649,
},
},
];

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(2);

const expectedPath = 'paths./v1/movies.post.parameters.0.schema';

const expectedMessages = [
`'minimum' value is out of safe range`,
`'maximum' value is out of safe range`,
];

for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessages[i]);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPath);
}
});
it('Integer schema int64 minimum is out of safe range', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
format: 'int64',
minimum: -9007199254740992,
maximum: 9007199254740992,
},
},
];

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(2);

const expectedPath = 'paths./v1/movies.post.parameters.0.schema';

const expectedMessages = [
`'minimum' value is out of safe range`,
`'maximum' value is out of safe range`,
];

for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessages[i]);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPath);
}
});
it('Integer schema doesnt have format defined', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.paths['/v1/movies'].post.parameters = [
{
name: 'max_movie_length',
in: 'query',
schema: {
type: 'integer',
minimum: 2,
maximum: 3,
},
},
];

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(1);

const expectedPath = 'paths./v1/movies.post.parameters.0.schema';

const expectedMessage = 'Integer schemas should specify format as one of';

for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toContain(expectedMessage);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPath);
}
});
});
});
1 change: 1 addition & 0 deletions packages/ruleset/test/test-utils/root-document.js
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,7 @@ module.exports = {
},
status_code: {
type: 'integer',
format: 'int32',
minimum: 0,
maximum: 599,
description: 'The HTTP status code.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ components:
type: string
limit:
type: integer
format: int32
minimum: 1
maximum: 100
description: for pagination
offset:
type: integer
format: int32
minimum: 0
maximum: 1024000
description: for pagination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,13 @@ components:
type: string
limit:
type: integer
format: int32
minimum: 1
maximum: 100
description: for pagination
offset:
type: integer
format: int32
minimum: 0
maximum: 1024000
description: for pagination
Expand Down