454 lines
16 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import time
import requests
from playwright.sync_api import expect
from tests.conftest import BASE_URL
from devplacepy.database import get_table
_counter_project_files = [0]
def _signup_project_files():
_counter_project_files[0] += 1
name = f"pf{int(time.time() * 1000)}{_counter_project_files[0]}"
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
key = get_table("users").find_one(username=name)["api_key"]
return name, key
def _h_project_files(key):
return {"X-API-KEY": key, "Accept": "application/json"}
def _create_project_project_files(key, title):
r = requests.post(
f"{BASE_URL}/projects/create",
headers=_h_project_files(key),
data={
"title": title,
"description": "filesystem test",
"project_type": "software",
"status": "In Development",
},
)
assert r.status_code == 200, r.text
return r.json()["data"]
def _write_project_files(key, slug, path, content):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/write",
headers=_h_project_files(key),
data={"path": path, "content": content},
allow_redirects=False,
)
def _mkdir(key, slug, path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/mkdir",
headers=_h_project_files(key),
data={"path": path},
allow_redirects=False,
)
def _move(key, slug, from_path, to_path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/move",
headers=_h_project_files(key),
data={"from_path": from_path, "to_path": to_path},
allow_redirects=False,
)
def _delete(key, slug, path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/delete",
headers=_h_project_files(key),
data={"path": path},
allow_redirects=False,
)
def _list(slug, key=None):
return requests.get(
f"{BASE_URL}/projects/{slug}/files",
headers=_h_project_files(key) if key else {"Accept": "application/json"},
)
def _raw_project_files(slug, path, key=None):
return requests.get(
f"{BASE_URL}/projects/{slug}/files/raw",
params={"path": path},
headers=_h_project_files(key) if key else {"Accept": "application/json"},
)
def _paths(slug, key=None):
return sorted(f["path"] for f in _list(slug, key).json()["files"])
def _make_project_ui(page, title):
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", title)
page.fill("#description", "Project for filesystem UI test")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
return page.url
def _open_files(page, title):
proj_url = _make_project_ui(page, title)
slug = proj_url.rstrip("/").split("/")[-1]
page.goto(proj_url + "/files", wait_until="domcontentloaded")
return slug
def _dialog_fill(page, value):
page.locator(".dialog-overlay.visible .dialog-input").wait_for(state="visible")
page.fill(".dialog-overlay.visible .dialog-input", value)
page.click(".dialog-overlay.visible .dialog-confirm")
def _dialog_confirm(page):
page.locator(".dialog-overlay.visible .dialog-confirm").wait_for(state="visible")
page.click(".dialog-overlay.visible .dialog-confirm")
def _dialog_cancel(page):
page.locator(".dialog-overlay.visible .dialog-cancel").wait_for(state="visible")
page.click(".dialog-overlay.visible .dialog-cancel")
def _new_folder(page, name):
page.click("#pf-new-folder")
_dialog_fill(page, name)
page.wait_for_selector(f".pf-node-row:has-text('{name.split('/')[-1]}')")
def _new_file(page, name):
page.click("#pf-new-file")
_dialog_fill(page, name)
def _row(page, name):
return page.locator(f".pf-node-row:has-text('{name}')").first
def _alice_key():
return get_table("users").find_one(username="alice_test")["api_key"]
def test_files_link_on_detail(alice):
page, _ = alice
_make_project_ui(page, "UI Files Link")
link = page.locator("a:has-text('Files')")
expect(link).to_be_visible()
link.click()
page.wait_for_url("**/files", wait_until="domcontentloaded")
expect(page.locator("#pf-tree")).to_be_visible()
def test_files_page_owner_toolbar(alice):
page, _ = alice
_make_project_ui(page, "UI Owner Toolbar")
page.goto(page.url + "/files", wait_until="domcontentloaded")
for sel in ("#pf-new-file", "#pf-new-folder", "#pf-upload", "#pf-save"):
expect(page.locator(sel)).to_be_visible()
def test_create_file_via_ui(alice):
page, _ = alice
_make_project_ui(page, "UI Create File")
page.goto(page.url + "/files", wait_until="domcontentloaded")
_new_file(page, "src/hello.py")
expect(page.locator(".pf-node-row:has-text('hello.py')")).to_be_visible()
expect(page.locator(".pf-node-row:has-text('src')")).to_be_visible()
def test_edit_and_save_via_ui(alice):
page, _ = alice
proj_url = _make_project_ui(page, "UI Edit Save")
slug = proj_url.rstrip("/").split("/")[-1]
page.goto(proj_url + "/files", wait_until="domcontentloaded")
_new_file(page, "main.py")
page.locator(".pf-node-row:has-text('main.py')").click()
page.wait_for_function(
"window.app && window.app.projectFiles && window.app.projectFiles.currentPath === 'main.py'"
)
page.wait_for_timeout(300)
page.evaluate("window.app.projectFiles.editor.setValue('print(42)')")
page.click("#pf-save")
expect(page.locator("#pf-status")).to_have_text("Saved", timeout=4000)
page.wait_for_timeout(300)
assert _raw_project_files(slug, "main.py").json()["content"] == "print(42)"
def test_create_folder_via_ui(alice):
page, _ = alice
_make_project_ui(page, "UI Create Folder")
page.goto(page.url + "/files", wait_until="domcontentloaded")
_new_folder(page, "components")
expect(page.locator(".pf-node-row:has-text('components')")).to_be_visible()
def test_upload_via_ui(alice):
page, _ = alice
_make_project_ui(page, "UI Upload")
page.goto(page.url + "/files", wait_until="domcontentloaded")
with page.expect_file_chooser() as fc:
page.click("#pf-upload")
fc.value.set_files(
[
{
"name": "data.json",
"mimeType": "application/json",
"buffer": b'{"a":1}',
}
]
)
expect(page.locator(".pf-node-row:has-text('data.json')")).to_be_visible()
def test_delete_via_ui(alice):
page, _ = alice
_make_project_ui(page, "UI Delete")
page.goto(page.url + "/files", wait_until="domcontentloaded")
_new_file(page, "trash.txt")
page.locator(".pf-node-row:has-text('trash.txt')").click()
page.click("#pf-delete")
_dialog_confirm(page)
expect(page.locator(".pf-node-row:has-text('trash.txt')")).to_have_count(0)
def test_non_owner_readonly_ui(alice, bob):
alice_page, _ = alice
proj_url = _make_project_ui(alice_page, "UI Readonly")
alice_page.goto(proj_url + "/files", wait_until="domcontentloaded")
_new_file(alice_page, "shared.py")
expect(alice_page.locator(".pf-node-row:has-text('shared.py')")).to_be_visible()
bob_page, _ = bob
bob_page.goto(proj_url + "/files", wait_until="domcontentloaded")
expect(bob_page.locator(".pf-node-row:has-text('shared.py')")).to_be_visible()
expect(bob_page.locator("#pf-new-file")).to_have_count(0)
expect(bob_page.locator("#pf-save")).to_have_count(0)
def test_add_file_into_selected_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd File")
_new_folder(page, "src")
page.locator(".pf-node-row:has-text('src')").click()
_new_file(page, "main.py")
expect(page.locator(".pf-node-row:has-text('main.py')")).to_be_visible()
page.wait_for_timeout(300)
assert _raw_project_files(slug, "src/main.py").status_code == 200
assert _raw_project_files(slug, "main.py").status_code == 404
def test_add_folder_into_selected_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Folder")
_new_folder(page, "src")
page.locator(".pf-node-row:has-text('src')").click()
_new_folder(page, "components")
expect(page.locator(".pf-node-row:has-text('components')")).to_be_visible()
page.wait_for_timeout(300)
assert "src/components" in _paths(slug)
def test_upload_into_selected_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Upload")
_new_folder(page, "assets")
page.locator(".pf-node-row:has-text('assets')").click()
with page.expect_file_chooser() as fc:
page.click("#pf-upload")
fc.value.set_files(
[
{
"name": "config.json",
"mimeType": "application/json",
"buffer": b'{"k":1}',
}
]
)
page.wait_for_timeout(400)
assert _raw_project_files(slug, "assets/config.json").status_code == 200
def test_add_file_into_nested_selected_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Nested")
_new_folder(page, "src/api")
page.locator(".pf-node-row:has-text('api')").click()
_new_file(page, "routes.py")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "src/api/routes.py").status_code == 200
def test_new_folder_then_add_file_inside_without_reselect(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Chained")
_new_folder(page, "lib")
_new_file(page, "index.js")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "lib/index.js").status_code == 200
def test_add_file_uses_parent_of_selected_file(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Sibling")
_new_folder(page, "src")
page.locator(".pf-node-row:has-text('src')").click()
_new_file(page, "a.py")
page.locator(".pf-node-row:has-text('a.py')").click()
_new_file(page, "b.py")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "src/a.py").status_code == 200
assert _raw_project_files(slug, "src/b.py").status_code == 200
def test_add_file_at_root_when_nothing_selected(alice):
page, _ = alice
slug = _open_files(page, "UI DirAdd Root")
_new_file(page, "top.py")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "top.py").status_code == 200
def test_context_menu_opens_on_right_click(alice):
page, _ = alice
_open_files(page, "UI CtxOpen")
_new_file(page, "a.py")
_row(page, "a.py").click(button="right")
menu = page.locator(".context-menu.visible")
expect(menu).to_be_visible()
expect(menu).to_contain_text("Rename")
expect(menu).to_contain_text("Delete")
def test_context_menu_closes_on_outside_click(alice):
page, _ = alice
_open_files(page, "UI CtxClose")
_new_file(page, "a.py")
_row(page, "a.py").click(button="right")
expect(page.locator(".context-menu.visible")).to_be_visible()
page.locator("#pf-current").click()
expect(page.locator(".context-menu.visible")).to_have_count(0)
def test_context_menu_rename(alice):
page, _ = alice
slug = _open_files(page, "UI CtxRename")
_new_file(page, "old.py")
_row(page, "old.py").click(button="right")
page.locator(".context-menu.visible .context-menu-item:has-text('Rename')").click()
_dialog_fill(page, "renamed.py")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "renamed.py").status_code == 200
assert _raw_project_files(slug, "old.py").status_code == 404
def test_context_menu_delete_confirm(alice):
page, _ = alice
slug = _open_files(page, "UI CtxDelete")
_new_file(page, "gone.py")
_row(page, "gone.py").click(button="right")
page.locator(".context-menu.visible .context-menu-item:has-text('Delete')").click()
_dialog_confirm(page)
page.wait_for_timeout(300)
assert _raw_project_files(slug, "gone.py").status_code == 404
def test_context_menu_delete_cancel_keeps_file(alice):
page, _ = alice
slug = _open_files(page, "UI CtxDeleteCancel")
_new_file(page, "stay.py")
_row(page, "stay.py").click(button="right")
page.locator(".context-menu.visible .context-menu-item:has-text('Delete')").click()
_dialog_cancel(page)
page.wait_for_timeout(200)
assert _raw_project_files(slug, "stay.py").status_code == 200
def test_context_menu_new_file_in_directory(alice):
page, _ = alice
slug = _open_files(page, "UI CtxNewInDir")
_new_folder(page, "src")
_row(page, "src").click(button="right")
page.locator(
".context-menu.visible .context-menu-item:has-text('New file here')"
).click()
_dialog_fill(page, "inside.py")
page.wait_for_timeout(300)
assert _raw_project_files(slug, "src/inside.py").status_code == 200
def test_multi_select_ctrl_and_delete(alice):
page, _ = alice
slug = _open_files(page, "UI MultiDelete")
_new_file(page, "a.py")
_new_file(page, "b.py")
_new_file(page, "c.py")
_row(page, "a.py").click(modifiers=["Control"])
_row(page, "c.py").click(modifiers=["Control"])
expect(page.locator(".pf-node-row.pf-selected")).to_have_count(2)
_row(page, "c.py").click(button="right")
page.locator(
".context-menu.visible .context-menu-item:has-text('Delete 2 items')"
).click()
_dialog_confirm(page)
page.wait_for_timeout(400)
assert _raw_project_files(slug, "a.py").status_code == 404
assert _raw_project_files(slug, "c.py").status_code == 404
assert _raw_project_files(slug, "b.py").status_code == 200
def test_shift_select_range(alice):
page, _ = alice
_open_files(page, "UI ShiftRange")
_new_file(page, "a.py")
_new_file(page, "b.py")
_new_file(page, "c.py")
_row(page, "a.py").click()
_row(page, "c.py").click(modifiers=["Shift"])
expect(page.locator(".pf-node-row.pf-selected")).to_have_count(3)
def test_drag_file_into_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DragIntoDir")
_new_folder(page, "src")
_new_file(page, "move.py")
_row(page, "move.py").drag_to(_row(page, "src"))
page.wait_for_timeout(400)
assert _raw_project_files(slug, "src/move.py").status_code == 200
assert _raw_project_files(slug, "move.py").status_code == 404
def test_drag_file_to_root(alice):
page, _ = alice
slug = _open_files(page, "UI DragToRoot")
_new_folder(page, "src")
_row(page, "src").click()
_new_file(page, "deep.py")
page.wait_for_timeout(200)
assert _raw_project_files(slug, "src/deep.py").status_code == 200
_row(page, "deep.py").drag_to(page.locator(".pf-root-row"))
page.wait_for_timeout(400)
assert _raw_project_files(slug, "deep.py").status_code == 200
assert _raw_project_files(slug, "src/deep.py").status_code == 404
def test_drag_multi_selection_into_directory(alice):
page, _ = alice
slug = _open_files(page, "UI DragMulti")
_new_folder(page, "dest")
_new_file(page, "x.py")
_new_file(page, "y.py")
_row(page, "x.py").click(modifiers=["Control"])
_row(page, "y.py").click(modifiers=["Control"])
_row(page, "x.py").drag_to(_row(page, "dest"))
page.wait_for_timeout(500)
assert _raw_project_files(slug, "dest/x.py").status_code == 200
assert _raw_project_files(slug, "dest/y.py").status_code == 200
def test_toolbar_delete_uses_custom_dialog(alice):
page, _ = alice
slug = _open_files(page, "UI ToolbarDelete")
_new_file(page, "t.py")
_row(page, "t.py").click()
page.click("#pf-delete")
expect(page.locator(".dialog-overlay.visible")).to_be_visible()
_dialog_confirm(page)
page.wait_for_timeout(300)
assert _raw_project_files(slug, "t.py").status_code == 404
def test_empty_project_shows_hint(alice):
page, _ = alice
_open_files(page, "EmptyHint")
expect(page.locator("#pf-empty")).to_be_visible()
expect(page.locator("#pf-editor-wrap")).to_be_hidden()
expect(page.locator(".pf-empty-msg")).to_contain_text("Create a file")