1use std::path::{Path, PathBuf};
5
6use base64::Engine;
7use serde::Serialize;
8use serde_cbor::Value as CborValue;
9use zip::result::ZipError;
10
11#[derive(Clone, Debug, Serialize)]
13pub struct DiscoveryResult {
14 pub domains: DetectedDomains,
15 pub providers: Vec<DetectedProvider>,
16 pub app_packs: Vec<DetectedProvider>,
17}
18
19#[derive(Clone, Debug, Serialize)]
21pub struct DetectedDomains {
22 pub messaging: bool,
23 pub events: bool,
24 pub oauth: bool,
25 pub state: bool,
26 pub secrets: bool,
27}
28
29#[derive(Clone, Debug, Serialize)]
31pub struct DetectedProvider {
32 pub provider_id: String,
33 pub display_name: Option<String>,
34 pub domain: String,
35 pub pack_path: PathBuf,
36 pub id_source: ProviderIdSource,
37 pub kind: DetectedPackKind,
38}
39
40#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum DetectedPackKind {
44 Provider,
45 App,
46}
47
48#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)]
50#[serde(rename_all = "lowercase")]
51pub enum ProviderIdSource {
52 Manifest,
53 Filename,
54}
55
56struct PackMeta {
58 pack_id: String,
59 display_name: Option<String>,
60}
61
62#[derive(Clone, Debug)]
64pub struct DiscoveredPackMeta {
65 pub pack_id: String,
66 pub display_name: Option<String>,
67}
68
69#[derive(Default)]
71pub struct DiscoveryOptions {
72 pub cbor_only: bool,
74}
75
76const DOMAIN_DIRS: &[(&str, &str)] = &[
78 ("messaging", "providers/messaging"),
79 ("events", "providers/events"),
80 ("oauth", "providers/oauth"),
81 ("state", "providers/state"),
82 ("secrets", "providers/secrets"),
83];
84
85pub fn discover(root: &Path) -> anyhow::Result<DiscoveryResult> {
87 discover_with_options(root, DiscoveryOptions::default())
88}
89
90pub fn discover_with_options(
92 root: &Path,
93 options: DiscoveryOptions,
94) -> anyhow::Result<DiscoveryResult> {
95 let mut providers = Vec::new();
96
97 for &(domain, dir) in DOMAIN_DIRS {
98 let providers_dir = root.join(dir);
99 if !providers_dir.exists() {
100 continue;
101 }
102 for entry in std::fs::read_dir(&providers_dir)? {
103 let entry = entry?;
104 if !entry.file_type()?.is_file() {
105 continue;
106 }
107 let path = entry.path();
108 if path.extension().and_then(|ext| ext.to_str()) != Some("gtpack") {
109 continue;
110 }
111 let (provider_id, display_name, id_source) =
112 read_pack_identity(&path, options.cbor_only)?;
113 providers.push(DetectedProvider {
114 provider_id,
115 display_name,
116 domain: domain.to_string(),
117 pack_path: path,
118 id_source,
119 kind: DetectedPackKind::Provider,
120 });
121 }
122 }
123
124 let providers_root = root.join("providers");
127 if providers_root.exists() {
128 let known_subdirs: std::collections::HashSet<&str> = DOMAIN_DIRS
129 .iter()
130 .filter_map(|(_, dir)| {
131 std::path::Path::new(dir)
132 .file_name()
133 .and_then(|name| name.to_str())
134 })
135 .collect();
136 for entry in std::fs::read_dir(&providers_root)? {
137 let entry = entry?;
138 let entry_path = entry.path();
139 let name_str = entry_path
140 .file_name()
141 .and_then(|s| s.to_str())
142 .unwrap_or_default();
143
144 let pack_path = if entry.file_type()?.is_file() {
147 if entry_path.extension().and_then(|e| e.to_str()) != Some("gtpack") {
148 continue;
149 }
150 entry_path.clone()
151 } else if entry.file_type()?.is_dir() {
152 if known_subdirs.contains(name_str) {
153 continue;
154 }
155 if !name_str.ends_with(".gtpack") {
156 continue;
157 }
158 let inner = entry_path.join(name_str);
159 if inner.is_file() {
160 inner
161 } else {
162 match std::fs::read_dir(&entry_path)?
164 .filter_map(|e| e.ok())
165 .find(|e| e.path().extension().and_then(|x| x.to_str()) == Some("gtpack"))
166 .map(|e| e.path())
167 {
168 Some(found) => found,
169 None => continue,
170 }
171 }
172 } else {
173 continue;
174 };
175
176 let (provider_id, display_name, id_source) =
177 read_pack_identity(&pack_path, options.cbor_only)?;
178
179 if providers.iter().any(|p| p.provider_id == provider_id) {
181 continue;
182 }
183 let domain = crate::cli_helpers::detect_domain_from_filename(
184 pack_path
185 .file_name()
186 .and_then(|s| s.to_str())
187 .unwrap_or_default(),
188 )
189 .to_string();
190 providers.push(DetectedProvider {
191 provider_id,
192 display_name,
193 domain,
194 pack_path,
195 id_source,
196 kind: DetectedPackKind::Provider,
197 });
198 }
199 }
200
201 let mut app_packs = Vec::new();
202 let packs_dir = root.join("packs");
203 if packs_dir.exists() {
204 for entry in std::fs::read_dir(&packs_dir)? {
205 let entry = entry?;
206 if !entry.file_type()?.is_file() {
207 continue;
208 }
209 let path = entry.path();
210 if path.extension().and_then(|ext| ext.to_str()) != Some("gtpack") {
211 continue;
212 }
213 let (provider_id, display_name, id_source) =
214 read_pack_identity(&path, options.cbor_only)?;
215 app_packs.push(DetectedProvider {
216 provider_id,
217 display_name,
218 domain: "app".to_string(),
219 pack_path: path,
220 id_source,
221 kind: DetectedPackKind::App,
222 });
223 }
224 }
225
226 providers.sort_by(|a, b| a.pack_path.cmp(&b.pack_path));
227 app_packs.sort_by(|a, b| a.pack_path.cmp(&b.pack_path));
228
229 let domains = DetectedDomains {
230 messaging: providers.iter().any(|p| p.domain == "messaging"),
231 events: providers.iter().any(|p| p.domain == "events"),
232 oauth: providers.iter().any(|p| p.domain == "oauth"),
233 state: providers.iter().any(|p| p.domain == "state"),
234 secrets: providers.iter().any(|p| p.domain == "secrets"),
235 };
236
237 Ok(DiscoveryResult {
238 domains,
239 providers,
240 app_packs,
241 })
242}
243
244pub fn persist(root: &Path, tenant: &str, discovery: &DiscoveryResult) -> anyhow::Result<()> {
246 let runtime_root = root.join("state").join("runtime").join(tenant);
247 std::fs::create_dir_all(&runtime_root)?;
248 let domains_path = runtime_root.join("detected_domains.json");
249 let providers_path = runtime_root.join("detected_providers.json");
250 let app_packs_path = runtime_root.join("detected_app_packs.json");
251 write_json(&domains_path, &discovery.domains)?;
252 write_json(&providers_path, &discovery.providers)?;
253 write_json(&app_packs_path, &discovery.app_packs)?;
254 Ok(())
255}
256
257impl DiscoveryResult {
258 pub fn setup_targets(&self) -> Vec<&DetectedProvider> {
260 self.providers.iter().chain(self.app_packs.iter()).collect()
261 }
262
263 pub fn find_setup_target(&self, provider_id: &str) -> Option<&DetectedProvider> {
265 self.providers
266 .iter()
267 .chain(self.app_packs.iter())
268 .find(|pack| pack.provider_id == provider_id)
269 }
270}
271
272fn read_pack_identity(
276 path: &Path,
277 cbor_only: bool,
278) -> anyhow::Result<(String, Option<String>, ProviderIdSource)> {
279 if cbor_only {
280 match read_pack_meta_cbor_only(path)? {
281 Some(meta) => Ok((meta.pack_id, meta.display_name, ProviderIdSource::Manifest)),
282 None => Err(missing_cbor_error(path)),
283 }
284 } else {
285 match read_pack_meta_from_manifest(path)? {
286 Some(meta) => Ok((meta.pack_id, meta.display_name, ProviderIdSource::Manifest)),
287 None => {
288 let stem = path
289 .file_stem()
290 .and_then(|v| v.to_str())
291 .unwrap_or_default()
292 .to_string();
293 Ok((stem, None, ProviderIdSource::Filename))
294 }
295 }
296 }
297}
298
299pub fn read_pack_meta(path: &Path) -> anyhow::Result<Option<DiscoveredPackMeta>> {
301 read_pack_meta_from_manifest(path).map(|meta| {
302 meta.map(|meta| DiscoveredPackMeta {
303 pack_id: meta.pack_id,
304 display_name: meta.display_name,
305 })
306 })
307}
308
309pub fn read_pack_extension(
311 path: &Path,
312 extension_key: &str,
313) -> anyhow::Result<Option<serde_json::Value>> {
314 let file = std::fs::File::open(path)?;
315 match zip::ZipArchive::new(file) {
316 Ok(mut archive) => {
317 if let Some(value) = read_manifest_extension_cbor(&mut archive, extension_key)? {
318 return Ok(Some(value));
319 }
320 if let Some(value) =
321 read_manifest_extension_json(&mut archive, "pack.manifest.json", extension_key)?
322 {
323 return Ok(Some(value));
324 }
325 }
326 Err(_) => {
327 if let Some(value) = read_manifest_extension_cbor_from_tar(path, extension_key)? {
328 return Ok(Some(value));
329 }
330 }
331 }
332 Ok(None)
333}
334
335fn write_json<T: Serialize>(path: &Path, value: &T) -> anyhow::Result<()> {
336 if let Some(parent) = path.parent() {
337 std::fs::create_dir_all(parent)?;
338 }
339 let payload = serde_json::to_string_pretty(value)?;
340 std::fs::write(path, payload)?;
341 Ok(())
342}
343
344fn read_pack_meta_from_manifest(path: &Path) -> anyhow::Result<Option<PackMeta>> {
347 let file = std::fs::File::open(path)?;
348 match zip::ZipArchive::new(file) {
349 Ok(mut archive) => {
350 if let Some(meta) = read_manifest_cbor(&mut archive)? {
351 return Ok(Some(meta));
352 }
353 if let Some(meta) = read_manifest_json(&mut archive, "pack.manifest.json")? {
354 return Ok(Some(meta));
355 }
356 }
357 Err(_) => {
358 if let Some(meta) = read_manifest_cbor_from_tar(path)? {
359 return Ok(Some(meta));
360 }
361 }
362 }
363 Ok(None)
364}
365
366fn read_pack_meta_cbor_only(path: &Path) -> anyhow::Result<Option<PackMeta>> {
367 let file = std::fs::File::open(path)?;
368 match zip::ZipArchive::new(file) {
369 Ok(mut archive) => read_manifest_cbor(&mut archive),
370 Err(_) => read_manifest_cbor_from_tar(path),
371 }
372}
373
374fn read_manifest_cbor(
375 archive: &mut zip::ZipArchive<std::fs::File>,
376) -> anyhow::Result<Option<PackMeta>> {
377 let mut file = match archive.by_name("manifest.cbor") {
378 Ok(file) => file,
379 Err(ZipError::FileNotFound) => return Ok(None),
380 Err(err) => return Err(err.into()),
381 };
382 let mut bytes = Vec::new();
383 std::io::Read::read_to_end(&mut file, &mut bytes)?;
384 let value: CborValue = serde_cbor::from_slice(&bytes)?;
385 extract_pack_meta_from_cbor(&value)
386}
387
388fn read_manifest_extension_cbor(
389 archive: &mut zip::ZipArchive<std::fs::File>,
390 extension_key: &str,
391) -> anyhow::Result<Option<serde_json::Value>> {
392 let mut file = match archive.by_name("manifest.cbor") {
393 Ok(file) => file,
394 Err(ZipError::FileNotFound) => return Ok(None),
395 Err(err) => return Err(err.into()),
396 };
397 let mut bytes = Vec::new();
398 std::io::Read::read_to_end(&mut file, &mut bytes)?;
399 let value: CborValue = serde_cbor::from_slice(&bytes)?;
400 let CborValue::Map(map) = &value else {
401 return Ok(None);
402 };
403 if let Some(value) = map_get(map, extension_key) {
404 return Ok(Some(cbor_to_json(value)));
405 }
406 let Some(CborValue::Map(extensions)) = map_get(map, "extensions") else {
407 return Ok(None);
408 };
409 Ok(map_get(extensions, extension_key).map(cbor_to_json))
410}
411
412fn read_manifest_json(
413 archive: &mut zip::ZipArchive<std::fs::File>,
414 name: &str,
415) -> anyhow::Result<Option<PackMeta>> {
416 let mut file = match archive.by_name(name) {
417 Ok(file) => file,
418 Err(ZipError::FileNotFound) => return Ok(None),
419 Err(err) => return Err(err.into()),
420 };
421 let mut contents = String::new();
422 std::io::Read::read_to_string(&mut file, &mut contents)?;
423 let parsed: serde_json::Value = serde_json::from_str(&contents)?;
424
425 let resolve_dn = |obj: &serde_json::Value| -> Option<String> {
426 obj.get("display_name")
427 .and_then(|v| v.as_str())
428 .or_else(|| obj.get("name").and_then(|v| v.as_str()))
429 .map(String::from)
430 };
431
432 let display_name = resolve_dn(&parsed);
433
434 if let Some(id) = parsed.get("pack_id").and_then(|v| v.as_str()) {
435 return Ok(Some(PackMeta {
436 pack_id: id.to_string(),
437 display_name,
438 }));
439 }
440 if let Some(meta) = parsed.get("meta")
441 && let Some(id) = meta.get("pack_id").and_then(|v| v.as_str())
442 {
443 let dn = resolve_dn(meta).or(display_name);
444 return Ok(Some(PackMeta {
445 pack_id: id.to_string(),
446 display_name: dn,
447 }));
448 }
449 Ok(None)
450}
451
452fn read_manifest_extension_json(
453 archive: &mut zip::ZipArchive<std::fs::File>,
454 name: &str,
455 extension_key: &str,
456) -> anyhow::Result<Option<serde_json::Value>> {
457 let mut file = match archive.by_name(name) {
458 Ok(file) => file,
459 Err(ZipError::FileNotFound) => return Ok(None),
460 Err(err) => return Err(err.into()),
461 };
462 let mut contents = String::new();
463 std::io::Read::read_to_string(&mut file, &mut contents)?;
464 let parsed: serde_json::Value = serde_json::from_str(&contents)?;
465 Ok(parsed.get(extension_key).cloned().or_else(|| {
466 parsed
467 .get("extensions")
468 .and_then(|extensions| extensions.get(extension_key))
469 .cloned()
470 }))
471}
472
473fn read_manifest_cbor_from_tar(path: &Path) -> anyhow::Result<Option<PackMeta>> {
474 let file = std::fs::File::open(path)?;
475 let mut archive = tar::Archive::new(file);
476 for entry in archive.entries()? {
477 let mut entry = entry?;
478 if entry.path()?.as_ref() != Path::new("manifest.cbor") {
479 continue;
480 }
481 let mut bytes = Vec::new();
482 std::io::Read::read_to_end(&mut entry, &mut bytes)?;
483 let value: CborValue = serde_cbor::from_slice(&bytes)?;
484 return extract_pack_meta_from_cbor(&value);
485 }
486 Ok(None)
487}
488
489fn read_manifest_extension_cbor_from_tar(
490 path: &Path,
491 extension_key: &str,
492) -> anyhow::Result<Option<serde_json::Value>> {
493 let file = std::fs::File::open(path)?;
494 let mut archive = tar::Archive::new(file);
495 for entry in archive.entries()? {
496 let mut entry = entry?;
497 if entry.path()?.as_ref() != Path::new("manifest.cbor") {
498 continue;
499 }
500 let mut bytes = Vec::new();
501 std::io::Read::read_to_end(&mut entry, &mut bytes)?;
502 let value: CborValue = serde_cbor::from_slice(&bytes)?;
503 let CborValue::Map(map) = &value else {
504 return Ok(None);
505 };
506 return Ok(map_get(map, extension_key).map(cbor_to_json));
507 }
508 Ok(None)
509}
510
511fn extract_pack_meta_from_cbor(value: &CborValue) -> anyhow::Result<Option<PackMeta>> {
512 let CborValue::Map(map) = value else {
513 return Ok(None);
514 };
515 let symbols = match map_get(map, "symbols") {
516 Some(CborValue::Map(map)) => Some(map),
517 _ => None,
518 };
519
520 let resolve_display_name =
521 |source_map: &std::collections::BTreeMap<CborValue, CborValue>| -> Option<String> {
522 map_get(source_map, "display_name")
523 .and_then(|v| match v {
524 CborValue::Text(text) => Some(text.clone()),
525 _ => resolve_string_symbol(v, symbols, "display_names")
526 .ok()
527 .flatten(),
528 })
529 .or_else(|| {
530 map_get(source_map, "name").and_then(|v| match v {
531 CborValue::Text(text) => Some(text.clone()),
532 _ => resolve_string_symbol(v, symbols, "names").ok().flatten(),
533 })
534 })
535 };
536
537 if let Some(pack_id) = map_get(map, "pack_id")
538 && let Some(id) = resolve_string_symbol(pack_id, symbols, "pack_ids")?
539 {
540 return Ok(Some(PackMeta {
541 pack_id: id,
542 display_name: resolve_display_name(map),
543 }));
544 }
545
546 if let Some(CborValue::Map(meta)) = map_get(map, "meta")
547 && let Some(pack_id) = map_get(meta, "pack_id")
548 && let Some(id) = resolve_string_symbol(pack_id, symbols, "pack_ids")?
549 {
550 return Ok(Some(PackMeta {
551 pack_id: id,
552 display_name: resolve_display_name(meta).or_else(|| resolve_display_name(map)),
553 }));
554 }
555
556 Ok(None)
557}
558
559fn resolve_string_symbol(
560 value: &CborValue,
561 symbols: Option<&std::collections::BTreeMap<CborValue, CborValue>>,
562 symbol_key: &str,
563) -> anyhow::Result<Option<String>> {
564 match value {
565 CborValue::Text(text) => Ok(Some(text.clone())),
566 CborValue::Integer(idx) => {
567 let Some(symbols) = symbols else {
568 return Ok(Some(idx.to_string()));
569 };
570 let Some(CborValue::Array(values)) = map_get(symbols, symbol_key)
571 .or_else(|| map_get(symbols, symbol_key.strip_suffix('s').unwrap_or(symbol_key)))
572 else {
573 return Ok(Some(idx.to_string()));
574 };
575 let idx = usize::try_from(*idx).unwrap_or(usize::MAX);
576 match values.get(idx) {
577 Some(CborValue::Text(text)) => Ok(Some(text.clone())),
578 _ => Ok(Some(idx.to_string())),
579 }
580 }
581 _ => Ok(None),
582 }
583}
584
585fn map_get<'a>(
586 map: &'a std::collections::BTreeMap<CborValue, CborValue>,
587 key: &str,
588) -> Option<&'a CborValue> {
589 map.iter().find_map(|(k, v)| match k {
590 CborValue::Text(text) if text == key => Some(v),
591 _ => None,
592 })
593}
594
595fn cbor_to_json(value: &CborValue) -> serde_json::Value {
596 match value {
597 CborValue::Null => serde_json::Value::Null,
598 CborValue::Bool(v) => serde_json::Value::Bool(*v),
599 CborValue::Integer(v) => i64::try_from(*v)
600 .map(serde_json::Number::from)
601 .map(serde_json::Value::Number)
602 .unwrap_or_else(|_| serde_json::Value::String(v.to_string())),
603 CborValue::Float(v) => serde_json::Number::from_f64(*v)
604 .map(serde_json::Value::Number)
605 .unwrap_or(serde_json::Value::Null),
606 CborValue::Bytes(bytes) => {
607 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(bytes))
608 }
609 CborValue::Text(text) => serde_json::Value::String(text.clone()),
610 CborValue::Array(values) => {
611 serde_json::Value::Array(values.iter().map(cbor_to_json).collect())
612 }
613 CborValue::Map(map) => {
614 let mut obj = serde_json::Map::new();
615 for (key, value) in map {
616 let key = match key {
617 CborValue::Text(text) => text.clone(),
618 CborValue::Integer(value) => value.to_string(),
619 other => serde_json::to_string(&cbor_to_json(other)).unwrap_or_default(),
620 };
621 obj.insert(key, cbor_to_json(value));
622 }
623 serde_json::Value::Object(obj)
624 }
625 _ => serde_json::Value::Null,
626 }
627}
628
629fn missing_cbor_error(path: &Path) -> anyhow::Error {
630 anyhow::anyhow!(
631 "demo packs must be CBOR-only (.gtpack must contain manifest.cbor). \
632 Rebuild the pack with greentic-pack build (do not use --dev). Missing in {}",
633 path.display()
634 )
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use std::io::Write;
641 use zip::write::{FileOptions, ZipWriter};
642
643 fn write_test_pack(path: &Path, pack_id: &str, display_name: &str) -> anyhow::Result<()> {
644 let file = std::fs::File::create(path)?;
645 let mut writer = ZipWriter::new(file);
646 let options: FileOptions<'_, ()> =
647 FileOptions::default().compression_method(zip::CompressionMethod::Stored);
648 writer.start_file("pack.manifest.json", options)?;
649 writer.write_all(
650 serde_json::json!({
651 "pack_id": pack_id,
652 "display_name": display_name,
653 })
654 .to_string()
655 .as_bytes(),
656 )?;
657 writer.finish()?;
658 Ok(())
659 }
660
661 fn write_test_pack_manifest(path: &Path, manifest: serde_json::Value) -> anyhow::Result<()> {
662 let file = std::fs::File::create(path)?;
663 let mut writer = ZipWriter::new(file);
664 let options: FileOptions<'_, ()> =
665 FileOptions::default().compression_method(zip::CompressionMethod::Stored);
666 writer.start_file("pack.manifest.json", options)?;
667 writer.write_all(manifest.to_string().as_bytes())?;
668 writer.finish()?;
669 Ok(())
670 }
671
672 #[test]
673 fn discover_picks_up_extension_provider_in_wrapper_dir() -> anyhow::Result<()> {
674 let temp = tempfile::tempdir()?;
682 let root = temp.path();
683 let wrapper = root.join("providers").join("messaging-webchat-gui.gtpack");
684 std::fs::create_dir_all(&wrapper)?;
685 let inner = wrapper.join("messaging-webchat-gui.gtpack");
686 write_test_pack(&inner, "messaging-webchat-gui", "WebChat GUI")?;
687
688 let discovered = discover(root)?;
689 assert_eq!(discovered.providers.len(), 1);
690 let provider = &discovered.providers[0];
691 assert_eq!(provider.provider_id, "messaging-webchat-gui");
692 assert_eq!(provider.domain, "messaging");
693 assert_eq!(provider.kind, DetectedPackKind::Provider);
694 assert_eq!(provider.pack_path, inner);
698 assert!(
699 discovered
700 .find_setup_target("messaging-webchat-gui")
701 .is_some()
702 );
703 Ok(())
704 }
705
706 #[test]
707 fn discover_does_not_double_count_when_pack_lives_in_both_locations() -> anyhow::Result<()> {
708 let temp = tempfile::tempdir()?;
712 let root = temp.path();
713 std::fs::create_dir_all(root.join("providers/messaging"))?;
714 write_test_pack(
715 &root
716 .join("providers/messaging")
717 .join("messaging-telegram.gtpack"),
718 "messaging-telegram",
719 "Telegram",
720 )?;
721 let wrapper = root.join("providers").join("messaging-telegram.gtpack");
722 std::fs::create_dir_all(&wrapper)?;
723 write_test_pack(
724 &wrapper.join("messaging-telegram.gtpack"),
725 "messaging-telegram",
726 "Telegram",
727 )?;
728
729 let discovered = discover(root)?;
730 let matching: Vec<_> = discovered
733 .providers
734 .iter()
735 .filter(|p| p.provider_id == "messaging-telegram")
736 .collect();
737 assert_eq!(
738 matching.len(),
739 1,
740 "expected exactly one entry, got {matching:?}"
741 );
742 Ok(())
743 }
744
745 #[test]
746 fn discover_includes_app_packs_in_setup_targets() -> anyhow::Result<()> {
747 let temp = tempfile::tempdir()?;
748 let root = temp.path();
749 std::fs::create_dir_all(root.join("providers/messaging"))?;
750 std::fs::create_dir_all(root.join("packs"))?;
751
752 write_test_pack(
753 &root
754 .join("providers")
755 .join("messaging")
756 .join("messaging-telegram.gtpack"),
757 "messaging-telegram",
758 "Telegram",
759 )?;
760 write_test_pack(
761 &root.join("packs").join("weather-app.gtpack"),
762 "weather-app",
763 "Weather App",
764 )?;
765
766 let discovered = discover(root)?;
767 assert_eq!(discovered.providers.len(), 1);
768 assert_eq!(discovered.app_packs.len(), 1);
769 assert_eq!(discovered.setup_targets().len(), 2);
770 assert_eq!(discovered.app_packs[0].provider_id, "weather-app");
771 assert_eq!(discovered.app_packs[0].domain, "app");
772 assert_eq!(discovered.app_packs[0].kind, DetectedPackKind::App);
773 Ok(())
774 }
775
776 #[test]
777 fn read_pack_extension_reads_json_manifest_extension() -> anyhow::Result<()> {
778 let temp = tempfile::tempdir()?;
779 let pack = temp.path().join("messaging-example.gtpack");
780 write_test_pack_manifest(
781 &pack,
782 serde_json::json!({
783 "pack_id": "messaging-example",
784 "extensions": {
785 "messaging.oauth.v1": {
786 "token_url": "https://example.com/token",
787 "secret_keys": ["EXAMPLE_TOKEN"]
788 }
789 }
790 }),
791 )?;
792 let extension = read_pack_extension(&pack, "messaging.oauth.v1")?.unwrap();
793 assert_eq!(extension["token_url"], "https://example.com/token");
794 Ok(())
795 }
796
797 #[test]
798 fn read_pack_extension_reads_cbor_manifest_extensions_map() -> anyhow::Result<()> {
799 let temp = tempfile::tempdir()?;
800 let pack = temp.path().join("messaging-example.gtpack");
801 let file = std::fs::File::create(&pack)?;
802 let mut writer = zip::ZipWriter::new(file);
803 let options: zip::write::FileOptions<'_, ()> =
804 zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored);
805 writer.start_file("manifest.cbor", options)?;
806 let manifest = serde_cbor::value::to_value(serde_json::json!({
807 "pack_id": "messaging-example",
808 "extensions": {
809 "messaging.oauth.v1": {
810 "inline": {
811 "token_url": "https://example.com/token"
812 }
813 }
814 }
815 }))?;
816 writer.write_all(&serde_cbor::to_vec(&manifest)?)?;
817 writer.finish()?;
818
819 let extension = read_pack_extension(&pack, "messaging.oauth.v1")?.unwrap();
820 assert_eq!(
821 extension["inline"]["token_url"],
822 "https://example.com/token"
823 );
824 Ok(())
825 }
826}