mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-07 10:04:38 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc5c56ecc3 | ||
|
|
de62a27fb6 | ||
|
|
438ea9035f | ||
|
|
943e083a89 | ||
|
|
91f9cbec0b |
+46
-6
@@ -1,13 +1,53 @@
|
||||
.env
|
||||
.venv
|
||||
# Git and repository metadata
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
compose.yml
|
||||
compose.testing.yml
|
||||
.todo
|
||||
.github
|
||||
.vscode
|
||||
.DS_Store
|
||||
**/node_modules
|
||||
.todo
|
||||
|
||||
# Local configuration and secrets
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/routstr_secret.key
|
||||
|
||||
# Python environments, caches, and build artifacts
|
||||
**/.venv
|
||||
**/__pycache__
|
||||
**/*.py[cod]
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
**/.coverage
|
||||
**/htmlcov
|
||||
**/*.egg-info
|
||||
**/build
|
||||
**/dist
|
||||
|
||||
# Runtime state must never be baked into the image
|
||||
logs
|
||||
logs.*
|
||||
**/*.log
|
||||
**/.wallet*
|
||||
**/.cashu
|
||||
**/*.db
|
||||
**/*.db-*
|
||||
**/*.sqlite3
|
||||
**/*.sqlite3-*
|
||||
**/keys
|
||||
**/proof_backups
|
||||
**/relay-data
|
||||
|
||||
# The UI output is built by its own service and mounted at runtime
|
||||
ui_out
|
||||
ui/.next
|
||||
ui/out
|
||||
**/node_modules
|
||||
|
||||
# Compose and local development files
|
||||
compose.yml
|
||||
compose.testing.yml
|
||||
compose.override.yml
|
||||
plans
|
||||
.worktrees
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""never reuse upstream provider ids
|
||||
|
||||
Revision ID: f2a7c9d4e8b1
|
||||
Revises: ecfa0d6e2a36
|
||||
Create Date: 2026-08-05 00:40:19.000000
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "f2a7c9d4e8b1"
|
||||
down_revision = "ecfa0d6e2a36"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def _recreate_sqlite_table(*, sqlite_autoincrement: bool) -> None:
|
||||
with op.batch_alter_table(
|
||||
"upstream_providers",
|
||||
recreate="always",
|
||||
table_kwargs={"sqlite_autoincrement": sqlite_autoincrement},
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# A bare SQLite INTEGER PRIMARY KEY can reuse the highest deleted row ID.
|
||||
# AUTOINCREMENT records a durable high-water mark in sqlite_sequence.
|
||||
if op.get_bind().dialect.name == "sqlite":
|
||||
_recreate_sqlite_table(sqlite_autoincrement=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if op.get_bind().dialect.name == "sqlite":
|
||||
_recreate_sqlite_table(sqlite_autoincrement=False)
|
||||
@@ -630,6 +630,7 @@ class UpstreamProviderRow(SQLModel, table=True): # type: ignore
|
||||
UniqueConstraint(
|
||||
"base_url", "api_key", name="uq_upstream_providers_base_url_api_key"
|
||||
),
|
||||
{"sqlite_autoincrement": True},
|
||||
)
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
slug: str | None = Field(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_backend_docker_context_excludes_generated_and_runtime_state() -> None:
|
||||
patterns = {
|
||||
line.strip()
|
||||
for line in (REPO_ROOT / ".dockerignore").read_text().splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
}
|
||||
|
||||
required_patterns = {
|
||||
"**/.env",
|
||||
"**/.env.*",
|
||||
"**/routstr_secret.key",
|
||||
"**/.venv",
|
||||
"**/__pycache__",
|
||||
"**/*.py[cod]",
|
||||
"**/.pytest_cache",
|
||||
"**/.mypy_cache",
|
||||
"**/.ruff_cache",
|
||||
"**/.coverage",
|
||||
"**/htmlcov",
|
||||
"**/*.egg-info",
|
||||
"**/build",
|
||||
"**/dist",
|
||||
"logs",
|
||||
"logs.*",
|
||||
"**/*.log",
|
||||
"**/.wallet*",
|
||||
"**/.cashu",
|
||||
"**/*.db",
|
||||
"**/*.db-*",
|
||||
"**/*.sqlite3",
|
||||
"**/*.sqlite3-*",
|
||||
"**/keys",
|
||||
"**/proof_backups",
|
||||
"**/relay-data",
|
||||
"ui_out",
|
||||
"ui/.next",
|
||||
"ui/out",
|
||||
}
|
||||
|
||||
assert required_patterns <= patterns, (
|
||||
"The backend Docker context must exclude local build artifacts and "
|
||||
f"runtime state; missing patterns: {sorted(required_patterns - patterns)}"
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _run_alembic(root: Path, database_url: str, command: str, revision: str) -> None:
|
||||
env = os.environ.copy()
|
||||
env["DATABASE_URL"] = database_url
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "alembic", command, revision],
|
||||
cwd=root,
|
||||
env=env,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _insert_provider(connection: sqlite3.Connection, slug: str) -> int:
|
||||
cursor = connection.execute(
|
||||
"INSERT INTO upstream_providers "
|
||||
"(slug, provider_type, base_url, api_key, enabled, provider_fee) "
|
||||
"VALUES (?, 'custom', ?, ?, 1, 1.01)",
|
||||
(slug, f"https://{slug}.example.com", f"key-{slug}"),
|
||||
)
|
||||
assert cursor.lastrowid is not None
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def test_provider_ids_are_not_reused_after_deletion(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
database_path = tmp_path / "provider-id-migration.db"
|
||||
database_url = f"sqlite+aiosqlite:///{database_path}"
|
||||
previous_head = "ecfa0d6e2a36"
|
||||
|
||||
_run_alembic(root, database_url, "upgrade", previous_head)
|
||||
|
||||
with sqlite3.connect(database_path) as connection:
|
||||
assert _insert_provider(connection, "first") == 1
|
||||
assert _insert_provider(connection, "second") == 2
|
||||
connection.execute("DELETE FROM upstream_providers WHERE id = 2")
|
||||
assert _insert_provider(connection, "before-migration") == 2
|
||||
connection.commit()
|
||||
|
||||
_run_alembic(root, database_url, "upgrade", "f2a7c9d4e8b1")
|
||||
|
||||
with sqlite3.connect(database_path) as connection:
|
||||
connection.execute("DELETE FROM upstream_providers WHERE id = 2")
|
||||
assert _insert_provider(connection, "after-migration") == 3
|
||||
table_sql = connection.execute(
|
||||
"SELECT sql FROM sqlite_master "
|
||||
"WHERE type = 'table' AND name = 'upstream_providers'"
|
||||
).fetchone()
|
||||
|
||||
assert table_sql is not None
|
||||
assert "PRIMARY KEY AUTOINCREMENT" in table_sql[0]
|
||||
|
||||
|
||||
def test_provider_id_migration_downgrades(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
database_path = tmp_path / "provider-id-downgrade.db"
|
||||
database_url = f"sqlite+aiosqlite:///{database_path}"
|
||||
|
||||
_run_alembic(root, database_url, "upgrade", "f2a7c9d4e8b1")
|
||||
_run_alembic(root, database_url, "downgrade", "ecfa0d6e2a36")
|
||||
|
||||
with sqlite3.connect(database_path) as connection:
|
||||
table_sql = connection.execute(
|
||||
"SELECT sql FROM sqlite_master "
|
||||
"WHERE type = 'table' AND name = 'upstream_providers'"
|
||||
).fetchone()
|
||||
|
||||
assert table_sql is not None
|
||||
assert "AUTOINCREMENT" not in table_sql[0]
|
||||
Reference in New Issue
Block a user