tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 14Markdown 114587 Definiciones

⎇ 09501911b7ec497215773d2cb7192a2e6ce17e02 crates/core/brahman-cards/tests/integration.rs Rust · 283 líneas · 9.4 KB Historia Quién tocó qué crudo

feat(brahman-cards): brazo unificado V1 — readers JSON + estructura canónica 09501911b · Sergio ·

1//! Integration tests del brazo brahman-cards.
2//!
3//! Cubre:
4//! 1. Cada reader matchea sólo el shape correcto.
5//! 2. El dispatcher (`load_card`/`dispatch`) elige el reader
6//!    correcto sin ambigüedad.
7//! 3. Round-trip: cada formato JSON cargado produce el variant
8//!    esperado del Card canónico con los campos del wrapper bien
9//!    derivados.
10//! 4. Rechazo gracioso de inputs no-matched + extensiones no
11//!    soportadas.
12
13use std::collections::BTreeMap;
14
15use brahman_cards::{
16    default_readers, load_card_with, Card, CardBody, CardLoadError, CardReader, EnteJsonReader,
17    MonadJsonReader, UiModuleJsonReader,
18};
19use serde_json::{json, Value};
20
21/// Helper: dispatch in-process desde un Value, sin tocar disco.
22/// Reproduce la lógica interna del dispatcher para no exigir I/O en
23/// los tests.
24fn dispatch(input: Value, readers: &[Box<dyn CardReader>]) -> Result<Card, CardLoadError> {
25    for r in readers {
26        if r.can_read(&input) {
27            return r.read(input);
28        }
29    }
30    Err(CardLoadError::NoMatchingReader)
31}
32
33// ===========================================================================
34// Reader detection (can_read)
35// ===========================================================================
36
37#[test]
38fn ui_module_reader_detects_only_ui_module_shape() {
39    let r = UiModuleJsonReader;
40    let ui = json!({"id": "x", "label": "X", "menu": [], "views": {}, "entities": []});
41    let ente = json!({"id": "x", "label": "X", "payload": "Virtual", "supervision": "OneShot"});
42    let monad = json!({"id": "x", "label": "X", "members": [], "cardinality": 0});
43    assert!(r.can_read(&ui), "UiModule reader debe matchear ui shape");
44    assert!(!r.can_read(&ente), "no debe matchear Ente");
45    assert!(!r.can_read(&monad), "no debe matchear Monad");
46    assert!(!r.can_read(&Value::Null), "no debe matchear non-object");
47}
48
49#[test]
50fn ente_reader_detects_only_ente_shape() {
51    let r = EnteJsonReader;
52    let ente = json!({"payload": "Virtual", "supervision": "OneShot"});
53    let monad = json!({"members": [], "cardinality": 0});
54    let ui = json!({"menu": [], "views": {}, "entities": []});
55    assert!(r.can_read(&ente));
56    assert!(!r.can_read(&monad));
57    assert!(!r.can_read(&ui));
58}
59
60#[test]
61fn monad_reader_detects_only_monad_shape() {
62    let r = MonadJsonReader;
63    let monad = json!({"members": [], "cardinality": 0});
64    let ente = json!({"payload": "Virtual", "supervision": "OneShot"});
65    let ui = json!({"menu": [], "views": {}, "entities": []});
66    assert!(r.can_read(&monad));
67    assert!(!r.can_read(&ente));
68    assert!(!r.can_read(&ui));
69}
70
71// ===========================================================================
72// Dispatch + variant projection
73// ===========================================================================
74
75#[test]
76fn loads_ui_module_to_card_ui_module_variant() {
77    let input = json!({
78        "id": "sales_engine",
79        "label": "Ventas",
80        "description": "Demo",
81        "entities": [],
82        "menu": [{"label": "Stock", "view": "stock_list"}],
83        "views": {
84            "stock_list": {
85                "kind": "list",
86                "title": "Stock",
87                "entity": "Stock",
88                "columns": []
89            }
90        }
91    });
92    let card = dispatch(input, &default_readers()).expect("dispatch ok");
93    assert_eq!(card.id, "sales_engine");
94    assert_eq!(card.label, "Ventas");
95    assert!(card.lineage.is_none(), "UiModule sin lineage");
96    assert_eq!(card.body.kind_name(), "ui_module");
97    match card.body {
98        CardBody::UiModule(m) => {
99            assert_eq!(m.id, "sales_engine");
100            assert_eq!(m.menu.len(), 1);
101        }
102        other => panic!("variant inesperado: {:?}", other.kind_name()),
103    }
104}
105
106#[test]
107fn loads_ente_to_card_ente_variant() {
108    // Ulid mínimo: 26 chars Crockford. Usamos uno conocido.
109    let ulid = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
110    let input = json!({
111        "schema_version": 1,
112        "id": ulid,
113        "label": "test-ente",
114        "payload": "Virtual",
115        "supervision": "OneShot"
116    });
117    let card = dispatch(input, &default_readers()).expect("dispatch ok");
118    assert_eq!(card.id, ulid);
119    assert_eq!(card.label, "test-ente");
120    assert_eq!(card.body.kind_name(), "ente");
121    match card.body {
122        CardBody::Ente(e) => {
123            assert_eq!(e.label, "test-ente");
124            assert_eq!(e.id.to_string(), ulid);
125        }
126        other => panic!("variant inesperado: {:?}", other.kind_name()),
127    }
128}
129
130#[test]
131fn loads_monad_to_card_monad_variant() {
132    let ulid = "01ARZ3NDEKTSV4RRFFQ69G5FB1";
133    let input = json!({
134        "schema_version": 1,
135        "id": ulid,
136        "label": "test-monad",
137        "members": [],
138        "cardinality": 0,
139        "created_at_ms": 0,
140        "updated_at_ms": 0
141    });
142    let card = dispatch(input, &default_readers()).expect("dispatch ok");
143    assert_eq!(card.id, ulid);
144    assert_eq!(card.label, "test-monad");
145    assert_eq!(card.body.kind_name(), "monad");
146    match card.body {
147        CardBody::Monad(m) => {
148            assert_eq!(m.label, "test-monad");
149            assert_eq!(m.cardinality, 0);
150        }
151        other => panic!("variant inesperado: {:?}", other.kind_name()),
152    }
153}
154
155// ===========================================================================
156// Negative cases
157// ===========================================================================
158
159#[test]
160fn rejects_input_no_matching_reader() {
161    let input = json!({"random": "shape", "without": "fingerprint"});
162    let err = dispatch(input, &default_readers()).unwrap_err();
163    assert!(
164        matches!(err, CardLoadError::NoMatchingReader),
165        "expected NoMatchingReader, got {err:?}"
166    );
167}
168
169#[test]
170fn rejects_non_object_input() {
171    let input = json!([1, 2, 3]);
172    let err = dispatch(input, &default_readers()).unwrap_err();
173    assert!(matches!(err, CardLoadError::NoMatchingReader));
174}
175
176#[test]
177fn ui_module_takes_priority_when_shape_overlaps_partial() {
178    // Sanity del orden: si alguien armara un input híbrido con
179    // `menu`+`views`+`entities` Y también `payload`+`supervision`,
180    // el UiModuleReader (primero en orden) debería ganar. Esto no
181    // debería ocurrir con inputs reales pero defendemos el contrato
182    // de orden documentado.
183    let input = json!({
184        "id": "weird",
185        "label": "Weird",
186        "menu": [],
187        "views": {},
188        "entities": [],
189        "payload": "Virtual",
190        "supervision": "OneShot"
191    });
192    let card = dispatch(input, &default_readers()).expect("dispatch ok");
193    assert_eq!(
194        card.body.kind_name(),
195        "ui_module",
196        "el UiModuleReader debería ganar por orden"
197    );
198}
199
200// ===========================================================================
201// load_card desde disco (e2e fino)
202// ===========================================================================
203
204#[test]
205fn load_card_from_disk_round_trip_ui_module() {
206    let tmp = tempfile_path("ui_module.json");
207    let input = json!({
208        "id": "demo",
209        "label": "Demo",
210        "entities": [],
211        "menu": [],
212        "views": {}
213    });
214    std::fs::write(&tmp, serde_json::to_vec_pretty(&input).unwrap()).unwrap();
215
216    let card = load_card_with(&tmp, &default_readers()).expect("load ok");
217    assert_eq!(card.body.kind_name(), "ui_module");
218    assert_eq!(card.id, "demo");
219
220    let _ = std::fs::remove_file(&tmp);
221}
222
223#[test]
224fn load_card_rejects_unsupported_extension() {
225    let tmp = tempfile_path("foo.toml");
226    std::fs::write(&tmp, b"[anything]\nx = 1").unwrap();
227    let err = load_card_with(&tmp, &default_readers()).unwrap_err();
228    match err {
229        CardLoadError::UnsupportedExtension { ext, supported } => {
230            assert_eq!(ext, "toml");
231            assert!(supported.contains(&"json"));
232        }
233        other => panic!("expected UnsupportedExtension, got {other:?}"),
234    }
235    let _ = std::fs::remove_file(&tmp);
236}
237
238// ===========================================================================
239// Custom reader sets
240// ===========================================================================
241
242#[test]
243fn custom_reader_set_can_restrict_supported_formats() {
244    // Sólo Ente: un input Monad debería rechazarse.
245    let only_ente: Vec<Box<dyn CardReader>> = vec![Box::new(EnteJsonReader)];
246    let monad_input = json!({"members": [], "cardinality": 0});
247    let err = dispatch(monad_input, &only_ente).unwrap_err();
248    assert!(matches!(err, CardLoadError::NoMatchingReader));
249}
250
251// ===========================================================================
252// Wrapper field invariants
253// ===========================================================================
254
255#[test]
256fn extensions_field_starts_empty_in_v1() {
257    // Documented: V1 no mueve el "extras" del crate origen al
258    // wrapper.extensions. Si esto cambia, este test se rompe como
259    // signal para actualizar el doc de readers.rs.
260    let input = json!({
261        "id": "demo",
262        "label": "Demo",
263        "entities": [],
264        "menu": [],
265        "views": {}
266    });
267    let card = dispatch(input, &default_readers()).unwrap();
268    assert_eq!(card.extensions, BTreeMap::new());
269}
270
271// ===========================================================================
272// Helpers de tests
273// ===========================================================================
274
275fn tempfile_path(name: &str) -> std::path::PathBuf {
276    let mut p = std::env::temp_dir();
277    p.push(format!(
278        "brahman-cards-test-{}-{}",
279        std::process::id(),
280        name
281    ));
282    p
283}