chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import inspect
|
||||
from types import SimpleNamespace
|
||||
|
||||
import aioresponses.core
|
||||
from aiohttp import ClientResponse
|
||||
|
||||
_NEEDS_STREAM_WRITER = "stream_writer" in inspect.signature(
|
||||
ClientResponse.__init__).parameters
|
||||
|
||||
|
||||
class _PatchedClientResponse(ClientResponse):
|
||||
|
||||
def __init__(self, *args: object, **kwargs: object) -> None:
|
||||
kwargs.setdefault("stream_writer", SimpleNamespace(output_size=0))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
if _NEEDS_STREAM_WRITER:
|
||||
aioresponses.core.ClientResponse = _PatchedClientResponse
|
||||
@@ -0,0 +1,187 @@
|
||||
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
|
||||
@@ -0,0 +1,135 @@
|
||||
import pytest
|
||||
from aioresponses import CallbackResult, aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.commands.builtin.onedrive import COMMANDS
|
||||
from mirage.core.onedrive._client import GraphError
|
||||
from mirage.core.onedrive.copy import copy
|
||||
from mirage.core.onedrive.truncate import truncate
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
_cp = next(c for c in COMMANDS if any(
|
||||
rc.name == "cp" for rc in getattr(c, "_registered_commands", [])))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy_posts_copy_action_with_name():
|
||||
body = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
body.update(kwargs.get("json") or {})
|
||||
return CallbackResult(status=202, payload={})
|
||||
|
||||
with aioresponses() as m:
|
||||
m.post(_BASE + "/root:/a.txt:/copy", callback=_cb)
|
||||
await copy(_accessor(), PathSpec.from_str_path("/a.txt"),
|
||||
PathSpec.from_str_path("/sub/b.txt"))
|
||||
assert body["name"] == "b.txt"
|
||||
assert "/root:/sub" in body["parentReference"]["path"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy_polls_monitor_until_completed():
|
||||
monitor = "https://monitor.example/op/123"
|
||||
with aioresponses() as m:
|
||||
m.post(_BASE + "/root:/a.txt:/copy",
|
||||
status=202,
|
||||
headers={"Location": monitor})
|
||||
m.get(monitor, payload={"status": "completed"})
|
||||
await copy(_accessor(), PathSpec.from_str_path("/a.txt"),
|
||||
PathSpec.from_str_path("/b.txt"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy_raises_when_monitor_reports_failed():
|
||||
monitor = "https://monitor.example/op/456"
|
||||
with aioresponses() as m:
|
||||
m.post(_BASE + "/root:/a.txt:/copy",
|
||||
status=202,
|
||||
headers={"Location": monitor})
|
||||
m.get(monitor,
|
||||
payload={
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "nameAlreadyExists",
|
||||
"message": "x"
|
||||
}
|
||||
})
|
||||
with pytest.raises(GraphError):
|
||||
await copy(_accessor(), PathSpec.from_str_path("/a.txt"),
|
||||
PathSpec.from_str_path("/b.txt"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cp_recursive_uses_server_side_folder_copy():
|
||||
src = PathSpec.from_str_path("/src")
|
||||
dst = PathSpec.from_str_path("/dst")
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/src:/children",
|
||||
payload={
|
||||
"value": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"file": {}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "sub",
|
||||
"folder": {
|
||||
"childCount": 1
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
m.get(_BASE + "/root:/src/sub:/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "3",
|
||||
"name": "b.txt",
|
||||
"size": 4,
|
||||
"file": {}
|
||||
}]
|
||||
})
|
||||
m.get(_BASE + "/root:/dst",
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound"
|
||||
}})
|
||||
m.get(_BASE + "/root:/src",
|
||||
payload={
|
||||
"id": "0",
|
||||
"name": "src",
|
||||
"folder": {
|
||||
"childCount": 2
|
||||
}
|
||||
})
|
||||
m.post(_BASE + "/root:/src:/copy", status=202, payload={})
|
||||
_out, io = await _cp.__wrapped__(_accessor(), [src, dst],
|
||||
r=True,
|
||||
index=None)
|
||||
assert set(io.writes) == {"/dst/a.txt", "/dst/sub/b.txt"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncate_shrinks_content():
|
||||
captured = {}
|
||||
|
||||
def _put_cb(url, **kwargs):
|
||||
captured["body"] = kwargs.get("data")
|
||||
return CallbackResult(status=200, payload={"id": "X"})
|
||||
|
||||
content = _BASE + "/root:/a.txt:/content"
|
||||
with aioresponses() as m:
|
||||
m.get(content, body=b"hello")
|
||||
m.put(content, callback=_put_cb)
|
||||
await truncate(_accessor(), PathSpec.from_str_path("/a.txt"), 3)
|
||||
assert captured["body"] == b"hel"
|
||||
@@ -0,0 +1,152 @@
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.commands.builtin.find_eval import Name, Not, Or
|
||||
from mirage.core.onedrive.du import du, du_all
|
||||
from mirage.core.onedrive.find import find
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
|
||||
def _tree(m):
|
||||
m.get(_BASE + "/root/children",
|
||||
payload={
|
||||
"value": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"file": {}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "sub",
|
||||
"folder": {
|
||||
"childCount": 1
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
m.get(_BASE + "/root:/sub:/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "3",
|
||||
"name": "b.txt",
|
||||
"size": 5,
|
||||
"file": {}
|
||||
}]
|
||||
})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_du_sums_all_files_recursively():
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
total = await du(_accessor(), PathSpec.from_str_path("/"))
|
||||
assert total == 8
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_du_all_lists_files_plus_total():
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
rows = await du_all(_accessor(), PathSpec.from_str_path("/"))
|
||||
assert ("/a.txt", 3) in rows
|
||||
assert ("/sub/b.txt", 5) in rows
|
||||
assert rows[-1][1] == 8
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_du_of_file_returns_its_own_size():
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/a.txt",
|
||||
payload={
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"file": {}
|
||||
})
|
||||
total = await du(_accessor(), PathSpec.from_str_path("/a.txt"))
|
||||
assert total == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_du_all_of_file_is_empty():
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/a.txt",
|
||||
payload={
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"file": {}
|
||||
})
|
||||
rows = await du_all(_accessor(), PathSpec.from_str_path("/a.txt"))
|
||||
assert rows == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_returns_files_and_folders():
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
out = await find(_accessor(), PathSpec.from_str_path("/"))
|
||||
assert out == ["/", "/a.txt", "/sub", "/sub/b.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_type_file_excludes_folders():
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
out = await find(_accessor(), PathSpec.from_str_path("/"), type="file")
|
||||
assert out == ["/a.txt", "/sub/b.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_name_glob():
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
out = await find(_accessor(), PathSpec.from_str_path("/"), name="b.*")
|
||||
assert out == ["/sub/b.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_honors_not_tree():
|
||||
tree = Not(Name("a.txt"))
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
out = await find(_accessor(), PathSpec.from_str_path("/"), tree=tree)
|
||||
assert "/a.txt" not in out
|
||||
assert "/sub/b.txt" in out
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_honors_or_tree():
|
||||
tree = Or([Name("a.txt"), Name("b.txt")])
|
||||
with aioresponses() as m:
|
||||
_tree(m)
|
||||
out = await find(_accessor(), PathSpec.from_str_path("/"), tree=tree)
|
||||
assert out == ["/a.txt", "/sub/b.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_empty_folder_emits_start_path():
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/empty:/children", payload={"value": []})
|
||||
m.get(_BASE + "/root:/empty",
|
||||
payload={
|
||||
"id": "9",
|
||||
"name": "empty",
|
||||
"folder": {
|
||||
"childCount": 0
|
||||
}
|
||||
})
|
||||
out = await find(_accessor(),
|
||||
PathSpec.from_str_path("/empty"),
|
||||
type="d")
|
||||
assert out == ["/empty"]
|
||||
@@ -0,0 +1,137 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from aioresponses import CallbackResult, aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.core.onedrive.read import read_bytes
|
||||
from mirage.observe.context import (RecordingScope, push_revisions,
|
||||
reset_revisions)
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
_CONTENT = _BASE + "/root:/Docs/a.txt:/content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_returns_current_content():
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT, body=b"current bytes")
|
||||
data = await read_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"))
|
||||
assert data == b"current bytes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_pinned_revision_hits_version_content():
|
||||
version_url = _BASE + "/root:/Docs/a.txt:/versions/3.0/content"
|
||||
token = push_revisions({"/Docs/a.txt": "3.0"})
|
||||
try:
|
||||
with aioresponses() as m:
|
||||
m.get(version_url, body=b"old version bytes")
|
||||
data = await read_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"))
|
||||
finally:
|
||||
reset_revisions(token)
|
||||
assert data == b"old version bytes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_range_sends_range_header():
|
||||
captured = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
captured["range"] = kwargs["headers"].get("Range")
|
||||
return CallbackResult(body=b"llo")
|
||||
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT, callback=_cb)
|
||||
data = await read_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"),
|
||||
offset=2,
|
||||
size=3)
|
||||
assert captured["range"] == "bytes=2-4"
|
||||
assert data == b"llo"
|
||||
|
||||
|
||||
_META = re.compile(r".*/root:/Docs/a\.txt(\?.*)?$")
|
||||
_DOWNLOAD = "https://download.example/pinned-bytes"
|
||||
|
||||
|
||||
def _meta_payload():
|
||||
return {
|
||||
"id":
|
||||
"01",
|
||||
"cTag":
|
||||
"ctag-xyz",
|
||||
"@microsoft.graph.downloadUrl":
|
||||
_DOWNLOAD,
|
||||
"versions": [
|
||||
{
|
||||
"id": "1.0",
|
||||
"lastModifiedDateTime": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "2.0",
|
||||
"lastModifiedDateTime": "2026-02-01T00:00:00Z"
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_captures_fingerprint_and_revision_when_recording():
|
||||
scope = RecordingScope()
|
||||
sink = scope.records
|
||||
try:
|
||||
with aioresponses() as m:
|
||||
m.get(_META, payload=_meta_payload())
|
||||
m.get(_DOWNLOAD, body=b"old version bytes")
|
||||
data = await read_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"))
|
||||
finally:
|
||||
scope.close()
|
||||
rec = sink[0]
|
||||
assert rec.fingerprint == "ctag-xyz"
|
||||
assert rec.revision == "2.0"
|
||||
assert data == b"old version bytes"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capture_reads_pinned_download_url_not_live_content():
|
||||
scope = RecordingScope()
|
||||
sink = scope.records
|
||||
try:
|
||||
with aioresponses() as m:
|
||||
m.get(_META, payload=_meta_payload())
|
||||
m.get(_DOWNLOAD, body=b"snapshot bytes")
|
||||
m.get(_CONTENT, body=b"live mutated bytes")
|
||||
data = await read_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"))
|
||||
finally:
|
||||
scope.close()
|
||||
assert data == b"snapshot bytes"
|
||||
assert sink[0].fingerprint == "ctag-xyz"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_missing_raises_file_not_found():
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT,
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "no"
|
||||
}})
|
||||
with pytest.raises(FileNotFoundError) as exc:
|
||||
await read_bytes(
|
||||
_accessor(),
|
||||
PathSpec.from_str_path("/od/Docs/a.txt",
|
||||
mount_key("/od/Docs/a.txt", "/od")))
|
||||
assert str(exc.value) == "/od/Docs/a.txt"
|
||||
@@ -0,0 +1,147 @@
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.cache.index import RAMIndexCacheStore
|
||||
from mirage.core.onedrive.readdir import readdir
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root_lists_children():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root/children",
|
||||
payload={
|
||||
"value": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 10,
|
||||
"file": {}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Docs",
|
||||
"folder": {
|
||||
"childCount": 0
|
||||
}
|
||||
},
|
||||
]
|
||||
})
|
||||
names = await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
assert names == ["/Docs", "/a.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_folder_records_file_and_folder_entries():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/Docs:/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "9",
|
||||
"name": "report.docx",
|
||||
"size": 55,
|
||||
"file": {}
|
||||
}]
|
||||
})
|
||||
names = await readdir(_accessor(), PathSpec.from_str_path("/Docs"),
|
||||
index)
|
||||
assert names == ["/Docs/report.docx"]
|
||||
lookup = await index.get("/Docs/report.docx")
|
||||
assert lookup.entry.resource_type == "file"
|
||||
assert lookup.entry.size == 55
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_of_file_raises_not_a_directory():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/a.txt:/children",
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "x"
|
||||
}})
|
||||
m.get(_BASE + "/root:/a.txt",
|
||||
payload={
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"file": {}
|
||||
})
|
||||
with pytest.raises(NotADirectoryError):
|
||||
await readdir(_accessor(), PathSpec.from_str_path("/a.txt"), index)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_paginates_nextlink():
|
||||
index = RAMIndexCacheStore()
|
||||
page2 = _BASE + "/root/children?$skiptoken=x"
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"file": {}
|
||||
}],
|
||||
"@odata.nextLink": page2,
|
||||
})
|
||||
m.get(page2,
|
||||
payload={"value": [{
|
||||
"id": "2",
|
||||
"name": "b.txt",
|
||||
"file": {}
|
||||
}]})
|
||||
names = await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
assert names == ["/a.txt", "/b.txt"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_stores_remote_time_for_files():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "1",
|
||||
"name": "a.txt",
|
||||
"size": 10,
|
||||
"file": {},
|
||||
"lastModifiedDateTime": "2026-06-19T09:28:00Z",
|
||||
}]
|
||||
})
|
||||
await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
lookup = await index.get("/a.txt")
|
||||
assert lookup.entry.remote_time == "2026-06-19T09:28:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_stores_remote_time_for_folders():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "2",
|
||||
"name": "Docs",
|
||||
"folder": {
|
||||
"childCount": 3
|
||||
},
|
||||
"size": 5000,
|
||||
"lastModifiedDateTime": "2026-05-28T02:10:00Z",
|
||||
}]
|
||||
})
|
||||
await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
lookup = await index.get("/Docs")
|
||||
assert lookup.entry.remote_time == "2026-05-28T02:10:00Z"
|
||||
assert lookup.entry.size == 5000
|
||||
@@ -0,0 +1,57 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveConfig
|
||||
from mirage.resource.onedrive import OneDriveResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace import Workspace
|
||||
from mirage.workspace.snapshot.drift import ContentDriftError, check_drift
|
||||
|
||||
_ITEM = re.compile(r".*/root:/a\.txt(\?.*)?$")
|
||||
|
||||
|
||||
def _ws() -> Workspace:
|
||||
backend = OneDriveResource(OneDriveConfig(access_token="tok"))
|
||||
return Workspace({"/od": (backend, MountMode.WRITE)}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
def test_drift_raises_when_live_ctag_differs():
|
||||
with aioresponses() as m:
|
||||
m.get(_ITEM,
|
||||
payload={
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"cTag": "live-ctag",
|
||||
"file": {}
|
||||
})
|
||||
with pytest.raises(ContentDriftError) as exc:
|
||||
asyncio.run(check_drift(_ws(), "/od/a.txt", "snapshot-ctag"))
|
||||
assert exc.value.path == "/od/a.txt"
|
||||
assert exc.value.live_fingerprint == "live-ctag"
|
||||
|
||||
|
||||
def test_no_drift_when_ctag_matches():
|
||||
with aioresponses() as m:
|
||||
m.get(_ITEM,
|
||||
payload={
|
||||
"name": "a.txt",
|
||||
"size": 3,
|
||||
"cTag": "same-ctag",
|
||||
"file": {}
|
||||
})
|
||||
asyncio.run(check_drift(_ws(), "/od/a.txt", "same-ctag"))
|
||||
|
||||
|
||||
def test_drift_raises_when_file_missing():
|
||||
with aioresponses() as m:
|
||||
m.get(_ITEM,
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "no"
|
||||
}})
|
||||
with pytest.raises(ContentDriftError):
|
||||
asyncio.run(check_drift(_ws(), "/od/a.txt", "snapshot-ctag"))
|
||||
@@ -0,0 +1,130 @@
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.cache.index import RAMIndexCacheStore
|
||||
from mirage.core.onedrive.readdir import readdir
|
||||
from mirage.core.onedrive.stat import stat
|
||||
from mirage.types import FileType, PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_FILE_URL = ("https://graph.microsoft.com/v1.0/me/drive"
|
||||
"/root:/Docs/report.docx")
|
||||
_DIR_URL = "https://graph.microsoft.com/v1.0/me/drive/root:/Docs"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_root_is_directory():
|
||||
result = await stat(_accessor(), PathSpec.from_str_path("/"))
|
||||
assert result.type == FileType.DIRECTORY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_file_carries_size_and_ctag_fingerprint():
|
||||
with aioresponses() as m:
|
||||
m.get(_FILE_URL,
|
||||
payload={
|
||||
"id": "01ITEM",
|
||||
"name": "report.docx",
|
||||
"size": 1234,
|
||||
"lastModifiedDateTime": "2026-05-01T10:00:00Z",
|
||||
"cTag": "ctag-abc",
|
||||
"eTag": "etag-xyz",
|
||||
"file": {
|
||||
"mimeType": "application/vnd.openxml"
|
||||
},
|
||||
})
|
||||
result = await stat(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/report.docx"))
|
||||
assert result.name == "report.docx"
|
||||
assert result.size == 1234
|
||||
assert result.fingerprint == "ctag-abc"
|
||||
assert result.extra["id"] == "01ITEM"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_folder_is_directory():
|
||||
with aioresponses() as m:
|
||||
m.get(_DIR_URL,
|
||||
payload={
|
||||
"id": "01FOLDER",
|
||||
"name": "Docs",
|
||||
"size": 4096,
|
||||
"lastModifiedDateTime": "2026-05-01T10:00:00Z",
|
||||
"folder": {
|
||||
"childCount": 2
|
||||
},
|
||||
})
|
||||
result = await stat(_accessor(), PathSpec.from_str_path("/Docs"))
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.name == "Docs"
|
||||
assert result.size == 4096
|
||||
assert result.modified == "2026-05-01T10:00:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_missing_raises_file_not_found():
|
||||
with aioresponses() as m:
|
||||
m.get(_FILE_URL,
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "no"
|
||||
}})
|
||||
with pytest.raises(FileNotFoundError) as exc:
|
||||
await stat(
|
||||
_accessor(),
|
||||
PathSpec.from_str_path(
|
||||
"/od/Docs/report.docx",
|
||||
mount_key("/od/Docs/report.docx", "/od")))
|
||||
assert str(exc.value) == "/od/Docs/report.docx"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_from_index_returns_modified():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get("https://graph.microsoft.com/v1.0/me/drive/root/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "1",
|
||||
"name": "notes.txt",
|
||||
"size": 42,
|
||||
"file": {},
|
||||
"lastModifiedDateTime": "2026-06-19T09:28:00Z",
|
||||
}]
|
||||
})
|
||||
await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
result = await stat(_accessor(), PathSpec.from_str_path("/notes.txt"),
|
||||
index)
|
||||
assert result.name == "notes.txt"
|
||||
assert result.size == 42
|
||||
assert result.modified == "2026-06-19T09:28:00Z"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stat_from_index_folder_returns_modified():
|
||||
index = RAMIndexCacheStore()
|
||||
with aioresponses() as m:
|
||||
m.get("https://graph.microsoft.com/v1.0/me/drive/root/children",
|
||||
payload={
|
||||
"value": [{
|
||||
"id": "2",
|
||||
"name": "Docs",
|
||||
"folder": {
|
||||
"childCount": 5
|
||||
},
|
||||
"size": 9000,
|
||||
"lastModifiedDateTime": "2026-05-28T02:10:00Z",
|
||||
}]
|
||||
})
|
||||
await readdir(_accessor(), PathSpec.from_str_path("/"), index)
|
||||
result = await stat(_accessor(), PathSpec.from_str_path("/Docs"), index)
|
||||
assert result.type == FileType.DIRECTORY
|
||||
assert result.modified == "2026-05-28T02:10:00Z"
|
||||
assert result.size == 9000
|
||||
@@ -0,0 +1,82 @@
|
||||
import pytest
|
||||
from aioresponses import aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.core.onedrive.stream import range_read, read_stream
|
||||
from mirage.types import PathSpec
|
||||
from mirage.utils.key_prefix import mount_key
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_CONTENT = ("https://graph.microsoft.com/v1.0/me/drive"
|
||||
"/root:/Docs/a.txt:/content")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_yields_full_content():
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT, body=b"abcdef")
|
||||
chunks = []
|
||||
async for chunk in read_stream(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt")):
|
||||
chunks.append(chunk)
|
||||
assert b"".join(chunks) == b"abcdef"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_missing_raises_file_not_found():
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT,
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "no"
|
||||
}})
|
||||
with pytest.raises(FileNotFoundError) as exc:
|
||||
async for _ in read_stream(
|
||||
_accessor(),
|
||||
PathSpec.from_str_path("/od/Docs/a.txt",
|
||||
mount_key("/od/Docs/a.txt",
|
||||
"/od"))):
|
||||
pass
|
||||
assert str(exc.value) == "/od/Docs/a.txt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_stream_refreshes_callable_token_on_401():
|
||||
calls = {"n": 0}
|
||||
|
||||
def provider():
|
||||
calls["n"] += 1
|
||||
return "fresh" if calls["n"] > 1 else "stale"
|
||||
|
||||
accessor = OneDriveAccessor(OneDriveConfig(access_token=provider))
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT, status=401, payload={"error": {"code": "expired"}})
|
||||
m.get(_CONTENT, body=b"abcdef")
|
||||
chunks = [
|
||||
c async for c in read_stream(accessor,
|
||||
PathSpec.from_str_path("/Docs/a.txt"))
|
||||
]
|
||||
assert b"".join(chunks) == b"abcdef"
|
||||
assert calls["n"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_range_read_returns_requested_bytes():
|
||||
captured = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
from aioresponses import CallbackResult
|
||||
captured["range"] = kwargs["headers"].get("Range")
|
||||
return CallbackResult(body=b"cde")
|
||||
|
||||
with aioresponses() as m:
|
||||
m.get(_CONTENT, callback=_cb)
|
||||
data = await range_read(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"), 2, 5)
|
||||
assert data == b"cde"
|
||||
assert captured["range"] == "bytes=2-4"
|
||||
@@ -0,0 +1,106 @@
|
||||
import pytest
|
||||
from aioresponses import CallbackResult, aioresponses
|
||||
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.core.onedrive.create import create
|
||||
from mirage.core.onedrive.exists import exists
|
||||
from mirage.core.onedrive.mkdir import mkdir
|
||||
from mirage.core.onedrive.rename import rename
|
||||
from mirage.core.onedrive.rm import rm_r
|
||||
from mirage.core.onedrive.rmdir import rmdir
|
||||
from mirage.core.onedrive.unlink import unlink
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkdir_posts_folder_to_parent_children():
|
||||
body = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
body.update(kwargs.get("json") or {})
|
||||
return CallbackResult(status=201, payload={"id": "F"})
|
||||
|
||||
with aioresponses() as m:
|
||||
m.post(_BASE + "/root:/a:/children", callback=_cb)
|
||||
await mkdir(_accessor(), PathSpec.from_str_path("/a/b"))
|
||||
assert body["name"] == "b"
|
||||
assert "folder" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_puts_empty_content():
|
||||
captured = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
captured["body"] = kwargs.get("data")
|
||||
return CallbackResult(status=201, payload={"id": "X"})
|
||||
|
||||
with aioresponses() as m:
|
||||
m.put(_BASE + "/root:/a.txt:/content", callback=_cb)
|
||||
await create(_accessor(), PathSpec.from_str_path("/a.txt"))
|
||||
assert captured["body"] in (b"", None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlink_deletes_item():
|
||||
with aioresponses() as m:
|
||||
m.delete(_BASE + "/root:/a.txt", status=204)
|
||||
await unlink(_accessor(), PathSpec.from_str_path("/a.txt"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rmdir_deletes_folder():
|
||||
with aioresponses() as m:
|
||||
m.delete(_BASE + "/root:/docs", status=204)
|
||||
await rmdir(_accessor(), PathSpec.from_str_path("/docs"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rm_r_deletes_tree():
|
||||
with aioresponses() as m:
|
||||
m.delete(_BASE + "/root:/docs", status=204)
|
||||
await rm_r(_accessor(), PathSpec.from_str_path("/docs"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename_patches_name():
|
||||
body = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
body.update(kwargs.get("json") or {})
|
||||
return CallbackResult(status=200, payload={"id": "X", "name": "b.txt"})
|
||||
|
||||
with aioresponses() as m:
|
||||
m.patch(_BASE + "/root:/a.txt", callback=_cb)
|
||||
await rename(_accessor(), PathSpec.from_str_path("/a.txt"),
|
||||
PathSpec.from_str_path("/b.txt"))
|
||||
assert body["name"] == "b.txt"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_exists_true_and_false():
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/a.txt",
|
||||
payload={
|
||||
"id": "X",
|
||||
"name": "a.txt",
|
||||
"file": {}
|
||||
})
|
||||
assert await exists(_accessor(),
|
||||
PathSpec.from_str_path("/a.txt")) is True
|
||||
with aioresponses() as m:
|
||||
m.get(_BASE + "/root:/missing.txt",
|
||||
status=404,
|
||||
payload={"error": {
|
||||
"code": "itemNotFound",
|
||||
"message": "no"
|
||||
}})
|
||||
assert await exists(_accessor(),
|
||||
PathSpec.from_str_path("/missing.txt")) is False
|
||||
@@ -0,0 +1,82 @@
|
||||
import pytest
|
||||
from aioresponses import CallbackResult, aioresponses
|
||||
|
||||
import mirage.core.onedrive.write as write_mod
|
||||
from mirage.accessor.onedrive import OneDriveAccessor, OneDriveConfig
|
||||
from mirage.core.onedrive.write import write_bytes
|
||||
from mirage.types import PathSpec
|
||||
|
||||
|
||||
def _accessor(**kw) -> OneDriveAccessor:
|
||||
return OneDriveAccessor(OneDriveConfig(access_token="tok", **kw))
|
||||
|
||||
|
||||
_BASE = "https://graph.microsoft.com/v1.0/me/drive"
|
||||
_CONTENT = _BASE + "/root:/Docs/a.txt:/content"
|
||||
_SESSION = _BASE + "/root:/Docs/a.txt:/createUploadSession"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_small_file_puts_content():
|
||||
captured = {}
|
||||
|
||||
def _cb(url, **kwargs):
|
||||
captured["body"] = kwargs.get("data")
|
||||
return CallbackResult(status=201, payload={"id": "X", "name": "a.txt"})
|
||||
|
||||
with aioresponses() as m:
|
||||
m.put(_CONTENT, callback=_cb)
|
||||
result = await write_bytes(_accessor(),
|
||||
PathSpec.from_str_path("/Docs/a.txt"),
|
||||
b"hello")
|
||||
assert result is None
|
||||
assert captured["body"] == b"hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_large_file_uses_upload_session(monkeypatch):
|
||||
monkeypatch.setattr(write_mod, "SIMPLE_UPLOAD_MAX", 4)
|
||||
monkeypatch.setattr(write_mod, "UPLOAD_CHUNK", 4)
|
||||
ranges = []
|
||||
|
||||
def _chunk_cb(url, **kwargs):
|
||||
ranges.append(kwargs["headers"]["Content-Range"])
|
||||
return CallbackResult(status=202, payload={})
|
||||
|
||||
def _final_cb(url, **kwargs):
|
||||
ranges.append(kwargs["headers"]["Content-Range"])
|
||||
return CallbackResult(status=201, payload={"id": "X"})
|
||||
|
||||
upload_url = "https://upload.example/session1"
|
||||
with aioresponses() as m:
|
||||
m.post(_SESSION, payload={"uploadUrl": upload_url})
|
||||
m.put(upload_url, callback=_chunk_cb)
|
||||
m.put(upload_url, callback=_final_cb)
|
||||
await write_bytes(_accessor(), PathSpec.from_str_path("/Docs/a.txt"),
|
||||
b"abcdef")
|
||||
assert ranges == ["bytes 0-3/6", "bytes 4-5/6"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upload_resumes_from_next_expected_ranges(monkeypatch):
|
||||
monkeypatch.setattr(write_mod, "SIMPLE_UPLOAD_MAX", 4)
|
||||
monkeypatch.setattr(write_mod, "UPLOAD_CHUNK", 4)
|
||||
ranges = []
|
||||
|
||||
def _chunk_cb(url, **kwargs):
|
||||
ranges.append(kwargs["headers"]["Content-Range"])
|
||||
return CallbackResult(status=202,
|
||||
payload={"nextExpectedRanges": ["2-5"]})
|
||||
|
||||
def _final_cb(url, **kwargs):
|
||||
ranges.append(kwargs["headers"]["Content-Range"])
|
||||
return CallbackResult(status=201, payload={"id": "X"})
|
||||
|
||||
upload_url = "https://upload.example/session2"
|
||||
with aioresponses() as m:
|
||||
m.post(_SESSION, payload={"uploadUrl": upload_url})
|
||||
m.put(upload_url, callback=_chunk_cb)
|
||||
m.put(upload_url, callback=_final_cb)
|
||||
await write_bytes(_accessor(), PathSpec.from_str_path("/Docs/a.txt"),
|
||||
b"abcdef")
|
||||
assert ranges == ["bytes 0-3/6", "bytes 2-5/6"]
|
||||
Reference in New Issue
Block a user