Skip to content

Commit de18a55

Browse files
authored
Feat/testing (#7)
* adding some tests and checks * add pre-commit with ruff * use PyCQA/bandit-action@v1 action * run pytest with poetry * improve badit warning Signed-off-by: Luiz Oliveira <ziuloliveira@gmail.com>
1 parent 0e15dba commit de18a55

8 files changed

Lines changed: 736 additions & 179 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,24 @@ jobs:
77
security:
88
name: security
99
runs-on: ubuntu-latest
10+
permissions:
11+
security-events: write
1012
steps:
1113
- uses: actions/checkout@v4
1214
- name: Secret Scanning
1315
uses: trufflesecurity/trufflehog@main
1416
with:
1517
base: ""
1618
head: ${{ github.ref_name }}
17-
extra_args: --results=verified,unknown
19+
extra_args: --results=verified,unknown
20+
- uses: astral-sh/ruff-action@v3
21+
name: Linting
22+
- name: Bandit
23+
uses: PyCQA/bandit-action@v1
24+
- name: Pytest
25+
run: |
26+
pip install poetry
27+
poetry install
28+
export PYTHONPATH=$(pwd)
29+
mkdir -p static/openapi
30+
poetry run pytest

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.4.4
4+
hooks:
5+
- id: ruff

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
[![Latest Release](https://img.shields.io/github/v/release/Ziul/swagger-operator?label=release&color=blue)](https://github.com/Ziul/swagger-operator/releases)
55
[![Docker Pulls](https://img.shields.io/docker/pulls/ziuloliveira/swagger-operator)](https://hub.docker.com/r/ziuloliveira/swagger-operator)
66
[![License](https://img.shields.io/github/license/Ziul/swagger-operator)](https://github.com/Ziul/swagger-operator/blob/main/LICENSE)
7-
7+
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8+
[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)
89

910
A Kubernetes operator that automatically discovers services annotated with OpenAPI/Swagger documentation and aggregates their documentation in a single UI.
1011

poetry.lock

Lines changed: 553 additions & 162 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[tool.poetry]
22
name = "swagger-operator"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = ""
5-
authors = ["Luiz Oliveira"]
5+
authors = ["Luiz Oliveira", "Gustavo Coelho"]
66
license = "MIT"
77
readme = "README.md"
88

@@ -18,7 +18,18 @@ itsdangerous = "^2.2.0"
1818

1919
[tool.poetry.group.dev.dependencies]
2020
ipdb = "^0.13.13"
21+
pytest = "^8.3.5"
22+
pytest-asyncio = "^0.26.0"
23+
pytest-cov = "^6.1.1"
24+
httpx = "^0.28.1"
25+
bandit = "^1.8.3"
26+
ruff = "^0.11.10"
27+
pre-commit = "^4.2.0"
2128

2229
[build-system]
2330
requires = ["poetry-core"]
2431
build-backend = "poetry.core.masonry.api"
32+
33+
[tool.bandit]
34+
exclude_dirs = ["tests", ".git", ".venv", ".github"]
35+
skips = ["B101", "B113"]

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
addopts = --cov=. --cov-report=term-missing -p no:warnings -q
3+
testpaths = tests
4+
norecursedirs = tests

server.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from fastapi import FastAPI, Request, HTTPException, Depends
1+
from fastapi import FastAPI, Request, HTTPException, Depends, status
22
from fastapi.responses import HTMLResponse, Response
33
from fastapi.staticfiles import StaticFiles
44
from fastapi.templating import Jinja2Templates
@@ -12,12 +12,12 @@
1212
from authlib.integrations.starlette_client import OAuth
1313
from starlette.config import Config
1414
from starlette.middleware.sessions import SessionMiddleware
15-
from fastapi import HTTPException, status
15+
import secrets
1616

1717
logger = logging.getLogger("uvicorn.error")
1818

1919
app = FastAPI()
20-
app.add_middleware(SessionMiddleware, secret_key="some-random-string", max_age=None)
20+
app.add_middleware(SessionMiddleware, secret_key=secrets.token_urlsafe(32), max_age=None)
2121

2222

2323
# static files
@@ -27,11 +27,11 @@
2727

2828
ENABLE_OIDC = os.environ.get("ENABLE_OIDC", "false").lower() == "true"
2929
AUTH_CALLBACK = os.environ.get("AUTH_CALLBACK", None)
30+
config = Config() # ou use variáveis de ambiente diretamente
31+
oauth = OAuth(config)
3032

3133

3234
if ENABLE_OIDC:
33-
config = Config() # ou use variáveis de ambiente diretamente
34-
oauth = OAuth(config)
3535
oauth.register(
3636
name='oidc',
3737
client_id=os.environ.get("OIDC_CLIENT_ID"),
@@ -134,6 +134,7 @@ async def docs(request: Request, template:str=None, user=Depends(require_login))
134134
logger.info(f"Loaded {len(swaggers)} URLs.")
135135
except FileNotFoundError:
136136
logger.error("File not found: static/openapi/urls.json")
137+
request.session['error'] = "File not found: static/openapi/urls.json"
137138
swaggers = [
138139
{
139140
"url": "/openapi.json",
@@ -143,6 +144,7 @@ async def docs(request: Request, template:str=None, user=Depends(require_login))
143144
]
144145
except json.JSONDecodeError:
145146
logger.error("Error decoding JSON from static/openapi/urls.json")
147+
request.session['error'] = "Error decoding JSON from static/openapi/urls.json"
146148
swaggers = [
147149
{
148150
"url": "/openapi.json",
@@ -183,14 +185,10 @@ async def config(request: Request):
183185
try:
184186
with open('static/openapi/urls.json') as f:
185187
swaggers = json.load(f)
186-
except:
187-
swaggers = [
188-
{
189-
"url": "/openapi.json",
190-
"name": "Swagger Aggregator",
191-
"header": "",
192-
}
193-
]
188+
except Exception as e:
189+
logger.error(f"Error loading configuration file: {e}")
190+
swaggers = []
191+
request.session['error'] = "Error loading configuration file."
194192

195193
return templates.TemplateResponse(
196194
"config.html",

tests/test_server.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import json
2+
from unittest.mock import patch, mock_open, MagicMock
3+
from fastapi.testclient import TestClient
4+
from server import app, apply_proxy_to_openapi
5+
from urllib.parse import unquote
6+
7+
client = TestClient(app)
8+
9+
def test_proxy_invalid_url():
10+
# Tests error when passing an invalid URL
11+
response = client.get("/proxy", params={"url": "http://invalid-url"})
12+
assert response.status_code == 500
13+
assert "Failed to fetch OpenAPI document" in response.text
14+
15+
def test_docs_returns_html():
16+
# Tests if the main route returns HTML
17+
response = client.get("/")
18+
assert response.status_code == 200
19+
assert "text/html" in response.headers["content-type"]
20+
21+
def test_proxy_with_headers():
22+
url = "http://example.com/openapi.json"
23+
headers_dict = {"Authorization": "Bearer token123"}
24+
headers_encoded = json.dumps(headers_dict) # The endpoint expects headers already in JSON
25+
26+
mock_response = MagicMock()
27+
mock_response.content = b'{"openapi": "3.0.0"}'
28+
mock_response.text = '{"openapi": "3.0.0"}'
29+
mock_response.headers = {"content-type": "application/json"}
30+
31+
with patch("server.requests.get", return_value=mock_response) as mock_get:
32+
response = client.get("/proxy", params={"url": url, "headers": headers_encoded})
33+
assert response.status_code == 200
34+
assert response.json() == {"openapi": "3.0.0"}
35+
mock_get.assert_called_once()
36+
# Checks if the headers were passed correctly
37+
called_headers = mock_get.call_args[1]["headers"]
38+
assert called_headers == headers_dict
39+
40+
def test_proxy_yaml_response():
41+
url = "http://example.com/openapi.yaml"
42+
headers_dict = {"Authorization": "Bearer token123"}
43+
headers_json = json.dumps(headers_dict)
44+
45+
yaml_content = """
46+
openapi: 3.0.0
47+
info:
48+
title: API Teste
49+
version: "1.0"
50+
paths: {}
51+
"""
52+
mock_response = MagicMock()
53+
mock_response.content = yaml_content.encode("utf-8")
54+
mock_response.text = yaml_content
55+
mock_response.headers = {"content-type": "application/yaml"}
56+
57+
with patch("server.requests.get", return_value=mock_response) as mock_get:
58+
response = client.get("/proxy", params={"url": url, "headers": headers_json})
59+
assert response.status_code == 200
60+
assert response.headers["content-type"].startswith("text/yaml")
61+
assert "openapi: 3.0.0" in response.text
62+
mock_get.assert_called_once()
63+
called_headers = mock_get.call_args[1]["headers"]
64+
assert called_headers == headers_dict
65+
66+
def test_docs_file_not_found(monkeypatch):
67+
# Simulates FileNotFoundError when opening the file
68+
with patch("builtins.open", side_effect=FileNotFoundError):
69+
response = client.get("/")
70+
assert response.status_code == 200
71+
# Should contain the default aggregator name
72+
assert "Swagger Aggregator" in response.text
73+
74+
def test_docs_json_decode_error(monkeypatch):
75+
# Simulates invalid JSON error when opening the file
76+
m = mock_open(read_data="not a json")
77+
with patch("builtins.open", m):
78+
with patch("json.load", side_effect=json.JSONDecodeError("msg", "doc", 0)):
79+
response = client.get("/")
80+
assert response.status_code == 200
81+
assert "Swagger Aggregator" in response.text
82+
83+
def test_config_json_decode_error():
84+
# Simulates invalid JSON error when opening the file
85+
m = mock_open(read_data="not a json")
86+
with patch("builtins.open", m):
87+
with patch("json.load", side_effect=Exception("invalid json")):
88+
response = client.get("/config")
89+
assert response.status_code == 200
90+
91+
def test_apply_proxy_to_openapi_with_http():
92+
url = "http://example.com/openapi.json"
93+
header = {"Authorization": "Bearer token"}
94+
result = apply_proxy_to_openapi(url, header)
95+
assert result.startswith("/proxy?url=http://example.com/openapi.json")
96+
assert "headers=" in result
97+
headers_param = result.split("headers=")[1]
98+
decoded = json.loads(unquote(headers_param)) # Fixed here!
99+
assert decoded == header
100+
101+
def test_apply_proxy_to_openapi_with_http_and_headers():
102+
url = "http://example.com/openapi.json"
103+
header = {"Authorization": "Bearer token"}
104+
result = apply_proxy_to_openapi(url, header)
105+
assert result.startswith("/proxy?url=http://example.com/openapi.json")
106+
assert "headers=" in result
107+
# Decodes and checks the header
108+
headers_param = result.split("headers=")[1]
109+
decoded = json.loads(unquote(headers_param))
110+
assert decoded == header
111+
112+
def test_apply_proxy_to_openapi_with_http_no_headers():
113+
url = "http://example.com/openapi.json"
114+
result = apply_proxy_to_openapi(url)
115+
assert result == f"/proxy?url={url}"
116+
117+
def test_apply_proxy_to_openapi_with_non_http_url():
118+
url = "/local/openapi.json"
119+
result = apply_proxy_to_openapi(url)
120+
assert result == url
121+
122+
def test_apply_proxy_to_openapi_with_empty_header():
123+
url = "http://example.com/openapi.json"
124+
result = apply_proxy_to_openapi(url, {})
125+
# Should not add headers if the dict is empty
126+
assert result == f"/proxy?url={url}"
127+
128+
def test_apply_proxy_to_openapi_with_special_characters_in_header():
129+
url = "http://example.com/openapi.json"
130+
header = {"X-Test": "çãõ@#%&"}
131+
result = apply_proxy_to_openapi(url, header)
132+
headers_param = result.split("headers=")[1]
133+
decoded = json.loads(unquote(headers_param))
134+
assert decoded == header

0 commit comments

Comments
 (0)