Files
galaxy/test/unit/model/test_keyset_token_pagination.py
T
John ChiltonandClaude 32b7ceee07 Migrate WES pagination to unified keyset-based approach
Replace offset-based and security.encode_id() pagination with generalized
keyset abstraction supporting both single-ID and composite keysets.

New abstraction (lib/galaxy/model/keyset_token_pagination.py):
- KeysetToken Protocol: polymorphic via to_values()/from_values()
- SingleKeysetToken: single ID keyset for runs pagination
- KeysetPagination: base64+JSON encoder/decoder
- TaskKeysetToken (wes.py): composite (step_order, job_index) keyset

Changes to WES pagination:
- list_runs: SingleKeysetToken with WHERE id < last_id (unchanged query)
- get_run_tasks: TaskKeysetToken with tuple comparison WHERE (step_order, job_index) > (last_step, last_idx)
- Removed offset-based _encode_page_token/_decode_page_token
- Removed _encode_keyset_token/_decode_keyset_token

Benefits:
- Cursor-stable pagination under concurrent changes
- Efficient composite keyset filtering on UNION queries
- Consistent token encoding across all WES endpoints
- Protocol-based design - no type checking needed

Tests: 9 unit tests for keyset pagination abstraction (all passing)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-20 14:30:53 -05:00

93 lines
3.1 KiB
Python

"""Tests for keyset token pagination."""
import pytest
from galaxy import exceptions
from galaxy.model.keyset_token_pagination import (
KeysetPagination,
SingleKeysetToken,
)
@pytest.fixture
def pagination():
"""Provide KeysetPagination instance."""
return KeysetPagination()
class TestSingleKeysetToken:
"""Test SingleKeysetToken implementation."""
def test_to_values(self):
"""Test converting token to values."""
token = SingleKeysetToken(last_id=42)
assert token.to_values() == [42]
def test_from_values(self):
"""Test reconstructing token from values."""
token = SingleKeysetToken.from_values([42])
assert token.last_id == 42
def test_from_values_multiple(self):
"""Test from_values uses first value only."""
token = SingleKeysetToken.from_values([42, 100, 200])
assert token.last_id == 42
def test_from_values_empty_raises(self):
"""Test from_values raises on empty values."""
with pytest.raises(ValueError, match="requires at least 1 value"):
SingleKeysetToken.from_values([])
class TestKeysetPagination:
"""Test KeysetPagination encoder/decoder."""
def test_encode_decode_roundtrip(self, pagination):
"""Test encoding and decoding roundtrip."""
original = SingleKeysetToken(last_id=123)
encoded = pagination.encode_token(original)
decoded = pagination.decode_token(encoded, token_class=SingleKeysetToken)
assert decoded is not None
assert decoded.last_id == 123
def test_decode_none_token_returns_none(self, pagination):
"""Test decoding None returns None."""
result = pagination.decode_token(None, token_class=SingleKeysetToken)
assert result is None
def test_decode_empty_string_returns_none(self, pagination):
"""Test decoding empty string returns None."""
result = pagination.decode_token("", token_class=SingleKeysetToken)
assert result is None
def test_decode_invalid_token_raises(self, pagination):
"""Test decoding invalid token raises MessageException."""
with pytest.raises(exceptions.MessageException, match="Invalid page_token"):
pagination.decode_token("invalid_token_!@#", token_class=SingleKeysetToken)
def test_encode_multiple_values(self, pagination):
"""Test encoding handles multiple values."""
# Create custom token class with multiple values
from dataclasses import dataclass
@dataclass
class MultiValueToken:
val1: int
val2: int
val3: int
def to_values(self):
return [self.val1, self.val2, self.val3]
@classmethod
def from_values(cls, values):
return cls(val1=values[0], val2=values[1], val3=values[2])
original = MultiValueToken(val1=1, val2=2, val3=3)
encoded = pagination.encode_token(original)
decoded = pagination.decode_token(encoded, token_class=MultiValueToken)
assert decoded is not None
assert decoded.val1 == 1
assert decoded.val2 == 2
assert decoded.val3 == 3