"""Tests para metabase_validate_card_payload y metabase_validate_dashboard_payload.""" import sys sys.path.insert(0, "/home/lucas/fn_registry/python/functions") from metabase.validation import ( metabase_validate_card_payload, metabase_validate_dashboard_payload, ) # --------------------------------------------------------------------------- # metabase_validate_card_payload # --------------------------------------------------------------------------- def _base_card() -> dict: return { "name": "Revenue by Month", "display": "line", "dataset_query": { "database": 1, "type": "native", "native": {"query": "SELECT 1"}, }, } def test_card_valido_retorna_lista_vacia(): issues = metabase_validate_card_payload(_base_card()) assert issues == [], f"Se esperaba lista vacia, got: {issues}" def test_card_display_invalido(): payload = _base_card() payload["display"] = "foobar" issues = metabase_validate_card_payload(payload) assert any("display" in i and "foobar" in i for i in issues), issues def test_card_display_ausente(): payload = _base_card() del payload["display"] issues = metabase_validate_card_payload(payload) assert any("display" in i and "ausente" in i for i in issues), issues def test_card_name_ausente(): payload = _base_card() del payload["name"] issues = metabase_validate_card_payload(payload) assert any("name" in i and "ausente" in i for i in issues), issues def test_card_name_vacio(): payload = _base_card() payload["name"] = " " issues = metabase_validate_card_payload(payload) assert any("name" in i for i in issues), issues def test_card_dataset_query_ausente(): payload = _base_card() del payload["dataset_query"] issues = metabase_validate_card_payload(payload) assert any("dataset_query" in i and "ausente" in i for i in issues), issues def test_card_dataset_query_sin_database(): payload = _base_card() del payload["dataset_query"]["database"] issues = metabase_validate_card_payload(payload) assert any("database" in i for i in issues), issues def test_card_nativa_sin_sql(): payload = _base_card() payload["dataset_query"]["native"]["query"] = "" issues = metabase_validate_card_payload(payload) assert any("SQL" in i or "native" in i for i in issues), issues def test_card_nativa_mbql5(): """Acepta SQL en stages[0].native (formato MBQL5).""" payload = { "name": "MBQL5 card", "display": "table", "dataset_query": { "database": 2, "stages": [{"native": "SELECT 1"}], }, } issues = metabase_validate_card_payload(payload) assert issues == [], f"Se esperaba lista vacia, got: {issues}" def test_card_type_invalido(): payload = _base_card() payload["type"] = "unknown_type" issues = metabase_validate_card_payload(payload) assert any("type" in i and "unknown_type" in i for i in issues), issues def test_card_type_valido(): for t in ("question", "model", "metric"): payload = _base_card() payload["type"] = t issues = metabase_validate_card_payload(payload) assert issues == [], f"type={t!r} no deberia dar issues: {issues}" def test_card_visualization_settings_no_dict(): payload = _base_card() payload["visualization_settings"] = "no es un dict" issues = metabase_validate_card_payload(payload) assert any("visualization_settings" in i for i in issues), issues def test_card_parameters_no_list(): payload = _base_card() payload["parameters"] = {"not": "a list"} issues = metabase_validate_card_payload(payload) assert any("parameters" in i for i in issues), issues def test_card_archived_no_bool(): payload = _base_card() payload["archived"] = "yes" issues = metabase_validate_card_payload(payload) assert any("archived" in i for i in issues), issues def test_card_acumula_multiples_errores(): """Un payload con varios errores debe retornar todos los issues, no solo el primero.""" payload = { "display": "invalid_display", "dataset_query": "not a dict", "archived": "yes", } issues = metabase_validate_card_payload(payload) assert len(issues) >= 3, f"Se esperaban >= 3 issues, got {len(issues)}: {issues}" # --------------------------------------------------------------------------- # metabase_validate_dashboard_payload # --------------------------------------------------------------------------- def _base_dashboard() -> dict: return {"name": "My Dashboard"} def test_dashboard_valido_sin_dashcards(): issues = metabase_validate_dashboard_payload(_base_dashboard(), known_card_ids=set()) assert issues == [], issues def test_dashboard_valido_con_dashcards(): payload = { "name": "KPIs", "dashcards": [ {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4}, {"card_id": 2, "row": 0, "col": 6, "size_x": 6, "size_y": 4}, ], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2}) assert issues == [], issues def test_dashboard_card_id_desconocido(): payload = { "name": "Dashboard", "dashcards": [{"card_id": 999, "row": 0, "col": 0, "size_x": 6, "size_y": 4}], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2}) assert any("999" in i for i in issues), issues def test_dashboard_card_virtual_null_permitido(): """card_id null = dashcard virtual (texto/heading), siempre permitido.""" payload = { "name": "Dashboard", "dashcards": [{"card_id": None, "row": 0, "col": 0, "size_x": 6, "size_y": 2}], } issues = metabase_validate_dashboard_payload(payload, known_card_ids=set()) assert issues == [], issues def test_dashboard_dashcards_solapadas(): payload = { "name": "Dashboard", "dashcards": [ {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4}, {"card_id": 2, "row": 2, "col": 2, "size_x": 4, "size_y": 4}, ], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2}) assert any("solapan" in i for i in issues), issues def test_dashboard_dashcards_adyacentes_no_solapan(): """Dos dashcards que se tocan en el borde NO solapan.""" payload = { "name": "Dashboard", "dashcards": [ {"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 4}, {"card_id": 2, "row": 0, "col": 6, "size_x": 6, "size_y": 4}, ], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1, 2}) assert not any("solapan" in i for i in issues), issues def test_dashboard_col_fuera_de_bounds(): payload = { "name": "Dashboard", "dashcards": [{"card_id": 1, "row": 0, "col": 25, "size_x": 1, "size_y": 1}], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1}) assert any("col" in i for i in issues), issues def test_dashboard_col_mas_size_x_excede_grid(): payload = { "name": "Dashboard", "dashcards": [{"card_id": 1, "row": 0, "col": 20, "size_x": 6, "size_y": 4}], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1}) assert any("24" in i for i in issues), issues def test_dashboard_size_y_fuera_de_bounds(): payload = { "name": "Dashboard", "dashcards": [{"card_id": 1, "row": 0, "col": 0, "size_x": 6, "size_y": 0}], } issues = metabase_validate_dashboard_payload(payload, known_card_ids={1}) assert any("size_y" in i for i in issues), issues def test_dashboard_name_ausente(): issues = metabase_validate_dashboard_payload({}, known_card_ids=set()) assert any("name" in i and "ausente" in i for i in issues), issues def test_dashboard_tabs_invalidos(): payload = {"name": "Dashboard", "tabs": [{"name": "Tab1"}]} # falta id issues = metabase_validate_dashboard_payload(payload, known_card_ids=set()) assert any("id" in i for i in issues), issues def test_dashboard_parameters_no_list(): payload = {"name": "Dashboard", "parameters": "not a list"} issues = metabase_validate_dashboard_payload(payload, known_card_ids=set()) assert any("parameters" in i for i in issues), issues # --------------------------------------------------------------------------- # metabase_validate_document_payload # --------------------------------------------------------------------------- from metabase.validation import metabase_validate_document_payload def _base_doc(content): return {"name": "Notas", "document": {"type": "doc", "content": content}} def test_document_valido_minimo(): issues = metabase_validate_document_payload(_base_doc([ {"type": "paragraph", "content": [{"type": "text", "text": "hola"}]} ])) assert issues == [], issues def test_document_name_ausente(): issues = metabase_validate_document_payload({"document": {"type": "doc", "content": []}}) assert any("name" in i and "ausente" in i for i in issues), issues def test_document_nodo_desconocido_callout(): issues = metabase_validate_document_payload(_base_doc([ {"type": "callout", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "x"}]}]} ])) assert any("callout" in i and "no soportado" in i for i in issues), issues def test_document_nodo_desconocido_taskList(): issues = metabase_validate_document_payload(_base_doc([ {"type": "taskList", "content": []} ])) assert any("taskList" in i for i in issues), issues def test_document_mark_desconocido_underline(): issues = metabase_validate_document_payload(_base_doc([ {"type": "paragraph", "content": [ {"type": "text", "marks": [{"type": "underline"}], "text": "x"} ]} ])) assert any("underline" in i and "no soportado" in i for i in issues), issues def test_document_heading_level_invalido(): issues = metabase_validate_document_payload(_base_doc([ {"type": "heading", "attrs": {"level": 9}, "content": [{"type": "text", "text": "x"}]} ])) assert any("level" in i for i in issues), issues def test_document_cardEmbed_sin_id_ni_slug(): issues = metabase_validate_document_payload(_base_doc([ {"type": "cardEmbed", "attrs": {}} ])) assert any("cardEmbed" in i and "id" in i for i in issues), issues def test_document_cardEmbed_con_id_valido(): issues = metabase_validate_document_payload(_base_doc([ {"type": "cardEmbed", "attrs": {"id": 42}} ])) assert issues == [], issues def test_document_cardEmbed_slug_desconocido(): issues = metabase_validate_document_payload( _base_doc([{"type": "cardEmbed", "attrs": {"card": "inventado"}}]), known_card_slugs={"real_one", "real_two"}, ) assert any("inventado" in i for i in issues), issues def test_document_flexContainer_demasiados_hijos(): children = [{"type": "cardEmbed", "attrs": {"id": 1}} for _ in range(4)] issues = metabase_validate_document_payload(_base_doc([ {"type": "flexContainer", "attrs": {"columnWidths": [25, 25, 25, 25]}, "content": children} ])) assert any("1-3" in i for i in issues), issues def test_document_flexContainer_hijo_invalido(): issues = metabase_validate_document_payload(_base_doc([ {"type": "flexContainer", "attrs": {"columnWidths": [100]}, "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "x"}]} ]} ])) assert any("flexContainer solo acepta" in i for i in issues), issues def test_document_flexContainer_columnWidths_mismatch(): issues = metabase_validate_document_payload(_base_doc([ {"type": "flexContainer", "attrs": {"columnWidths": [50, 50]}, "content": [ {"type": "cardEmbed", "attrs": {"id": 1}} ]} ])) assert any("columnWidths" in i for i in issues), issues def test_document_vacio_string_es_valido(): issues = metabase_validate_document_payload({"name": "vacio", "document": ""}) assert issues == [], issues def test_document_kitchen_sink_valido(): """Simula el kitchen_sink real y debe pasar sin issues.""" content = [ {"type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "T"}]}, {"type": "paragraph", "content": [ {"type": "text", "text": "a "}, {"type": "text", "marks": [{"type": "bold"}], "text": "b"}, {"type": "text", "marks": [{"type": "link", "attrs": {"href": "https://x"}}], "text": "l"}, ]}, {"type": "bulletList", "content": [ {"type": "listItem", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "i"}]} ]} ]}, {"type": "blockquote", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "q"}]} ]}, {"type": "codeBlock", "attrs": {"language": "py"}, "content": [{"type": "text", "text": "x=1"}]}, {"type": "horizontalRule"}, {"type": "cardEmbed", "attrs": {"id": 1}}, {"type": "flexContainer", "attrs": {"columnWidths": [50, 50]}, "content": [ {"type": "cardEmbed", "attrs": {"id": 1}}, {"type": "cardEmbed", "attrs": {"id": 2}}, ]}, ] issues = metabase_validate_document_payload(_base_doc(content)) assert issues == [], issues