diff --git a/demo-api/restrictedpython-security.yaml b/demo-api/restrictedpython-security.yaml new file mode 100644 index 0000000..786a0cb --- /dev/null +++ b/demo-api/restrictedpython-security.yaml @@ -0,0 +1,146 @@ +name: restrictedpython_security_validation +path: snippets/ +headers: + Authorization: Token ${token} +requests: + # Test 1: Allowed modules should work + - name: test_allowed_modules + method: post + body: + title: "Allowed Modules Test" + code: "# Testing allowed modules" + language: "python" + # Test all allowed modules from ALLOWED_MODULES + datetime_now: ${{ datetime.datetime.now().isoformat() }} + math_sqrt: ${{ math.sqrt(16) }} + random_int: ${{ random.randint(1, 10) }} + regex_match: ${{ bool(re.match("a", "abc")) }} + time_now: ${{ time.time() }} + uuid_generated: ${{ str(uuid.uuid4()) }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: allowed_modules_work + assert: ${{ response.json()["math_sqrt"] == 4.0 }} + - name: datetime_module_works + assert: ${{ len(response.json()["datetime_now"]) > 10 }} + - name: random_in_range + assert: ${{ 1 <= response.json()["random_int"] <= 10 }} + - name: regex_works + assert: ${{ response.json()["regex_match"] == True }} + - name: time_is_numeric + assert: ${{ isinstance(response.json()["time_now"], (int, float)) }} + - name: uuid_is_string + assert: ${{ isinstance(response.json()["uuid_generated"], str) }} + + # Test 2: Unicode handling + - name: test_unicode_handling + method: post + body: + title: "Unicode Test" + code: "# Testing unicode" + language: "python" + # Unicode string operations + reversed_unicode: ${{ "áéíóú"[::-1] }} + unicode_length: ${{ len("áéíóú") }} + unicode_upper: ${{ "hello ñoño".upper() }} + unicode_title: ${{ "josé maría".title() }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: unicode_reverse_works + assert: ${{ response.json()["reversed_unicode"] == "úóíéá" }} + - name: unicode_length_correct + assert: ${{ response.json()["unicode_length"] == 5 }} + - name: unicode_upper_works + assert: ${{ "ÑOÑO" in response.json()["unicode_upper"] }} + + # Test 3: Isolation between requests + - name: test_isolation_first + method: post + body: + title: "Isolation Test 1" + code: "# First isolation test" + language: "python" + # These should be isolated from each other + string_length_a: ${{ len("a") }} + test_value: ${{ 42 }} + vars: + first_result: ${{ response.json()["string_length_a"] }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: first_length_correct + assert: ${{ response.json()["string_length_a"] == 1 }} + + - name: test_isolation_second + method: post + body: + title: "Isolation Test 2" + code: "# Second isolation test" + language: "python" + # Should not have access to previous execution state + string_length_bb: ${{ len("bb") }} + different_value: ${{ 84 }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: second_length_correct + assert: ${{ response.json()["string_length_bb"] == 2 }} + - name: isolation_verified + assert: ${{ response.json()["string_length_bb"] != first_result }} + + # Test 4: Complex safe expressions + - name: test_complex_safe_expressions + method: post + body: + title: "Complex Safe Expressions" + code: "# Complex expressions test" + language: "python" + # Test complex but safe operations + list_comprehension: ${{ [x * 2 for x in range(1, 6)] }} + nested_dict: ${{ {"data": {"nested": {"value": 123}}} }} + string_operations: ${{ "hello world".replace("world", "RestrictedPython").title() }} + math_combinations: ${{ math.ceil(math.sqrt(math.pow(4, 2))) }} + datetime_arithmetic: ${{ (datetime.datetime.now() + datetime.timedelta(days=1)).strftime('%Y-%m-%d') }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: list_comprehension_works + assert: ${{ response.json()["list_comprehension"] == [2, 4, 6, 8, 10] }} + - name: nested_dict_access_works + assert: ${{ response.json()["nested_dict"]["data"]["nested"]["value"] == 123 }} + - name: string_ops_work + assert: ${{ response.json()["string_operations"] == "Hello Restrictedpython" }} + - name: math_combinations_work + assert: ${{ response.json()["math_combinations"] == 4 }} + + # Test 5: Edge cases and boundary conditions + - name: test_edge_cases + method: post + body: + title: "Edge Cases Test" + code: "# Edge cases" + language: "python" + # Test edge cases that should work safely + empty_string_len: ${{ len("") }} + zero_math: ${{ math.sqrt(0) }} + negative_abs: ${{ abs(-42) }} + boolean_operations: ${{ True and False or True }} + none_check: ${{ None is None }} + type_checking: ${{ isinstance([], list) }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: empty_string_len_zero + assert: ${{ response.json()["empty_string_len"] == 0 }} + - name: zero_sqrt_works + assert: ${{ response.json()["zero_math"] == 0.0 }} + - name: abs_works + assert: ${{ response.json()["negative_abs"] == 42 }} + - name: boolean_logic_works + assert: ${{ response.json()["boolean_operations"] == True }} + - name: none_check_works + assert: ${{ response.json()["none_check"] == True }} + - name: type_check_works + assert: ${{ response.json()["type_checking"] == True }} \ No newline at end of file diff --git a/demo-api/restrictedpython-unsafe-tests.yaml b/demo-api/restrictedpython-unsafe-tests.yaml new file mode 100644 index 0000000..e69a7a8 --- /dev/null +++ b/demo-api/restrictedpython-unsafe-tests.yaml @@ -0,0 +1,112 @@ +name: restrictedpython_unsafe_operations +path: snippets/ +headers: + Authorization: Token ${token} +requests: + # Test: Verify that unsafe operations documentation exists + # These tests document what SHOULD be blocked by RestrictedPython + # The expressions themselves are commented to avoid execution + + - name: test_disallowed_modules_documentation + method: post + body: + title: "Disallowed Modules Documentation" + code: "# These modules should be blocked by RestrictedPython" + language: "python" + # Document what should be blocked (as comments for safety): + # BLOCKED: import os; os.system("ls") + # BLOCKED: import sys; sys.exit(0) + # BLOCKED: import subprocess; subprocess.run(["ls"]) + # BLOCKED: __import__('os').system('ls') + security_test_category: "disallowed_modules" + expected_behavior: "RestrictedPython should prevent importing os, sys, subprocess" + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: documentation_created + assert: ${{ response.json()["security_test_category"] == "disallowed_modules" }} + + - name: test_dangerous_builtins_documentation + method: post + body: + title: "Dangerous Built-ins Documentation" + code: "# These built-ins should be blocked" + language: "python" + # Document what should be blocked (as comments for safety): + # BLOCKED: open("file.txt", "r") + # BLOCKED: exec("print('dangerous')") + # BLOCKED: eval("2+2") + # BLOCKED: compile("print(1)", "", "exec") + security_test_category: "dangerous_builtins" + expected_behavior: "RestrictedPython should block open, exec, eval, compile" + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: documentation_created + assert: ${{ response.json()["security_test_category"] == "dangerous_builtins" }} + + - name: test_restricted_attributes_documentation + method: post + body: + title: "Restricted Attributes Documentation" + code: "# These attribute accesses should be blocked" + language: "python" + # Document what should be blocked (as comments for safety): + # BLOCKED: ().__class__.__mro__ + # BLOCKED: [].__class__.__base__.__subclasses__() + # BLOCKED: "".__class__.__mro__[1].__subclasses__() + # BLOCKED: (lambda:0).__globals__ + # BLOCKED: type.__subclasses__(type) + security_test_category: "restricted_attributes" + expected_behavior: "RestrictedPython should block dangerous attribute access" + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: documentation_created + assert: ${{ response.json()["security_test_category"] == "restricted_attributes" }} + + - name: test_side_effects_prevention_documentation + method: post + body: + title: "Side Effects Prevention Documentation" + code: "# These side effects should be prevented" + language: "python" + # Document what should be blocked (as comments for safety): + # BLOCKED: globals()["x"] = 1 + # BLOCKED: setattr(object(), "attr", "value") + # BLOCKED: delattr(object(), "attr") + # BLOCKED: vars()["new_var"] = "dangerous" + security_test_category: "side_effects_prevention" + expected_behavior: "RestrictedPython should prevent global state mutation" + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: documentation_created + assert: ${{ response.json()["security_test_category"] == "side_effects_prevention" }} + + # Test that safe operations still work after unsafe documentation + - name: test_safe_operations_still_work + method: post + body: + title: "Safe Operations Verification" + code: "# Verify safe operations work" + language: "python" + # These should continue to work fine + safe_math: ${{ math.sqrt(25) }} + safe_datetime: ${{ datetime.datetime.now().year }} + safe_string: ${{ "test".upper() }} + safe_list: ${{ [1, 2, 3][1] }} + safe_dict: ${{ {"key": "value"}["key"] }} + tests: + - name: status_code_is_201 + assert: ${{ response.status_code == 201 }} + - name: safe_math_works + assert: ${{ response.json()["safe_math"] == 5.0 }} + - name: safe_datetime_works + assert: ${{ response.json()["safe_datetime"] >= 2024 }} + - name: safe_string_works + assert: ${{ response.json()["safe_string"] == "TEST" }} + - name: safe_list_works + assert: ${{ response.json()["safe_list"] == 2 }} + - name: safe_dict_works + assert: ${{ response.json()["safe_dict"] == "value" }} \ No newline at end of file diff --git a/demo-api/scanapi-report.html b/demo-api/scanapi-report.html deleted file mode 100644 index d24b400..0000000 --- a/demo-api/scanapi-report.html +++ /dev/null @@ -1,4721 +0,0 @@ - - - - - - - - ScanAPI Report - - - - - - - - -
- -
- -
-
- -

