mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-10 18:53:03 +02:00
Add global message search and more e2e tests
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { seedChannelMessages } from '../helpers/seed';
|
||||
import { ensureFlightlessChannel } from '../helpers/api';
|
||||
|
||||
const CHANNEL_NAME = '#flightless';
|
||||
const SEED_COUNT = 30;
|
||||
|
||||
/**
|
||||
* Seed #flightless with unique, searchable messages.
|
||||
* Returns the channel key and a unique search token.
|
||||
*/
|
||||
function seedFlightlessMessages() {
|
||||
const token = `e2e-search-${Date.now()}`;
|
||||
const seeded = seedChannelMessages({
|
||||
channelName: CHANNEL_NAME,
|
||||
count: SEED_COUNT,
|
||||
// Use unique text so search can find them reliably
|
||||
// Note: seedChannelMessages uses "seed-N" as text by default,
|
||||
// but we need our unique token in there. We'll search for "seed-" instead
|
||||
// since that's what the seed helper generates.
|
||||
outgoingEvery: 5,
|
||||
includePaths: true,
|
||||
});
|
||||
return { key: seeded.key, token };
|
||||
}
|
||||
|
||||
test.describe('Channel info pane', () => {
|
||||
let channelKey: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await ensureFlightlessChannel();
|
||||
const seeded = seedFlightlessMessages();
|
||||
channelKey = seeded.key;
|
||||
});
|
||||
|
||||
test('opens channel info pane and shows message activity', async ({ page }) => {
|
||||
await page.goto(`/#channel/${channelKey}/flightless`);
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
// Wait for messages to load
|
||||
await expect(page.getByText('seed-0')).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Click the channel name in the header to open info pane
|
||||
const headerTitle = page.locator('h2').filter({ hasText: '#flightless' });
|
||||
await headerTitle.click();
|
||||
|
||||
// Channel info pane should open as a sheet
|
||||
const infoPane = page.getByRole('dialog');
|
||||
await expect(infoPane).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Should show channel name
|
||||
await expect(infoPane.getByText('#flightless')).toBeVisible();
|
||||
|
||||
// Should show channel key
|
||||
await expect(infoPane.getByText(channelKey.toLowerCase())).toBeVisible();
|
||||
|
||||
// Should show "Hashtag" badge
|
||||
await expect(infoPane.getByText('Hashtag')).toBeVisible();
|
||||
|
||||
// Should show "Message Activity" section with counts
|
||||
await expect(infoPane.getByText('Message Activity')).toBeVisible();
|
||||
await expect(infoPane.getByText('All Time')).toBeVisible();
|
||||
|
||||
// All Time count should be non-zero (our seeded messages)
|
||||
// InfoItem renders: <span>All Time</span><p>VALUE</p> — use CSS sibling selector
|
||||
const allTimeValue = infoPane.locator('span:text-is("All Time") + p');
|
||||
const count = await allTimeValue.textContent();
|
||||
expect(Number(count?.replace(/,/g, ''))).toBeGreaterThanOrEqual(SEED_COUNT);
|
||||
|
||||
// Should show "First Message" section
|
||||
await expect(infoPane.getByText('First Message')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Message search and jump-to-message', () => {
|
||||
let channelKey: string;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await ensureFlightlessChannel();
|
||||
const seeded = seedFlightlessMessages();
|
||||
channelKey = seeded.key;
|
||||
});
|
||||
|
||||
test('search finds seeded messages', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
// Open search view via sidebar
|
||||
await page.getByText('Message Search').click();
|
||||
|
||||
// Should show search input
|
||||
const searchInput = page.getByPlaceholder('Search all messages...');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// Search for seeded messages (seed helper creates "seed-N" text)
|
||||
await searchInput.fill('seed-1');
|
||||
|
||||
// Wait for search results to appear
|
||||
await expect(page.getByText('seed-1', { exact: false }).first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// Results should show the channel name
|
||||
await expect(page.getByText('#flightless').first()).toBeVisible();
|
||||
|
||||
// Results should show "Channel" badge
|
||||
await expect(page.getByText('Channel').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('clicking a search result jumps to the message in conversation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
// Open search
|
||||
await page.getByText('Message Search').click();
|
||||
|
||||
const searchInput = page.getByPlaceholder('Search all messages...');
|
||||
await searchInput.fill('seed-15');
|
||||
|
||||
// Wait for results
|
||||
const result = page.getByText('seed-15', { exact: false }).first();
|
||||
await expect(result).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Click the search result to jump to it
|
||||
await result.click();
|
||||
|
||||
// Should navigate to the #flightless conversation
|
||||
await expect(page.getByPlaceholder(/message #flightless/i)).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// The target message should be visible in the conversation (not search results)
|
||||
// Scope to [data-message-id] to avoid matching leftover search <mark> elements
|
||||
const messageContainer = page.locator('[data-message-id]').filter({ hasText: 'seed-15' });
|
||||
await expect(messageContainer.first()).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test('search returns no results for nonsense query', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByText('Connected')).toBeVisible();
|
||||
|
||||
await page.getByText('Message Search').click();
|
||||
|
||||
const searchInput = page.getByPlaceholder('Search all messages...');
|
||||
await searchInput.fill('zzz-nonexistent-query-zzz');
|
||||
|
||||
await expect(page.getByText(/No messages found/)).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
@@ -322,6 +322,7 @@ class TestContactMessageCLIFiltering:
|
||||
"signature",
|
||||
"outgoing",
|
||||
"acked",
|
||||
"sender_name",
|
||||
}
|
||||
|
||||
with (
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
"""Tests for message search, around, and forward pagination."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.radio import radio_manager
|
||||
from app.repository import MessageRepository
|
||||
|
||||
CHAN_KEY = "ABC123DEF456ABC123DEF456ABC12345"
|
||||
DM_KEY = "aa" * 32
|
||||
OTHER_CHAN_KEY = "FF" * 16
|
||||
|
||||
|
||||
class TestMessageSearch:
|
||||
"""Tests for the q (search) parameter on get_all."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_search(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello world",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="goodbye moon",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="hello")
|
||||
assert len(results) == 1
|
||||
assert results[0].text == "hello world"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_case_insensitive(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="Hello World",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="hello")
|
||||
assert len(results) == 1
|
||||
|
||||
results = await MessageRepository.get_all(q="HELLO")
|
||||
assert len(results) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_pagination(self, test_db):
|
||||
for i in range(5):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"test message {i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="test message", limit=2)
|
||||
assert len(results) == 2
|
||||
|
||||
results = await MessageRepository.get_all(q="test message", limit=2, offset=2)
|
||||
assert len(results) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_within_conversation(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello from channel",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello from other",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="hello", conversation_key=CHAN_KEY)
|
||||
assert len(results) == 1
|
||||
assert results[0].text == "hello from channel"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_no_results(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello world",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="nonexistent")
|
||||
assert len(results) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_across_types(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="search target in chan",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="search target in dm",
|
||||
conversation_key=DM_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="search target")
|
||||
assert len(results) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_returns_sender_name(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="Alice: hello world",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
sender_name="Alice",
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="hello")
|
||||
assert len(results) == 1
|
||||
assert results[0].sender_name == "Alice"
|
||||
|
||||
|
||||
class TestMessagesAround:
|
||||
"""Tests for get_around()."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_context(self, test_db):
|
||||
ids = []
|
||||
for i in range(10):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
# Get around the middle message (index 5)
|
||||
messages, has_older, has_newer = await MessageRepository.get_around(
|
||||
message_id=ids[5],
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
)
|
||||
|
||||
assert len(messages) == 10
|
||||
assert not has_older # Only 5 before, context_size defaults to 100
|
||||
assert not has_newer # Only 4 after
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_has_older_has_newer(self, test_db):
|
||||
ids = []
|
||||
for i in range(20):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
messages, has_older, has_newer = await MessageRepository.get_around(
|
||||
message_id=ids[10],
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
context_size=3,
|
||||
)
|
||||
|
||||
# 3 before + target + 3 after = 7
|
||||
assert len(messages) == 7
|
||||
assert has_older # 10 messages before, context_size=3
|
||||
assert has_newer # 9 messages after, context_size=3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nonexistent_message(self, test_db):
|
||||
messages, has_older, has_newer = await MessageRepository.get_around(
|
||||
message_id=99999,
|
||||
)
|
||||
assert messages == []
|
||||
assert not has_older
|
||||
assert not has_newer
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conversation_key_filter(self, test_db):
|
||||
# Create messages in two channels
|
||||
for i in range(5):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"chan1 msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
for i in range(5):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"chan2 msg{i}",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
|
||||
# Get the target from channel 1
|
||||
all_chan1 = await MessageRepository.get_all(conversation_key=CHAN_KEY)
|
||||
target_id = all_chan1[2].id
|
||||
|
||||
messages, _, _ = await MessageRepository.get_around(
|
||||
message_id=target_id,
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
)
|
||||
|
||||
# All returned messages should be from channel 1
|
||||
for msg in messages:
|
||||
assert msg.conversation_key == CHAN_KEY
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_context_size(self, test_db):
|
||||
ids = []
|
||||
for i in range(10):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
messages, has_older, has_newer = await MessageRepository.get_around(
|
||||
message_id=ids[5],
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
context_size=2,
|
||||
)
|
||||
|
||||
# 2 before + target + 2 after = 5
|
||||
assert len(messages) == 5
|
||||
assert has_older # 5 before, context=2
|
||||
assert has_newer # 4 after, context=2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_target_not_in_filtered_conversation_returns_empty(self, test_db):
|
||||
target_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="target in channel 1",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="message in channel 2",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
messages, has_older, has_newer = await MessageRepository.get_around(
|
||||
message_id=target_id,
|
||||
msg_type="CHAN",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
)
|
||||
|
||||
assert messages == []
|
||||
assert not has_older
|
||||
assert not has_newer
|
||||
|
||||
|
||||
class TestForwardPagination:
|
||||
"""Tests for the after/after_id forward cursor on get_all."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_pagination(self, test_db):
|
||||
ids = []
|
||||
for i in range(5):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
# Get first page (DESC order)
|
||||
page1 = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
limit=3,
|
||||
)
|
||||
assert len(page1) == 3
|
||||
# Page 1 is DESC: msg4, msg3, msg2
|
||||
|
||||
# Get forward from msg2 (oldest in page1)
|
||||
newest = page1[0] # msg4
|
||||
forward = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
after=newest.received_at,
|
||||
after_id=newest.id,
|
||||
limit=10,
|
||||
)
|
||||
# Nothing newer than msg4
|
||||
assert len(forward) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_pagination_returns_asc(self, test_db):
|
||||
ids = []
|
||||
for i in range(5):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
# Forward from the first message
|
||||
forward = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=CHAN_KEY,
|
||||
after=100,
|
||||
after_id=ids[0],
|
||||
limit=10,
|
||||
)
|
||||
assert len(forward) == 4 # msg1, msg2, msg3, msg4
|
||||
# Should be ASC order
|
||||
for i in range(len(forward) - 1):
|
||||
assert forward[i].received_at <= forward[i + 1].received_at
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_with_conversation_key(self, test_db):
|
||||
for i in range(3):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"chan1 msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
for i in range(3):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"chan2 msg{i}",
|
||||
conversation_key=OTHER_CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
|
||||
chan1_msgs = await MessageRepository.get_all(conversation_key=CHAN_KEY)
|
||||
oldest = chan1_msgs[-1]
|
||||
|
||||
forward = await MessageRepository.get_all(
|
||||
conversation_key=CHAN_KEY,
|
||||
after=oldest.received_at,
|
||||
after_id=oldest.id,
|
||||
limit=10,
|
||||
)
|
||||
for msg in forward:
|
||||
assert msg.conversation_key == CHAN_KEY
|
||||
|
||||
|
||||
class TestSearchLikeEscaping:
|
||||
"""Tests for LIKE wildcard escaping in search."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_percent_in_query_is_literal(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="100% done",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="100 items done",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="100%")
|
||||
assert len(results) == 1
|
||||
assert results[0].text == "100% done"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_underscore_in_query_is_literal(self, test_db):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello_world",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="helloXworld",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
results = await MessageRepository.get_all(q="hello_world")
|
||||
assert len(results) == 1
|
||||
assert results[0].text == "hello_world"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_radio_state():
|
||||
"""Save/restore radio_manager state so tests don't leak."""
|
||||
prev = radio_manager._meshcore
|
||||
prev_lock = radio_manager._operation_lock
|
||||
yield
|
||||
radio_manager._meshcore = prev
|
||||
radio_manager._operation_lock = prev_lock
|
||||
|
||||
|
||||
class TestMessagesAroundEndpoint:
|
||||
"""HTTP-level tests for GET /api/messages/around/{id}."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_around_returns_context(self, test_db, client):
|
||||
ids = []
|
||||
for i in range(10):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
response = await client.get(
|
||||
f"/api/messages/around/{ids[5]}",
|
||||
params={"type": "CHAN", "conversation_key": CHAN_KEY},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert "messages" in body
|
||||
assert "has_older" in body
|
||||
assert "has_newer" in body
|
||||
assert len(body["messages"]) == 10
|
||||
assert not body["has_older"]
|
||||
assert not body["has_newer"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_around_nonexistent_returns_empty(self, test_db, client):
|
||||
response = await client.get("/api/messages/around/99999")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["messages"] == []
|
||||
assert not body["has_older"]
|
||||
assert not body["has_newer"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_around_respects_context_param(self, test_db, client):
|
||||
ids = []
|
||||
for i in range(20):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
response = await client.get(
|
||||
f"/api/messages/around/{ids[10]}",
|
||||
params={"type": "CHAN", "conversation_key": CHAN_KEY, "context": 3},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
# 3 before + target + 3 after = 7
|
||||
assert len(body["messages"]) == 7
|
||||
assert body["has_older"]
|
||||
assert body["has_newer"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_around_message_fields_serialized(self, test_db, client):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="Alice: test message",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
sender_name="Alice",
|
||||
)
|
||||
|
||||
response = await client.get(f"/api/messages/around/{msg_id}")
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body["messages"]) == 1
|
||||
msg = body["messages"][0]
|
||||
assert msg["id"] == msg_id
|
||||
assert msg["type"] == "CHAN"
|
||||
assert msg["text"] == "Alice: test message"
|
||||
assert msg["sender_name"] == "Alice"
|
||||
|
||||
|
||||
class TestSearchEndpoint:
|
||||
"""HTTP-level tests for GET /api/messages?q=..."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_via_endpoint(self, test_db, client):
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="hello world",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100,
|
||||
received_at=100,
|
||||
)
|
||||
await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text="goodbye moon",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=101,
|
||||
received_at=101,
|
||||
)
|
||||
|
||||
response = await client.get("/api/messages", params={"q": "hello"})
|
||||
assert response.status_code == 200
|
||||
results = response.json()
|
||||
assert len(results) == 1
|
||||
assert results[0]["text"] == "hello world"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_pagination_via_endpoint(self, test_db, client):
|
||||
ids = []
|
||||
for i in range(5):
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=f"msg{i}",
|
||||
conversation_key=CHAN_KEY,
|
||||
sender_timestamp=100 + i,
|
||||
received_at=100 + i,
|
||||
)
|
||||
ids.append(msg_id)
|
||||
|
||||
response = await client.get(
|
||||
"/api/messages",
|
||||
params={
|
||||
"type": "CHAN",
|
||||
"conversation_key": CHAN_KEY,
|
||||
"after": 100,
|
||||
"after_id": ids[0],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
results = response.json()
|
||||
assert len(results) == 4
|
||||
# Forward results should be ASC
|
||||
for i in range(len(results) - 1):
|
||||
assert results[i]["received_at"] <= results[i + 1]["received_at"]
|
||||
Reference in New Issue
Block a user