mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
test: add unit tests for Dropbox integration, covering delta sync methods, file type filtering, and re-authentication behavior
This commit is contained in:
parent
b5a15b7681
commit
caca491774
7 changed files with 843 additions and 0 deletions
0
surfsense_backend/tests/unit/connectors/__init__.py
Normal file
0
surfsense_backend/tests/unit/connectors/__init__.py
Normal file
115
surfsense_backend/tests/unit/connectors/test_dropbox_client.py
Normal file
115
surfsense_backend/tests/unit/connectors/test_dropbox_client.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""Tests for DropboxClient delta-sync methods (get_latest_cursor, get_changes)."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.connectors.dropbox.client import DropboxClient
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_client() -> DropboxClient:
|
||||
"""Create a DropboxClient with a mocked DB session so no real DB needed."""
|
||||
client = DropboxClient.__new__(DropboxClient)
|
||||
client._session = MagicMock()
|
||||
client._connector_id = 1
|
||||
return client
|
||||
|
||||
|
||||
# ---------- C1: get_latest_cursor ----------
|
||||
|
||||
async def test_get_latest_cursor_returns_cursor_string(monkeypatch):
|
||||
client = _make_client()
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.status_code = 200
|
||||
fake_resp.json.return_value = {"cursor": "AAHbKxRZ9enq…"}
|
||||
|
||||
monkeypatch.setattr(client, "_request", AsyncMock(return_value=fake_resp))
|
||||
|
||||
cursor, error = await client.get_latest_cursor("/my-folder")
|
||||
|
||||
assert cursor == "AAHbKxRZ9enq…"
|
||||
assert error is None
|
||||
client._request.assert_called_once_with(
|
||||
"/2/files/list_folder/get_latest_cursor",
|
||||
{"path": "/my-folder", "recursive": False, "include_non_downloadable_files": True},
|
||||
)
|
||||
|
||||
|
||||
# ---------- C2: get_changes returns entries and new cursor ----------
|
||||
|
||||
async def test_get_changes_returns_entries_and_cursor(monkeypatch):
|
||||
client = _make_client()
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.status_code = 200
|
||||
fake_resp.json.return_value = {
|
||||
"entries": [
|
||||
{".tag": "file", "name": "new.txt", "id": "id:abc"},
|
||||
{".tag": "deleted", "name": "old.txt"},
|
||||
],
|
||||
"cursor": "cursor-v2",
|
||||
"has_more": False,
|
||||
}
|
||||
monkeypatch.setattr(client, "_request", AsyncMock(return_value=fake_resp))
|
||||
|
||||
entries, new_cursor, error = await client.get_changes("cursor-v1")
|
||||
|
||||
assert error is None
|
||||
assert new_cursor == "cursor-v2"
|
||||
assert len(entries) == 2
|
||||
assert entries[0]["name"] == "new.txt"
|
||||
assert entries[1][".tag"] == "deleted"
|
||||
|
||||
|
||||
# ---------- C3: get_changes handles pagination ----------
|
||||
|
||||
async def test_get_changes_handles_pagination(monkeypatch):
|
||||
client = _make_client()
|
||||
|
||||
page1 = MagicMock()
|
||||
page1.status_code = 200
|
||||
page1.json.return_value = {
|
||||
"entries": [{".tag": "file", "name": "a.txt", "id": "id:a"}],
|
||||
"cursor": "cursor-page2",
|
||||
"has_more": True,
|
||||
}
|
||||
page2 = MagicMock()
|
||||
page2.status_code = 200
|
||||
page2.json.return_value = {
|
||||
"entries": [{".tag": "file", "name": "b.txt", "id": "id:b"}],
|
||||
"cursor": "cursor-final",
|
||||
"has_more": False,
|
||||
}
|
||||
|
||||
request_mock = AsyncMock(side_effect=[page1, page2])
|
||||
monkeypatch.setattr(client, "_request", request_mock)
|
||||
|
||||
entries, new_cursor, error = await client.get_changes("cursor-v1")
|
||||
|
||||
assert error is None
|
||||
assert new_cursor == "cursor-final"
|
||||
assert len(entries) == 2
|
||||
assert {e["name"] for e in entries} == {"a.txt", "b.txt"}
|
||||
assert request_mock.call_count == 2
|
||||
|
||||
|
||||
# ---------- C4: get_changes raises on 401 ----------
|
||||
|
||||
async def test_get_changes_returns_error_on_401(monkeypatch):
|
||||
client = _make_client()
|
||||
|
||||
fake_resp = MagicMock()
|
||||
fake_resp.status_code = 401
|
||||
fake_resp.text = "Unauthorized"
|
||||
|
||||
monkeypatch.setattr(client, "_request", AsyncMock(return_value=fake_resp))
|
||||
|
||||
entries, new_cursor, error = await client.get_changes("old-cursor")
|
||||
|
||||
assert error is not None
|
||||
assert "401" in error
|
||||
assert entries == []
|
||||
assert new_cursor is None
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
"""Tests for Dropbox file type filtering (should_skip_file)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.connectors.dropbox.file_types import should_skip_file
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def test_folder_item_is_skipped():
|
||||
item = {".tag": "folder", "name": "My Folder"}
|
||||
assert should_skip_file(item) is True
|
||||
|
||||
|
||||
def test_paper_file_is_not_skipped():
|
||||
item = {".tag": "file", "name": "notes.paper", "is_downloadable": False}
|
||||
assert should_skip_file(item) is False
|
||||
|
||||
|
||||
def test_non_downloadable_item_is_skipped():
|
||||
item = {".tag": "file", "name": "locked.gdoc", "is_downloadable": False}
|
||||
assert should_skip_file(item) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
[
|
||||
"archive.zip", "backup.tar", "data.gz", "stuff.rar", "pack.7z",
|
||||
"program.exe", "lib.dll", "module.so", "image.dmg", "disk.iso",
|
||||
"movie.mov", "clip.avi", "video.mkv", "film.wmv", "stream.flv",
|
||||
"icon.svg", "anim.gif", "photo.webp", "shot.heic", "favicon.ico",
|
||||
"raw.cr2", "photo.nef", "image.arw", "pic.dng",
|
||||
"design.psd", "vector.ai", "mockup.sketch", "proto.fig",
|
||||
"font.ttf", "font.otf", "font.woff", "font.woff2",
|
||||
"model.stl", "scene.fbx", "mesh.blend",
|
||||
"local.db", "data.sqlite", "access.mdb",
|
||||
],
|
||||
)
|
||||
def test_non_parseable_extensions_are_skipped(filename):
|
||||
item = {".tag": "file", "name": filename}
|
||||
assert should_skip_file(item) is True, f"{filename} should be skipped"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
[
|
||||
"report.pdf", "document.docx", "sheet.xlsx", "slides.pptx",
|
||||
"old.doc", "legacy.xls", "deck.ppt",
|
||||
"readme.txt", "data.csv", "page.html", "notes.md",
|
||||
"config.json", "feed.xml",
|
||||
],
|
||||
)
|
||||
def test_parseable_documents_are_not_skipped(filename):
|
||||
item = {".tag": "file", "name": filename}
|
||||
assert should_skip_file(item) is False, f"{filename} should NOT be skipped"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
["photo.jpg", "image.jpeg", "screenshot.png", "scan.bmp", "page.tiff", "doc.tif"],
|
||||
)
|
||||
def test_universal_images_are_not_skipped(filename):
|
||||
item = {".tag": "file", "name": filename}
|
||||
assert should_skip_file(item) is False, f"{filename} should NOT be skipped"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename",
|
||||
["icon.svg", "anim.gif", "photo.webp", "live.heic"],
|
||||
)
|
||||
def test_non_universal_images_are_skipped(filename):
|
||||
item = {".tag": "file", "name": filename}
|
||||
assert should_skip_file(item) is True, f"{filename} should be skipped"
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
"""Test that Dropbox re-auth preserves folder_cursors in connector config."""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def test_reauth_preserves_folder_cursors():
|
||||
"""G1: re-authentication preserves folder_cursors alongside cursor."""
|
||||
old_config = {
|
||||
"access_token": "old-token-enc",
|
||||
"refresh_token": "old-refresh-enc",
|
||||
"cursor": "old-cursor-abc",
|
||||
"folder_cursors": {"/docs": "cursor-docs-123", "/photos": "cursor-photos-456"},
|
||||
"_token_encrypted": True,
|
||||
"auth_expired": True,
|
||||
}
|
||||
|
||||
new_connector_config = {
|
||||
"access_token": "new-token-enc",
|
||||
"refresh_token": "new-refresh-enc",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 14400,
|
||||
"expires_at": "2026-04-06T16:00:00+00:00",
|
||||
"_token_encrypted": True,
|
||||
}
|
||||
|
||||
existing_cursor = old_config.get("cursor")
|
||||
existing_folder_cursors = old_config.get("folder_cursors")
|
||||
merged_config = {
|
||||
**new_connector_config,
|
||||
"cursor": existing_cursor,
|
||||
"folder_cursors": existing_folder_cursors,
|
||||
"auth_expired": False,
|
||||
}
|
||||
|
||||
assert merged_config["access_token"] == "new-token-enc"
|
||||
assert merged_config["cursor"] == "old-cursor-abc"
|
||||
assert merged_config["folder_cursors"] == {
|
||||
"/docs": "cursor-docs-123",
|
||||
"/photos": "cursor-photos-456",
|
||||
}
|
||||
assert merged_config["auth_expired"] is False
|
||||
Loading…
Add table
Add a link
Reference in a new issue