"""Tests for `raycast_api.ai.files.FilesAPI`. Three endpoints with three different signing quirks: - POST /ai/files — normal signed JSON body, then unsigned PUT to a presigned URL on a different host. - GET /ai/files/{id} — signed body is the LITERAL two-byte string `"{}"` (not empty!). This is the one trap callers must not break. - DELETE /ai/files — signed JSON body sent ON a DELETE request. """ from __future__ import annotations import hashlib from pathlib import Path from typing import Any import pytest from aioresponses import aioresponses from raycast_api.client import Client from raycast_api.config import Config from raycast_api.signing import Signer from raycast_api.signing_spec import RotRange, SigningSpec REFERENCE_SECRET = "6bc455473576ce2cd6f70426caff867aabbe3f7291c1a79681af5e8ce0ca1408" DEVICE_ID = "20eca913cada74f879e6535304f9d44da380c28eb855065c0d71017a3d7c3099" FIXED_TIMESTAMP = 1778858809 def _config() -> Config: return Config( signature_secret=REFERENCE_SECRET, signing_spec=SigningSpec( rot_fn_name="Sur", signing_fn_name="Nkt", rot_ranges=[ RotRange(start=65, end=90, shift=13), RotRange(start=97, end=122, shift=13), RotRange(start=48, end=57, shift=5), ], ), app_version="0.60.1.0", user_agent="Raycast/0.60.1.0 (x-macOS Version 26.3.1)", bundle_hash="0" * 64, launcher_hash="0" * 64, ) def _client() -> Client: return Client( config=_config(), bearer_token="rca_test", device_id=DEVICE_ID, clock=lambda: FIXED_TIMESTAMP, ) class TestUpload: @pytest.mark.asyncio async def test_upload_registers_then_puts(self, tmp_path: Path) -> None: f = tmp_path / "hello.txt" f.write_bytes(b"hi there") expected_checksum = hashlib.sha256(b"hi there").hexdigest() captured: dict[str, Any] = {} def _register_cb(url: Any, **kwargs: Any) -> Any: captured["register_data"] = kwargs.get("data") captured["register_headers"] = dict(kwargs.get("headers") or {}) from aioresponses import CallbackResult return CallbackResult( status=200, payload={ "id": "file_abc123", "direct_upload": { "url": "https://blobs.example.com/u/abc", "headers": {"X-Upload-Token": "deadbeef"}, }, }, ) def _put_cb(url: Any, **kwargs: Any) -> Any: captured["put_data"] = kwargs.get("data") captured["put_headers"] = dict(kwargs.get("headers") or {}) from aioresponses import CallbackResult return CallbackResult(status=200) with aioresponses() as mocked: mocked.post( "https://backend.raycast.com/api/v1/ai/files", callback=_register_cb ) mocked.put("https://blobs.example.com/u/abc", callback=_put_cb) async with _client() as client: meta = await client.files.upload(path=f, chat_id="chat_1") assert meta.file_id == "file_abc123" assert meta.checksum == expected_checksum body = captured["register_data"] if hasattr(body, "_value"): body = body._value import json as _json parsed = _json.loads(bytes(body)) assert parsed == { "chat_id": "chat_1", "blob": { "filename": "hello.txt", "byte_size": 8, "content_type": "text/plain", "checksum": expected_checksum, }, } assert "X-Raycast-Signature-v2" in captured["register_headers"] assert "X-Raycast-Signature-v2" not in captured["put_headers"] assert "Authorization" not in captured["put_headers"] assert captured["put_headers"]["X-Upload-Token"] == "deadbeef" assert bytes(captured["put_data"]) == b"hi there" class TestGetSignsBraceBody: @pytest.mark.asyncio async def test_get_signs_two_byte_brace_body(self) -> None: """⚠ `GET /ai/files/{id}` signs the literal string `"{}"`, not `""`. This is the BUNDLE_NOTES "surprise" — `uV` differs from the resume GET. Server validates against the bytes the client claims to have sent, so we must match. """ captured: dict[str, Any] = {} def _cb(url: Any, **kwargs: Any) -> Any: captured["headers"] = dict(kwargs.get("headers") or {}) captured["data"] = kwargs.get("data") from aioresponses import CallbackResult return CallbackResult(status=200, body=b"file-contents") with aioresponses() as mocked: mocked.get("https://backend.raycast.com/api/v1/ai/files/F1", callback=_cb) async with _client() as client: data = await client.files.get("F1") assert data == b"file-contents" wire_body = captured["data"] if hasattr(wire_body, "_value"): wire_body = wire_body._value assert bytes(wire_body) == b"{}" signer = Signer(spec=_config().signing_spec, secret=REFERENCE_SECRET) expected = signer.sign( timestamp=str(FIXED_TIMESTAMP), device_id=DEVICE_ID, body=b"{}" ) assert captured["headers"]["X-Raycast-Signature-v2"] == expected class TestDelete: @pytest.mark.asyncio async def test_delete_with_chat_ids_body(self) -> None: captured: dict[str, Any] = {} def _cb(url: Any, **kwargs: Any) -> Any: captured["headers"] = dict(kwargs.get("headers") or {}) captured["data"] = kwargs.get("data") from aioresponses import CallbackResult return CallbackResult(status=204) with aioresponses() as mocked: mocked.delete("https://backend.raycast.com/api/v1/ai/files", callback=_cb) async with _client() as client: await client.files.delete(chat_ids=["c1", "c2"]) body = captured["data"] if hasattr(body, "_value"): body = body._value assert bytes(body) == b'{"chat_ids":["c1","c2"]}' assert "X-Raycast-Signature-v2" in captured["headers"] assert captured["headers"]["Content-Type"] == "application/json"