From fe10469ca1babdd459510f8b0facb341af07d3b5 Mon Sep 17 00:00:00 2001 From: Timur Osmanov Date: Mon, 23 Dec 2024 10:42:39 +0300 Subject: [PATCH] add: test and fix preprocessor --- .github/workflows/python-test.yml | 27 + .gitignore | 3 +- README.md | 14 +- .../preprocessors/swaggerdoc/swaggerdoc.py | 14 +- test.sh | 11 + test_in_docker.sh | 14 + tests/data/expected/test1.md | 628 ++++++++++++++++++ tests/data/input/test1.md | 1 + tests/data/petstore_spec.json | 177 +++++ tests/test_swaggerdoc.py | 33 + 10 files changed, 909 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/python-test.yml create mode 100755 test.sh create mode 100755 test_in_docker.sh create mode 100644 tests/data/expected/test1.md create mode 100644 tests/data/input/test1.md create mode 100644 tests/data/petstore_spec.json create mode 100644 tests/test_swaggerdoc.py diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..c8b11aa --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,27 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip3 install . + pip3 install --upgrade foliantcontrib.test_framework + npm install -g widdershins + - name: Test with unittest + run: | + python3 -m unittest discover \ No newline at end of file diff --git a/.gitignore b/.gitignore index 81954e2..29faac9 100755 --- a/.gitignore +++ b/.gitignore @@ -100,4 +100,5 @@ ENV/ # mypy .mypy_cache/ -.DS_store \ No newline at end of file +.DS_store +.swaggercache diff --git a/README.md b/README.md index f22d132..88106e6 100755 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ preprocessors: mode: widdershins template: swagger.j2 environment: env.yaml - strick: false + strict: false ``` `spec_url` @@ -131,3 +131,15 @@ In `jinja` mode the output markdown is generated by the [Jinja2](http://jinja.po To customize the output create a template which suits your needs. Then supply the path to it in the `template` parameter. If you wish to use the default template as a starting point, build the foliant project with `swaggerdoc` preprocessor turned on. After the first build the default template will appear in your foliant project dir under name `swagger.j2`. + +## Tests + +To run the tests locally, you will need to install NodeJS. +If you have nodejs installed, then run: +```bash +./test.sh +``` +Alternatively, you can also use a Docker image to run the tests. To do so, run: +```bash +./test_in_docker.sh +``` diff --git a/foliant/preprocessors/swaggerdoc/swaggerdoc.py b/foliant/preprocessors/swaggerdoc/swaggerdoc.py index ba4e47e..f0dd9f3 100644 --- a/foliant/preprocessors/swaggerdoc/swaggerdoc.py +++ b/foliant/preprocessors/swaggerdoc/swaggerdoc.py @@ -30,7 +30,6 @@ from foliant.preprocessors.utils.preprocessor_ext import allow_fail from foliant.utils import output - class Preprocessor(BasePreprocessorExt): tags = ('swaggerdoc',) @@ -42,7 +41,7 @@ class Preprocessor(BasePreprocessorExt): 'spec_path': '', 'mode': 'widdershins', 'template': 'swagger.j2', - 'strict': False + 'strict': True } def __init__(self, *args, **kwargs): @@ -69,7 +68,6 @@ def __init__(self, *args, **kwargs): self.options = Options(self.options, validators={'json_path': validate_exists, 'spec_path': validate_exists}) - self.critical_error = [] def _gather_specs(self, urls: list, @@ -93,8 +91,8 @@ def _gather_specs(self, msg = f'\nCannot retrieve swagger spec file from url {url}.' if self.options['strict']: self.logger.error(msg) - self.critical_error.append(msg) output(f'ERROR: {msg}') + os._exit(1) else: self._warning(f'{msg}. Skipping.', error=e) @@ -217,10 +215,4 @@ def process_swaggerdoc_blocks(self, block) -> str: def apply(self): self._process_tags_for_all_files(func=self.process_swaggerdoc_blocks) - if len(self.critical_error) > 0: - self.logger.info('Critical errors have occurred') - errors = '\n'.join(self.critical_error) - output(f'\nBuild failed: swaggerdoc preprocessor errors: \n{errors}\n') - os._exit(2) - else: - self.logger.info('Preprocessor applied') + self.logger.info('Preprocessor applied') diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..225cf12 --- /dev/null +++ b/test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# before testing make sure that you have installed the fresh version of preprocessor: +pip3 install . +# also make sure that fresh version of test framework is installed: +pip3 install --upgrade foliantcontrib.test_framework + +# install dependencies +npm install -g widdershins + +python3 -m unittest discover -v diff --git a/test_in_docker.sh b/test_in_docker.sh new file mode 100755 index 0000000..d4a8698 --- /dev/null +++ b/test_in_docker.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Write Dockerfile +echo "FROM python:3.9.21-alpine3.20" > Dockerfile +echo "RUN apk add --no-cache --upgrade bash && pip install --no-build-isolation pyyaml==5.4.1" >> Dockerfile +echo "RUN apk add nodejs npm" >> Dockerfile + +# Run tests in docker +docker build . -t test-swaggerdoc:latest + +docker run --rm -it -v "./:/app/" -w /app/ test-swaggerdoc:latest "./test.sh" + +# Remove Dockerfile +rm Dockerfile \ No newline at end of file diff --git a/tests/data/expected/test1.md b/tests/data/expected/test1.md new file mode 100644 index 0000000..27ec7e0 --- /dev/null +++ b/tests/data/expected/test1.md @@ -0,0 +1,628 @@ +--- +title: Swagger Petstore v1.0.0 +language_tabs: + - shell: Shell + - http: HTTP + - javascript: JavaScript + - ruby: Ruby + - python: Python + - php: PHP + - java: Java + - go: Go +toc_footers: [] +includes: [] +search: true +highlight_theme: darkula +headingLevel: 2 + +--- + + + +

