Files

254 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import shutil
import subprocess
from urllib.parse import quote, unquote
import httpx
import pytest
from fastapi.testclient import TestClient
from app.server import Settings, create_app
class FakeZipline:
def __init__(self):
self.names = set() # names Zipline already has
self.race = set() # free at HEAD time, taken by the time we upload
self.fail_with = None # force this status on /api/upload
self.raw_urls = False # return the url unencoded, like a careless server would
self.head_fails = False # HEAD raises, as if Zipline went away mid-request
self.uploads = []
def handler(self, request: httpx.Request) -> httpx.Response:
if request.method == "HEAD" and self.head_fails:
raise httpx.ConnectError("down")
if request.method == "HEAD":
name = unquote(request.url.path[1:])
if name in self.names:
# Zipline redirects text files (and password-protected ones) to /view/<name>
return httpx.Response(302 if name.endswith(".txt") else 200)
return httpx.Response(404)
if request.url.path == "/api/upload":
if self.fail_with:
return httpx.Response(self.fail_with, json={"error": "boom"})
stem = unquote(request.headers["x-zipline-filename"])
name = stem + request.headers.get("x-zipline-file-extension", "")
if name in self.names or name in self.race:
self.race.discard(name)
self.names.add(name)
return httpx.Response(400, json={"error": "file with the same name already exists"})
self.names.add(name)
self.uploads.append({"name": name, "headers": dict(request.headers), "body": request.read()})
return httpx.Response(200, json={"files": [{
"id": "1", "name": name, "type": "video/mp4",
"url": f"https://clips.example.com/{name if self.raw_urls else quote(name)}",
}]})
return httpx.Response(404)
@pytest.fixture
def zl():
return FakeZipline()
@pytest.fixture
def client(zl, tmp_path):
settings = Settings(zipline_url="http://zipline:3000", site_origin="https://clips.example.com",
upload_url="https://upload.example.com", max_bytes=1000, deletes_at="90d", work_dir=tmp_path)
return TestClient(create_app(settings, transport=httpx.MockTransport(zl.handler)))
AUTH = {"Authorization": "tok"}
def up(client, name, data=b"x" * 10, headers=AUTH):
return client.post("/upload", files={"file": (name, data, "application/octet-stream")}, headers=headers)
def test_health(client):
assert client.get("/health").json() == {"ok": True}
def test_missing_token_is_401(client, zl):
r = up(client, "a.mp4", headers={})
assert r.status_code == 401
assert zl.uploads == []
def test_missing_file_field_is_400(client):
r = client.post("/upload", data={"nope": "1"}, headers=AUTH)
assert r.status_code == 400
def test_spaces_become_dashes_and_headers(client, zl):
r = up(client, "my clip 1.mp4")
assert r.status_code == 200
assert r.json() == {"url": "https://clips.example.com/my-clip-1.mp4", "name": "my-clip-1.mp4", "remuxed": False}
h = zl.uploads[0]["headers"]
assert h["authorization"] == "tok"
assert h["x-zipline-format"] == "name"
assert h["x-zipline-filename"] == "my-clip-1"
assert h["x-zipline-file-extension"] == ".mp4"
assert h["x-zipline-deletes-at"] == "90d"
assert h["x-zipline-original-name"] == "true"
assert zl.uploads[0]["body"].count(b"x" * 10) == 1
def test_duplicate_gets_suffix(client, zl):
zl.names |= {"clip.mp4", "clip-2.mp4"}
assert up(client, "clip.mp4").json()["name"] == "clip-3.mp4"
def test_taken_text_file_redirect_counts_as_taken(client, zl):
zl.names |= {"n.txt", "n-2.txt", "n-3.txt"}
r = up(client, "n.txt")
assert r.status_code == 200
assert r.json()["name"] == "n-4.txt"
def test_race_retries_next_name(client, zl):
zl.race.add("clip.mp4")
r = up(client, "clip.mp4")
assert r.status_code == 200
assert r.json()["name"] == "clip-2.mp4"
def test_unicode_and_special_chars(client, zl):
r = up(client, "çılgın an #1.mp4")
assert r.status_code == 200
assert r.json()["name"] == "çılgın-an-#1.mp4"
assert zl.uploads[0]["headers"]["x-zipline-filename"] == quote("çılgın-an-#1")
def test_no_extension(client, zl):
r = up(client, "README")
assert r.json()["name"] == "README"
assert "x-zipline-file-extension" not in zl.uploads[0]["headers"]
def test_exact_limit_ok(client):
assert up(client, "a.bin", data=b"x" * 1000).status_code == 200
def test_one_byte_over_is_413(client, zl):
r = up(client, "a.bin", data=b"x" * 1001)
assert r.status_code == 413
assert r.json()["error"] == f"file is larger than the {1000 / 1024**3:g} GB limit"
assert zl.uploads == []
def test_huge_content_length_rejected_early(client, zl):
r = client.post("/upload", content=b"x", headers={**AUTH, "content-length": str(10**12),
"content-type": "multipart/form-data; boundary=b"})
assert r.status_code == 413
def test_zipline_error_is_502_and_cleans_up(client, zl, tmp_path):
zl.fail_with = 500
r = up(client, "a b.mkv", data=b"garbage")
assert r.status_code == 502
assert list(tmp_path.iterdir()) == []
def test_zipline_4xx_passed_through(client, zl):
zl.fail_with = 401
assert up(client, "a.mp4").status_code == 401
def test_bad_mkv_kept_as_mkv(client, zl):
r = up(client, "a b.mkv", data=b"not a video")
assert r.status_code == 200
assert r.json() == {"url": "https://clips.example.com/a-b.mkv", "name": "a-b.mkv", "remuxed": False}
@pytest.mark.skipif(shutil.which("ffmpeg") is None, reason="ffmpeg not installed")
def test_good_mkv_becomes_mp4(zl, tmp_path):
src = tmp_path / "src.mkv"
subprocess.run(["ffmpeg", "-v", "error", "-y", "-f", "lavfi", "-i", "testsrc=size=320x240:rate=30",
"-t", "1", "-c:v", "libx264", str(src)], check=True)
work = tmp_path / "work"
work.mkdir()
settings = Settings(zipline_url="http://zipline:3000", site_origin="https://clips.example.com",
upload_url="https://upload.example.com", max_bytes=10**8, deletes_at="90d", work_dir=work)
c = TestClient(create_app(settings, transport=httpx.MockTransport(zl.handler)))
r = up(c, "my clip.mkv", data=src.read_bytes())
assert r.json()["name"] == "my-clip.mp4"
assert r.json()["remuxed"] is True
assert zl.uploads[0]["headers"]["x-zipline-file-extension"] == ".mp4"
assert list(work.iterdir()) == []
def test_generic_content_type_is_guessed_from_extension(client, zl):
# curl/ShareX send application/octet-stream; Discord only plays video/mp4 inline
assert up(client, "clip.mp4").status_code == 200
assert b"Content-Type: video/mp4" in zl.uploads[0]["body"]
def test_unknown_extension_stays_octet_stream(client, zl):
assert up(client, "data.zzz9").status_code == 200
assert b"Content-Type: application/octet-stream" in zl.uploads[0]["body"]
def test_upload_with_ticket(client, zl):
ticket = client.app.state.tickets.issue("tok-from-ticket")
r = up(client, "t.mp4", headers={"x-upload-ticket": ticket})
assert r.status_code == 200
assert zl.uploads[0]["headers"]["authorization"] == "tok-from-ticket"
def test_ticket_single_use(client, zl):
ticket = client.app.state.tickets.issue("tok")
assert up(client, "a.mp4", headers={"x-upload-ticket": ticket}).status_code == 200
r = up(client, "b.mp4", headers={"x-upload-ticket": ticket})
assert r.status_code == 401
assert r.json() == {"error": "upload ticket expired, try again"}
assert len(zl.uploads) == 1
def test_bad_ticket_is_401(client, zl):
assert up(client, "a.mp4", headers={"x-upload-ticket": "made-up"}).status_code == 401
assert zl.uploads == []
def test_cors_preflight_for_site_origin(client):
r = client.options("/upload", headers={
"origin": "https://clips.example.com",
"access-control-request-method": "POST",
"access-control-request-headers": "x-upload-ticket",
})
assert r.status_code == 200
assert r.headers["access-control-allow-origin"] == "https://clips.example.com"
assert "x-upload-ticket" in r.headers["access-control-allow-headers"].lower()
def test_cors_refuses_other_origin(client):
r = client.options("/upload", headers={
"origin": "https://evil.example",
"access-control-request-method": "POST",
})
assert "access-control-allow-origin" not in r.headers
def test_cors_headers_on_error_responses(client):
r = up(client, "a.bin", data=b"x" * 1001, headers={**AUTH, "origin": "https://clips.example.com"})
assert r.status_code == 413
assert r.headers["access-control-allow-origin"] == "https://clips.example.com"
def test_link_is_built_from_the_name_not_ziplines_url(client, zl):
# a '#' left unencoded in Zipline's url would cut the shared link off
zl.raw_urls = True
r = up(client, "çılgın an #1.mp4")
assert r.json()["url"] == "https://clips.example.com/" + quote("çılgın-an-#1.mp4")
def test_zipline_down_during_name_check_is_502(client, zl):
zl.head_fails = True
r = up(client, "a.mp4")
assert r.status_code == 502
def test_no_free_name_is_409(client, zl, monkeypatch):
from app import server
monkeypatch.setattr(server, "next_free_name", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("full")))
assert up(client, "a.mp4").status_code == 409