import pytest import requests from playwright.sync_api import expect from helpers import Actor COLLECTION = "dev.starhaven.mod.version" LISTING_COLLECTION = "dev.starhaven.mod.listing" def test_publish_version_writes_to_pds_and_serves_the_file(actor: Actor): actor.login() actor.create_mod("test-mod") actor.publish_version("test-mod", "v1.0", "Added cats", deliverable=b"patch bytes") [record] = actor.records(COLLECTION) assert record["value"]["$type"] == COLLECTION assert record["value"]["name"] == "v1.0" assert record["value"]["description"] == "Added cats" # The version points at the listing it is a version of. [listing] = actor.records(LISTING_COLLECTION) assert record["value"]["mod"] == listing["uri"] # The deliverable is readable by anyone, with no signature. response = requests.get(record["value"]["deliverable"], timeout=30) response.raise_for_status() assert response.content == b"patch bytes" def test_the_card_offers_the_newest_version_and_the_tab_lists_them_all(actor: Actor): actor.login() actor.create_mod("test-mod") expect(actor.page.get_by_text("This mod has no downloads yet.")).to_be_visible() actor.publish_version("test-mod", "v1.0", "Added cats") actor.publish_version("test-mod", "v2.0", "Added dogs") actor.page.goto(f"{actor.app_url}/{actor.handle}/test-mod") expect(actor.page.get_by_role("link", name="Download v2.0")).to_be_visible() actor.page.get_by_role("link", name="Versions").click() expect(actor.page.locator(".version-header h2").first).to_be_visible() names = actor.page.locator(".version-header h2").all_text_contents() assert names == ["v2.0", "v1.0"] def test_the_version_form_still_works_after_an_htmx_tab_swap(actor: Actor): """The dialog and its upload script arrive in the fragment, not the document.""" actor.login() actor.create_mod("test-mod") actor.page.goto(f"{actor.app_url}/{actor.handle}/test-mod") # Survives the swap only if the document was never reloaded. actor.page.evaluate("window.__notReloaded = true") actor.page.get_by_role("link", name="Versions").click() expect(actor.page).to_have_url(f"{actor.app_url}/{actor.handle}/test-mod/versions") assert actor.page.evaluate("window.__notReloaded") is True # The form is folded away until asked for, then publishes for real. version = actor.page.get_by_label("Version", exact=True) expect(version).to_be_hidden() actor.publish_version("test-mod", "v1.0", "Added cats", goto=False) [record] = actor.records(COLLECTION) assert record["value"]["name"] == "v1.0" def test_the_form_marks_no_field_until_one_is_actually_wrong(actor: Actor): actor.login() actor.create_mod("test-mod") actor.page.goto(f"{actor.app_url}/{actor.handle}/test-mod/versions") actor.page.get_by_role("button", name="Upload a new version").click() # An empty error slot must not make its field look rejected. marked = ".form-item:has(.error:not([hidden]))" assert actor.page.locator(marked).count() == 0 actor.page.get_by_role("button", name="Publish version").click() # Every empty field is reported at once, not one per attempt, and each # message appears against its own field as well as in the summary. summary = actor.page.locator("#version-dialog-summary") expect(summary).to_be_visible() assert summary.locator("li").all_text_contents() == [ "Enter a version", "Enter what changed", "Select a file", ] for field in ["version-name", "version-description", "version-file"]: expect(actor.page.locator(f"#{field}-error")).to_be_visible() # Filling one in takes its error away and leaves the others. actor.page.get_by_label("Version", exact=True).fill("v1.0") actor.page.get_by_role("button", name="Publish version").click() assert summary.locator("li").all_text_contents() == [ "Enter what changed", "Select a file", ] expect(actor.page.locator("#version-name-error")).to_be_hidden() def test_a_stranger_can_neither_publish_nor_be_offered_the_form(actor: Actor): actor.login() actor.create_mod("test-mod") actor.page.context.clear_cookies() actor.page.goto(f"{actor.app_url}/{actor.handle}/test-mod/versions") expect(actor.page.get_by_role("button", name="Upload a new version")).to_have_count(0) # The UI hiding it is not the check that matters. response = requests.post( f"{actor.app_url}/{actor.handle}/test-mod/versions/presign", json={"filename": "mod.bps", "size": 1024}, timeout=10, ) assert response.status_code == 403 def test_deleting_a_mod_takes_its_versions_off_the_site(actor: Actor): actor.login() actor.create_mod("test-mod") actor.publish_version("test-mod", "v1.0", "Added cats") actor.delete_mod("test-mod") actor.page.goto(f"{actor.app_url}/{actor.handle}/test-mod/versions") expect(actor.page.get_by_role("heading", name="v1.0")).to_have_count(0) # 12KB of room, so two 8KB uploads fit individually but not together. @pytest.mark.parametrize( "server_args", [["--s3-user-quota-bytes", "12288"]], indirect=True ) def test_the_quota_counts_every_mod_the_user_has(actor: Actor): actor.login() actor.create_mod("first-mod", title="First Mod") actor.create_mod("second-mod", title="Second Mod") # Fits on its own, and uses most of the quota. actor.publish_version("first-mod", "v1.0", "Added cats", deliverable=b"x" * 8000) # Would fit on its own too, but not on top of the first - and it is a # version of a different mod, so this is the per-user total being counted. actor.submit_version("second-mod", "v1.0", "Added dogs", deliverable=b"x" * 8000) assert "you have left" in actor.version_error() assert len(actor.records(COLLECTION)) == 1