Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__/
flagged/
/venv/
/venv/
.env
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
testpaths = tests
addopts = -ra
5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
openai==2.6.0
gradio==5.49.1
gradio==5.49.1
pytest
pytest-mock
pytest-cov
56 changes: 56 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import json
import importlib
import sys
import gradio as gr
import pytest
import sys
from pathlib import Path

ROOT = str(Path(__file__).resolve().parents[1])
if ROOT not in sys.path:
sys.path.insert(0, ROOT)

@pytest.fixture
def sample_request_body():
return {
"prompt": "$INPUT",
"metadata": {
"language": "en",
"extras": ["a", "b"],
},
}

@pytest.fixture
def sample_response_structure():
return {
"data": {
"result": "$OUTPUT",
}
}

@pytest.fixture
def sample_response_payload():
return {
"data": {
"result": "system prompt here",
}
}

@pytest.fixture
def request_body_json(sample_request_body):
return json.dumps(sample_request_body)

@pytest.fixture
def response_body_json(sample_response_structure):
return json.dumps(sample_response_structure)

@pytest.fixture
def ui_app(monkeypatch):
pytest.importorskip("core.whistleblower", reason="core.whistleblower module missing")
monkeypatch.setattr(gr.Blocks, "launch", lambda self: None)
module_name = "ui.app"
if module_name in sys.modules:
module = importlib.reload(sys.modules[module_name])
else:
module = importlib.import_module(module_name)
return module
43 changes: 43 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from types import SimpleNamespace
import pytest
from core.api import call_external_api

def test_call_external_api_replaces_placeholders(monkeypatch, sample_request_body, sample_response_structure, sample_response_payload):
captured = {}

def fake_post(url, json, headers):
captured["url"] = url
captured["json"] = json
captured["headers"] = headers
return SimpleNamespace(json=lambda: sample_response_payload)

monkeypatch.setattr("core.api.requests.post", fake_post)

output = call_external_api(
"https://api.example.com/chat",
"leak prompt",
sample_request_body,
sample_response_structure,
api_key="secret-token",
)

assert captured["url"] == "https://api.example.com/chat"
assert captured["json"]["prompt"] == "leak prompt"
assert captured["headers"] == {"X-repello-api-key": "secret-token"}
assert output == "system prompt here"

def test_call_external_api_passes_through_when_no_key(monkeypatch, sample_request_body, sample_response_structure, sample_response_payload):
def fake_post(url, json, headers):
assert headers == {}
return SimpleNamespace(json=lambda: sample_response_payload)

monkeypatch.setattr("core.api.requests.post", fake_post)

output = call_external_api(
"https://api.example.com/chat",
"test prompt",
sample_request_body,
sample_response_structure,
)

assert output == "system prompt here"
52 changes: 52 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import json
import sys
from pathlib import Path
import pytest

main = pytest.importorskip("main", reason="main module missing")
whistleblower = pytest.importorskip("core.whistleblower", reason="core.whistleblower module missing")

def test_read_json_file_valid(tmp_path):
data = {"api_url": "http://example.com"}
json_path = tmp_path / "config.json"
json_path.write_text(json.dumps(data))

loaded = whistleblower.read_json_file(str(json_path))

assert loaded == data

def test_read_json_file_invalid(tmp_path, capsys):
json_path = tmp_path / "invalid.json"
json_path.write_text("{invalid")

loaded = whistleblower.read_json_file(str(json_path))

assert loaded == {}

def test_main_invokes_whistleblower(monkeypatch, tmp_path):
config_path = tmp_path / "input.json"
config_path.write_text(
json.dumps(
{
"api_url": "http://example.com",
"request_body": {"prompt": "$INPUT"},
"response_body": {"response": "$OUTPUT"},
"OpenAI_api_key": "key",
"model": "gpt-4",
}
)
)

called = {}

def fake_whistleblower(args):
called["json_file"] = args.json_file
return "done"

monkeypatch.setattr(main, "whistleblower", fake_whistleblower)
monkeypatch.setattr(sys, "argv", ["main.py", "--json_file", str(config_path)])

result = main.main()

assert called["json_file"] == str(config_path)
assert result == "done"
153 changes: 153 additions & 0 deletions tests/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import json

import gradio as gr
import pytest


def test_check_for_placeholders(ui_app, request_body_json):
assert ui_app.check_for_placeholders(request_body_json, "$INPUT")
assert not ui_app.check_for_placeholders({"prompt": "hello"}, "$INPUT")


def test_check_for_placeholders_list(ui_app):
payload = json.dumps({"messages": [{"text": "$INPUT"}]})

assert ui_app.check_for_placeholders(payload, "$INPUT")