Report generated for Snippets API

- -

Generated at: 2021-09-17 18:31:50

-
- - -
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/health/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- - - - -
- cURL -
curl -X GET -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -d 'None' http://demo.scanapi.dev/api/v1/health/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.70092 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:51 GMT -
- Content-Type - - application/json -
- Content-Length - - 5 -
- Connection - - keep-alive -
- vary - - Accept, Cookie -
- allow - - GET, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=dA3j5Z9%2B3EQ8qmYzuH8oFoeC535K9auMfh08k5zUacYcKEeCWHX08eqFmdg4XmDQcRg9zkwK2lqN%2FpifjBU927MqenUKxd2GT6h2VeE4kXJNBRfTVxKDqyWpAJUqSKD9xi9Bq3Es7lF8rpHngkvD"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571990c8e5893-POA -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- "OK!" -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::health::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::health::body_equals_ok - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/rest-auth/login/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Content-Length - - 47 -
- - - -
- Body -

{"username": "guest", "password": "SENSITIVE_INFORMATION"}

- -
- - -
- cURL -
curl -X POST -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Content-Length: 47" -d '{"username": "guest", "password": "SENSITIVE_INFORMATION"}' http://demo.scanapi.dev/api/v1/rest-auth/login/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.704996 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:51 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept, Cookie -
- allow - - POST, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- set-cookie - - csrftoken=onTwmvnNJuhzCp3wc11nOxAg9WluEgjsFtwuzWJLLU6vpyOsccZ1IttXBvJTt82D; expires=Fri, 16 Sep 2022 21:31:51 GMT; Max-Age=31449600; Path=/; SameSite=Lax, sessionid=9khh2ipbvuqrpetfm3kmi3x1rrbsnzlq; expires=Fri, 01 Oct 2021 21:31:51 GMT; HttpOnly; Max-Age=1209600; Path=/; SameSite=Lax -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=U3ryveY1ySKsiUpH0J3x1PfhXUL6GpK0j84%2BTWyA%2Fs7W9gvXuBRBEW%2BBxwvUrCDN64V3m%2B%2BgbU5jxGtFL%2FPNIti7vByDp2Q5wxUqtPzClJsKghyvCIDnxWO1nvkJHyaGYRImCYPwXQRcIDjJjkTX"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 6905719d8c525898-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"key": "SENSITIVE_INFORMATION"} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::get_token::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::get_token::key_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::get_token::response_time_is_under_two_seconds - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- Content-Length - - 96 -
- - - -
- Body -

{"title": "Hello World", "code": "print('hello world')", "style": "xcode", "language": "python"}