Swagger Petstore v1.0.0

+ +> Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu. + +Base URLs: + +* http://petstore.swagger.io/v1 + + License: MIT + +

pets

+ +## listPets + + + +> Code samples + +```shell +# You can also use wget +curl -X GET http://petstore.swagger.io/v1/pets \ + -H 'Accept: application/json' + +``` + +```http +GET http://petstore.swagger.io/v1/pets HTTP/1.1 +Host: petstore.swagger.io +Accept: application/json + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('http://petstore.swagger.io/v1/pets', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```ruby +require 'rest-client' +require 'json' + +headers = { + 'Accept' => 'application/json' +} + +result = RestClient.get 'http://petstore.swagger.io/v1/pets', + params: { + }, headers: headers + +p JSON.parse(result) + +``` + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('http://petstore.swagger.io/v1/pets', headers = headers) + +print(r.json()) + +``` + +```php + 'application/json', +); + +$client = new \GuzzleHttp\Client(); + +// Define array of request body. +$request_body = array(); + +try { + $response = $client->request('GET','http://petstore.swagger.io/v1/pets', array( + 'headers' => $headers, + 'json' => $request_body, + ) + ); + print_r($response->getBody()->getContents()); + } + catch (\GuzzleHttp\Exception\BadResponseException $e) { + // handle exception or api errors. + print_r($e->getMessage()); + } + + // ... + +``` + +```java +URL obj = new URL("http://petstore.swagger.io/v1/pets"); +HttpURLConnection con = (HttpURLConnection) obj.openConnection(); +con.setRequestMethod("GET"); +int responseCode = con.getResponseCode(); +BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream())); +String inputLine; +StringBuffer response = new StringBuffer(); +while ((inputLine = in.readLine()) != null) { + response.append(inputLine); +} +in.close(); +System.out.println(response.toString()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Accept": []string{"application/json"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("GET", "http://petstore.swagger.io/v1/pets", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +`GET /pets` + +*List all pets* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|limit|query|integer(int32)|false|How many items to return at one time (max 100)| + +> Example responses + +> 200 Response + +```json +[ + { + "id": 0, + "name": "string", + "tag": "string" + } +] +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|A paged array of pets|[Pets](#schemapets)| +|default|Default|unexpected error|[Error](#schemaerror)| + +### Response Headers + +|Status|Header|Type|Format|Description| +|---|---|---|---|---| +|200|x-next|string||A link to the next page of responses| + + + +## createPets + + + +> Code samples + +```shell +# You can also use wget +curl -X POST http://petstore.swagger.io/v1/pets \ + -H 'Accept: application/json' + +``` + +```http +POST http://petstore.swagger.io/v1/pets HTTP/1.1 +Host: petstore.swagger.io +Accept: application/json + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('http://petstore.swagger.io/v1/pets', +{ + method: 'POST', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```ruby +require 'rest-client' +require 'json' + +headers = { + 'Accept' => 'application/json' +} + +result = RestClient.post 'http://petstore.swagger.io/v1/pets', + params: { + }, headers: headers + +p JSON.parse(result) + +``` + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.post('http://petstore.swagger.io/v1/pets', headers = headers) + +print(r.json()) + +``` + +```php + 'application/json', +); + +$client = new \GuzzleHttp\Client(); + +// Define array of request body. +$request_body = array(); + +try { + $response = $client->request('POST','http://petstore.swagger.io/v1/pets', array( + 'headers' => $headers, + 'json' => $request_body, + ) + ); + print_r($response->getBody()->getContents()); + } + catch (\GuzzleHttp\Exception\BadResponseException $e) { + // handle exception or api errors. + print_r($e->getMessage()); + } + + // ... + +``` + +```java +URL obj = new URL("http://petstore.swagger.io/v1/pets"); +HttpURLConnection con = (HttpURLConnection) obj.openConnection(); +con.setRequestMethod("POST"); +int responseCode = con.getResponseCode(); +BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream())); +String inputLine; +StringBuffer response = new StringBuffer(); +while ((inputLine = in.readLine()) != null) { + response.append(inputLine); +} +in.close(); +System.out.println(response.toString()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Accept": []string{"application/json"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("POST", "http://petstore.swagger.io/v1/pets", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +`POST /pets` + +*Create a pet* + +> Example responses + +> default Response + +```json +{ + "code": 0, + "message": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|201|[Created](https://tools.ietf.org/html/rfc7231#section-6.3.2)|Null response|None| +|default|Default|unexpected error|[Error](#schemaerror)| + + + +## showPetById + + + +> Code samples + +```shell +# You can also use wget +curl -X GET http://petstore.swagger.io/v1/pets/{petId} \ + -H 'Accept: application/json' + +``` + +```http +GET http://petstore.swagger.io/v1/pets/{petId} HTTP/1.1 +Host: petstore.swagger.io +Accept: application/json + +``` + +```javascript + +const headers = { + 'Accept':'application/json' +}; + +fetch('http://petstore.swagger.io/v1/pets/{petId}', +{ + method: 'GET', + + headers: headers +}) +.then(function(res) { + return res.json(); +}).then(function(body) { + console.log(body); +}); + +``` + +```ruby +require 'rest-client' +require 'json' + +headers = { + 'Accept' => 'application/json' +} + +result = RestClient.get 'http://petstore.swagger.io/v1/pets/{petId}', + params: { + }, headers: headers + +p JSON.parse(result) + +``` + +```python +import requests +headers = { + 'Accept': 'application/json' +} + +r = requests.get('http://petstore.swagger.io/v1/pets/{petId}', headers = headers) + +print(r.json()) + +``` + +```php + 'application/json', +); + +$client = new \GuzzleHttp\Client(); + +// Define array of request body. +$request_body = array(); + +try { + $response = $client->request('GET','http://petstore.swagger.io/v1/pets/{petId}', array( + 'headers' => $headers, + 'json' => $request_body, + ) + ); + print_r($response->getBody()->getContents()); + } + catch (\GuzzleHttp\Exception\BadResponseException $e) { + // handle exception or api errors. + print_r($e->getMessage()); + } + + // ... + +``` + +```java +URL obj = new URL("http://petstore.swagger.io/v1/pets/{petId}"); +HttpURLConnection con = (HttpURLConnection) obj.openConnection(); +con.setRequestMethod("GET"); +int responseCode = con.getResponseCode(); +BufferedReader in = new BufferedReader( + new InputStreamReader(con.getInputStream())); +String inputLine; +StringBuffer response = new StringBuffer(); +while ((inputLine = in.readLine()) != null) { + response.append(inputLine); +} +in.close(); +System.out.println(response.toString()); + +``` + +```go +package main + +import ( + "bytes" + "net/http" +) + +func main() { + + headers := map[string][]string{ + "Accept": []string{"application/json"}, + } + + data := bytes.NewBuffer([]byte{jsonReq}) + req, err := http.NewRequest("GET", "http://petstore.swagger.io/v1/pets/{petId}", data) + req.Header = headers + + client := &http.Client{} + resp, err := client.Do(req) + // ... +} + +``` + +`GET /pets/{petId}` + +*Info for a specific pet* + +

