Open-source Grok free registration CLI, xai_enroller auth pipeline, local auth service, tests and docs.
299 lines
8.5 KiB
Python
299 lines
8.5 KiB
Python
import asyncio
|
|
import threading
|
|
|
|
from xai_enroller.auth_pipeline import AuthPipeline
|
|
from xai_enroller.ledger import Ledger
|
|
from xai_enroller.models import (
|
|
AuthorizationResult,
|
|
AuthorizationStatus,
|
|
DeviceFlow,
|
|
JobStatus,
|
|
OAuthCredential,
|
|
PipelineState,
|
|
SinkReceipt,
|
|
SourceRecord,
|
|
)
|
|
|
|
|
|
class EmptySource:
|
|
async def records(self):
|
|
if False:
|
|
yield None
|
|
|
|
async def close(self):
|
|
return None
|
|
|
|
|
|
class NoopExecutor:
|
|
async def close(self):
|
|
return None
|
|
|
|
|
|
def _pipeline(tmp_path):
|
|
return AuthPipeline(
|
|
source=EmptySource(),
|
|
protocol=object(),
|
|
executor=NoopExecutor(),
|
|
sink=object(),
|
|
ledger=Ledger(tmp_path / "ledger.db", b"salt"),
|
|
)
|
|
|
|
|
|
def test_cancel_active_is_idempotent_while_cleanup_is_in_progress(tmp_path):
|
|
async def scenario():
|
|
pipeline = _pipeline(tmp_path)
|
|
|
|
async def wait_forever():
|
|
await asyncio.Future()
|
|
|
|
task = asyncio.create_task(wait_forever())
|
|
pipeline._authorization_task = task
|
|
pipeline._authorization_cancellable = True
|
|
assert await pipeline.cancel_active()
|
|
assert not await pipeline.cancel_active()
|
|
await asyncio.gather(task, return_exceptions=True)
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
class SingleSource:
|
|
async def records(self):
|
|
yield SourceRecord("stock", "opaque")
|
|
|
|
async def close(self):
|
|
return None
|
|
|
|
|
|
class ImmediateProtocol:
|
|
async def start_device_flow(self):
|
|
return DeviceFlow(
|
|
"device",
|
|
"code",
|
|
"https://accounts.x.ai/oauth2/device",
|
|
600,
|
|
0,
|
|
)
|
|
|
|
async def poll_token(self, **_kwargs):
|
|
return OAuthCredential(
|
|
"access",
|
|
"refresh",
|
|
None,
|
|
"Bearer",
|
|
3600,
|
|
"later",
|
|
"now",
|
|
"subject",
|
|
"https://auth.x.ai/oauth2/token",
|
|
)
|
|
|
|
|
|
class ImmediateExecutor:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
async def start(self):
|
|
return None
|
|
|
|
async def close(self):
|
|
return None
|
|
|
|
async def confirm(self, *_args):
|
|
self.calls += 1
|
|
return AuthorizationResult(AuthorizationStatus.AUTHORIZED, "confirmed")
|
|
|
|
|
|
class BlockingThreadSink:
|
|
def __init__(self):
|
|
self.entered = threading.Event()
|
|
self.release = threading.Event()
|
|
self.pipeline = None
|
|
self.calls = 0
|
|
|
|
def _store(self):
|
|
self.calls += 1
|
|
self.entered.set()
|
|
self.release.wait(2)
|
|
return SinkReceipt("opaque")
|
|
|
|
async def store(self, _credential):
|
|
receipt = await asyncio.to_thread(self._store)
|
|
self.pipeline.request_stop()
|
|
return receipt
|
|
|
|
|
|
def test_sink_and_ledger_commit_cannot_be_cancelled_mid_commit(tmp_path):
|
|
async def scenario():
|
|
executor = ImmediateExecutor()
|
|
sink = BlockingThreadSink()
|
|
ledger = Ledger(tmp_path / "ledger.db", b"salt")
|
|
events = []
|
|
pipeline = AuthPipeline(
|
|
source=SingleSource(),
|
|
protocol=ImmediateProtocol(),
|
|
executor=executor,
|
|
sink=sink,
|
|
ledger=ledger,
|
|
timeout=2,
|
|
event_callback=lambda kind, data: events.append((kind, data)),
|
|
)
|
|
sink.pipeline = pipeline
|
|
run = asyncio.create_task(pipeline.run())
|
|
assert await asyncio.to_thread(sink.entered.wait, 1)
|
|
assert not await pipeline.cancel_active()
|
|
sink.release.set()
|
|
await asyncio.wait_for(run, 2)
|
|
assert executor.calls == 1
|
|
assert sink.calls == 1
|
|
assert ledger.aggregate_counts()["imported_unique"] == 1
|
|
started = next(data for kind, data in events if kind == "authorization_started")
|
|
imported = next(
|
|
data
|
|
for kind, data in events
|
|
if kind == "result" and data["status"] == "imported"
|
|
)
|
|
assert imported["task_number"] == started["task_number"]
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_pending_total_uses_current_snapshot_set_difference(tmp_path):
|
|
class SnapshotSource(EmptySource):
|
|
snapshot_fingerprints = frozenset({"a", "b", "c"})
|
|
|
|
ledger = Ledger(tmp_path / "ledger.db", b"salt")
|
|
imported = ledger.start_fingerprint("b")
|
|
ledger.finish(imported, JobStatus.IMPORTED, "imported", "receipt-b")
|
|
outside = ledger.start_fingerprint("outside")
|
|
ledger.finish(outside, JobStatus.IMPORTED, "imported", "receipt-outside")
|
|
pipeline = AuthPipeline(
|
|
source=SnapshotSource(),
|
|
protocol=object(),
|
|
executor=NoopExecutor(),
|
|
sink=object(),
|
|
ledger=ledger,
|
|
)
|
|
|
|
assert pipeline.status()["pending_total"] == 2
|
|
assert pipeline.status()["five_minute_imports_per_minute"] is None
|
|
assert pipeline.status()["lifetime_imports_per_minute"] is None
|
|
|
|
|
|
def test_pending_total_is_unknown_before_first_valid_snapshot(tmp_path):
|
|
pipeline = _pipeline(tmp_path)
|
|
|
|
assert pipeline.status()["pending_total"] is None
|
|
|
|
|
|
def test_rate_limit_gate_grows_cooldown_on_consecutive_trips():
|
|
class Clock:
|
|
def __init__(self):
|
|
self.now = 0.0
|
|
|
|
def __call__(self):
|
|
return self.now
|
|
|
|
async def scenario():
|
|
from xai_enroller.auth_pipeline import GlobalRateLimitGate
|
|
|
|
clock = Clock()
|
|
gate = GlobalRateLimitGate(clock=clock, base_cooldown=60.0)
|
|
assert await gate.rate_limited()
|
|
assert gate.COOLDOWN_SECONDS == 60.0
|
|
assert gate.snapshot()["cooldown_seconds"] == 60.0
|
|
|
|
resume = asyncio.Event()
|
|
resume.set()
|
|
clock.now = 60.0
|
|
probe = await gate.wait_for_permission(resume)
|
|
assert probe is not None
|
|
assert await gate.rate_limited(probe)
|
|
assert gate.COOLDOWN_SECONDS == 90.0
|
|
assert gate.snapshot()["cooldown_remaining_seconds"] == 90.0
|
|
|
|
clock.now = 150.0
|
|
probe = await gate.wait_for_permission(resume)
|
|
elapsed = await gate.authorized(probe)
|
|
assert elapsed == 150.0
|
|
assert gate.COOLDOWN_SECONDS == 60.0
|
|
assert gate.snapshot()["rate_limit_trips"] == 0
|
|
|
|
asyncio.run(scenario())
|
|
|
|
|
|
def test_minimum_start_interval_raises_after_rate_limit_and_decays():
|
|
from xai_enroller.auth_pipeline import MinimumStartInterval
|
|
|
|
def approx(value, rel=1e-6):
|
|
class Approx:
|
|
def __eq__(self, other):
|
|
return abs(float(other) - float(value)) <= rel * max(1.0, abs(value))
|
|
|
|
def __repr__(self):
|
|
return f"approx({value})"
|
|
|
|
return Approx()
|
|
|
|
interval = MinimumStartInterval(10.0)
|
|
assert interval.seconds == 10.0
|
|
raised = interval.note_rate_limited()
|
|
assert raised == 18.0
|
|
raised = interval.note_rate_limited()
|
|
assert raised == approx(18.0 * 1.55)
|
|
for _ in range(MinimumStartInterval.SUCCESS_STREAK_TO_DECAY):
|
|
interval.note_success()
|
|
assert interval.seconds < raised
|
|
# Keep decaying until the base floor is restored.
|
|
for _ in range(40):
|
|
interval.note_success()
|
|
assert interval.seconds == 10.0
|
|
|
|
|
|
def test_queued_source_imported_while_paused_is_not_authorized(tmp_path):
|
|
async def scenario():
|
|
executor = ImmediateExecutor()
|
|
ledger = Ledger(tmp_path / "ledger.db", b"salt")
|
|
pipeline = AuthPipeline(
|
|
source=EmptySource(),
|
|
protocol=ImmediateProtocol(),
|
|
executor=executor,
|
|
sink=object(),
|
|
ledger=ledger,
|
|
timeout=2,
|
|
)
|
|
source = SourceRecord("stock", "opaque")
|
|
fingerprint = ledger.fingerprint(source.source_id)
|
|
|
|
assert await pipeline.admit(source)
|
|
pipeline.pause()
|
|
await pipeline.rate_gate.rate_limited()
|
|
run = asyncio.create_task(pipeline.run())
|
|
try:
|
|
async with asyncio.timeout(1):
|
|
while pipeline._authorization_task is None:
|
|
await asyncio.sleep(0)
|
|
|
|
external_job_id = ledger.start_fingerprint(fingerprint)
|
|
ledger.finish(
|
|
external_job_id,
|
|
JobStatus.IMPORTED,
|
|
"imported",
|
|
"external-receipt",
|
|
)
|
|
pipeline.resume()
|
|
|
|
async with asyncio.timeout(1):
|
|
while (
|
|
executor.calls == 0
|
|
and pipeline._states.get(fingerprint) is not PipelineState.IMPORTED
|
|
):
|
|
await asyncio.sleep(0)
|
|
|
|
assert executor.calls == 0
|
|
assert pipeline._states[fingerprint] is PipelineState.IMPORTED
|
|
finally:
|
|
pipeline.request_stop()
|
|
await asyncio.wait_for(run, 2)
|
|
|
|
asyncio.run(scenario())
|