def test_validate_input_json_success(ui_app, request_body_json, response_body_json, monkeypatch):
captured = {}

def fake_generate_output(*args):
captured["args"] = args
return "result-json"

monkeypatch.setattr(ui_app, "generate_output", fake_generate_output)

output = ui_app.validate_input(
"https://api.example.com",
"api-key",
"JSON",
"",
request_body_json,
"",
response_body_json,
"openai-key",
"gpt-4",
)

assert output == "result-json"
assert captured["args"][0] == "https://api.example.com"
assert json.loads(captured["args"][2])["prompt"] == "$INPUT"


def test_validate_input_json_empty_request_raises(ui_app):
with pytest.raises(gr.Error) as excinfo:
ui_app.validate_input(
"https://api.example.com",
"",
"JSON",
"",
"",
"",
"{}",
"",
"gpt-4o",
)

assert "Request body cannot be empty" in str(excinfo.value)


def test_validate_input_json_invalid_request_raises(ui_app):
with pytest.raises(gr.Error) as excinfo:
ui_app.validate_input(
"https://api.example.com",
"",
"JSON",
"",
"{bad json",
"",
'{"response": "$OUTPUT"}',
"",
"gpt-4o",
)

assert "Invalid JSON format in request body." in str(excinfo.value)


def test_validate_input_json_missing_output_placeholder_raises(ui_app):
with pytest.raises(gr.Error) as excinfo:
ui_app.validate_input(
"https://api.example.com",
"",
"JSON",
"",
'{"prompt": "$INPUT"}',
"",
'{"response": "missing"}',
"",
"gpt-4o",
)

assert "Response body must contain the $OUTPUT placeholder." in str(excinfo.value)


def test_validate_input_key_value_success(ui_app, monkeypatch):
captured = {}

def fake_generate_output(*args):
captured["args"] = args
return "result-kv"

monkeypatch.setattr(ui_app, "generate_output", fake_generate_output)

output = ui_app.validate_input(
"https://api.example.com",
"",
"Key-Value",
"prompt: $INPUT\nstatic: value",
"",
"response: $OUTPUT",
"",
"service-key",
"gpt-4o",
)

assert output == "result-kv"
request_dict = captured["args"][2]
assert request_dict["prompt"] == "$INPUT"
assert request_dict["static"] == "value"
response_dict = captured["args"][3]
assert response_dict["response"] == "$OUTPUT"


def test_validate_input_key_value_empty_fields_raise(ui_app):
with pytest.raises(gr.Error) as excinfo:
ui_app.validate_input(
"https://api.example.com",
"",
"Key-Value",
"",
"",
"response: $OUTPUT",
"",
"openai-key",
"gpt-4o",
)

assert "Request body cannot be empty." in str(excinfo.value)


def test_validate_input_missing_placeholder_raises(ui_app):
with pytest.raises(gr.Error):
ui_app.validate_input(
"https://api.example.com",
"",
"JSON",
"",
'{"prompt": "no placeholder"}',
"",
'{"response": "$OUTPUT"}',
"",
"gpt-4o",
)
46 changes: 46 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from copy import deepcopy

from core.utils import extract_nested_value, replace_nested_value


def test_extract_nested_value_success(sample_response_payload, sample_response_structure):
value = extract_nested_value(sample_response_payload, sample_response_structure, "$OUTPUT")
assert value == "system prompt here"


def test_extract_nested_value_handles_list_structure():
payload = {"items": [{"value": "skip"}, {"value": "system prompt here"}]}
structure = {"items": [{}, {"value": "$OUTPUT"}]}

value = extract_nested_value(payload, structure, "$OUTPUT")

assert value == "system prompt here"


def test_extract_nested_value_missing(sample_response_payload):
structure_without_placeholder = {"data": {"result": "not-a-placeholder"}}
value = extract_nested_value(sample_response_payload, structure_without_placeholder, "$OUTPUT")
assert value is None


def test_extract_nested_value_returns_none_for_none_payload():
payload = {"data": {"result": None}}
structure = {"data": {"result": "$OUTPUT"}}

assert extract_nested_value(payload, structure, "$OUTPUT") is None


def test_replace_nested_value_updates_only_placeholder(sample_request_body):
updated = replace_nested_value(deepcopy(sample_request_body), "$INPUT", "example message")
assert updated["prompt"] == "example message"
assert updated["metadata"]["language"] == "en"


def test_replace_nested_value_updates_nested_lists():
data = {"items": ["$INPUT", {"nested": ["keep", "$INPUT"]}]}

updated = replace_nested_value(deepcopy(data), "$INPUT", "filled")

assert updated["items"][0] == "filled"
assert updated["items"][1]["nested"][1] == "filled"
assert updated["items"][1]["nested"][0] == "keep"
Loading