Fix Zotero table models for Zotero 9:
- Remove stale Annotations/Highlights/Transaction* models
- Add ItemAnnotations, RetractedItems, DeletedCollections,
DeletedSearches, DbDebug1
- Fix ItemAttachments, Libraries, Users column mismatches
New test files covering all major modules:
- cli/{bib,prisma,rec,zot,mail,run} deep exercising tests
- mail/{droplet,postmark,resend,cloudflare} lifecycle tests
- bib/{iom,oig,pincite,sync,regulations_gov,email_ingest,format,store}
- prisma/{vpn,fetch,export,llm,screen,eligibility,extract,project,ingest,flow}
- aco/lake/{unity,quality,deploy} + api/aco coverage gaps
- zot/{ops,db,extract,duck} + rec/{report,engine,base,pricers}
- pfs/{pipe,rules,eq,files}
Add pytest-xdist for parallel test execution.
Tracks #353
115 lines
3.3 KiB
Python
115 lines
3.3 KiB
Python
"""Exercise mail/droplet.py lifecycle functions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from mail.droplet import (
|
|
HOSTNAME,
|
|
_ssh_args,
|
|
_wait_for_active,
|
|
apply_dns,
|
|
attach_smarthost,
|
|
down,
|
|
rotate_creds,
|
|
status,
|
|
up,
|
|
)
|
|
|
|
|
|
class TestSshArgs:
|
|
@patch.dict("os.environ", {"STACK_SSH_KEY": "/path/to/key"})
|
|
def test_with_override(self):
|
|
assert _ssh_args() == ["-i", "/path/to/key"]
|
|
|
|
@patch.dict("os.environ", {}, clear=True)
|
|
def test_without_override(self):
|
|
assert _ssh_args() == []
|
|
|
|
|
|
class TestWaitForActive:
|
|
def test_returns_ip(self):
|
|
client = MagicMock()
|
|
client.droplets.get.return_value = {
|
|
"droplet": {
|
|
"status": "active",
|
|
"networks": {"v4": [{"ip_address": "1.2.3.4", "type": "public"}]},
|
|
}
|
|
}
|
|
ip = _wait_for_active(client, 123, timeout=5)
|
|
assert ip == "1.2.3.4"
|
|
|
|
def test_timeout(self):
|
|
client = MagicMock()
|
|
client.droplets.get.return_value = {
|
|
"droplet": {"status": "new", "networks": {"v4": []}}
|
|
}
|
|
with pytest.raises(RuntimeError, match="never went active"):
|
|
_wait_for_active(client, 123, timeout=1)
|
|
|
|
|
|
class TestUp:
|
|
@patch("mail.droplet._do_client")
|
|
def test_adopts_existing(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {
|
|
"droplets": [
|
|
{
|
|
"id": 1,
|
|
"name": HOSTNAME,
|
|
"region": {"slug": "nyc3"},
|
|
"status": "active",
|
|
"created_at": "2026-01-01",
|
|
"networks": {"v4": [{"ip_address": "5.6.7.8", "type": "public"}]},
|
|
}
|
|
]
|
|
}
|
|
with patch("mail.droplet._save_json"):
|
|
result = up()
|
|
assert result["id"] == 1
|
|
|
|
|
|
class TestDown:
|
|
@patch("mail.droplet._do_client")
|
|
def test_noop_when_none(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {"droplets": []}
|
|
down(confirm=True) # should not raise
|
|
|
|
|
|
class TestStatus:
|
|
@patch("mail.droplet._do_client")
|
|
def test_returns_none(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {"droplets": []}
|
|
assert status() is None
|
|
|
|
|
|
class TestApplyDns:
|
|
@patch("mail.droplet._do_client")
|
|
def test_raises_no_droplet(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {"droplets": []}
|
|
with pytest.raises(RuntimeError, match="no mail droplet"):
|
|
apply_dns()
|
|
|
|
|
|
class TestRotateCreds:
|
|
@patch("mail.droplet._do_client")
|
|
def test_raises_no_droplet(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {"droplets": []}
|
|
with pytest.raises(RuntimeError, match="no mail droplet"):
|
|
rotate_creds("git")
|
|
|
|
|
|
class TestAttachSmarthost:
|
|
@patch("mail.droplet._do_client")
|
|
def test_raises_no_droplet(self, mock_client):
|
|
client = mock_client.return_value
|
|
client.droplets.list.return_value = {"droplets": []}
|
|
with pytest.raises(RuntimeError, match="no mail droplet"):
|
|
attach_smarthost()
|