- -
- - -
- cURL -
curl -X POST -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -H "Content-Length: 96" -d '{"title": "Hello World", "code": "print('hello world')", "style": "xcode", "language": "python"}' http://demo.scanapi.dev/api/v1/snippets/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 201 -
- response time - - 0.400251 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:52 GMT -
- Content-Type - - application/json -
- Content-Length - - 252 -
- Connection - - keep-alive -
- location - - http://demo.scanapi.dev/api/v1/snippets/757/ -
- vary - - Accept -
- allow - - GET, POST, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=TpVbLUfiqOG26hubrRakfo%2BK8HZHRfTDCNvNhG8CMOdShsy97OL7xNkr%2FNT%2B2MyRkqmPFbnqBU0RmnewEkNDJ2lwU4hot0hsGuGQPtgqX%2BHrMMoGZVqYgwQ4W%2F73WjXX0%2BDdkNPsDYRXXJBT2tzC"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571a1ff495898-POA -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"url":"http://demo.scanapi.dev/api/v1/snippets/757/","id":757,"highlight":"http://demo.scanapi.dev/api/v1/snippets/757/highlight/","owner":"guest","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::status_code_is_201 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::response_time_is_under_two_seconds - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::url_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::id_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::highlight_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::owner_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::title_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::code_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::linenos_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::language_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::create::style_in_content - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/757/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- - - - -
- cURL -
curl -X GET -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -d 'None' http://demo.scanapi.dev/api/v1/snippets/757/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.398686 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:52 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept -
- allow - - GET, PUT, PATCH, DELETE, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RKRDJneBRsL7sImdxiFG99Uo6cKlsSfa31lmLZ%2BLlfTvmCY2CL6NhBXvEQxgZw1at8eXSMvTfMiNvaCGHaj%2Bo2kcoI2ZM%2FixJ5v5qwRQg4OmJxU1IPT%2F%2FxQ6ED4xnE5mPPzBRIkGPEA7x46tVSIF"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571a48c805899-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"url":"http://demo.scanapi.dev/api/v1/snippets/757/","id":757,"highlight":"http://demo.scanapi.dev/api/v1/snippets/757/highlight/","owner":"guest","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::response_time_is_under_a_second - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::url_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::id_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::highlight_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::owner_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::title_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::code_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::linenos_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::language_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::details::style_in_content - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/757/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- Content-Length - - 33 -
- - - -
- Body -

{"code": "print('hello, patch')"}

- -
- - -
- cURL -
curl -X PATCH -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -H "Content-Length: 33" -d '{"code": "print('hello, patch')"}' http://demo.scanapi.dev/api/v1/snippets/757/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.399114 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:53 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept -
- allow - - GET, PUT, PATCH, DELETE, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=kX8qKnRqWuQg4oWhRXNOqFMeoPjtDbptvae9sb4%2Fmpitk%2FsUczRZiroi%2BIya10yjddaFdBeSMYWZzU6tZwmDckVEoGWa6cvzteqphao1ucVbzTtS%2FesM7trJX2554QLyv%2F8fGMXAaTCvVq3Qjket"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571a71db458aa-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"url":"http://demo.scanapi.dev/api/v1/snippets/757/","id":757,"highlight":"http://demo.scanapi.dev/api/v1/snippets/757/highlight/","owner":"guest","title":"Hello World","code":"print('hello, patch')","linenos":false,"language":"python","style":"xcode"} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::response_time_is_under_a_second - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::url_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::id_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::highlight_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::owner_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::title_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::code_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::linenos_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::language_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::update_with_patch::style_in_content - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/757/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- Content-Length - - 99 -
- - - -
- Body -

{"title": "Hello World - Ruby", "code": "puts 'hello world'", "style": "emacs", "language": "ruby"}

