mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
a4000f64ce
Signed-off-by: Joe P <joe@basicmemory.com> Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com> Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Paul Hernandez <60959+phernandez@users.noreply.github.com> Co-authored-by: phernandez <paul@basicmachines.co>
274 lines
8.4 KiB
Python
274 lines
8.4 KiB
Python
"""Tests for V2 resource API routes (ID-based endpoints)."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from basic_memory.models import Project
|
|
from basic_memory.schemas.v2.resource import ResourceResponse
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_resource(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test creating a new resource via v2 POST endpoint."""
|
|
create_data = {
|
|
"file_path": "test-resources/test-file.md",
|
|
"content": "# Test Resource\n\nThis is test content.",
|
|
}
|
|
|
|
response = await client.post(
|
|
f"{v2_project_url}/resource",
|
|
json=create_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
result = ResourceResponse.model_validate(response.json())
|
|
|
|
# V2 must return entity_id
|
|
assert result.entity_id is not None
|
|
assert isinstance(result.entity_id, int)
|
|
assert result.file_path == "test-resources/test-file.md"
|
|
assert result.checksum is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_resource_duplicate_fails(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test that creating a resource at an existing path returns 409."""
|
|
create_data = {
|
|
"file_path": "duplicate-test.md",
|
|
"content": "First version",
|
|
}
|
|
|
|
# Create first time - should succeed
|
|
response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert response.status_code == 200
|
|
|
|
# Try to create again - should fail with 409
|
|
response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert response.status_code == 409
|
|
assert "already exists" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_resource_by_id(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test getting resource content by external_id."""
|
|
# First create a resource
|
|
test_content = "# Test Resource\n\nThis is test content."
|
|
create_data = {
|
|
"file_path": "test-get.md",
|
|
"content": test_content,
|
|
}
|
|
|
|
create_response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert create_response.status_code == 200
|
|
created = ResourceResponse.model_validate(create_response.json())
|
|
|
|
# Now get it by external_id
|
|
response = await client.get(f"{v2_project_url}/resource/{created.external_id}")
|
|
|
|
assert response.status_code == 200
|
|
# Normalize line endings for cross-platform compatibility
|
|
assert test_content.replace("\n", "") in response.text.replace("\r\n", "").replace("\n", "")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_resource_not_found(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test getting a non-existent resource returns 404."""
|
|
fake_uuid = "00000000-0000-0000-0000-000000000000"
|
|
response = await client.get(f"{v2_project_url}/resource/{fake_uuid}")
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_resource(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test updating resource content by external_id."""
|
|
# Create a resource
|
|
create_data = {
|
|
"file_path": "test-update.md",
|
|
"content": "Original content",
|
|
}
|
|
create_response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert create_response.status_code == 200
|
|
created = ResourceResponse.model_validate(create_response.json())
|
|
|
|
# Update it
|
|
update_data = {
|
|
"content": "Updated content",
|
|
}
|
|
response = await client.put(
|
|
f"{v2_project_url}/resource/{created.external_id}",
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
result = ResourceResponse.model_validate(response.json())
|
|
assert result.external_id == created.external_id
|
|
assert result.file_path == "test-update.md"
|
|
|
|
# Verify content was updated
|
|
get_response = await client.get(f"{v2_project_url}/resource/{created.external_id}")
|
|
assert "Updated content" in get_response.text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_resource_and_move(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test updating resource content and moving it to a new path."""
|
|
# Create a resource
|
|
create_data = {
|
|
"file_path": "original-location.md",
|
|
"content": "Original content",
|
|
}
|
|
create_response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert create_response.status_code == 200
|
|
created = ResourceResponse.model_validate(create_response.json())
|
|
|
|
# Update content and move file
|
|
update_data = {
|
|
"content": "Updated content in new location",
|
|
"file_path": "moved/new-location.md",
|
|
}
|
|
response = await client.put(
|
|
f"{v2_project_url}/resource/{created.external_id}",
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
result = ResourceResponse.model_validate(response.json())
|
|
assert result.external_id == created.external_id
|
|
assert result.file_path == "moved/new-location.md"
|
|
|
|
# Verify content at new location
|
|
get_response = await client.get(f"{v2_project_url}/resource/{created.external_id}")
|
|
assert "Updated content in new location" in get_response.text
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_resource_not_found(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test updating a non-existent resource returns 404."""
|
|
fake_uuid = "00000000-0000-0000-0000-000000000000"
|
|
update_data = {
|
|
"content": "New content",
|
|
}
|
|
response = await client.put(
|
|
f"{v2_project_url}/resource/{fake_uuid}",
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_resource_invalid_path(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test creating a resource with path traversal attempt fails."""
|
|
create_data = {
|
|
"file_path": "../../../etc/passwd",
|
|
"content": "malicious content",
|
|
}
|
|
|
|
response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
|
|
assert response.status_code == 400
|
|
assert "Invalid file path" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_resource_invalid_path(
|
|
client: AsyncClient,
|
|
test_project: Project,
|
|
v2_project_url: str,
|
|
):
|
|
"""Test updating a resource with path traversal attempt fails."""
|
|
# Create a valid resource first
|
|
create_data = {
|
|
"file_path": "valid.md",
|
|
"content": "Valid content",
|
|
}
|
|
create_response = await client.post(f"{v2_project_url}/resource", json=create_data)
|
|
assert create_response.status_code == 200
|
|
created = ResourceResponse.model_validate(create_response.json())
|
|
|
|
# Try to move it to an invalid path
|
|
update_data = {
|
|
"content": "Updated content",
|
|
"file_path": "../../../etc/passwd",
|
|
}
|
|
response = await client.put(
|
|
f"{v2_project_url}/resource/{created.external_id}",
|
|
json=update_data,
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
assert "Invalid file path" in response.json()["detail"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_resource_invalid_project_id(
|
|
client: AsyncClient,
|
|
):
|
|
"""Test resource endpoints with invalid project external_id return 404."""
|
|
fake_project_uuid = "00000000-0000-0000-0000-000000000000"
|
|
fake_entity_uuid = "00000000-0000-0000-0000-000000000001"
|
|
|
|
# Test create
|
|
response = await client.post(
|
|
f"/v2/projects/{fake_project_uuid}/resource",
|
|
json={"file_path": "test.md", "content": "test"},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
# Test get
|
|
response = await client.get(f"/v2/projects/{fake_project_uuid}/resource/{fake_entity_uuid}")
|
|
assert response.status_code == 404
|
|
|
|
# Test update
|
|
response = await client.put(
|
|
f"/v2/projects/{fake_project_uuid}/resource/{fake_entity_uuid}",
|
|
json={"content": "test"},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_v2_resource_endpoints_use_project_id_not_name(
|
|
client: AsyncClient, test_project: Project
|
|
):
|
|
"""Verify v2 resource endpoints require project external_id UUID, not name."""
|
|
# Try using project name instead of external_id - should fail
|
|
fake_entity_uuid = "00000000-0000-0000-0000-000000000000"
|
|
response = await client.get(f"/v2/projects/{test_project.name}/resource/{fake_entity_uuid}")
|
|
|
|
# Should get 404 because name is not a valid project external_id
|
|
assert response.status_code == 404
|