-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of https://github.com/moleculekayak/fertilizer
- Loading branch information
Showing
14 changed files
with
248 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -133,3 +133,4 @@ cython_debug/ | |
|
||
.env | ||
tests/support/torrents/example.torrent | ||
scratchpad.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
bencoder | ||
colorama | ||
requests | ||
flask | ||
pytest | ||
requests-mock | ||
ruff |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import os | ||
from flask import Flask, request | ||
|
||
from src.parser import is_valid_infohash | ||
from src.torrent import generate_new_torrent_from_file | ||
from src.errors import TorrentAlreadyExistsError, TorrentNotFoundError | ||
|
||
app = Flask(__name__) | ||
|
||
|
||
@app.route("/api/webhook", methods=["POST"]) | ||
def webhook(): | ||
config = app.config | ||
request_form = request.form.to_dict() | ||
infohash = request_form.get("infohash") | ||
# NOTE: always ensure safety checks are done before this filepath is ever used | ||
filepath = f"{config['input_dir']}/{infohash}.torrent" | ||
|
||
if infohash is None: | ||
return http_error("Request must include an 'infohash' parameter", 400) | ||
if not is_valid_infohash(infohash): | ||
return http_error("Invalid infohash", 400) | ||
if not os.path.exists(filepath): | ||
return http_error(f"No torrent found at {filepath}", 404) | ||
|
||
try: | ||
_, new_filepath = generate_new_torrent_from_file( | ||
filepath, | ||
config["output_dir"], | ||
config["red_api"], | ||
config["ops_api"], | ||
) | ||
|
||
return http_success(new_filepath, 201) | ||
except TorrentAlreadyExistsError as e: | ||
return http_error(str(e), 409) | ||
except TorrentNotFoundError as e: | ||
return http_error(str(e), 404) | ||
except Exception as e: | ||
return http_error(str(e), 500) | ||
|
||
|
||
@app.errorhandler(404) | ||
def page_not_found(_e): | ||
return http_error("Not found", 404) | ||
|
||
|
||
def http_success(message, code): | ||
return {"status": "success", "message": message}, code | ||
|
||
|
||
def http_error(message, code): | ||
return {"status": "error", "message": message}, code | ||
|
||
|
||
def run_webserver(input_dir, output_dir, red_api, ops_api, host="0.0.0.0", port=9713): | ||
app.config.update( | ||
{ | ||
"input_dir": input_dir, | ||
"output_dir": output_dir, | ||
"red_api": red_api, | ||
"ops_api": ops_api, | ||
} | ||
) | ||
|
||
app.run(debug=False, host=host, port=port) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,25 @@ | ||
import os | ||
import shutil | ||
|
||
|
||
def get_torrent_path(name): | ||
return f"tests/support/torrents/{name}.torrent" | ||
|
||
|
||
class SetupTeardown: | ||
TORRENT_SUCCESS_RESPONSE = {"status": "success", "response": {"torrent": {"filePath": "foo", "id": 123}}} | ||
TORRENT_KNOWN_BAD_RESPONSE = {"status": "failure", "error": "bad hash parameter"} | ||
TORRENT_UNKNOWN_BAD_RESPONSE = {"status": "failure", "error": "unknown error"} | ||
ANNOUNCE_SUCCESS_RESPONSE = {"status": "success", "response": {"passkey": "bar"}} | ||
|
||
def setup_method(self): | ||
for f in os.listdir("/tmp"): | ||
if f.endswith(".torrent"): | ||
os.remove(os.path.join("/tmp", f)) | ||
|
||
os.makedirs("/tmp/input", exist_ok=True) | ||
os.makedirs("/tmp/output", exist_ok=True) | ||
|
||
def teardown_method(self): | ||
pass | ||
shutil.rmtree("/tmp/input", ignore_errors=True) | ||
shutil.rmtree("/tmp/output", ignore_errors=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import re | ||
import os | ||
import pytest | ||
import shutil | ||
import requests_mock | ||
|
||
from .support import SetupTeardown, get_torrent_path | ||
|
||
from src.webserver import app as webserver_app | ||
|
||
|
||
@pytest.fixture() | ||
def app(red_api, ops_api): | ||
webserver_app.config.update( | ||
{ | ||
"input_dir": "/tmp/input", | ||
"output_dir": "/tmp/output", | ||
"red_api": red_api, | ||
"ops_api": ops_api, | ||
} | ||
) | ||
|
||
yield webserver_app | ||
|
||
|
||
@pytest.fixture() | ||
def client(app): | ||
return app.test_client() | ||
|
||
|
||
@pytest.fixture() | ||
def infohash(): | ||
return "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33" | ||
|
||
|
||
class TestWebserverNotFound(SetupTeardown): | ||
def test_returns_not_found(self, client): | ||
response = client.get("/api/does-not-exist") | ||
assert response.status_code == 404 | ||
assert response.json == {"status": "error", "message": "Not found"} | ||
|
||
|
||
class TestWebserverWebhook(SetupTeardown): | ||
def test_requires_infohash_parameter(self, client): | ||
response = client.post("/api/webhook", data={}) | ||
assert response.status_code == 400 | ||
assert response.json == {"status": "error", "message": "Request must include an 'infohash' parameter"} | ||
|
||
def test_rejects_invalid_infohash(self, client): | ||
responses = [ | ||
client.post("/api/webhook", data={"infohash": "abc"}), | ||
client.post("/api/webhook", data={"infohash": "mnopqrstuvwx"}), | ||
client.post("/api/webhook", data={"infohash": 123}), | ||
] | ||
|
||
for response in responses: | ||
assert response.status_code == 400 | ||
assert response.json == {"status": "error", "message": "Invalid infohash"} | ||
|
||
def test_requires_existing_torrent_file(self, client, infohash): | ||
response = client.post("/api/webhook", data={"infohash": infohash}) | ||
assert response.status_code == 404 | ||
assert response.json == {"status": "error", "message": f"No torrent found at /tmp/input/{infohash}.torrent"} | ||
|
||
def test_generates_new_torrent_file(self, client, infohash): | ||
shutil.copy(get_torrent_path("red_source"), f"/tmp/input/{infohash}.torrent") | ||
|
||
with requests_mock.Mocker() as m: | ||
m.get(re.compile("action=torrent"), json=self.TORRENT_SUCCESS_RESPONSE) | ||
m.get(re.compile("action=index"), json=self.ANNOUNCE_SUCCESS_RESPONSE) | ||
|
||
response = client.post("/api/webhook", data={"infohash": infohash}) | ||
assert response.status_code == 201 | ||
assert response.json == {"status": "success", "message": "/tmp/output/foo [OPS].torrent"} | ||
assert os.path.exists("/tmp/output/foo [OPS].torrent") | ||
|
||
def test_returns_error_if_torrent_already_exists(self, client, infohash): | ||
shutil.copy(get_torrent_path("red_source"), f"/tmp/input/{infohash}.torrent") | ||
shutil.copy(get_torrent_path("red_source"), "/tmp/output/foo [OPS].torrent") | ||
|
||
with requests_mock.Mocker() as m: | ||
m.get(re.compile("action=torrent"), json=self.TORRENT_SUCCESS_RESPONSE) | ||
m.get(re.compile("action=index"), json=self.ANNOUNCE_SUCCESS_RESPONSE) | ||
|
||
response = client.post("/api/webhook", data={"infohash": infohash}) | ||
assert response.status_code == 409 | ||
assert response.json == { | ||
"status": "error", | ||
"message": "Torrent file already exists at /tmp/output/foo [OPS].torrent", | ||
} | ||
|
||
def test_returns_error_if_torrent_not_found(self, client, infohash): | ||
shutil.copy(get_torrent_path("red_source"), f"/tmp/input/{infohash}.torrent") | ||
|
||
with requests_mock.Mocker() as m: | ||
m.get(re.compile("action=torrent"), json=self.TORRENT_KNOWN_BAD_RESPONSE) | ||
m.get(re.compile("action=index"), json=self.ANNOUNCE_SUCCESS_RESPONSE) | ||
|
||
response = client.post("/api/webhook", data={"infohash": infohash}) | ||
assert response.status_code == 404 | ||
assert response.json == {"status": "error", "message": "Torrent could not be found on OPS"} | ||
|
||
def test_returns_error_if_unknown_error(self, client, infohash): | ||
shutil.copy(get_torrent_path("red_source"), f"/tmp/input/{infohash}.torrent") | ||
|
||
with requests_mock.Mocker() as m: | ||
m.get(re.compile("action=torrent"), json=self.TORRENT_UNKNOWN_BAD_RESPONSE) | ||
m.get(re.compile("action=index"), json=self.ANNOUNCE_SUCCESS_RESPONSE) | ||
|
||
response = client.post("/api/webhook", data={"infohash": infohash}) | ||
assert response.status_code == 500 | ||
assert response.json == {"status": "error", "message": "An unknown error occurred in the API response from OPS"} |