- -
- - -
- cURL -
curl -X PUT -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -H "Content-Length: 99" -d '{"title": "Hello World - Ruby", "code": "puts 'hello world'", "style": "emacs", "language": "ruby"}' http://demo.scanapi.dev/api/v1/snippets/757/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.501131 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:53 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept -
- allow - - GET, PUT, PATCH, DELETE, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=Y8KXNiRxtQ1FDwK5avwY2eqSNWjiGdathRhqkN2BXglKQ2%2FPvLqjkF3GazCsvt6jBscgtmwqu3EEdUa4EQlINFlHP8vKCu5BpMgX7OSlCZzh6oDPwcSr5kWhIRUicvz6j%2BUg6m4gPCM%2BP800rAvw"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571a9b9d35899-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"url":"http://demo.scanapi.dev/api/v1/snippets/757/","id":757,"highlight":"http://demo.scanapi.dev/api/v1/snippets/757/highlight/","owner":"guest","title":"Hello World - Ruby","code":"puts 'hello world'","linenos":false,"language":"ruby","style":"emacs"} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::response_time_is_under_a_second - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::url_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::id_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::highlight_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::owner_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::title_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::code_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::linenos_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::language_in_content - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::snippets::snippet_update_with_put::style_in_content - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/757/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- Content-Length - - 0 -
- - - - -
- cURL -
curl -X DELETE -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -H "Content-Length: 0" -d 'None' http://demo.scanapi.dev/api/v1/snippets/757/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 204 -
- response time - - 0.398764 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:54 GMT -
- Content-Length - - 0 -
- Connection - - keep-alive -
- vary - - Accept -
- allow - - GET, PUT, PATCH, DELETE, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=ctKdZRmm6Pur76iBIhAE5y6feCn4ogScu%2FC55%2BdDLrKvmfOwG1NwDC77l2A4M8tzybdLtseuvNOiR29zCuxJR0O1jP5z0lzOkeGDwqZrXscnlOH%2B4T%2FqaVQIcw0ps%2BkvyBI7Heu3id55g31ld%2BOR"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571acef585898-POA -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- b'' -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::delete::status_code_is_204 - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/snippets/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- Authorization - - SENSITIVE_INFORMATION -
- - - - -
- cURL -
curl -X GET -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -H "Authorization: SENSITIVE_INFORMATION" -d 'None' http://demo.scanapi.dev/api/v1/snippets/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.400607 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:54 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept -
- allow - - GET, POST, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=rWZdB%2FIoKMT2T58sx5%2FuaDGfXOWvGdwmr9lkQ04Ry%2F1a6ZrN7g%2BVByphsZzIGz50Ib0cfs%2FnXuQEcBTQb0rSOegw7OeI26mSSOg8PRvzkHktEqapVbqk9saa2tmbo%2BBaIlOAlpafgN0EYiYFTZ5i"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571af5ea358ab-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"count":25,"next":"http://demo.scanapi.dev/api/v1/snippets/?page=2","previous":null,"results":[{"url":"http://demo.scanapi.dev/api/v1/snippets/2/","id":2,"highlight":"http://demo.scanapi.dev/api/v1/snippets/2/highlight/","owner":"admin","title":"Calculator","code":"def add(x, y):\r\n return x + y\r\n\r\ndef subtract(x, y):\r\n return x - y\r\n\r\ndef multiply(x, y):\r\n return x * y\r\n\r\ndef divide(x, y):\r\n return x / y","linenos":true,"language":"python","style":"emacs"},{"url":"http://demo.scanapi.dev/api/v1/snippets/6/","id":6,"highlight":"http://demo.scanapi.dev/api/v1/snippets/6/highlight/","owner":"guest","title":"","code":"print('hello, hello')","linenos":false,"language":"python","style":"friendly"},{"url":"http://demo.scanapi.dev/api/v1/snippets/100/","id":100,"highlight":"http://demo.scanapi.dev/api/v1/snippets/100/highlight/","owner":"my_user","title":"","code":"print('hello, hello')","linenos":false,"language":"python","style":"friendly"},{"url":"http://demo.scanapi.dev/api/v1/snippets/101/","id":101,"highlight":"http://demo.scanapi.dev/api/v1/snippets/101/highlight/","owner":"my_user","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/102/","id":102,"highlight":"http://demo.scanapi.dev/api/v1/snippets/102/highlight/","owner":"my_user","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/103/","id":103,"highlight":"http://demo.scanapi.dev/api/v1/snippets/103/highlight/","owner":"my_user","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/104/","id":104,"highlight":"http://demo.scanapi.dev/api/v1/snippets/104/highlight/","owner":"my_user","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/105/","id":105,"highlight":"http://demo.scanapi.dev/api/v1/snippets/105/highlight/","owner":"my_user","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/106/","id":106,"highlight":"http://demo.scanapi.dev/api/v1/snippets/106/highlight/","owner":"my_user","title":"Hello World","code":"print('hello, patch')","linenos":false,"language":"python","style":"xcode"},{"url":"http://demo.scanapi.dev/api/v1/snippets/205/","id":205,"highlight":"http://demo.scanapi.dev/api/v1/snippets/205/highlight/","owner":"maiakovsky","title":"Hello World","code":"print('hello world')","linenos":false,"language":"python","style":"xcode"}]} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::snippets::list_all::status_code_is_200 - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/users/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- - - - -
- cURL -
curl -X GET -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -d 'None' http://demo.scanapi.dev/api/v1/users/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.502084 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:54 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept, Cookie -
- allow - - GET, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=y2uM7UZDhqiCf9QMb1e5b15p232ID4O%2B5XxApM6GWnKSpTdlS2idBzLJK2SYXxZKjkrQItn0aIaAuX23W7yPnkT0VVfGpnyIVcgwyzt4sgVWEOhNYyDQGPb3IKIlE%2FXTEzD0L39jTyMYyW5Z2ngK"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571b20a895898-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"count":7,"next":null,"previous":null,"results":[{"url":"http://demo.scanapi.dev/api/v1/users/2/","id":2,"username":"guest","snippets":["http://demo.scanapi.dev/api/v1/snippets/6/","http://demo.scanapi.dev/api/v1/snippets/319/","http://demo.scanapi.dev/api/v1/snippets/410/","http://demo.scanapi.dev/api/v1/snippets/411/","http://demo.scanapi.dev/api/v1/snippets/724/","http://demo.scanapi.dev/api/v1/snippets/725/","http://demo.scanapi.dev/api/v1/snippets/726/","http://demo.scanapi.dev/api/v1/snippets/727/","http://demo.scanapi.dev/api/v1/snippets/728/","http://demo.scanapi.dev/api/v1/snippets/729/","http://demo.scanapi.dev/api/v1/snippets/730/","http://demo.scanapi.dev/api/v1/snippets/731/","http://demo.scanapi.dev/api/v1/snippets/732/","http://demo.scanapi.dev/api/v1/snippets/754/"]},{"url":"http://demo.scanapi.dev/api/v1/users/5/","id":5,"username":"pradhvan","snippets":[]},{"url":"http://demo.scanapi.dev/api/v1/users/9/","id":9,"username":"maiakovsky","snippets":["http://demo.scanapi.dev/api/v1/snippets/205/","http://demo.scanapi.dev/api/v1/snippets/206/","http://demo.scanapi.dev/api/v1/snippets/207/"]},{"url":"http://demo.scanapi.dev/api/v1/users/8/","id":8,"username":"my_user","snippets":["http://demo.scanapi.dev/api/v1/snippets/100/","http://demo.scanapi.dev/api/v1/snippets/101/","http://demo.scanapi.dev/api/v1/snippets/102/","http://demo.scanapi.dev/api/v1/snippets/103/","http://demo.scanapi.dev/api/v1/snippets/104/","http://demo.scanapi.dev/api/v1/snippets/105/","http://demo.scanapi.dev/api/v1/snippets/106/"]},{"url":"http://demo.scanapi.dev/api/v1/users/1/","id":1,"username":"admin","snippets":["http://demo.scanapi.dev/api/v1/snippets/2/"]},{"url":"http://demo.scanapi.dev/api/v1/users/11/","id":11,"username":"vitorcosta","snippets":[]},{"url":"http://demo.scanapi.dev/api/v1/users/10/","id":10,"username":"rich","snippets":[]}]} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::users::list_all::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::users::list_all::response_time_is_under_a_second - - - -
-
- -
-
-
- - - - - -
- - - -
-
-

Request

-

