from uuid import uuid4 import pytest from httpx import AsyncClient from lnbits.core.crud.assets import get_user_asset from lnbits.core.services.assets import create_user_asset from tests.helpers import get_png_bytes, get_user_token_headers, make_upload_file @pytest.mark.anyio async def test_asset_api_upload_list_update_and_delete( client: AsyncClient, user_headers_from: dict[str, str], ): payload = get_png_bytes() upload = await client.post( "/api/v1/assets?public_asset=false", headers={"Authorization": user_headers_from["Authorization"]}, files={"file": ("note.png", payload, "image/png")}, ) assert upload.status_code == 200 asset = upload.json() assert asset["name"] == "note.png" assert asset["is_public"] is False page = await client.get("/api/v1/assets/paginated", headers=user_headers_from) assert page.status_code == 200 assert any(item["id"] == asset["id"] for item in page.json()["data"]) info = await client.get(f"/api/v1/assets/{asset['id']}", headers=user_headers_from) assert info.status_code == 200 assert info.json()["name"] == "note.png" data = await client.get( f"/api/v1/assets/{asset['id']}/data", headers=user_headers_from ) assert data.status_code == 200 assert data.content == payload assert data.headers["content-type"] == "image/png" assert data.headers["content-disposition"] == 'inline; filename="note.png"' assert data.headers["x-content-type-options"] == "nosniff" assert data.headers["content-security-policy"].startswith("sandbox") updated = await client.put( f"/api/v1/assets/{asset['id']}", headers=user_headers_from, json={"name": "renamed.png", "is_public": True}, ) assert updated.status_code == 200 assert updated.json()["name"] == "renamed.png" assert updated.json()["is_public"] is True public_data = await client.get(f"/api/v1/assets/{asset['id']}/data") assert public_data.status_code == 200 assert public_data.content == payload deleted = await client.delete( f"/api/v1/assets/{asset['id']}", headers=user_headers_from ) assert deleted.status_code == 200 assert deleted.json()["success"] is True missing = await client.get( f"/api/v1/assets/{asset['id']}", headers=user_headers_from ) assert missing.status_code == 404 @pytest.mark.anyio async def test_asset_api_enforces_visibility_and_supports_admin_updates( client: AsyncClient, from_user, to_user, superuser_token: str, ): private_asset = await create_user_asset( from_user.id, make_upload_file( get_png_bytes(), filename=f"private_{uuid4().hex[:8]}.png", content_type="image/png", ), is_public=False, ) other_user_headers = await get_user_token_headers(client, to_user.id) anonymous = await client.get(f"/api/v1/assets/{private_asset.id}/data") assert anonymous.status_code == 404 wrong_user = await client.get( f"/api/v1/assets/{private_asset.id}/data", headers=other_user_headers ) assert wrong_user.status_code == 404 admin_updated = await client.put( f"/api/v1/assets/{private_asset.id}", headers={"Authorization": f"Bearer {superuser_token}"}, json={"is_public": True, "name": "admin-visible.png"}, ) assert admin_updated.status_code == 200 assert admin_updated.json()["is_public"] is True assert admin_updated.json()["name"] == "admin-visible.png" image_data = await client.get(f"/api/v1/assets/{private_asset.id}/data") assert image_data.status_code == 200 assert image_data.headers["content-type"] == "image/png" assert image_data.headers["content-disposition"] == ( 'inline; filename="admin-visible.png"' ) assert image_data.headers["x-content-type-options"] == "nosniff" thumbnail = await client.get(f"/api/v1/assets/{private_asset.id}/thumbnail") assert thumbnail.status_code == 200 assert thumbnail.content assert thumbnail.headers["content-type"] == "image/png" assert thumbnail.headers["content-disposition"] == ( 'inline; filename="admin-visible.png"' ) @pytest.mark.anyio async def test_asset_api_blocks_non_image_uploads( client: AsyncClient, user_headers_from: dict[str, str], ): payload = ( b'' b'' b'' b"" b"" b"" ) blocked = await client.post( "/api/v1/assets", headers={"Authorization": user_headers_from["Authorization"]}, files={"file": ("payload.xsl", payload, "text/xml")}, ) assert blocked.status_code == 400 assert blocked.json()["detail"] == "File type 'text/xml' not allowed." @pytest.mark.anyio async def test_asset_api_validates_uploads_and_missing_assets( client: AsyncClient, to_user, user_headers_from: dict[str, str], ): invalid = await client.post( "/api/v1/assets", headers={"Authorization": user_headers_from["Authorization"]}, files={"file": ("payload.exe", b"boom", "application/x-msdownload")}, ) assert invalid.status_code == 400 assert "not allowed" in invalid.json()["detail"] fake_image_headers = await get_user_token_headers(client, to_user.id) fake_image = await client.post( "/api/v1/assets", headers={"Authorization": fake_image_headers["Authorization"]}, files={"file": ("fake.png", b"", "image/png")}, ) assert fake_image.status_code == 400 assert "does not match declared file type" in fake_image.json()["detail"] missing = await client.delete( f"/api/v1/assets/{uuid4().hex}", headers=user_headers_from, ) assert missing.status_code == 404 missing_thumb = await client.get(f"/api/v1/assets/{uuid4().hex}/thumbnail") assert missing_thumb.status_code == 404 stored = await create_user_asset( "missing-user-check", make_upload_file( get_png_bytes(), filename="content.png", content_type="image/png" ), is_public=True, ) fetched = await get_user_asset("missing-user-check", stored.id) assert fetched is not None