feat: add backup.retain_days config and local docker-compose for integration tests

- Add [backup] retain_days to config (default 1); --days becomes optional
- Add scripts/docker-compose.yml (azurite) for local integration testing
- Integration tests now use assets/local.toml and localhost endpoints
- Update .gitignore and CLAUDE.md accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 13:27:24 +08:00
parent 673ad95bcb
commit bc119ff2ae
10 changed files with 38 additions and 7 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ backups/
scan/
dist-source/
.worktrees/
scripts/azurite/

View File

@@ -29,7 +29,10 @@ uv run pytest tests/ -v -m "not integration"
# Run a single test file
uv run pytest tests/test_backup.py -v
# Run integration tests (requires Azure Storage emulator at 192.168.9.101)
# Start Azure Storage emulator (required for integration tests)
docker compose -f scripts/docker-compose.yml up -d
# Run integration tests (requires emulator running via docker compose above)
uv run pytest tests/test_integration.py -v
# Seed test data into the emulator
@@ -72,4 +75,4 @@ Override config path with `order-bak --config /path/to/config.toml <command>`.
### Integration Tests
The integration test (`tests/test_integration.py`) hits a real Azure Storage emulator at `192.168.9.101:10002`. Override the endpoint via `ORDER_BAK_TEST_CONNECTION_STRING` env var. Tests are gated by the `integration` pytest marker.
Integration tests (`tests/test_integration.py`) require the Azure Storage emulator running locally via `scripts/docker-compose.yml` (azurite on `localhost:10002`). Start it with `docker compose -f scripts/docker-compose.yml up -d` before running. Tests are gated by the `integration` pytest marker. Override the endpoint via `ORDER_BAK_TEST_CONNECTION_STRING` env var if needed.

View File

@@ -9,6 +9,9 @@ connection_string = ""
# delay_ms = 200
# scan_dir = "./scan"
[backup]
# retain_days = 1
[delete]
# batch_size = 100
# delay_ms = 300

View File

@@ -7,6 +7,9 @@ page_size = 500
delay_ms = 200
scan_dir = "./scan"
[backup]
retain_days = 1
[delete]
batch_size = 100
delay_ms = 300

View File

@@ -7,6 +7,9 @@ page_size = 500
delay_ms = 200
scan_dir = "./scan"
[backup]
retain_days = 1
[delete]
batch_size = 100
delay_ms = 300

View File

@@ -7,6 +7,9 @@ page_size = 500
delay_ms = 200
scan_dir = "./scan"
[backup]
retain_days = 1
[delete]
batch_size = 100
delay_ms = 300

View File

@@ -0,0 +1,9 @@
services:
azure:
image: mcr.microsoft.com/azure-storage/azurite
ports:
- 10000:10000
- 10001:10001
- 10002:10002
volumes:
- ./azurite:/data

View File

@@ -31,7 +31,7 @@ def scan(ctx, count):
@main.command()
@click.option("--days", required=True, type=int, help="Backup orders older than N days (min 1)")
@click.option("--days", default=None, type=int, help="Backup orders older than N days (min 1); default from config backup.retain_days")
@click.option("--scan", "scan_path", default=None, type=click.Path(), help="Path to scan CSV (default: latest in scan_dir)")
@click.pass_context
def backup(ctx, days, scan_path):
@@ -42,6 +42,9 @@ def backup(ctx, days, scan_path):
cfg = load_config(ctx.obj["config_path"])
from order_bak.backup import backup_orders
if days is None:
days = cfg.backup_retain_days
if scan_path:
scan_file = Path(scan_path)
else:

View File

@@ -12,6 +12,7 @@ class Config:
scan_dir: Path = Path("./scan")
delete_batch_size: int = 100
delete_delay_ms: int = 300
backup_retain_days: int = 1
def load_config(config_path: Path) -> Config:
@@ -30,6 +31,7 @@ def load_config(config_path: Path) -> Config:
scan = raw.get("scan", {})
delete = raw.get("delete", {})
backup = raw.get("backup", {})
return Config(
azure_connection_string=connection_string,
@@ -38,4 +40,5 @@ def load_config(config_path: Path) -> Config:
scan_dir=Path(scan.get("scan_dir", "./scan")),
delete_batch_size=delete.get("batch_size", 100),
delete_delay_ms=delete.get("delay_ms", 300),
backup_retain_days=backup.get("retain_days", 1),
)

View File

@@ -14,7 +14,7 @@ from order_bak.csv_utils import read_csv
from order_bak.delete import delete_orders
from order_bak.scan import scan_orders
CONFIG_PATH = Path(__file__).parent.parent / "assets" / "s101.toml"
CONFIG_PATH = Path(__file__).parent.parent / "assets" / "local.toml"
CONNECTION_STRING = os.environ.get(
"ORDER_BAK_TEST_CONNECTION_STRING",
@@ -23,9 +23,9 @@ CONNECTION_STRING = os.environ.get(
"AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/"
"K1SZFPTOtr/KBHBeksoGMGw==;"
"DefaultEndpointsProtocol=http;"
"BlobEndpoint=http://192.168.9.101:10000/devstoreaccount1;"
"QueueEndpoint=http://192.168.9.101:10001/devstoreaccount1;"
"TableEndpoint=http://192.168.9.101:10002/devstoreaccount1;"
"BlobEndpoint=http://localhost:10000/devstoreaccount1;"
"QueueEndpoint=http://localhost:10001/devstoreaccount1;"
"TableEndpoint=http://localhost:10002/devstoreaccount1;"
),
)