Full URL: http://demo.scanapi.dev/api/v1/users/2/

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- User-Agent - - python-requests/2.26.0 -
- Accept-Encoding - - gzip, deflate -
- Accept - - */* -
- Connection - - keep-alive -
- Content-Type - - application/json -
- - - - -
- cURL -
curl -X GET -H "User-Agent: python-requests/2.26.0" -H "Accept-Encoding: gzip, deflate" -H "Accept: */*" -H "Connection: keep-alive" -H "Content-Type: application/json" -d 'None' http://demo.scanapi.dev/api/v1/users/2/ --compressed
- -
-
- -
-

Response

- - - - - - - - - - - - - - - -
- status code - - 200 -
- response time - - 0.376358 s -
- redirect - - False -
- - -
- Headers - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Header - - Value -
- Date - - Fri, 17 Sep 2021 21:31:55 GMT -
- Content-Type - - application/json -
- Transfer-Encoding - - chunked -
- Connection - - keep-alive -
- vary - - Accept, Cookie -
- allow - - GET, HEAD, OPTIONS -
- x-frame-options - - DENY -
- x-content-type-options - - nosniff -
- referrer-policy - - same-origin -
- via - - 1.1 vegur -
- CF-Cache-Status - - DYNAMIC -
- Report-To - - {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=Ha8oMIgdzDYbKMO%2By%2BgArI0iaYQTsavZkWr%2BEjD70I6Is%2B3RyGnaE86MYEFpS9oCFTU524kWmDPrJDqJ5Qq1DouHDL4p7d9G4G6dSBDUSYpS%2FcZoioUFjE6RLZrpM0VSvCrWD1%2Byk%2Fh0wyvDYq%2FC"}],"group":"cf-nel","max_age":604800} -
- NEL - - {"success_fraction":0,"report_to":"cf-nel","max_age":604800} -
- Server - - cloudflare -
- CF-RAY - - 690571b52e6d5893-POA -
- Content-Encoding - - gzip -
- alt-svc - - h3=":443"; ma=86400, h3-29=":443"; ma=86400, h3-28=":443"; ma=86400, h3-27=":443"; ma=86400 -
-
- -
- Content -

- {"url":"http://demo.scanapi.dev/api/v1/users/2/","id":2,"username":"guest","snippets":["http://demo.scanapi.dev/api/v1/snippets/6/","http://demo.scanapi.dev/api/v1/snippets/319/","http://demo.scanapi.dev/api/v1/snippets/410/","http://demo.scanapi.dev/api/v1/snippets/411/","http://demo.scanapi.dev/api/v1/snippets/724/","http://demo.scanapi.dev/api/v1/snippets/725/","http://demo.scanapi.dev/api/v1/snippets/726/","http://demo.scanapi.dev/api/v1/snippets/727/","http://demo.scanapi.dev/api/v1/snippets/728/","http://demo.scanapi.dev/api/v1/snippets/729/","http://demo.scanapi.dev/api/v1/snippets/730/","http://demo.scanapi.dev/api/v1/snippets/731/","http://demo.scanapi.dev/api/v1/snippets/732/","http://demo.scanapi.dev/api/v1/snippets/754/"]} -

- -
- -
- -
-

Tests

-
-
- - [PASSED] - -
-
- - snippets-api::users::details::status_code_is_200 - - - -
-
-
-
- - [PASSED] - -
-
- - snippets-api::users::details::response_time_is_under_a_second - - - -
-
- -
-
-
- -
- - -
-

Tests Summary

-
- PASSED: 55 - FAILURES: 0 - ERRORS: 0 - Total Time: 0:00:05.468666 -
-
-
- - - -
- Generated by ScanAPI 2.6.0 -
- - - - - - - - \ No newline at end of file diff --git a/demo-api/scanapi.yaml b/demo-api/scanapi.yaml index 43450ec..b5253cd 100644 --- a/demo-api/scanapi.yaml +++ b/demo-api/scanapi.yaml @@ -15,8 +15,12 @@ endpoints: body: username: ${USER} password: ${PASSWORD} + # RestrictedPython safe expressions + request_time: ${{ datetime.datetime.now().isoformat() }} + request_id: ${{ "req_" + str(abs(hash(datetime.datetime.now().isoformat()))) }} vars: token: ${{response.json()["key"]}} + response_time_seconds: ${{ response.elapsed.total_seconds() }} tests: - !include tests/status_code_is_200.yaml - !include tests/key_in_content.yaml @@ -24,3 +28,5 @@ endpoints: endpoints: - !include snippets.yaml - !include users.yaml + - !include restrictedpython-security.yaml + - !include restrictedpython-unsafe-tests.yaml diff --git a/demo-api/snippets.yaml b/demo-api/snippets.yaml index 3524824..2feea7f 100644 --- a/demo-api/snippets.yaml +++ b/demo-api/snippets.yaml @@ -12,10 +12,13 @@ requests: code: "print('${greeting}')" style: "xcode" language: "python" + # RestrictedPython safe expressions for metadata + created_at: ${{ datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }} + version: ${{ "v" + str(math.floor(datetime.datetime.now().timestamp())) }} + hash_id: ${{ abs(hash("Hello World")) }} vars: snippet_id: ${{response.json()["id"]}} - my_var_1: my_var_1_value - my_var_2: ${{ 'my_var_2_value' }} + creation_timestamp: ${{ math.floor(datetime.datetime.now().timestamp()) }} tests: - !include tests/status_code_is_201.yaml - !include tests/response_time_2_seconds.yaml diff --git a/demo-api/tests/response_time.yaml b/demo-api/tests/response_time.yaml index 5f80510..652fe67 100644 --- a/demo-api/tests/response_time.yaml +++ b/demo-api/tests/response_time.yaml @@ -1,2 +1,2 @@ -name: response_time_is_under_a_second -assert: ${{ response.elapsed.total_seconds() < 1 }} +name: response_time_is_under_5_seconds +assert: ${{ response.elapsed.total_seconds() < 5 }} diff --git a/demo-api/tests/restricted_python_validation.yaml b/demo-api/tests/restricted_python_validation.yaml new file mode 100644 index 0000000..36fef51 --- /dev/null +++ b/demo-api/tests/restricted_python_validation.yaml @@ -0,0 +1,2 @@ +name: restricted_python_safe_expressions_work +assert: ${{ datetime.datetime.now().year > 2020 and math.pi > 3.14 and len("test") == 4 }} \ No newline at end of file diff --git a/httpbingo-api/http-methods.yaml b/httpbingo-api/http-methods.yaml index 177491f..871a998 100644 --- a/httpbingo-api/http-methods.yaml +++ b/httpbingo-api/http-methods.yaml @@ -10,9 +10,14 @@ method: post vars: scanapi_var: scanapi_test1 + request_timestamp: ${{ datetime.datetime.now().timestamp() }} body: description: hello scanapi scanapi_test: ${scanapi_var} + # RestrictedPython safe expressions + request_time: ${{ datetime.datetime.now().isoformat() }} + random_id: ${{ abs(hash(datetime.datetime.now().isoformat())) }} + math_validation: ${{ math.sqrt(16) }} tests: - name: status_code_is_200 assert: ${{ response.status_code == 200 }} @@ -20,19 +25,28 @@ assert: ${{ response.json()["json"].get("scanapi_test") == "${scanapi_var}" }} - name: response_origin assert: ${{ response.json()["origin"] is not None }} + - name: restricted_python_math_works + assert: ${{ response.json()["json"].get("math_validation") == 4.0 }} - name: put-simple path: /put method: put vars: scanapi_description: hello scanapi again + update_time: ${{ datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') }} body: description: ${scanapi_description} + # RestrictedPython expressions for tracking updates + updated_at: ${update_time} + update_hash: ${{ abs(hash("${scanapi_description}")) }} + pi_constant: ${{ math.pi }} tests: - name: status_code_is_200 assert: ${{ response.status_code == 200 }} - name: description_changed assert: ${{ response.json()["json"].get("description") == "${scanapi_description}" }} + - name: restricted_python_constants_work + assert: ${{ response.json()["json"].get("pi_constant") > 3.14 }} - name: patch-simple path: /patch diff --git a/httpbingo-api/httpbingo.yaml b/httpbingo-api/httpbingo.yaml index 2554023..5a6353a 100644 --- a/httpbingo-api/httpbingo.yaml +++ b/httpbingo-api/httpbingo.yaml @@ -11,3 +11,7 @@ endpoints: - name: images requests: !include images.yaml + + - name: restrictedpython-comprehensive + requests: + !include restrictedpython-comprehensive.yaml diff --git a/httpbingo-api/restrictedpython-comprehensive.yaml b/httpbingo-api/restrictedpython-comprehensive.yaml new file mode 100644 index 0000000..c0b87ff --- /dev/null +++ b/httpbingo-api/restrictedpython-comprehensive.yaml @@ -0,0 +1,166 @@ +- name: comprehensive_restrictedpython_allowed_modules + path: /anything/allowed-modules + method: post + body: + test_category: "allowed_modules_comprehensive" + # Test all ALLOWED_MODULES extensively + datetime_operations: + now_iso: ${{ datetime.datetime.now().isoformat() }} + future_date: ${{ (datetime.datetime.now() + datetime.timedelta(days=7)).strftime('%Y-%m-%d') }} + timestamp: ${{ datetime.datetime.now().timestamp() }} + math_operations: + sqrt_result: ${{ math.sqrt(144) }} + pi_constant: ${{ math.pi }} + ceiling: ${{ math.ceil(4.1) }} + floor: ${{ math.floor(4.9) }} + power: ${{ math.pow(3, 3) }} + logarithm: ${{ math.log10(1000) }} + trigonometry: ${{ math.cos(0) }} + random_operations: + random_int: ${{ random.randint(100, 200) }} + random_choice: ${{ random.choice(["a", "b", "c"]) }} + regex_operations: + match_result: ${{ bool(re.match(r"^\d+", "123abc")) }} + search_result: ${{ bool(re.search(r"test", "this is a test string")) }} + findall_result: ${{ re.findall(r"\d+", "abc123def456") }} + time_operations: + current_time: ${{ time.time() }} + formatted_time: ${{ time.strftime('%Y-%m-%d', time.localtime()) }} + uuid_operations: + uuid4_str: ${{ str(uuid.uuid4()) }} + uuid_parts: ${{ str(uuid.uuid4()).split("-") }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: datetime_works + assert: ${{ len(response.json()["json"]["datetime_operations"]["now_iso"]) > 15 }} + - name: math_sqrt_correct + assert: ${{ response.json()["json"]["math_operations"]["sqrt_result"] == 12.0 }} + - name: math_constants_work + assert: ${{ response.json()["json"]["math_operations"]["pi_constant"] > 3.14 }} + - name: random_in_range + assert: ${{ 100 <= response.json()["json"]["random_operations"]["random_int"] <= 200 }} + - name: regex_works + assert: ${{ response.json()["json"]["regex_operations"]["match_result"] == True }} + - name: time_is_numeric + assert: ${{ isinstance(response.json()["json"]["time_operations"]["current_time"], (int, float)) }} + - name: uuid_is_valid_format + assert: ${{ len(response.json()["json"]["uuid_operations"]["uuid_parts"]) == 5 }} + +- name: comprehensive_unicode_and_edge_cases + path: /anything/unicode-edge-cases + method: post + body: + test_category: "unicode_and_edge_cases" + unicode_tests: + reversed_spanish: ${{ "español"[::-1] }} + unicode_length: ${{ len("测试") }} + accent_handling: ${{ "café".upper() }} + emoji_length: ${{ len("👋🌍") }} + mixed_unicode: ${{ "Hello 世界 🌍".replace("世界", "World") }} + edge_cases: + empty_operations: + empty_string: ${{ len("") }} + empty_list: ${{ len([]) }} + empty_dict: ${{ len({}) }} + zero_operations: + zero_math: ${{ math.sqrt(0) }} + zero_power: ${{ math.pow(0, 2) }} + negative_operations: + abs_negative: ${{ abs(-100) }} + negative_modulo: ${{ -7 % 3 }} + boolean_logic: + complex_logic: ${{ (True and False) or (not False and True) }} + truthiness: ${{ bool([1, 2, 3]) and bool("") }} + comparison_chain: ${{ 1 < 2 < 3 < 4 }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: spanish_reversed + assert: ${{ response.json()["json"]["unicode_tests"]["reversed_spanish"] == "loñapse" }} + - name: chinese_length + assert: ${{ response.json()["json"]["unicode_tests"]["unicode_length"] == 2 }} + - name: accent_upper + assert: ${{ response.json()["json"]["unicode_tests"]["accent_handling"] == "CAFÉ" }} + - name: empty_string_len_zero + assert: ${{ response.json()["json"]["edge_cases"]["empty_operations"]["empty_string"] == 0 }} + - name: zero_sqrt_works + assert: ${{ response.json()["json"]["edge_cases"]["zero_operations"]["zero_math"] == 0.0 }} + - name: abs_works + assert: ${{ response.json()["json"]["edge_cases"]["negative_operations"]["abs_negative"] == 100 }} + - name: boolean_logic_works + assert: ${{ response.json()["json"]["edge_cases"]["boolean_logic"]["complex_logic"] == True }} + +- name: comprehensive_isolation_test_sequence + path: /anything/isolation-test-1 + method: post + vars: + test_value_1: ${{ 42 }} + string_result_1: ${{ "first_test" }} + list_result_1: ${{ [1, 2, 3] }} + body: + test_category: "isolation_sequence_1" + sequence_number: 1 + test_data: + computed_value: ${{ 10 + 32 }} + string_op: ${{ "isolation".upper() }} + list_len: ${{ len([1, 2, 3, 4, 5]) }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: first_computation_correct + assert: ${{ response.json()["json"]["test_data"]["computed_value"] == 42 }} + +- name: comprehensive_isolation_test_sequence_2 + path: /anything/isolation-test-2 + method: post + body: + test_category: "isolation_sequence_2" + sequence_number: 2 + test_data: + # These should NOT have access to previous test variables + different_computation: ${{ 20 + 64 }} + different_string: ${{ "different".lower() }} + different_list: ${{ len([10, 20]) }} + # Verify independence + same_operation_different_result: ${{ "isolation".lower() }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: second_computation_different + assert: ${{ response.json()["json"]["test_data"]["different_computation"] == 84 }} + - name: isolation_verified + assert: ${{ response.json()["json"]["test_data"]["same_operation_different_result"] == "isolation" }} + - name: no_state_leak + # This test verifies that the computations are independent + assert: ${{ response.json()["json"]["test_data"]["different_computation"] != test_value_1 }} + +- name: security_documentation_comprehensive + path: /anything/security-documentation + method: post + body: + test_category: "security_documentation_comprehensive" + documentation: + blocked_modules: "os, sys, subprocess, socket, urllib, http" + blocked_builtins: "open, exec, eval, compile, __import__" + blocked_attributes: "__class__, __bases__, __subclasses__, __globals__" + blocked_side_effects: "globals(), locals(), setattr(), delattr()" + expected_behavior: "All dangerous operations should raise InvalidPythonCodeError" + security_note: "RestrictedPython prevents code injection and system access" + # Safe operations that should continue working + safe_verification: + math_still_works: ${{ math.sqrt(49) }} + datetime_still_works: ${{ datetime.datetime.now().year }} + string_still_works: ${{ "SAFE".lower() }} + list_still_works: ${{ [1, 2, 3][-1] }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: safe_math_verified + assert: ${{ response.json()["json"]["safe_verification"]["math_still_works"] == 7.0 }} + - name: safe_datetime_verified + assert: ${{ response.json()["json"]["safe_verification"]["datetime_still_works"] >= 2024 }} + - name: safe_string_verified + assert: ${{ response.json()["json"]["safe_verification"]["string_still_works"] == "safe" }} + - name: safe_list_verified + assert: ${{ response.json()["json"]["safe_verification"]["list_still_works"] == 3 }} \ No newline at end of file diff --git a/httpbingo-api/tests/restricted_python_validation.yaml b/httpbingo-api/tests/restricted_python_validation.yaml new file mode 100644 index 0000000..36fef51 --- /dev/null +++ b/httpbingo-api/tests/restricted_python_validation.yaml @@ -0,0 +1,2 @@ +name: restricted_python_safe_expressions_work +assert: ${{ datetime.datetime.now().year > 2020 and math.pi > 3.14 and len("test") == 4 }} \ No newline at end of file diff --git a/pokeapi/scanapi.yaml b/pokeapi/scanapi.yaml index b9c2c9a..0cf50f9 100644 --- a/pokeapi/scanapi.yaml +++ b/pokeapi/scanapi.yaml @@ -9,6 +9,10 @@ endpoints: method: get vars: pokemon_name: ${{ response.json()["results"][0]["name"] }} + # RestrictedPython safe expressions + request_timestamp: ${{ datetime.datetime.now().timestamp() }} + total_pokemon_sqrt: ${{ math.sqrt(response.json()["count"]) }} + request_date: ${{ datetime.datetime.now().strftime('%Y-%m-%d') }} tests: - name: status_code_is_200 assert: ${{ response.status_code == 200 }} @@ -18,9 +22,19 @@ endpoints: assert: ${{ len(response.json()["results"]) == 20 }} - name: count_is_gte_1118 assert: ${{ response.json()["count"] >= 1118 }} + - name: restricted_python_math_works + assert: ${{ math.sqrt(1600) == 40.0 }} + - name: restricted_python_datetime_works + assert: ${{ datetime.datetime.now().year >= 2024 }} - name: details method: get path: ${pokemon_name} + vars: + # RestrictedPython expressions for pokemon details + pokemon_id_doubled: ${{ response.json()["id"] * 2 }} + weight_kg: ${{ response.json()["weight"] / 10 }} + height_m: ${{ response.json()["height"] / 10 }} + name_length: ${{ len(response.json()["name"]) }} tests: - name: status_code_is_200 assert: ${{ response.status_code == 200 }} @@ -28,3 +42,115 @@ endpoints: assert: ${{ response.elapsed.total_seconds() < 0.5 }} - name: id_is_one assert: ${{ response.json()["id"] == 1 }} + - name: restricted_python_calculations_work + assert: ${{ response.json()["id"] * 2 == 2 }} + - name: restricted_python_string_ops_work + assert: ${{ len(response.json()["name"]) > 0 }} + + - name: restrictedpython_comprehensive_validation + path: pokemon/pikachu + requests: + - name: comprehensive_allowed_modules_test + method: get + vars: + # Comprehensive test of all ALLOWED_MODULES with Pokemon data + pokemon_data_analysis: + name_hash: ${{ abs(hash(response.json()["name"])) }} + weight_sqrt: ${{ math.sqrt(response.json()["weight"]) }} + height_calculation: ${{ math.ceil(response.json()["height"] / 10.0) }} + abilities_count: ${{ len(response.json()["abilities"]) }} + types_joined: ${{ ", ".join([t["type"]["name"] for t in response.json()["types"]]) }} + datetime_analysis: + request_timestamp: ${{ datetime.datetime.now().timestamp() }} + request_date: ${{ datetime.datetime.now().strftime('%Y-%m-%d') }} + future_cache_expire: ${{ (datetime.datetime.now() + datetime.timedelta(hours=1)).isoformat() }} + random_analysis: + random_seed: ${{ random.randint(1, 1000) }} + random_ability: ${{ random.choice(["electric", "static", "lightning-rod"]) }} + regex_analysis: + name_pattern: ${{ bool(re.match(r"^[a-z]+$", response.json()["name"])) }} + type_search: ${{ bool(re.search(r"electric", str(response.json()["types"]))) }} + time_analysis: + request_time: ${{ time.time() }} + formatted_time: ${{ time.strftime('%H:%M:%S') }} + uuid_analysis: + session_id: ${{ str(uuid.uuid4()) }} + request_id: ${{ str(uuid.uuid4())[:8] }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: pokemon_is_pikachu + assert: ${{ response.json()["name"] == "pikachu" }} + - name: math_operations_work + assert: ${{ isinstance(math.sqrt(response.json()["weight"]), float) }} + - name: datetime_operations_work + assert: ${{ len(datetime.datetime.now().strftime('%Y-%m-%d')) == 10 }} + - name: string_operations_work + assert: ${{ len(", ".join([t["type"]["name"] for t in response.json()["types"]])) > 0 }} + - name: regex_works_on_pokemon_data + assert: ${{ re.match(r"^[a-z]+$", response.json()["name"]) is not None }} + - name: uuid_format_valid + assert: ${{ len(str(uuid.uuid4())[:8]) == 8 }} + + - name: unicode_and_complex_operations_test + method: get + vars: + unicode_operations: + # Test unicode with Pokemon names (some have special chars in other languages) + name_reversed: ${{ response.json()["name"][::-1] }} + name_title: ${{ response.json()["name"].title() }} + name_length: ${{ len(response.json()["name"]) }} + complex_calculations: + # Complex mathematical operations with Pokemon stats + bmi_like_calculation: ${{ response.json()["weight"] / (response.json()["height"] ** 2) if response.json()["height"] > 0 else 0 }} + stat_total: ${{ sum([stat["base_stat"] for stat in response.json()["stats"]]) }} + avg_stat: ${{ sum([stat["base_stat"] for stat in response.json()["stats"]]) / len(response.json()["stats"]) }} + max_stat: ${{ max([stat["base_stat"] for stat in response.json()["stats"]]) }} + min_stat: ${{ min([stat["base_stat"] for stat in response.json()["stats"]]) }} + list_comprehensions: + stat_names: ${{ [stat["stat"]["name"] for stat in response.json()["stats"]] }} + high_stats: ${{ [stat["base_stat"] for stat in response.json()["stats"] if stat["base_stat"] > 50] }} + ability_names: ${{ [ability["ability"]["name"] for ability in response.json()["abilities"]] }} + isolation_test_data: + test_value: ${{ 999 }} + test_string: ${{ "isolation_test" }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: unicode_reverse_works + assert: ${{ response.json()["name"][::-1] == "uhcakip" }} + - name: complex_math_works + assert: ${{ isinstance(sum([stat["base_stat"] for stat in response.json()["stats"]]), int) }} + - name: list_comprehensions_work + assert: ${{ len([stat["stat"]["name"] for stat in response.json()["stats"]]) == 6 }} + - name: min_max_functions_work + assert: ${{ max([stat["base_stat"] for stat in response.json()["stats"]]) >= min([stat["base_stat"] for stat in response.json()["stats"]]) }} + + - name: security_documentation_with_pokemon_context + method: get + vars: + security_documentation: + test_category: "pokemon_api_security_validation" + safe_operations_verified: "All Pokemon data processing uses only safe RestrictedPython operations" + # Document what should be blocked (safely in comments) + blocked_operations_note: "File access, system calls, dangerous imports are prevented" + safe_pokemon_operations: + # These operations on Pokemon data should always work safely + pokemon_name_safe: ${{ response.json()["name"].upper() }} + pokemon_id_safe: ${{ response.json()["id"] * 10 }} + pokemon_abilities_safe: ${{ len(response.json()["abilities"]) }} + pokemon_types_safe: ${{ [t["type"]["name"] for t in response.json()["types"]] }} + # Mathematical analysis of Pokemon stats - all safe + total_stats_safe: ${{ sum([s["base_stat"] for s in response.json()["stats"]]) }} + stat_variance_safe: ${{ max([s["base_stat"] for s in response.json()["stats"]]) - min([s["base_stat"] for s in response.json()["stats"]]) }} + tests: + - name: status_code_is_200 + assert: ${{ response.status_code == 200 }} + - name: safe_string_ops_work + assert: ${{ response.json()["name"].upper() == "PIKACHU" }} + - name: safe_math_ops_work + assert: ${{ response.json()["id"] * 10 == 250 }} + - name: safe_list_ops_work + assert: ${{ len(response.json()["abilities"]) > 0 }} + - name: safe_complex_calculations_work + assert: ${{ sum([s["base_stat"] for s in response.json()["stats"]]) > 100 }} diff --git a/pokeapi/tests/restricted_python_validation.yaml b/pokeapi/tests/restricted_python_validation.yaml new file mode 100644 index 0000000..36fef51 --- /dev/null +++ b/pokeapi/tests/restricted_python_validation.yaml @@ -0,0 +1,2 @@ +name: restricted_python_safe_expressions_work +assert: ${{ datetime.datetime.now().year > 2020 and math.pi > 3.14 and len("test") == 4 }} \ No newline at end of file