From 597202e2b8364f1507674ceca81c741dc4dcedaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Musil?= Date: Tue, 14 Feb 2023 13:36:40 +0100 Subject: [PATCH] fix: input validation to work with None values --- CHANGELOG.md | 1 + src/core/events/reporter/reporter.test.ts | 20 ++++---- .../profile-parameter-validator.test.ts | 50 +++++++++++++++++++ .../profile-parameter-validator.ts | 27 +++++++--- 4 files changed, 81 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 875e7644..2a15f5c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed - Correctly handle objects with `null` prototype in `isClassInstance` - [#333](https://github.com/superfaceai/one-sdk-js/pull/333) +- Treat not defined input in profile as nullable data structure - [#334](https://github.com/superfaceai/one-sdk-js/pull/334) ## [2.3.0] - 2023-02-07 ### Changed diff --git a/src/core/events/reporter/reporter.test.ts b/src/core/events/reporter/reporter.test.ts index 6fcbc820..7c635c28 100644 --- a/src/core/events/reporter/reporter.test.ts +++ b/src/core/events/reporter/reporter.test.ts @@ -335,7 +335,7 @@ describe('MetricReporter', () => { const profile = await client.getProfile('test-profile'); - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(2000); while (await eventEndpoint.isPending()) { await new Promise(setImmediate); @@ -382,7 +382,9 @@ describe('MetricReporter', () => { const profile = await client.getProfile('test-profile'); - await expect(profile.getUseCase('Test').perform({})).rejects.toThrow(); + await expect( + profile.getUseCase('Test').perform(undefined) + ).rejects.toThrow(); client.timers.tick(2000); let requests = await eventEndpoint.getSeenRequests(); while (requests.length < 2) { @@ -460,7 +462,7 @@ describe('MetricReporter', () => { const profile = await client.getProfile('test-profile'); - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(800); while (await eventEndpoint.isPending()) { await new Promise(setImmediate); @@ -496,8 +498,8 @@ describe('MetricReporter', () => { ); const profile = await client.getProfile('test-profile'); - await profile.getUseCase('Test').perform({}); - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(2000); let requests = await eventEndpoint.getSeenRequests(); @@ -544,14 +546,14 @@ describe('MetricReporter', () => { ); const profile = await client.getProfile('test-profile'); - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(2000); let requests = await eventEndpoint.getSeenRequests(); while (requests.length < 1) { await new Promise(setImmediate); requests = await eventEndpoint.getSeenRequests(); } - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(1000); while (requests.length < 2) { await new Promise(setImmediate); @@ -618,7 +620,7 @@ describe('MetricReporter', () => { const profile = await client.getProfile('test-profile'); for (let i = 0; i < 100; i++) { - await profile.getUseCase('Test').perform({}); + await profile.getUseCase('Test').perform(undefined); client.timers.tick(900); currentTime = currentTime.valueOf() + 900; } @@ -696,7 +698,7 @@ describe('MetricReporter', () => { ); const profile = await client.getProfile('test-profile'); - void profile.getUseCase('Test').perform({}); + void profile.getUseCase('Test').perform(undefined); let requests = await eventEndpoint.getSeenRequests(); while (requests.length < 2) { currentTime += 0.1; diff --git a/src/core/interpreter/profile-parameter-validator.test.ts b/src/core/interpreter/profile-parameter-validator.test.ts index 0ce12de3..28c435b2 100644 --- a/src/core/interpreter/profile-parameter-validator.test.ts +++ b/src/core/interpreter/profile-parameter-validator.test.ts @@ -857,6 +857,7 @@ describe('ProfileParameterValidator', () => { }); it('returns ok for valid input data', () => { + parameterValidator = new ProfileParameterValidator(ast); expect( parameterValidator .validate( @@ -874,6 +875,55 @@ describe('ProfileParameterValidator', () => { .isOk() ).toBeTruthy(); }); + + it("returns error if input isn't passed", () => { + expect( + parameterValidator.validate(null, 'input', 'Test').isErr() + ).toBeTruthy(); + }); + + describe('profile without input defined', () => { + const astNoInput = parseProfileFromSource(` + usecase Test { + } + `); + + beforeEach(() => { + parameterValidator = new ProfileParameterValidator(astNoInput); + }); + + it('returns ok if null passed', () => { + expect( + parameterValidator.validate(null, 'input', 'Test').isOk() + ).toBeTruthy(); + }); + + it('returns ok if non primitive value without additional fields is passed', () => { + expect( + parameterValidator.validate({}, 'input', 'Test').isOk() + ).toBeTruthy(); + }); + + it('returns error if non primitive value with additional fields is passed', () => { + expect( + parameterValidator + .validate( + { + additionalField: 'value', + }, + 'input', + 'Test' + ) + .isErr() + ).toBeTruthy(); + }); + + it('returns error if primitive value is passed', () => { + expect( + parameterValidator.validate('string', 'input', 'Test').isErr() + ).toBeTruthy(); + }); + }); }); describe('result validation', () => { diff --git a/src/core/interpreter/profile-parameter-validator.ts b/src/core/interpreter/profile-parameter-validator.ts index 41a152bd..04ba268e 100644 --- a/src/core/interpreter/profile-parameter-validator.ts +++ b/src/core/interpreter/profile-parameter-validator.ts @@ -34,7 +34,7 @@ import type { ProfileParameterError, } from '../../interfaces'; import type { Result } from '../../lib'; -import { err, isNone, ok, UnexpectedError } from '../../lib'; +import { err, isNone, isNonPrimitive, ok, UnexpectedError } from '../../lib'; import type { ProfileVisitor } from './interfaces'; import type { ValidationError } from './profile-parameter-validator.errors'; import { @@ -581,20 +581,31 @@ export class ProfileParameterValidator implements ProfileVisitor { kind: ProfileParameterKind, usecase: string ): ValidationFunction { - if (kind === 'input' && node.input) { - return addPath(this.visit(node.input.value, kind, usecase), 'input'); + if (kind === 'input' && node.input !== undefined) { + return addPath((input): ValidationResult => { + if (isNone(input)) { + return [false, [{ kind: 'nullInNonNullable' }]]; + } + + if (node.input !== undefined) { + return this.visit(node.input.value, kind, usecase)(input); + } + + return [true]; + }, 'input'); } if (kind === 'result' && node.result) { return addPath(this.visit(node.result.value, kind, usecase), 'result'); } + // input or result isn't defined return (input: unknown): ValidationResult => { - if ( - typeof input === 'undefined' || - (typeof input === 'object' && - (input === null || Object.keys(input).length === 0)) - ) { + if (isNone(input)) { + return [true]; + } + + if (isNonPrimitive(input) && Object.keys(input).length === 0) { return [true]; }