bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
188 lines
6.0 KiB
Python
188 lines
6.0 KiB
Python
import pytest
|
|
from aioresponses import aioresponses
|
|
|
|
from mirage.accessor.onedrive import OneDriveConfig
|
|
from mirage.core.onedrive._client import (GraphError, drive_base, graph_get,
|
|
graph_get_bytes, graph_list, headers,
|
|
item_url, split_path)
|
|
from mirage.types import PathSpec
|
|
from mirage.utils.key_prefix import mount_key
|
|
|
|
|
|
def test_split_path_strips_real_prefix():
|
|
p = PathSpec(resource_path=mount_key("/od/a.txt", "/od"),
|
|
virtual="/od/a.txt",
|
|
directory="/od/a.txt")
|
|
assert split_path(p) == ("/od", "a.txt")
|
|
|
|
|
|
def test_split_path_does_not_strip_sibling_prefix_match():
|
|
# `/data` must not be stripped from the sibling `/database.txt`; the
|
|
# boundary guard lives in mount_key, whose output split_path returns.
|
|
p = PathSpec(resource_path=mount_key("/database.txt", "/data"),
|
|
virtual="/database.txt",
|
|
directory="/database.txt")
|
|
assert split_path(p) == ("", "database.txt")
|
|
|
|
|
|
def test_headers_resolves_callable_token():
|
|
h = headers(OneDriveConfig(access_token=lambda: "live-token"))
|
|
assert h["Authorization"] == "Bearer live-token"
|
|
|
|
|
|
def _cfg(**kw) -> OneDriveConfig:
|
|
return OneDriveConfig(access_token="tok", **kw)
|
|
|
|
|
|
def test_drive_base_defaults_to_me_drive():
|
|
assert drive_base(_cfg()) == "https://graph.microsoft.com/v1.0/me/drive"
|
|
|
|
|
|
def test_drive_base_uses_drive_id():
|
|
base = drive_base(_cfg(drive_id="b!abc"))
|
|
assert base == "https://graph.microsoft.com/v1.0/drives/b!abc"
|
|
|
|
|
|
def test_drive_base_uses_site_default_drive():
|
|
base = drive_base(_cfg(site_id="site123"))
|
|
assert base == "https://graph.microsoft.com/v1.0/sites/site123/drive"
|
|
|
|
|
|
def test_item_url_root_children():
|
|
url = item_url(_cfg(), "/", action="/children")
|
|
assert url == "https://graph.microsoft.com/v1.0/me/drive/root/children"
|
|
|
|
|
|
def test_item_url_nested_metadata_no_action():
|
|
url = item_url(_cfg(), "/Docs/report.docx")
|
|
assert url == (
|
|
"https://graph.microsoft.com/v1.0/me/drive/root:/Docs/report.docx")
|
|
|
|
|
|
def test_item_url_nested_content():
|
|
url = item_url(_cfg(), "/Docs/report.docx", action="/content")
|
|
assert url == ("https://graph.microsoft.com/v1.0/me/drive"
|
|
"/root:/Docs/report.docx:/content")
|
|
|
|
|
|
def test_item_url_nested_children():
|
|
url = item_url(_cfg(), "/Docs", action="/children")
|
|
assert url == (
|
|
"https://graph.microsoft.com/v1.0/me/drive/root:/Docs:/children")
|
|
|
|
|
|
def test_item_url_applies_key_prefix():
|
|
url = item_url(_cfg(key_prefix="team/files"), "/a.txt", action="/content")
|
|
assert url == ("https://graph.microsoft.com/v1.0/me/drive"
|
|
"/root:/team/files/a.txt:/content")
|
|
|
|
|
|
def test_item_url_quotes_spaces():
|
|
url = item_url(_cfg(), "/My Folder/a b.txt")
|
|
assert url == ("https://graph.microsoft.com/v1.0/me/drive"
|
|
"/root:/My%20Folder/a%20b.txt")
|
|
|
|
|
|
def test_headers_carry_bearer_token():
|
|
h = headers(_cfg())
|
|
assert h["Authorization"] == "Bearer tok"
|
|
|
|
|
|
_ROOT = "https://graph.microsoft.com/v1.0/me/drive/root"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_get_returns_parsed_json():
|
|
with aioresponses() as m:
|
|
m.get(_ROOT, payload={"id": "01ABC", "name": "root"})
|
|
result = await graph_get(_cfg(), _ROOT)
|
|
assert result["id"] == "01ABC"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_get_raises_grapherror_with_status():
|
|
with aioresponses() as m:
|
|
m.get(_ROOT,
|
|
status=404,
|
|
payload={"error": {
|
|
"code": "itemNotFound",
|
|
"message": "nope"
|
|
}})
|
|
with pytest.raises(GraphError) as exc:
|
|
await graph_get(_cfg(), _ROOT)
|
|
assert exc.value.status == 404
|
|
assert exc.value.code == "itemNotFound"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_list_follows_odata_nextlink():
|
|
page2 = _ROOT + "/children?$skiptoken=abc"
|
|
with aioresponses() as m:
|
|
m.get(_ROOT + "/children",
|
|
payload={
|
|
"value": [{
|
|
"id": "a"
|
|
}],
|
|
"@odata.nextLink": page2
|
|
})
|
|
m.get(page2, payload={"value": [{"id": "b"}]})
|
|
items = await graph_list(_cfg(), _ROOT + "/children")
|
|
assert [i["id"] for i in items] == ["a", "b"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_graph_get_bytes_returns_raw_content():
|
|
url = _ROOT + ":/a.txt:/content"
|
|
with aioresponses() as m:
|
|
m.get(url, body=b"hello bytes")
|
|
data = await graph_get_bytes(_cfg(), url)
|
|
assert data == b"hello bytes"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_retries_on_429_then_succeeds():
|
|
with aioresponses() as m:
|
|
m.get(_ROOT, status=429, headers={"Retry-After": "0"})
|
|
m.get(_ROOT, payload={"id": "ok"})
|
|
result = await graph_get(_cfg(), _ROOT)
|
|
assert result["id"] == "ok"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_gives_up_after_max_retries():
|
|
with aioresponses() as m:
|
|
for _ in range(3):
|
|
m.get(_ROOT, status=429, headers={"Retry-After": "0"})
|
|
with pytest.raises(GraphError) as exc:
|
|
await graph_get(_cfg(max_retries=2), _ROOT)
|
|
assert exc.value.status == 429
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_401_refreshes_callable_token_once_then_succeeds():
|
|
calls = {"n": 0}
|
|
|
|
def provider():
|
|
calls["n"] += 1
|
|
return "fresh" if calls["n"] > 1 else "stale"
|
|
|
|
with aioresponses() as m:
|
|
m.get(_ROOT,
|
|
status=401,
|
|
payload={"error": {
|
|
"code": "InvalidAuthenticationToken"
|
|
}})
|
|
m.get(_ROOT, payload={"id": "ok"})
|
|
result = await graph_get(OneDriveConfig(access_token=provider), _ROOT)
|
|
assert result["id"] == "ok"
|
|
assert calls["n"] == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_401_with_static_token_does_not_retry():
|
|
with aioresponses() as m:
|
|
m.get(_ROOT, status=401, payload={"error": {"code": "x"}})
|
|
with pytest.raises(GraphError) as exc:
|
|
await graph_get(_cfg(), _ROOT)
|
|
assert exc.value.status == 401
|