import pytest
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from intent.comments import services as comment_services
from intent.comments.models import CommentThread, ThreadKind
from intent.features.services import create_feature
from intent.files import services
from intent.files.models import FeatureFileVersion, FileKind
from intent.issues.services import promotion_text
PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
def upload(name="mockup.png", data=PNG):
return SimpleUploadedFile(name, data, content_type="application/octet-stream")
def furl(route, file=None, feature=None, **kwargs):
feature = feature or file.feature
kwargs = {
"org_slug": "catbriar",
"project_slug": "intent",
"feature_key": feature.key,
**kwargs,
}
if file is not None:
kwargs["file_id"] = file.pk
return reverse(f"files:{route}", kwargs=kwargs)
def raw_url(version):
return furl("raw", version.file, version=version.version, name=version.original_name)
@pytest.fixture
def feature(open_project, owner):
return create_feature(open_project, owner, title="Sign in")
@pytest.fixture
def image(feature, owner):
return services.add_file(
feature, owner, kind=FileKind.PROTOTYPE, upload=upload(), note="First mockup"
)
def current(file):
return file.versions.get(version=file.current_version)
def test_adding_a_file_is_a_revision(feature, image):
feature.refresh_from_db()
revision = feature.revisions.get(revision=2)
assert feature.current_revision == 2 and image.added_in_revision == 2
assert revision.files_change == "Added prototype “mockup.png”"
assert revision.note == "First mockup"
assert current(image).content_type == "image/png"
def test_type_is_decided_by_extension_and_checked(feature, owner):
with pytest.raises(ValidationError, match="isn't allowed"):
services.add_file(feature, owner, kind="prototype", upload=upload("x.exe"), note="n")
with pytest.raises(ValidationError, match="empty"):
services.add_file(feature, owner, kind="prototype", upload=upload(data=b""), note="n")
def test_size_cap(feature, owner, monkeypatch):
monkeypatch.setattr(services, "MAX_BYTES", 10)
with pytest.raises(ValidationError, match="up to"):
services.add_file(feature, owner, kind="prototype", upload=upload(), note="n")
def test_note_required_and_editors_only(feature, owner, collaborator):
with pytest.raises(ValidationError):
services.add_file(feature, owner, kind="prototype", upload=upload(), note=" ")
with pytest.raises(PermissionDenied):
services.add_file(feature, collaborator, kind="prototype", upload=upload(), note="n")
def test_replace_keeps_old_versions_and_their_threads(image, owner, outsider):
old = current(image)
pin = comment_services.start_file_thread(old, outsider, body="Here", x=0.25, y=0.5)
services.replace_file(image, owner, upload=upload("mockup-2.png"), note="Tweak")
image.refresh_from_db()
assert (image.current_version, image.name) == (2, "mockup-2.png")
assert image.feature.revisions.last().files_change == "Replaced “mockup.png” with version 2"
assert CommentThread.objects.get(pk=pin.pk).file_version == old
with pytest.raises(ValidationError, match="latest"):
comment_services.start_file_thread(old, outsider, body="Late")
def test_remove_and_files_at_revision(feature, image, owner):
services.replace_file(image, owner, upload=upload("v2.png"), note="v2") # revision 3
services.remove_file(image, owner, note="Not needed") # revision 4
image.refresh_from_db()
assert image.removed_in_revision == 4
assert [v.version for v in services.files_at(feature, 1)] == []
assert [v.version for v in services.files_at(feature, 2)] == [1]
assert [v.version for v in services.files_at(feature, 3)] == [2]
assert services.files_at(feature, 4) == []
with pytest.raises(ValidationError):
services.remove_file(image, owner, note="Again")
def test_image_pins_need_an_image_and_a_point(feature, image, owner, outsider):
doc = services.add_file(
feature, owner, kind="documentation", upload=upload("notes.md", b"# Notes"), note="n"
)
with pytest.raises(ValidationError, match="Only images"):
comment_services.start_file_thread(current(doc), outsider, body="x", x=0.1, y=0.1)
with pytest.raises(ValidationError, match="point"):
comment_services.start_file_thread(current(image), outsider, body="x", x=0.1)
with pytest.raises(ValidationError, match="point"):
comment_services.start_file_thread(current(image), outsider, body="x", x=1.5, y=0.1)
file_thread = comment_services.start_file_thread(current(doc), outsider, body="Whole")
assert file_thread.kind == ThreadKind.FILE
def test_promotion_text_mentions_the_pin(image, outsider):
thread = comment_services.start_file_thread(
current(image), outsider, body="Button too small", x=0.25, y=0.5
)
_, body = promotion_text(thread)
assert "file “mockup.png” version 1, at 25% across and 50% down" in body
# Views
def test_upload_via_feature_page(client, feature, editor):
client.force_login(editor)
response = client.post(
furl("add", feature=feature),
{"kind": "prototype", "upload": upload(), "note": "Mockup"},
)
file = feature.files.get()
assert response["Location"] == furl("detail", file)
page = client.get(
reverse(
"features:detail",
kwargs={"org_slug": "catbriar", "project_slug": "intent", "feature_key": "INT-1"},
)
).content.decode()
assert furl("detail", file) in page
def test_bad_upload_via_page_shows_error(client, feature, editor):
client.force_login(editor)
response = client.post(
furl("add", feature=feature),
{"kind": "prototype", "upload": upload("evil.exe"), "note": "x"},
follow=True,
)
assert b"isn't allowed" in response.content
assert not feature.files.exists()
def test_pin_via_form_and_page_shows_it(client, image, outsider):
client.force_login(outsider)
response = client.post(
furl("thread_create", image), {"body": "Too small", "across": "25", "down": "50"}
)
thread = CommentThread.objects.get()
assert (thread.kind, thread.x, thread.y) == (ThreadKind.IMAGE, 0.25, 0.5)
assert response["Location"] == furl("detail", image) + f"#thread-{thread.pk}"
page = client.get(furl("detail", image)).content.decode()
assert '
hi
" def test_svg_and_zip_cannot_run_or_render(client, feature, owner): svg = services.add_file( feature, owner, kind="prototype", upload=upload("a.svg", b""), note="n" ) zipped = services.add_file( feature, owner, kind="documentation", upload=upload("a.zip", b"PK"), note="n" ) client.force_login(owner) svg_response = client.get(raw_url(current(svg))) assert svg_response["Content-Security-Policy"].startswith("sandbox;") zip_response = client.get(raw_url(current(zipped))) assert zip_response["Content-Disposition"].startswith("attachment") def test_bucket_storage_hands_off_to_a_signed_url(client, image, owner, monkeypatch): version = current(image) storage = version.blob.storage calls = {} def fake_url(name, parameters=None, **kwargs): calls.update(parameters) return f"https://bucket.example/{name}?signature=x" monkeypatch.setattr(storage, "querystring_auth", True, raising=False) monkeypatch.setattr(storage, "url", fake_url) client.force_login(owner) response = client.get(raw_url(version)) assert response.status_code == 302 assert response["Location"].startswith("https://bucket.example/files/") assert calls["ResponseContentType"] == "image/png" assert calls["ResponseContentDisposition"].startswith("inline") def test_markdown_file_renders_on_its_page(client, feature, owner): doc = services.add_file( feature, owner, kind="documentation", upload=upload("notes.md", b"# Notes\n\nHi"), note="n", ) # fmt: skip client.force_login(owner) page = client.get(furl("detail", doc)).content.decode() assert "