kaledonya clips: a Discord-friendly clip site on top of Zipline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
e6f8905377
34 files changed
+2917
No files matched your search
@@ -0,0 +1,253 @@
|
||||
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
|
||||
Reference in new issue
Block a user