Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
7 changes: 6 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
openai==2.6.0
gradio==5.49.1
gradio==5.49.1
openai
Comment thread
Aaditya-G marked this conversation as resolved.
Outdated
gradio
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"
5 changes: 3 additions & 2 deletions ui/app.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import sys
import os
import json
from pathlib import Path

# Add the parent directory to the Python path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import gradio as gr
from core.whistleblower import generate_output

with open('styles.css', 'r') as file:
css = file.read()
css_path = Path(__file__).with_name('styles.css')
css = css_path.read_text() if css_path.exists() else ""

def check_for_placeholders(data, placeholder):
data = json.loads(data) if isinstance(data, str) else data
Expand Down