feat(infra): auto-commit con 56 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
---
|
||||
name: metabase_client_from_pass
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def metabase_client_from_pass(pass_key: str, base_url: str, mode: str = 'auto') -> MetabaseClient | dict"
|
||||
description: "Lee las credenciales de Metabase desde pass y devuelve un MetabaseClient autenticado, en una sola llamada. Elimina el patron inline repetido de cargar la credencial del password store y montar el cliente. Soporta dos instancias: API-key (metabase/aurgi-api-key -> header X-API-KEY) y usuario/password (captacion/metabase multi-linea -> login POST /api/session). mode='auto' detecta el formato. Compone pass_get_secret + parse_metabase_secret + metabase_auth/MetabaseClient sin reimplementarlos. Devuelve el cliente o {status:error, error} sin lanzar."
|
||||
tags: [metabase, pass, secret, credential, auth, client]
|
||||
uses_functions: ["pass_get_secret_py_infra", "parse_metabase_secret_py_infra", "metabase_auth_py_infra"]
|
||||
uses_types: []
|
||||
params:
|
||||
- name: pass_key
|
||||
desc: "Ruta del secreto en el password store (p.ej. 'metabase/aurgi-api-key' o 'captacion/metabase')."
|
||||
- name: base_url
|
||||
desc: "URL base de la instancia Metabase (p.ej. 'https://reports.autingo.es' o 'http://localhost:3030')."
|
||||
- name: mode
|
||||
desc: "'api_key', 'session' o 'auto' (default). En auto se detecta el formato del secreto: una sola linea de clave -> api_key; multi-linea con email/usuario -> session."
|
||||
output: "MetabaseClient autenticado en exito. En fallo (sin lanzar): {status:'error', error:str} para secreto inexistente en pass, formato no parseable, o fallo de autenticacion contra Metabase."
|
||||
returns: []
|
||||
returns_optional: true
|
||||
error_type: "error_go_core"
|
||||
imports: [subprocess, httpx]
|
||||
tested: true
|
||||
tests: ["test_api_key_builds_client_with_x_api_key", "test_session_secret_parsed_and_auth_called", "test_auto_mode_detects_session", "test_missing_secret_returns_error_dict", "test_session_without_email_returns_error_dict"]
|
||||
test_file_path: "python/functions/metabase/metabase_client_from_pass_test.py"
|
||||
file_path: "python/functions/metabase/metabase_client_from_pass.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from metabase.metabase_client_from_pass import metabase_client_from_pass
|
||||
|
||||
# Aurgi (API-key en pass, header X-API-KEY):
|
||||
client = metabase_client_from_pass(
|
||||
"metabase/aurgi-api-key", "https://reports.autingo.es", mode="api_key")
|
||||
# client.request("GET", "/api/user/current")
|
||||
|
||||
# Captacion (usuario/password multi-linea en pass, login /api/session):
|
||||
client = metabase_client_from_pass(
|
||||
"captacion/metabase", "http://localhost:3030", mode="session")
|
||||
|
||||
# Sin especificar mode: se autodetecta por el formato del secreto.
|
||||
client = metabase_client_from_pass("metabase/aurgi-api-key", "https://reports.autingo.es")
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites un `MetabaseClient` autenticado y la credencial vive en `pass`:
|
||||
en vez de escribir a mano el `pass show ...` + parseo + `metabase_auth` /
|
||||
`MetabaseClient(...)`, llama a esta funcion con la ruta del secreto y la URL.
|
||||
Cubre tanto instancias con API-key (Aurgi) como con usuario/password
|
||||
(captacion). Es el punto de entrada unico para abrir un cliente desde el
|
||||
password store.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura**: lanza el subproceso `pass show` y abre conexion HTTP a Metabase.
|
||||
El secreto nunca se logea, pero el `MetabaseClient` retornado lleva el token en
|
||||
memoria.
|
||||
- En `mode='session'` el secreto debe tener una linea de usuario con prefijo
|
||||
`email:` / `login:` / `username:` / `user:`; si falta, devuelve error dict (no
|
||||
lanza).
|
||||
- La deteccion de API-key se basa en que la clave empieza por `mb_` (lo gestiona
|
||||
`MetabaseClient`). Una API-key con otro prefijo se enviaria como session token
|
||||
-> usa `mode='api_key'` explicito si tu key no empieza por `mb_`.
|
||||
- Cierra el cliente cuando termines: `client.close()` o usalo como context
|
||||
manager (`with metabase_client_from_pass(...) as client:`).
|
||||
- Los fallos de auth (401, instancia caida) se devuelven como
|
||||
`{status:'error', error}` — comprueba el tipo del retorno antes de usarlo.
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Construye un MetabaseClient autenticado leyendo credenciales desde `pass`.
|
||||
|
||||
Elimina el patron inline repetido de "leer la credencial de Metabase del
|
||||
password store y montar un cliente autenticado", que hoy se reescribe a mano
|
||||
para dos instancias distintas:
|
||||
|
||||
- Aurgi: API-key (``metabase/aurgi-api-key``, una sola linea ``mb_...``) ->
|
||||
header ``X-API-KEY``.
|
||||
- Captacion: usuario/password (``captacion/metabase``, multi-linea: primera
|
||||
linea password, linea ``email:`` con el usuario) -> login via
|
||||
``POST /api/session``.
|
||||
|
||||
Compone tres funciones del registry: ``pass_get_secret`` (lee el secreto),
|
||||
``parse_metabase_secret`` (parser puro que distingue api_key vs session) y
|
||||
``metabase_auth`` / ``MetabaseClient`` (auth). No reimplementa ninguna.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from infra.pass_get_secret import pass_get_secret
|
||||
from metabase.parse_metabase_secret import parse_metabase_secret
|
||||
from metabase.client import MetabaseClient, metabase_auth
|
||||
|
||||
# Tope de lineas a leer del secreto. pass_get_secret devuelve una linea por
|
||||
# llamada; leemos hasta este maximo o hasta "out of range" para reconstruir el
|
||||
# texto completo sin reimplementar el subproceso de pass.
|
||||
_MAX_SECRET_LINES = 16
|
||||
|
||||
|
||||
def metabase_client_from_pass(
|
||||
pass_key: str,
|
||||
base_url: str,
|
||||
mode: str = "auto",
|
||||
) -> MetabaseClient | dict:
|
||||
"""Lee credenciales de Metabase de `pass` y devuelve un cliente autenticado.
|
||||
|
||||
Args:
|
||||
pass_key: ruta del secreto en el password store (p.ej.
|
||||
``"metabase/aurgi-api-key"`` o ``"captacion/metabase"``).
|
||||
base_url: URL base de la instancia Metabase (p.ej.
|
||||
``"https://reports.autingo.es"``).
|
||||
mode: ``"api_key"``, ``"session"`` o ``"auto"`` (default). En ``auto`` se
|
||||
detecta el formato del secreto: una sola linea de clave -> api_key;
|
||||
multi-linea con email/usuario -> session.
|
||||
|
||||
Returns:
|
||||
``MetabaseClient`` autenticado en exito. En caso de fallo (sin lanzar):
|
||||
``{"status": "error", "error": str}`` para: secreto no encontrado en
|
||||
pass, formato no parseable, o fallo de autenticacion contra Metabase.
|
||||
|
||||
Example:
|
||||
>>> client = metabase_client_from_pass(
|
||||
... "metabase/aurgi-api-key", "https://reports.autingo.es", mode="api_key")
|
||||
>>> client.request("GET", "/api/user/current") # doctest: +SKIP
|
||||
"""
|
||||
secret_text = _read_secret_text(pass_key)
|
||||
if isinstance(secret_text, dict):
|
||||
return secret_text # error dict de pass
|
||||
|
||||
parsed = parse_metabase_secret(secret_text, mode=mode)
|
||||
if parsed["status"] != "ok":
|
||||
return {"status": "error", "error": parsed["error"]}
|
||||
|
||||
try:
|
||||
if parsed["mode"] == "api_key":
|
||||
# MetabaseClient detecta "mb_" -> usa header X-API-KEY.
|
||||
return MetabaseClient(base_url, parsed["api_key"])
|
||||
# mode == "session": login con email/password via POST /api/session.
|
||||
return metabase_auth(base_url, parsed["email"], parsed["password"])
|
||||
except Exception as exc: # noqa: BLE001 - cualquier fallo de red/auth se reporta
|
||||
return {"status": "error", "error": f"metabase auth failed: {exc}"}
|
||||
|
||||
|
||||
def _read_secret_text(pass_key: str) -> str | dict:
|
||||
"""Reconstruye el texto multi-linea del secreto via pass_get_secret.
|
||||
|
||||
Llama a pass_get_secret linea por linea (1-indexed) hasta agotar las lineas
|
||||
("line N out of range") o llegar al tope. Devuelve el texto unido por
|
||||
``\\n`` o un dict de error si la primera lectura falla (pass no instalado,
|
||||
entry inexistente, etc.).
|
||||
"""
|
||||
first = pass_get_secret(pass_key, line=1)
|
||||
if first["status"] != "ok":
|
||||
return {"status": "error", "error": first["error"]}
|
||||
|
||||
lines = [first["value"]]
|
||||
for n in range(2, _MAX_SECRET_LINES + 1):
|
||||
res = pass_get_secret(pass_key, line=n)
|
||||
if res["status"] != "ok":
|
||||
break # out of range = fin del secreto
|
||||
lines.append(res["value"])
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Tests para metabase_client_from_pass (pass mockeado, sin red real)."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
import metabase.metabase_client_from_pass as mod
|
||||
from metabase.client import MetabaseClient
|
||||
|
||||
|
||||
def _fake_pass(store: dict):
|
||||
"""Devuelve un pass_get_secret falso que lee de un dict {key: [lineas]}."""
|
||||
|
||||
def _impl(path, *, line=1, timeout_s=10.0):
|
||||
lines = store.get(path)
|
||||
if lines is None:
|
||||
return {"status": "error", "error": f"{path} is not in the password store"}
|
||||
if line < 1 or line > len(lines):
|
||||
return {"status": "error", "error": f"line {line} out of range"}
|
||||
return {"status": "ok", "value": lines[line - 1]}
|
||||
|
||||
return _impl
|
||||
|
||||
|
||||
def test_api_key_builds_client_with_x_api_key(monkeypatch):
|
||||
store = {"metabase/aurgi-api-key": ["mb_fakekey1234567890"]}
|
||||
monkeypatch.setattr(mod, "pass_get_secret", _fake_pass(store))
|
||||
|
||||
client = mod.metabase_client_from_pass(
|
||||
"metabase/aurgi-api-key", "https://reports.example.test", mode="api_key"
|
||||
)
|
||||
assert isinstance(client, MetabaseClient)
|
||||
assert client.token == "mb_fakekey1234567890"
|
||||
# API key -> header X-API-KEY (no toca red).
|
||||
assert client._http.headers.get("X-API-KEY") == "mb_fakekey1234567890"
|
||||
client.close()
|
||||
|
||||
|
||||
def test_session_secret_parsed_and_auth_called(monkeypatch):
|
||||
# Secreto multi-linea estilo captacion/metabase.
|
||||
store = {
|
||||
"captacion/metabase": [
|
||||
"hunter2pass",
|
||||
"email: admin@captacion.local",
|
||||
"url: http://localhost:3030",
|
||||
]
|
||||
}
|
||||
monkeypatch.setattr(mod, "pass_get_secret", _fake_pass(store))
|
||||
|
||||
captured = {}
|
||||
|
||||
def _fake_auth(base_url, email, password):
|
||||
captured["base_url"] = base_url
|
||||
captured["email"] = email
|
||||
captured["password"] = password
|
||||
return MetabaseClient(base_url, "session_token_abc")
|
||||
|
||||
monkeypatch.setattr(mod, "metabase_auth", _fake_auth)
|
||||
|
||||
client = mod.metabase_client_from_pass(
|
||||
"captacion/metabase", "http://localhost:3030", mode="session"
|
||||
)
|
||||
assert isinstance(client, MetabaseClient)
|
||||
assert captured["email"] == "admin@captacion.local"
|
||||
assert captured["password"] == "hunter2pass"
|
||||
assert captured["base_url"] == "http://localhost:3030"
|
||||
client.close()
|
||||
|
||||
|
||||
def test_auto_mode_detects_session(monkeypatch):
|
||||
store = {"x/mb": ["pw", "login: bob@host.io"]}
|
||||
monkeypatch.setattr(mod, "pass_get_secret", _fake_pass(store))
|
||||
monkeypatch.setattr(
|
||||
mod, "metabase_auth", lambda b, e, p: MetabaseClient(b, "tok")
|
||||
)
|
||||
client = mod.metabase_client_from_pass("x/mb", "http://h", mode="auto")
|
||||
assert isinstance(client, MetabaseClient)
|
||||
client.close()
|
||||
|
||||
|
||||
def test_missing_secret_returns_error_dict(monkeypatch):
|
||||
monkeypatch.setattr(mod, "pass_get_secret", _fake_pass({}))
|
||||
res = mod.metabase_client_from_pass("does/not/exist", "http://h")
|
||||
assert isinstance(res, dict)
|
||||
assert res["status"] == "error"
|
||||
|
||||
|
||||
def test_session_without_email_returns_error_dict(monkeypatch):
|
||||
store = {"x/mb": ["onlypassword", "url: http://h"]}
|
||||
monkeypatch.setattr(mod, "pass_get_secret", _fake_pass(store))
|
||||
res = mod.metabase_client_from_pass("x/mb", "http://h", mode="session")
|
||||
assert isinstance(res, dict)
|
||||
assert res["status"] == "error"
|
||||
assert "email" in res["error"]
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
name: parse_metabase_secret
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def parse_metabase_secret(secret_text: str, mode: str = 'auto') -> dict"
|
||||
description: "Parser puro que extrae credenciales de Metabase del texto crudo de un secreto de pass. Distingue API-key (una sola linea, p.ej. mb_... de metabase/aurgi-api-key) de sesion (multi-linea estilo captacion/metabase: primera linea password, linea email:/user:/login:/username: con el usuario). mode='auto' detecta el formato; mode='api_key' o 'session' fuerzan. No hace I/O. Devuelve dict {status, mode, api_key} o {status, mode, email, password} o {status:error, error}. Nunca lanza ni logea el secreto."
|
||||
tags: [metabase, pass, secret, credential, parse, pure]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
params:
|
||||
- name: secret_text
|
||||
desc: "Contenido completo del secreto leido de pass (varias lineas separadas por \\n). Primera linea = password/clave por convencion de pass; lineas siguientes = metadata."
|
||||
- name: mode
|
||||
desc: "'api_key', 'session' o 'auto' (default). En auto: si hay linea email/usuario reconocible -> session; si no -> api_key."
|
||||
output: "Dict. api_key: {status:'ok', mode:'api_key', api_key:str}. session: {status:'ok', mode:'session', email:str, password:str}. error: {status:'error', error:str} para texto vacio, modo invalido o session sin email/password."
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["test_api_key_explicit", "test_session_multiline_email_prefix", "test_auto_detects_session_when_email_present", "test_auto_detects_api_key_single_line", "test_session_without_email_line_errors", "test_empty_secret_errors", "test_invalid_mode_errors", "test_user_prefix_variant", "test_email_value_preserves_case"]
|
||||
test_file_path: "python/functions/metabase/parse_metabase_secret_test.py"
|
||||
file_path: "python/functions/metabase/parse_metabase_secret.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from metabase.parse_metabase_secret import parse_metabase_secret
|
||||
|
||||
# API-key (una sola linea):
|
||||
parse_metabase_secret("mb_abc123")
|
||||
# {'status': 'ok', 'mode': 'api_key', 'api_key': 'mb_abc123'}
|
||||
|
||||
# Sesion (multi-linea estilo captacion/metabase):
|
||||
parse_metabase_secret("hunter2\nemail: a@b.com\nurl: http://x")
|
||||
# {'status': 'ok', 'mode': 'session', 'email': 'a@b.com', 'password': 'hunter2'}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando ya tienes el texto de un secreto de Metabase (leido de `pass` u otra
|
||||
fuente) y necesitas separarlo en credenciales utilizables sin tocar disco ni
|
||||
red. Es el nucleo puro y testeable de `metabase_client_from_pass`: separa el
|
||||
parseo (determinista) de la lectura del secreto y la autenticacion (impuras).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- La linea del email/usuario se identifica por prefijo: `email:`, `login:`,
|
||||
`username:` o `user:` (case-insensitive). Otros formatos no se detectan como
|
||||
sesion y `auto` los tratara como api_key.
|
||||
- Funcion pura: NO lee `pass` ni llama a Metabase. El caller le pasa el texto ya
|
||||
resuelto. No logea el secreto, pero el dict de retorno SI lleva el valor en
|
||||
claro — el caller debe tratarlo como sensible.
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Parsea el texto de un secreto de `pass` para credenciales de Metabase.
|
||||
|
||||
Distingue dos formatos sin tocar disco ni red (funcion pura):
|
||||
|
||||
- API-key: una sola linea con la clave (las API keys de Metabase empiezan por
|
||||
``mb_``, p.ej. el secreto ``metabase/aurgi-api-key``).
|
||||
- Sesion: multi-linea estilo ``captacion/metabase`` — la primera linea es la
|
||||
contrasena y una linea posterior lleva el email/usuario con un prefijo
|
||||
reconocible (``email:``, ``user:``, ``login:`` o ``username:``).
|
||||
|
||||
El caller decide el ``mode`` y este parser solo extrae los campos del texto.
|
||||
"""
|
||||
|
||||
# Prefijos (case-insensitive) que identifican la linea del email/usuario en un
|
||||
# secreto multi-linea de pass. Se prueban en este orden.
|
||||
_EMAIL_PREFIXES = ("email:", "login:", "username:", "user:")
|
||||
|
||||
|
||||
def parse_metabase_secret(secret_text: str, mode: str = "auto") -> dict:
|
||||
"""Extrae credenciales de Metabase del texto crudo de un secreto de pass.
|
||||
|
||||
No ejecuta `pass` ni hace I/O: recibe el texto ya leido y lo interpreta.
|
||||
Funcion pura y determinista, apta para tests unitarios.
|
||||
|
||||
Args:
|
||||
secret_text: contenido completo del secreto (varias lineas separadas por
|
||||
``\\n``). Por convencion de pass la primera linea es la
|
||||
contrasena/clave; las siguientes son metadata.
|
||||
mode: ``"api_key"``, ``"session"`` o ``"auto"`` (default). En ``auto`` se
|
||||
detecta el formato: si hay una linea de email/usuario reconocible se
|
||||
asume sesion; si no, se asume api_key (una sola linea de clave).
|
||||
|
||||
Returns:
|
||||
Dict. Nunca lanza:
|
||||
|
||||
- api_key -> ``{"status": "ok", "mode": "api_key", "api_key": str}``
|
||||
- session -> ``{"status": "ok", "mode": "session", "email": str,
|
||||
"password": str}``
|
||||
- error -> ``{"status": "error", "error": str}`` para texto vacio, modo
|
||||
invalido, o session sin email/password localizables.
|
||||
|
||||
Example:
|
||||
>>> parse_metabase_secret("mb_abc123")
|
||||
{'status': 'ok', 'mode': 'api_key', 'api_key': 'mb_abc123'}
|
||||
>>> parse_metabase_secret("hunter2\\nemail: a@b.com\\nurl: http://x")
|
||||
{'status': 'ok', 'mode': 'session', 'email': 'a@b.com', 'password': 'hunter2'}
|
||||
"""
|
||||
if mode not in ("api_key", "session", "auto"):
|
||||
return {"status": "error", "error": f"invalid mode {mode!r}"}
|
||||
|
||||
lines = secret_text.splitlines()
|
||||
if not lines or not lines[0].strip():
|
||||
return {"status": "error", "error": "empty secret"}
|
||||
|
||||
email = _find_email(lines)
|
||||
|
||||
if mode == "auto":
|
||||
mode = "session" if email is not None else "api_key"
|
||||
|
||||
if mode == "api_key":
|
||||
return {
|
||||
"status": "ok",
|
||||
"mode": "api_key",
|
||||
"api_key": lines[0].strip(),
|
||||
}
|
||||
|
||||
# mode == "session"
|
||||
if email is None:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": (
|
||||
"session secret without email/user line "
|
||||
f"(expected one of {', '.join(_EMAIL_PREFIXES)})"
|
||||
),
|
||||
}
|
||||
password = lines[0].strip()
|
||||
if not password:
|
||||
return {"status": "error", "error": "session secret without password"}
|
||||
return {
|
||||
"status": "ok",
|
||||
"mode": "session",
|
||||
"email": email,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
|
||||
def _find_email(lines: list[str]) -> str | None:
|
||||
"""Devuelve el email/usuario de la primera linea con prefijo reconocido."""
|
||||
for raw in lines[1:]:
|
||||
low = raw.strip().lower()
|
||||
for prefix in _EMAIL_PREFIXES:
|
||||
if low.startswith(prefix):
|
||||
# Conserva el valor original (no el lowercased) tras el prefijo.
|
||||
value = raw.strip()[len(prefix):].strip()
|
||||
if value:
|
||||
return value
|
||||
return None
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Tests para parse_metabase_secret."""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from metabase.parse_metabase_secret import parse_metabase_secret
|
||||
|
||||
|
||||
def test_api_key_explicit():
|
||||
res = parse_metabase_secret("mb_abc123def", mode="api_key")
|
||||
assert res == {"status": "ok", "mode": "api_key", "api_key": "mb_abc123def"}
|
||||
|
||||
|
||||
def test_session_multiline_email_prefix():
|
||||
secret = "hunter2\nemail: admin@captacion.local\nurl: http://localhost:3030"
|
||||
res = parse_metabase_secret(secret, mode="session")
|
||||
assert res == {
|
||||
"status": "ok",
|
||||
"mode": "session",
|
||||
"email": "admin@captacion.local",
|
||||
"password": "hunter2",
|
||||
}
|
||||
|
||||
|
||||
def test_auto_detects_session_when_email_present():
|
||||
secret = "secretpass\nlogin: bob@example.com"
|
||||
res = parse_metabase_secret(secret, mode="auto")
|
||||
assert res["mode"] == "session"
|
||||
assert res["email"] == "bob@example.com"
|
||||
assert res["password"] == "secretpass"
|
||||
|
||||
|
||||
def test_auto_detects_api_key_single_line():
|
||||
res = parse_metabase_secret("mb_singleLineKey", mode="auto")
|
||||
assert res["mode"] == "api_key"
|
||||
assert res["api_key"] == "mb_singleLineKey"
|
||||
|
||||
|
||||
def test_session_without_email_line_errors():
|
||||
res = parse_metabase_secret("onlypassword\nurl: http://x", mode="session")
|
||||
assert res["status"] == "error"
|
||||
assert "email" in res["error"]
|
||||
|
||||
|
||||
def test_empty_secret_errors():
|
||||
res = parse_metabase_secret("", mode="auto")
|
||||
assert res["status"] == "error"
|
||||
assert res["error"] == "empty secret"
|
||||
|
||||
|
||||
def test_invalid_mode_errors():
|
||||
res = parse_metabase_secret("mb_x", mode="bogus")
|
||||
assert res["status"] == "error"
|
||||
assert "invalid mode" in res["error"]
|
||||
|
||||
|
||||
def test_user_prefix_variant():
|
||||
secret = "pw\nuser: someone@host.io"
|
||||
res = parse_metabase_secret(secret, mode="auto")
|
||||
assert res["email"] == "someone@host.io"
|
||||
|
||||
|
||||
def test_email_value_preserves_case():
|
||||
secret = "pw\nEMAIL: MixedCase@Host.COM"
|
||||
res = parse_metabase_secret(secret, mode="session")
|
||||
assert res["email"] == "MixedCase@Host.COM"
|
||||
Reference in New Issue
Block a user