From 6c5a4608570ab1b199212b28878446fe332fac61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Musil?= Date: Tue, 15 Aug 2023 17:10:40 +0200 Subject: [PATCH] feat: OneClient throws validation error --- core/core/src/mock.rs | 11 +++++++- core/core/src/sf_core.rs | 8 +++--- examples/node_js/index.mjs | 4 ++- examples/python/__main__.py | 4 ++- host/javascript/README.md | 5 ++++ host/javascript/src/common/app.test.ts | 26 ++++++++++++++---- host/javascript/src/common/app.ts | 12 +++++--- host/javascript/src/common/error.ts | 6 ++++ host/python/README.md | 2 ++ host/python/src/one_sdk/__init__.py | 2 +- host/python/src/one_sdk/app.py | 7 +++-- host/python/src/one_sdk/error.py | 4 +++ host/python/tests/test_app.py | 38 ++++++++++++++++++++++++++ 13 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 host/python/tests/test_app.py diff --git a/core/core/src/mock.rs b/core/core/src/mock.rs index 05741664..8df0a1d4 100644 --- a/core/core/src/mock.rs +++ b/core/core/src/mock.rs @@ -3,9 +3,11 @@ //! Use usecase to request some behaviour for perform: //! - CORE_PERFORM_PANIC //! - CORE_PERFORM_TRUE +//! - CORE_PERFORM_INPUT_VALIDATION_ERROR use sf_std::unstable::{ - perform::{set_perform_output_result_in, PerformInput}, + exception::{PerformException, PerformExceptionErrorCode}, + perform::{set_perform_output_exception_in, set_perform_output_result_in, PerformInput}, HostValue, }; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; @@ -43,6 +45,13 @@ pub fn __export_oneclient_core_perform() { "CORE_PERFORM_TRUE" => { set_perform_output_result_in(HostValue::Bool(true), MessageExchangeFfi) } + "CORE_PERFORM_INPUT_VALIDATION_ERROR" => set_perform_output_exception_in( + PerformException { + error_code: PerformExceptionErrorCode::InputValidationError, + message: "Test validation error".to_string(), + }, + MessageExchangeFfi, + ), _ => panic!("Unknown usecase: {}", perform_input.usecase), }; } diff --git a/core/core/src/sf_core.rs b/core/core/src/sf_core.rs index 9be72031..349bb09b 100644 --- a/core/core/src/sf_core.rs +++ b/core/core/src/sf_core.rs @@ -55,6 +55,8 @@ pub struct OneClientCore { } impl OneClientCore { const MAP_STDLIB_JS: &str = include_str!("../assets/js/map_std.js"); + const SECURITY_VALUES_JSON_SCHEMA: &str = + include_str!("../assets/schemas/security_values.json"); // TODO: Use thiserror and define specific errors pub fn new(config: &CoreConfiguration) -> anyhow::Result { @@ -67,10 +69,8 @@ impl OneClientCore { provider_cache: DocumentCache::new(config.cache_duration, config.registry_url.clone()), map_cache: DocumentCache::new(config.cache_duration, config.registry_url.clone()), security_validator: JsonSchemaValidator::new( - &serde_json::Value::from_str(include_str!( - "../assets/schemas/security_values.json" - )) - .expect("Valid JSON"), + &serde_json::Value::from_str(&OneClientCore::SECURITY_VALUES_JSON_SCHEMA) + .expect("Valid JSON"), ) .expect("Valid JSON Schema for security values exists"), mapstd_config: MapStdImplConfig { diff --git a/examples/node_js/index.mjs b/examples/node_js/index.mjs index 85b72c35..386fa2ed 100644 --- a/examples/node_js/index.mjs +++ b/examples/node_js/index.mjs @@ -1,5 +1,5 @@ import { createServer } from 'http'; -import { OneClient, PerformError, UnexpectedError } from '../../host/javascript/node/index.js'; +import { OneClient, PerformError, UnexpectedError, ValidationError } from '../../host/javascript/node/index.js'; async function startLocalhostServer() { const server = createServer((req, res) => { @@ -45,6 +45,8 @@ try { } catch (e) { if (e instanceof PerformError) { console.log('ERROR RESULT:', e.errorResult); + } else if (e instanceof ValidationError) { + console.error('VALIDATION ERROR:', e.message); } else if (e instanceof UnexpectedError) { console.error('ERROR:', e); } else { diff --git a/examples/python/__main__.py b/examples/python/__main__.py index 2bb0cc9a..5b76964d 100644 --- a/examples/python/__main__.py +++ b/examples/python/__main__.py @@ -4,7 +4,7 @@ import sys from http.server import BaseHTTPRequestHandler, HTTPServer -from one_sdk import OneClient, PerformError, UnexpectedError +from one_sdk import OneClient, PerformError, ValidationError, UnexpectedError class MyServer(BaseHTTPRequestHandler): def do_GET(self): @@ -41,6 +41,8 @@ def do_GET(self): print(f"RESULT: {r}") except PerformError as e: print(f"ERROR RESULT: {e.error_result}") +except ValidationError as e: + print(f"INVALID INPUT: {e.message}", file = sys.stderr) except UnexpectedError as e: print(f"ERROR:", e, file = sys.stderr) finally: diff --git a/host/javascript/README.md b/host/javascript/README.md index 2fed9e2a..fa6ca3cf 100644 --- a/host/javascript/README.md +++ b/host/javascript/README.md @@ -78,6 +78,7 @@ import { OneClient, PerformError, UnexpectedError, + ValidationError, } from '@superfaceai/one-sdk/node'; const client = new OneClient(); @@ -103,9 +104,13 @@ try { }, } ); + + console.log('RESULT:', result); } catch (e) { if (e instanceof PerformError) { console.log('ERROR RESULT:', e.errorResult); + } else if (e instanceof ValidationError) { + console.error('VALIDATION ERROR:', e.message); } else if (e instanceof UnexpectedError) { console.error('ERROR:', e); } else { diff --git a/host/javascript/src/common/app.test.ts b/host/javascript/src/common/app.test.ts index 2152108e..b58ea501 100644 --- a/host/javascript/src/common/app.test.ts +++ b/host/javascript/src/common/app.test.ts @@ -5,8 +5,8 @@ import { createRequire } from 'module'; import { WASI } from 'wasi'; import { App } from './app.js'; -import { UnexpectedError } from './error.js'; -import { FileSystem, Persistence, Network, TextCoder, Timers, WasiContext } from './interfaces.js'; +import { UnexpectedError, ValidationError } from './error.js'; +import { FileSystem, Persistence, Network, TextCoder, Timers } from './interfaces.js'; class TestNetwork implements Network { @@ -52,8 +52,8 @@ class TestTimers implements Timers { } class TestPersistence implements Persistence { - async persistMetrics(events: string[]): Promise {} - async persistDeveloperDump(events: string[]): Promise {} + async persistMetrics(events: string[]): Promise { } + async persistDeveloperDump(events: string[]): Promise { } } describe('App', () => { @@ -73,7 +73,7 @@ describe('App', () => { await readFile(createRequire(import.meta.url).resolve('../../assets/test-core-async.wasm')) ); - await app.init(new WASI()); + await app.init(new WASI({ version: 'preview1' } as any)); handleMessage = jest.spyOn(app, 'handleMessage'); handleMessage.mockImplementation(async (message) => { @@ -130,7 +130,7 @@ describe('App', () => { ); } catch (e) { } - await app.init(new WASI()); + await app.init(new WASI({ version: 'preview1' } as any)); const result = await app.perform( '', @@ -144,4 +144,18 @@ describe('App', () => { expect(result).toBe(true); }); + + test('invalid user input', async () => { + handleMessage.mockRestore(); + + await expect(app.perform( + '', + '', + '', + 'CORE_PERFORM_INPUT_VALIDATION_ERROR', + null, + {}, + {}, + )).rejects.toThrowError(ValidationError); + }); }); \ No newline at end of file diff --git a/host/javascript/src/common/app.ts b/host/javascript/src/common/app.ts index 0b4b0d58..c2872cd2 100644 --- a/host/javascript/src/common/app.ts +++ b/host/javascript/src/common/app.ts @@ -1,7 +1,7 @@ import { Asyncify } from './asyncify.js'; import { HandleMap } from './handle_map.js'; -import { PerformError, UnexpectedError, UninitializedError, WasiErrno, WasiError } from './error.js'; +import { PerformError, UnexpectedError, UninitializedError, ValidationError, WasiErrno, WasiError } from './error.js'; import { AppContext, FileSystem, Network, TextCoder, Timers, WasiContext, Persistence } from './interfaces.js'; import { SecurityValuesMap } from './security.js'; import * as sf_host from './sf_host.js'; @@ -239,7 +239,7 @@ export class App implements AppContext { } /** - * @throws {PerformError | UnexpectedError} + * @throws {PerformError | ValidationError | UnexpectedError} */ public async perform( profileUrl: string, @@ -273,7 +273,7 @@ export class App implements AppContext { } public async handleMessage(message: any): Promise { - switch (message['kind']) { + switch (message.kind) { case 'perform-input': return { kind: 'ok', @@ -295,7 +295,11 @@ export class App implements AppContext { return { kind: 'ok' }; case 'perform-output-exception': - this.performState!.exception = new UnexpectedError(message.exception.error_code, message.exception.message); + if (message.exception.error_code === "InputValidationError") { + this.performState!.exception = new ValidationError(message.exception.message); + } else { + this.performState!.exception = new UnexpectedError(message.exception.error_code, message.exception.message); + } return { kind: 'ok' }; case 'file-open': { diff --git a/host/javascript/src/common/error.ts b/host/javascript/src/common/error.ts index 7185c274..0b9ed68f 100644 --- a/host/javascript/src/common/error.ts +++ b/host/javascript/src/common/error.ts @@ -11,6 +11,12 @@ export class PerformError extends BaseError { } } +export class ValidationError extends BaseError { + constructor(message: string) { + super(ValidationError.name, message); + } +} + export class UnexpectedError extends BaseError { constructor(name: string, message: string) { super(name, message); diff --git a/host/python/README.md b/host/python/README.md index 0afef92f..812b2bfc 100644 --- a/host/python/README.md +++ b/host/python/README.md @@ -101,6 +101,8 @@ try: print(f"RESULT: {r}") except PerformError as e: print(f"ERROR RESULT: {e.error_result}") +except ValidationError as e: + print(f"INVALID INPUT: {e.message}", file = sys.stderr) except UnexpectedError as e: print(f"ERROR:", e, file = sys.stderr) finally: diff --git a/host/python/src/one_sdk/__init__.py b/host/python/src/one_sdk/__init__.py index 103019ef..cd3fb443 100644 --- a/host/python/src/one_sdk/__init__.py +++ b/host/python/src/one_sdk/__init__.py @@ -1,2 +1,2 @@ from one_sdk.client import OneClient -from one_sdk.error import UnexpectedError, PerformError +from one_sdk.error import UnexpectedError, ValidationError, PerformError diff --git a/host/python/src/one_sdk/app.py b/host/python/src/one_sdk/app.py index d66587d0..4f3c7f94 100644 --- a/host/python/src/one_sdk/app.py +++ b/host/python/src/one_sdk/app.py @@ -10,7 +10,7 @@ from one_sdk.handle_map import HandleMap from one_sdk.sf_host import Ptr, Size, link as sf_host_link -from one_sdk.error import HostError, ErrorCode, PerformError, UnexpectedError, UninitializedError, WasiError, WasiErrno +from one_sdk.error import HostError, ErrorCode, PerformError, ValidationError, UnexpectedError, UninitializedError, WasiError, WasiErrno from one_sdk.platform import PythonFilesystem, PythonNetwork, PythonPersistence, DeferredHttpResponse, HttpResponse # TODO: TypeAlias - needs 3.10 @@ -133,7 +133,10 @@ def handle_message(self, message: Any) -> Any: self._perform_state.error = PerformError(message["error"]) return { "kind": "ok" } elif message["kind"] == "perform-output-exception": - self._perform_state.exception = UnexpectedError(message["exception"]["error_code"], message["exception"]["message"]) + if message["exception"]["error_code"] == "InputValidationError": + self._perform_state.exception = ValidationError(message["exception"]["message"]) + else: + self._perform_state.exception = UnexpectedError(message["exception"]["error_code"], message["exception"]["message"]) return { "kind": "ok" } elif message["kind"] == "file-open": try: diff --git a/host/python/src/one_sdk/error.py b/host/python/src/one_sdk/error.py index d14210a6..5dd5e204 100644 --- a/host/python/src/one_sdk/error.py +++ b/host/python/src/one_sdk/error.py @@ -15,6 +15,10 @@ def __init__(self, error_result: Any): super().__init__("PerformError", str(error_result)) self.error_result = error_result +class ValidationError(BaseError): + def __init__(self, message: str): + super().__init__("ValidationError", message) + class UnexpectedError(BaseError): def __init__(self, name: str, message: str): super().__init__(name, message) diff --git a/host/python/tests/test_app.py b/host/python/tests/test_app.py new file mode 100644 index 00000000..795ee594 --- /dev/null +++ b/host/python/tests/test_app.py @@ -0,0 +1,38 @@ +import os +import unittest + +from one_sdk import ValidationError +from one_sdk.app import WasiApp +from one_sdk.platform import PythonFilesystem, PythonNetwork, PythonPersistence + +class TestApp(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.app = WasiApp( + filesystem = PythonFilesystem, + network = PythonNetwork, + persistence = PythonPersistence + ) + + with open(os.path.abspath(os.path.join(__file__, "../../src/one_sdk/assets/test-core.wasm")), "rb") as file: + cls.app.load_core(file.read()) + cls.app.init() + + @classmethod + def tearDownClass(cls): + cls.app.destroy() + + def test_invalid_user_input(self): + with self.assertRaises(ValidationError): + self.app.perform( + profile_url = '', + provider_url = '', + map_url = '', + usecase = 'CORE_PERFORM_INPUT_VALIDATION_ERROR', + input = {}, + parameters = {}, + security = {} + ) + +if __name__ == '__main__': + unittest.main()