Parameters

+ +|Name|In|Type|Required|Description| +|---|---|---|---|---| +|petId|path|string|true|The id of the pet to retrieve| + +> Example responses + +> 200 Response + +```json +{ + "id": 0, + "name": "string", + "tag": "string" +} +``` + +

Responses

+ +|Status|Meaning|Description|Schema| +|---|---|---|---| +|200|[OK](https://tools.ietf.org/html/rfc7231#section-6.3.1)|Expected response to a valid request|[Pet](#schemapet)| +|default|Default|unexpected error|[Error](#schemaerror)| + + + +# Schemas + +

Pet

+ + + + + + +```json +{ + "id": 0, + "name": "string", + "tag": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|id|integer(int64)|true|none|none| +|name|string|true|none|none| +|tag|string|false|none|none| + +

Pets

+ + + + + + +```json +[ + { + "id": 0, + "name": "string", + "tag": "string" + } +] + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|*anonymous*|[[Pet](#schemapet)]|false|none|none| + +

Error

+ + + + + + +```json +{ + "code": 0, + "message": "string" +} + +``` + +### Properties + +|Name|Type|Required|Restrictions|Description| +|---|---|---|---|---| +|code|integer(int32)|true|none|none| +|message|string|true|none|none| + diff --git a/tests/data/input/test1.md b/tests/data/input/test1.md new file mode 100644 index 0000000..fa7930b --- /dev/null +++ b/tests/data/input/test1.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/data/petstore_spec.json b/tests/data/petstore_spec.json new file mode 100644 index 0000000..6904267 --- /dev/null +++ b/tests/data/petstore_spec.json @@ -0,0 +1,177 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "license": { + "name": "MIT" + } + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "description": "How many items to return at one time (max 100)", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "A paged array of pets", + "headers": { + "x-next": { + "description": "A link to the next page of responses", + "schema": { + "type": "string" + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "summary": "Create a pet", + "operationId": "createPets", + "tags": [ + "pets" + ], + "responses": { + "201": { + "description": "Null response" + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/pets/{petId}": { + "get": { + "summary": "Info for a specific pet", + "operationId": "showPetById", + "tags": [ + "pets" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "required": true, + "description": "The id of the pet to retrieve", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Expected response to a valid request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Pets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "Error": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + } + } + } \ No newline at end of file diff --git a/tests/test_swaggerdoc.py b/tests/test_swaggerdoc.py new file mode 100644 index 0000000..e05ee84 --- /dev/null +++ b/tests/test_swaggerdoc.py @@ -0,0 +1,33 @@ +import os + +from pathlib import Path +from foliant_test.preprocessor import PreprocessorTestFramework +from unittest import TestCase + +def rel_name(path:str): + return os.path.join(os.path.dirname(__file__), path) + +def data_file_content(path: str) -> str: + '''read data file by path relative to this module and return its contents''' + with open(rel_name(path), encoding='utf8') as f: + return f.read() + +class TestSwaggerdoc(TestCase): + def setUp(self): + self.ptf = PreprocessorTestFramework('swaggerdoc') + self.ptf.context['project_path'] = Path('.') + self.ptf.options = { + 'json_path': rel_name(os.path.join('data', 'petstore_spec.json')), + } + + def test_petstore_spec(self): + input_content = data_file_content(os.path.join('data', 'input', 'test1.md')) + expected_content = data_file_content(os.path.join('data', 'expected', 'test1.md')) + self.ptf.test_preprocessor( + input_mapping = { + 'index.md': input_content + }, + expected_mapping = { + 'index.md': expected_content + } + ) \ No newline at end of file