feat: workspace-first UI refresh - PR-1 backend endpoints
- Add FileService for workspace-scoped file operations
- Add GitOperations service for workspace-scoped git commands
- Add workspace_files API: GET/POST /workspaces/{id}/files
- Add workspace_git API: status, branches, commit, push, pull, fetch, checkout, history
- Add workspace_instances API: list instances per workspace
- Add top-level POST /workspaces/ (accepts repo_id directly)
- Enrich GET /projects/ with nested repositories and workspaces
- Register all new routers in main.py
- 23 tests passing (17 existing + 6 new)
Quality gates: ruff clean
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Unit tests for FileService."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from src.models.workspace import Workspace
|
||||
from src.services.file_service import FileService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_workspace():
|
||||
"""Create a temporary workspace directory."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
ws = Workspace(
|
||||
id="00000000-0000-0000-0000-000000000001",
|
||||
name="test-ws",
|
||||
repo_id="00000000-0000-0000-0000-000000000002",
|
||||
user_id="00000000-0000-0000-0000-000000000003",
|
||||
branch="main",
|
||||
path=tmpdir,
|
||||
)
|
||||
yield ws
|
||||
|
||||
|
||||
class TestFileService:
|
||||
"""Tests for FileService."""
|
||||
|
||||
def test_list_directory_empty(self, temp_workspace: Workspace):
|
||||
"""Returns empty list for empty directory."""
|
||||
service = FileService()
|
||||
entries = service.list_directory(temp_workspace)
|
||||
assert entries == []
|
||||
|
||||
def test_list_directory_with_files(self, temp_workspace: Workspace):
|
||||
"""Returns entries sorted (dirs first, then files)."""
|
||||
# Create files and dirs
|
||||
os.makedirs(os.path.join(temp_workspace.path, "src"))
|
||||
with open(os.path.join(temp_workspace.path, "README.md"), "w") as f:
|
||||
f.write("# Test")
|
||||
with open(os.path.join(temp_workspace.path, "main.py"), "w") as f:
|
||||
f.write("print('hello')")
|
||||
|
||||
service = FileService()
|
||||
entries = service.list_directory(temp_workspace)
|
||||
|
||||
assert len(entries) == 3
|
||||
assert entries[0].name == "src" and entries[0].type == "directory"
|
||||
assert entries[1].name == "main.py" and entries[1].type == "file"
|
||||
assert entries[2].name == "README.md" and entries[2].type == "file"
|
||||
|
||||
def test_read_file(self, temp_workspace: Workspace):
|
||||
"""Reads text file content."""
|
||||
with open(os.path.join(temp_workspace.path, "test.txt"), "w") as f:
|
||||
f.write("hello world")
|
||||
|
||||
service = FileService()
|
||||
content = service.read_file(temp_workspace, "test.txt")
|
||||
assert content == "hello world"
|
||||
|
||||
def test_read_binary_file_rejected(self, temp_workspace: Workspace):
|
||||
"""Rejects binary files."""
|
||||
with open(os.path.join(temp_workspace.path, "binary.bin"), "wb") as f:
|
||||
f.write(b"\x00\x01\x02")
|
||||
|
||||
service = FileService()
|
||||
with pytest.raises(ValueError, match="Binary"):
|
||||
service.read_file(temp_workspace, "binary.bin")
|
||||
|
||||
def test_write_file(self, temp_workspace: Workspace):
|
||||
"""Writes file to workspace."""
|
||||
service = FileService()
|
||||
service.write_file(temp_workspace, "nested/file.txt", "content")
|
||||
|
||||
assert os.path.exists(os.path.join(temp_workspace.path, "nested", "file.txt"))
|
||||
with open(os.path.join(temp_workspace.path, "nested", "file.txt")) as f:
|
||||
assert f.read() == "content"
|
||||
|
||||
def test_path_escapes_workspace(self, temp_workspace: Workspace):
|
||||
"""Rejects paths that escape workspace directory."""
|
||||
service = FileService()
|
||||
with pytest.raises(ValueError, match="escapes"):
|
||||
service.list_directory(temp_workspace, "../outside")
|
||||
Reference in New Issue
Block a user