1375 lines
46 KiB
Python
1375 lines
46 KiB
Python
import datetime
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from nanoid import generate as generate_nanoid
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from src import models
|
|
from src.config import settings
|
|
from src.models import Peer, Workspace
|
|
from src.security import JWTParams, create_jwt
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Test message",
|
|
"peer_id": test_peer.name,
|
|
"metadata": {"message_key": "message_value"},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
message = data[0]
|
|
assert message["content"] == "Test message"
|
|
assert message["peer_id"] == test_peer.name
|
|
assert message["session_id"] == test_session.name
|
|
assert message["metadata"] == {"message_key": "message_value"}
|
|
assert "id" in message
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_schedules_immediate_embed(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Creating messages should schedule the immediate-embed background task with
|
|
the created messages' public ids."""
|
|
test_workspace, test_peer = sample_data
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
with (
|
|
patch("src.config.settings.EMBED_MESSAGES", True),
|
|
patch.object(settings.EMBEDDING, "MAX_PENDING_EMBED_TASKS", 50),
|
|
patch(
|
|
"src.reconciler.embed_now.embed_messages_now", new=AsyncMock()
|
|
) as mock_embed_now,
|
|
):
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={"messages": [{"content": "hello", "peer_id": test_peer.name}]},
|
|
)
|
|
assert response.status_code == 201
|
|
public_id = response.json()[0]["id"]
|
|
mock_embed_now.assert_awaited_once_with([public_id])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_skips_embed_when_disabled(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""When EMBED_MESSAGES is disabled, the immediate-embed task is not scheduled."""
|
|
test_workspace, test_peer = sample_data
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
with (
|
|
patch("src.config.settings.EMBED_MESSAGES", False),
|
|
patch(
|
|
"src.reconciler.embed_now.embed_messages_now", new=AsyncMock()
|
|
) as mock_embed_now,
|
|
):
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={"messages": [{"content": "hello", "peer_id": test_peer.name}]},
|
|
)
|
|
assert response.status_code == 201
|
|
mock_embed_now.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_defers_embed_when_saturated(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""When the immediate-embed task cap is saturated, message creation still
|
|
succeeds and no embed task runs — rows stay pending for the reconciler."""
|
|
test_workspace, test_peer = sample_data
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
with (
|
|
patch("src.config.settings.EMBED_MESSAGES", True),
|
|
patch.object(settings.EMBEDDING, "MAX_PENDING_EMBED_TASKS", 0),
|
|
patch(
|
|
"src.reconciler.embed_now.embed_messages_now", new=AsyncMock()
|
|
) as mock_embed_now,
|
|
):
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={"messages": [{"content": "hello", "peer_id": test_peer.name}]},
|
|
)
|
|
assert response.status_code == 201
|
|
mock_embed_now.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_file_upload_schedules_immediate_embed(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""The file-upload path schedules the immediate-embed task with the created
|
|
messages' public ids, mirroring the session-message path."""
|
|
import io
|
|
|
|
test_workspace, test_peer = sample_data
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
with (
|
|
patch("src.config.settings.EMBED_MESSAGES", True),
|
|
patch.object(settings.EMBEDDING, "MAX_PENDING_EMBED_TASKS", 50),
|
|
patch(
|
|
"src.reconciler.embed_now.embed_messages_now", new=AsyncMock()
|
|
) as mock_embed_now,
|
|
):
|
|
files = {"file": ("note.txt", io.BytesIO(b"hello world"), "text/plain")}
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/upload",
|
|
files=files,
|
|
data={"peer_id": test_peer.name},
|
|
)
|
|
assert response.status_code == 201
|
|
expected_ids = [m["id"] for m in response.json()]
|
|
mock_embed_now.assert_awaited_once_with(expected_ids)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_batch_messages_with_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test batch message creation with metadata for each message"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Message 1",
|
|
"peer_id": test_peer.name,
|
|
"metadata": {"type": "question", "priority": "high"},
|
|
},
|
|
{
|
|
"content": "Message 2",
|
|
"peer_id": test_peer.name,
|
|
"metadata": {"type": "answer", "priority": "low"},
|
|
},
|
|
]
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 2
|
|
|
|
# Check first message
|
|
assert data[0]["content"] == "Message 1"
|
|
assert data[0]["metadata"] == {"type": "question", "priority": "high"}
|
|
|
|
# Check second message
|
|
assert data[1]["content"] == "Message 2"
|
|
assert data[1]["metadata"] == {"type": "answer", "priority": "low"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_batch_messages_without_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test batch message creation without metadata (should default to empty dict)"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Message without metadata",
|
|
"peer_id": test_peer.name,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0]["content"] == "Message without metadata"
|
|
assert data[0]["metadata"] == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_batch_messages_with_null_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test batch message creation with null metadata"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Message with null metadata",
|
|
"peer_id": test_peer.name,
|
|
"metadata": None,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0]["content"] == "Message with null metadata"
|
|
assert data[0]["metadata"] == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert len(data["items"]) > 0
|
|
assert data["items"][0]["content"] == "Test message"
|
|
assert data["items"][0]["peer_id"] == test_peer.name
|
|
assert data["items"][0]["session_id"] == test_session.name
|
|
assert data["items"][0]["metadata"] == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_member_peer_key_reads_session_but_cannot_write(
|
|
client: TestClient,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""A peer-scoped key may read sessions its peer belongs to (membership-based
|
|
cross-scope read), but not write to them. Non-member peer keys and session
|
|
keys on peer routes are denied. Exercises the real session_peers lookup."""
|
|
test_workspace, alice = sample_data
|
|
session_name = str(generate_nanoid())
|
|
base = f"/v3/workspaces/{test_workspace.name}/sessions/{session_name}"
|
|
|
|
# Setup with auth disabled: create the session with alice as an active
|
|
# member, then commit so the independent tracked_db session in auth() (which
|
|
# only sees committed rows) can resolve membership.
|
|
client.post(f"{base}/peers", json={alice.name: {}})
|
|
await db_session.commit()
|
|
|
|
# Enforce auth for the assertions below.
|
|
monkeypatch.setattr(settings.AUTH, "USE_AUTH", True)
|
|
monkeypatch.setattr(settings.AUTH, "JWT_SECRET", "test-secret")
|
|
|
|
# Member peer key: reads allowed.
|
|
client.headers["Authorization"] = (
|
|
f"Bearer {create_jwt(JWTParams(w=test_workspace.name, p=alice.name))}"
|
|
)
|
|
assert client.post(f"{base}/messages/list", json={}).status_code == 200
|
|
assert client.get(f"{base}/context").status_code == 200
|
|
|
|
# Member peer key: writes denied (write routes don't opt into member read).
|
|
assert (
|
|
client.post(
|
|
f"{base}/messages",
|
|
json={"messages": [{"content": "nope", "peer_id": alice.name}]},
|
|
).status_code
|
|
== 401
|
|
)
|
|
|
|
# Non-member peer key: even reads denied.
|
|
client.headers["Authorization"] = (
|
|
f"Bearer {create_jwt(JWTParams(w=test_workspace.name, p='not-a-member'))}"
|
|
)
|
|
assert client.post(f"{base}/messages/list", json={}).status_code == 401
|
|
|
|
# Session key: no cross-scope access to peer routes.
|
|
client.headers["Authorization"] = (
|
|
f"Bearer {create_jwt(JWTParams(w=test_workspace.name, s=session_name))}"
|
|
)
|
|
assert (
|
|
client.get(
|
|
f"/v3/workspaces/{test_workspace.name}/peers/{alice.name}/card"
|
|
).status_code
|
|
== 401
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_member_peer_key_reads_only_own_session_peer_config(
|
|
client: TestClient,
|
|
db_session: AsyncSession,
|
|
sample_data: tuple[Workspace, Peer],
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
):
|
|
"""A member peer key may read its OWN per-session config but not a
|
|
co-member's. The route opts into member read, so without the in-handler
|
|
self-check alice could read bob's config."""
|
|
test_workspace, alice = sample_data
|
|
bob_name = str(generate_nanoid())
|
|
session_name = str(generate_nanoid())
|
|
base = f"/v3/workspaces/{test_workspace.name}/sessions/{session_name}"
|
|
|
|
# Create the session with alice and bob as active members; commit so the
|
|
# independent read-only tracked_db in auth() can resolve membership.
|
|
client.post(f"{base}/peers", json={alice.name: {}, bob_name: {}})
|
|
await db_session.commit()
|
|
|
|
monkeypatch.setattr(settings.AUTH, "USE_AUTH", True)
|
|
monkeypatch.setattr(settings.AUTH, "JWT_SECRET", "test-secret")
|
|
|
|
client.headers["Authorization"] = (
|
|
f"Bearer {create_jwt(JWTParams(w=test_workspace.name, p=alice.name))}"
|
|
)
|
|
# Own config: allowed.
|
|
assert client.get(f"{base}/peers/{alice.name}/config").status_code == 200
|
|
# Co-member's config: denied even though alice is a session member.
|
|
assert client.get(f"{base}/peers/{bob_name}/config").status_code == 401
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_with_reverse(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages with reverse parameter"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Create multiple messages to test ordering
|
|
test_message1 = models.Message(
|
|
session_name=test_session.name,
|
|
content="First message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
test_message2 = models.Message(
|
|
session_name=test_session.name,
|
|
content="Second message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=2,
|
|
)
|
|
db_session.add(test_message1)
|
|
db_session.add(test_message2)
|
|
await db_session.commit()
|
|
|
|
# Test normal order
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={},
|
|
)
|
|
assert response.status_code == 200
|
|
normal_data = response.json()
|
|
|
|
# Test reversed order
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list?reverse=true",
|
|
json={},
|
|
)
|
|
assert response.status_code == 200
|
|
reversed_data = response.json()
|
|
|
|
# Both should have items
|
|
assert len(normal_data["items"]) >= 2
|
|
assert len(reversed_data["items"]) >= 2
|
|
|
|
# Order should be different (first item in normal should be last in reversed)
|
|
if len(normal_data["items"]) > 1 and len(reversed_data["items"]) > 1:
|
|
assert normal_data["items"][0]["id"] != reversed_data["items"][0]["id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_with_empty_filter(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages with empty filter object"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={"filters": {}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert isinstance(data["items"], list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_with_null_filter(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages with null filter"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={"filters": None},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert isinstance(data["items"], list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_no_body(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages without any request body"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list"
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert isinstance(data["items"], list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_filtered_messages(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and messages
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"key": "value"},
|
|
seq_in_session=1,
|
|
)
|
|
test_message2 = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message 2",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"key": "value2"},
|
|
seq_in_session=2,
|
|
)
|
|
db_session.add(test_message)
|
|
db_session.add(test_message2)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={"filters": {"metadata": {"key": "value2"}}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert len(data["items"]) == 1
|
|
assert data["items"][0]["content"] == "Test message 2"
|
|
assert data["items"][0]["peer_id"] == test_peer.name
|
|
assert data["items"][0]["session_id"] == test_session.name
|
|
assert data["items"][0]["metadata"] == {"key": "value2"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_filtered_messages_with_complex_filter(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages with complex metadata filter"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and messages
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message1 = models.Message(
|
|
session_name=test_session.name,
|
|
content="Message 1",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"type": "question", "priority": "high", "category": "technical"},
|
|
seq_in_session=1,
|
|
)
|
|
test_message2 = models.Message(
|
|
session_name=test_session.name,
|
|
content="Message 2",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"type": "answer", "priority": "high", "category": "technical"},
|
|
seq_in_session=2,
|
|
)
|
|
test_message3 = models.Message(
|
|
session_name=test_session.name,
|
|
content="Message 3",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"type": "question", "priority": "low", "category": "general"},
|
|
seq_in_session=3,
|
|
)
|
|
db_session.add(test_message1)
|
|
db_session.add(test_message2)
|
|
db_session.add(test_message3)
|
|
await db_session.commit()
|
|
|
|
# Test old-style filter (backward compatibility)
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={"filters": {"metadata": {"priority": "high", "category": "technical"}}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
# Should return messages 1 and 2 (both have high priority and technical category)
|
|
assert len(data["items"]) >= 2
|
|
|
|
# Test new-style filter with AND operator
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={
|
|
"filters": {
|
|
"AND": [
|
|
{"metadata": {"priority": "high"}},
|
|
{"metadata": {"category": "technical"}},
|
|
]
|
|
}
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert len(data["items"]) == 2
|
|
|
|
# Test OR filter to get high priority OR question type
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={
|
|
"filters": {
|
|
"OR": [
|
|
{"metadata": {"priority": "high"}},
|
|
{"metadata": {"type": "question"}},
|
|
]
|
|
}
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert len(data["items"]) == 3 # All messages should match
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_message(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
|
|
json={"metadata": {"new_key": "new_value"}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["metadata"] == {"new_key": "new_value"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_message_with_complex_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test updating message with complex metadata structure"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
complex_metadata = {
|
|
"tags": ["important", "follow-up"],
|
|
"score": 8.5,
|
|
"nested": {"category": "technical", "subcategory": "api"},
|
|
"processed": True,
|
|
}
|
|
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
|
|
json={"metadata": complex_metadata},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["metadata"] == complex_metadata
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_message_empty_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
# note that this should not change the metadata of the message.
|
|
# this test is to ensure that the metadata is not changed when it is set to None.
|
|
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"test_key": "test_value"},
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
|
|
json={"metadata": None},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# now ensure that the metadata is not changed
|
|
response = client.get(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}"
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["metadata"] == {"test_key": "test_value"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_message_with_empty_dict_metadata(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test updating message with empty dictionary metadata"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
h_metadata={"old_key": "old_value"},
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
|
|
json={"metadata": {}},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["metadata"] == {}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_single_message(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
response = client.get(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}"
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["content"] == "Test message"
|
|
assert data["peer_id"] == test_peer.name
|
|
assert data["session_id"] == test_session.name
|
|
assert data["workspace_id"] == test_workspace.name
|
|
assert data["id"] == test_message.public_id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_nonexistent_message(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting a message that doesn't exist"""
|
|
test_workspace, _ = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
nonexistent_message_id = str(generate_nanoid())
|
|
response = client.get(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{nonexistent_message_id}"
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_nonexistent_message(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test updating a message that doesn't exist"""
|
|
test_workspace, _ = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
nonexistent_message_id = str(generate_nanoid())
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{nonexistent_message_id}",
|
|
json={"metadata": {"key": "value"}},
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_messages_for_nonexistent_session(
|
|
client: TestClient, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating messages for a session that doesn't exist - should create the session"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
nonexistent_session_id = str(generate_nanoid())
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Test message",
|
|
"peer_id": test_peer.name,
|
|
}
|
|
]
|
|
},
|
|
)
|
|
# Should create the session and return 201 with the created message
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0]["content"] == "Test message"
|
|
assert data[0]["workspace_id"] == test_workspace.name
|
|
assert data[0]["session_id"] == nonexistent_session_id
|
|
assert data[0]["peer_id"] == test_peer.name
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_for_nonexistent_session(
|
|
client: TestClient, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test getting messages for a session that doesn't exist - should return empty list"""
|
|
test_workspace, _ = sample_data
|
|
|
|
nonexistent_session_id = str(generate_nanoid())
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{nonexistent_session_id}/messages/list",
|
|
json={},
|
|
)
|
|
# Should return 200 with empty results (session doesn't exist = no messages)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert len(data["items"]) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_empty_batch_messages(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating an empty batch of messages"""
|
|
test_workspace, _ = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={"messages": []},
|
|
)
|
|
# Should return 422 for validation error (empty list not allowed)
|
|
assert response.status_code == 422
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_batch_messages_max_limit(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating batch messages at the maximum limit (100 messages)"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Create exactly 100 messages (the maximum allowed)
|
|
messages = [
|
|
{"content": f"Message {i}", "peer_id": test_peer.name, "metadata": {"index": i}}
|
|
for i in range(100)
|
|
]
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={"messages": messages},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 100
|
|
assert data[0]["content"] == "Message 0"
|
|
assert data[99]["content"] == "Message 99"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_messages_handles_crud_value_error(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test that ValueError from CRUD is properly handled in get_messages"""
|
|
test_workspace, _test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Mock the CRUD function to raise ValueError
|
|
with patch("src.routers.messages.crud.get_messages") as mock_get:
|
|
mock_get.side_effect = ValueError("Test CRUD error")
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/list",
|
|
json={},
|
|
)
|
|
|
|
# Should raise ResourceNotFoundException which gets converted to 404
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_message_handles_not_found(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test that ResourceNotFoundException is properly handled in get_message"""
|
|
test_workspace, _ = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Try to get a non-existent message
|
|
with patch("src.routers.messages.crud.get_message") as mock_get:
|
|
mock_get.return_value = None
|
|
|
|
response = client.get(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/nonexistent"
|
|
)
|
|
|
|
# Should raise ResourceNotFoundException which gets converted to 404
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_message_handles_crud_value_error(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test that ValueError from CRUD is properly handled in update_message"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session and message
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
test_message = models.Message(
|
|
session_name=test_session.name,
|
|
content="Test message",
|
|
workspace_name=test_workspace.name,
|
|
peer_name=test_peer.name,
|
|
seq_in_session=1,
|
|
)
|
|
db_session.add(test_message)
|
|
await db_session.commit()
|
|
|
|
# Mock the CRUD function to raise ValueError
|
|
with patch("src.routers.messages.crud.update_message") as mock_update:
|
|
mock_update.side_effect = ValueError("Test CRUD error")
|
|
|
|
response = client.put(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/{test_message.public_id}",
|
|
json={"metadata": {"key": "value"}},
|
|
)
|
|
|
|
# Should raise ResourceNotFoundException which gets converted to 404
|
|
assert response.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_messages_with_file_too_large(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test that FileTooLargeError is properly handled in create_messages_with_file"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Create a large file that exceeds the size limit
|
|
import io
|
|
|
|
large_content = b"x" * (10 * 1024 * 1024) # 10MB file
|
|
file_data = io.BytesIO(large_content)
|
|
|
|
# Mock the settings to make the test deterministic
|
|
with patch(
|
|
"src.routers.messages.settings.MAX_FILE_SIZE", 5 * 1024 * 1024
|
|
): # 5MB limit
|
|
files = {"file": ("large_file.txt", file_data, "text/plain")}
|
|
form_data = {"peer_id": test_peer.name}
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages/upload",
|
|
files=files,
|
|
data=form_data,
|
|
)
|
|
|
|
# Should raise FileTooLargeError which gets converted to 413
|
|
assert response.status_code == 413
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_with_timestamp(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating a message with custom timestamp"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Use a specific timestamp for testing
|
|
custom_timestamp = datetime.datetime(
|
|
2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
|
|
)
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Test message with timestamp",
|
|
"peer_id": test_peer.name,
|
|
"created_at": custom_timestamp.isoformat(),
|
|
"metadata": {"test": "with_timestamp"},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
message = data[0]
|
|
assert message["content"] == "Test message with timestamp"
|
|
assert message["peer_id"] == test_peer.name
|
|
assert message["metadata"] == {"test": "with_timestamp"}
|
|
|
|
# Verify the created_at field matches our custom timestamp
|
|
# Pydantic serializes UTC timezone as 'Z' format (ISO 8601 standard)
|
|
expected_timestamp = "2023-01-01T12:00:00Z"
|
|
assert message["created_at"] == expected_timestamp
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_without_timestamp_uses_default(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating a message without timestamp uses default timestamp"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Pad the window to absorb client/Postgres clock skew under Docker.
|
|
before_request = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Test message without timestamp",
|
|
"peer_id": test_peer.name,
|
|
"metadata": {"test": "no_timestamp"},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
after_request = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
message = data[0]
|
|
assert message["content"] == "Test message without timestamp"
|
|
assert message["peer_id"] == test_peer.name
|
|
assert message["metadata"] == {"test": "no_timestamp"}
|
|
|
|
# Verify the created_at field is between our before/after times
|
|
message_created_at = datetime.datetime.fromisoformat(
|
|
message["created_at"].replace("Z", "+00:00")
|
|
)
|
|
assert before_request <= message_created_at <= after_request
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_batch_messages_with_mixed_timestamps(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating batch messages with some having custom timestamps and others using default"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
# Use specific timestamps for testing
|
|
timestamp1 = datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
|
timestamp2 = datetime.datetime(2023, 1, 2, 12, 0, 0, tzinfo=datetime.timezone.utc)
|
|
|
|
before_request = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Message 1 with timestamp",
|
|
"peer_id": test_peer.name,
|
|
"created_at": timestamp1.isoformat(),
|
|
"metadata": {"type": "custom_timestamp"},
|
|
},
|
|
{
|
|
"content": "Message 2 without timestamp",
|
|
"peer_id": test_peer.name,
|
|
"metadata": {"type": "default_timestamp"},
|
|
},
|
|
{
|
|
"content": "Message 3 with timestamp",
|
|
"peer_id": test_peer.name,
|
|
"created_at": timestamp2.isoformat(),
|
|
"metadata": {"type": "custom_timestamp"},
|
|
},
|
|
]
|
|
},
|
|
)
|
|
|
|
after_request = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 3
|
|
|
|
# Check first message with custom timestamp
|
|
assert data[0]["content"] == "Message 1 with timestamp"
|
|
assert (
|
|
data[0]["created_at"] == "2023-01-01T12:00:00Z"
|
|
) # Pydantic converts to Z format
|
|
assert data[0]["metadata"] == {"type": "custom_timestamp"}
|
|
|
|
# Check second message with default timestamp
|
|
assert data[1]["content"] == "Message 2 without timestamp"
|
|
message2_created_at = datetime.datetime.fromisoformat(
|
|
data[1]["created_at"].replace("Z", "+00:00")
|
|
)
|
|
assert before_request <= message2_created_at <= after_request
|
|
assert data[1]["metadata"] == {"type": "default_timestamp"}
|
|
|
|
# Check third message with custom timestamp
|
|
assert data[2]["content"] == "Message 3 with timestamp"
|
|
assert (
|
|
data[2]["created_at"] == "2023-01-02T12:00:00Z"
|
|
) # Pydantic converts to Z format
|
|
assert data[2]["metadata"] == {"type": "custom_timestamp"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_message_with_null_timestamp(
|
|
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
|
):
|
|
"""Test creating a message with null timestamp uses default"""
|
|
test_workspace, test_peer = sample_data
|
|
|
|
# Create a test session
|
|
test_session = models.Session(
|
|
workspace_name=test_workspace.name, name=str(generate_nanoid())
|
|
)
|
|
db_session.add(test_session)
|
|
await db_session.commit()
|
|
|
|
before_request = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
response = client.post(
|
|
f"/v3/workspaces/{test_workspace.name}/sessions/{test_session.name}/messages",
|
|
json={
|
|
"messages": [
|
|
{
|
|
"content": "Test message with null timestamp",
|
|
"peer_id": test_peer.name,
|
|
"created_at": None,
|
|
"metadata": {"test": "null_timestamp"},
|
|
}
|
|
]
|
|
},
|
|
)
|
|
|
|
after_request = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(
|
|
seconds=1
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
message = data[0]
|
|
assert message["content"] == "Test message with null timestamp"
|
|
assert message["metadata"] == {"test": "null_timestamp"}
|
|
|
|
# Verify the created_at field uses default (current time)
|
|
message_created_at = datetime.datetime.fromisoformat(
|
|
message["created_at"].replace("Z", "+00:00")
|
|
)
|
|
assert before_request <= message_created_at <= after_request
|