diff --git a/agent_v0/server_v1/agent_registry.py b/agent_v0/server_v1/agent_registry.py index 629d2c326..6903845fa 100644 --- a/agent_v0/server_v1/agent_registry.py +++ b/agent_v0/server_v1/agent_registry.py @@ -111,6 +111,20 @@ class AgentRegistry: "CREATE INDEX IF NOT EXISTS idx_enrolled_agents_machine " "ON enrolled_agents(machine_id)" ) + # WP-C Patch 1 : colonnes « token par poste », migration additive + # idempotente. Inertes tant que l'auth par poste n'est pas branchée + # (patchs WP-C ultérieurs). Voir DETTE-015. + existing_cols = { + row[1] + for row in conn.execute( + "PRAGMA table_info(enrolled_agents)" + ).fetchall() + } + for col in ("token_hash", "token_issued_at"): + if col not in existing_cols: + conn.execute( + f"ALTER TABLE enrolled_agents ADD COLUMN {col} TEXT" + ) # ------------------------------------------------------------------ # Lecture diff --git a/tests/unit/test_wpc_migration.py b/tests/unit/test_wpc_migration.py new file mode 100644 index 000000000..55d9ee7e9 --- /dev/null +++ b/tests/unit/test_wpc_migration.py @@ -0,0 +1,92 @@ +"""WP-C Patch 1 — migration additive idempotente des colonnes « token par poste ». + +Ajoute `token_hash` et `token_issued_at` à la table `enrolled_agents`, sans +casser les bases existantes ni perdre de données. Comportement runtime inchangé : +les colonnes restent inertes tant que l'auth par poste n'est pas branchée +(patchs WP-C ultérieurs). Voir DETTE-015 / PLAN-WPC-TDD-EXECUTABLE. +""" +from __future__ import annotations + +import sqlite3 + +from agent_v0.server_v1.agent_registry import AgentRegistry + + +def _columns(db_path) -> set[str]: + conn = sqlite3.connect(str(db_path)) + try: + rows = conn.execute("PRAGMA table_info(enrolled_agents)").fetchall() + return {r[1] for r in rows} + finally: + conn.close() + + +def test_token_columns_present_after_init(tmp_path): + """Une base neuve doit contenir les colonnes token dès l'init.""" + db = tmp_path / "fleet.db" + AgentRegistry(db_path=db) + cols = _columns(db) + assert "token_hash" in cols + assert "token_issued_at" in cols + + +def test_token_columns_idempotent(tmp_path): + """Ré-initialiser plusieurs fois ne doit jamais lever (migration idempotente).""" + db = tmp_path / "fleet.db" + AgentRegistry(db_path=db) + AgentRegistry(db_path=db) + AgentRegistry(db_path=db) + cols = _columns(db) + assert "token_hash" in cols + assert "token_issued_at" in cols + + +def test_migration_preserves_existing_rows(tmp_path): + """Une base ancienne (sans les colonnes) est migrée sans perte de données.""" + db = tmp_path / "fleet.db" + # Ancien schéma, sans les colonnes token, avec une ligne existante. + conn = sqlite3.connect(str(db)) + conn.execute( + """ + CREATE TABLE enrolled_agents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + machine_id TEXT NOT NULL UNIQUE, + user_name TEXT, + user_email TEXT, + user_id TEXT, + hostname TEXT, + os_info TEXT, + version TEXT, + status TEXT NOT NULL DEFAULT 'active', + enrolled_at TEXT NOT NULL, + last_seen_at TEXT, + uninstalled_at TEXT, + uninstall_reason TEXT + ) + """ + ) + conn.execute( + "INSERT INTO enrolled_agents (machine_id, status, enrolled_at) " + "VALUES ('PC-LEGACY', 'active', '2026-01-01T00:00:00+00:00')" + ) + conn.commit() + conn.close() + + # Le démarrage du registry doit migrer la base existante. + AgentRegistry(db_path=db) + + cols = _columns(db) + assert "token_hash" in cols + assert "token_issued_at" in cols + + conn = sqlite3.connect(str(db)) + try: + row = conn.execute( + "SELECT machine_id, token_hash FROM enrolled_agents " + "WHERE machine_id = 'PC-LEGACY'" + ).fetchone() + finally: + conn.close() + assert row is not None + assert row[0] == "PC-LEGACY" + assert row[1] is None # colonne ajoutée, NULL par défaut