1use devboy_core::{
2 Error, KnowledgeBaseProvider, MeetingNotesProvider, MessengerProvider, Provider, Result,
3 ToolEnricher,
4};
5
6use crate::context::{
7 ClickUpScope, ConfluenceAuthConfig, ConfluenceScope, GitHubScope, GitLabScope, JiraScope,
8 LinearScope, ProviderConfig, ProviderMetadata, ProxyConfig, SlackScope, TelegramScope,
9 YouGileScope,
10};
11
12pub fn create_provider(
22 config: &ProviderConfig,
23 proxy: Option<&ProxyConfig>,
24) -> Result<Box<dyn Provider>> {
25 match config {
26 ProviderConfig::GitLab {
27 base_url,
28 access_token,
29 scope,
30 ..
31 } => match scope {
32 GitLabScope::Project { id } => {
33 let client = if let Some(proxy) = proxy {
34 devboy_gitlab::GitLabClient::with_base_url(
35 &proxy.url,
36 id,
37 access_token.clone(),
38 )
39 .with_proxy(proxy.headers.clone())
40 } else {
41 devboy_gitlab::GitLabClient::with_base_url(
42 base_url,
43 id,
44 access_token.clone(),
45 )
46 };
47 Ok(Box::new(client))
48 }
49 GitLabScope::Group { id } => Err(Error::ProviderUnsupported {
50 provider: "gitlab".into(),
51 operation: format!("group scope (group_id: {id}) not yet implemented"),
52 }),
53 GitLabScope::Global => Err(Error::ProviderUnsupported {
54 provider: "gitlab".into(),
55 operation: "global scope not yet implemented".into(),
56 }),
57 },
58
59 ProviderConfig::GitHub {
60 base_url,
61 access_token,
62 scope,
63 ..
64 } => match scope {
65 GitHubScope::Repository { owner, repo } => {
66 Ok(Box::new(devboy_github::GitHubClient::with_base_url(
67 base_url,
68 owner,
69 repo,
70 access_token.clone(),
71 )))
72 }
73 GitHubScope::Organization { name } => Err(Error::ProviderUnsupported {
74 provider: "github".into(),
75 operation: format!("organization scope (org: {name}) not yet implemented"),
76 }),
77 GitHubScope::Global => Err(Error::ProviderUnsupported {
78 provider: "github".into(),
79 operation: "global scope not yet implemented".into(),
80 }),
81 },
82
83 ProviderConfig::ClickUp {
84 access_token,
85 scope,
86 ..
87 } => match scope {
88 ClickUpScope::List { id, team_id } => {
89 let mut client = devboy_clickup::ClickUpClient::new(id, access_token.clone());
90 if let Some(tid) = team_id {
91 client = client.with_team_id(tid);
92 }
93 Ok(Box::new(client))
94 }
95 },
96
97 ProviderConfig::Jira {
98 base_url,
99 access_token,
100 email,
101 scope,
102 flavor,
103 ..
104 } => match scope {
105 JiraScope::Project { key } => {
106 let mut client = if let Some(proxy) = proxy {
107 devboy_jira::JiraClient::new(
108 &proxy.url,
109 key,
110 email,
111 access_token.clone(),
112 )
113 .with_proxy(proxy.headers.clone())
114 .with_instance_url(base_url)
115 } else {
116 devboy_jira::JiraClient::new(base_url, key, email, access_token.clone())
117 };
118 if let Some(f) = flavor {
119 client = client.with_flavor(*f);
120 }
121 Ok(Box::new(client))
122 }
123 JiraScope::MultiProject { keys } => Err(Error::ProviderUnsupported {
124 provider: "jira".into(),
125 operation: format!(
126 "multi-project scope ({}) not yet implemented",
127 keys.join(", ")
128 ),
129 }),
130 },
131
132 ProviderConfig::Linear {
133 base_url,
134 access_token,
135 scope,
136 ..
137 } => match scope {
138 LinearScope::Team { id, key } => {
139 let mut client =
140 devboy_linear::LinearClient::with_base_url(base_url, id, access_token.clone());
141 if let Some(key) = key {
142 client = client.with_team_key(key);
143 }
144 Ok(Box::new(client))
145 }
146 },
147 ProviderConfig::YouGile {
148 base_url,
149 access_token,
150 scope,
151 ..
152 } => match scope {
153 YouGileScope::Board { id } => Ok(Box::new(devboy_yougile::YouGileClient::with_base_url(
154 base_url,
155 id,
156 access_token.clone(),
157 ))),
158 },
159
160 ProviderConfig::Confluence { .. } => Err(Error::ProviderUnsupported {
161 provider: "confluence".into(),
162 operation: "Confluence is a KnowledgeBaseProvider, not a Provider. Use create_knowledge_base_provider() instead.".into(),
163 }),
164
165 ProviderConfig::Fireflies { .. } => Err(Error::ProviderUnsupported {
166 provider: "fireflies".into(),
167 operation: "Fireflies is a MeetingNotesProvider, not a Provider. Use create_meeting_notes_provider() instead.".into(),
168 }),
169
170 ProviderConfig::Slack { .. } => Err(Error::ProviderUnsupported {
171 provider: "slack".into(),
172 operation: "Slack is a MessengerProvider, not a Provider. Use create_messenger_provider() instead.".into(),
173 }),
174
175 ProviderConfig::Telegram { .. } => Err(Error::ProviderUnsupported {
176 provider: "telegram".into(),
177 operation: "Telegram is a MessengerProvider, not a Provider. Use create_messenger_provider() instead.".into(),
178 }),
179
180 ProviderConfig::Custom { name, .. } => Err(Error::ProviderNotFound(format!(
181 "custom provider '{name}' not yet supported"
182 ))),
183 }
184}
185
186pub fn create_knowledge_base_provider(
187 config: &ProviderConfig,
188 proxy: Option<&ProxyConfig>,
189) -> Result<Box<dyn KnowledgeBaseProvider>> {
190 match config {
191 ProviderConfig::Confluence {
192 base_url,
193 auth,
194 flavor,
195 cloud_id,
196 api_version,
197 scope: ConfluenceScope::Space { .. },
198 ..
199 } => {
200 let mut client = if let Some(proxy) = proxy {
201 devboy_confluence::ConfluenceClient::new(
202 &proxy.url,
203 devboy_confluence::ConfluenceAuth::None,
204 )
205 .with_api_version(api_version.as_deref())
206 .with_proxy(proxy.headers.clone())
207 .with_instance_url(base_url)
210 } else {
211 devboy_confluence::ConfluenceClient::new(base_url, confluence_auth(auth))
212 .with_api_version(api_version.as_deref())
213 };
214 if let Some(flavor) = flavor {
215 client = client.with_flavor(*flavor);
216 } else if cloud_id.is_some() {
217 client = client.with_flavor(devboy_confluence::ConfluenceFlavor::Cloud);
218 }
219 if let Some(cloud_id) = cloud_id.as_deref() {
220 client = client.with_cloud_id(cloud_id);
221 }
222 Ok(Box::new(client))
223 }
224 other => Err(Error::ProviderUnsupported {
225 provider: other.provider_name().into(),
226 operation: "not a knowledge base provider".into(),
227 }),
228 }
229}
230
231pub fn create_knowledge_base_enricher(config: &ProviderConfig) -> Option<Box<dyn ToolEnricher>> {
237 match config {
238 ProviderConfig::Confluence { .. } => {
239 Some(Box::new(devboy_confluence::ConfluenceSchemaEnricher::new()))
240 }
241 _ => None,
242 }
243}
244
245pub fn create_meeting_notes_provider(
250 config: &ProviderConfig,
251) -> Result<Box<dyn MeetingNotesProvider>> {
252 match config {
253 ProviderConfig::Fireflies { api_key, .. } => Ok(Box::new(
254 devboy_fireflies::FirefliesClient::new(api_key.clone()),
255 )),
256 other => Err(Error::ProviderUnsupported {
257 provider: other.provider_name().into(),
258 operation: "not a meeting notes provider".into(),
259 }),
260 }
261}
262
263pub fn create_messenger_provider(config: &ProviderConfig) -> Result<Box<dyn MessengerProvider>> {
264 match config {
265 ProviderConfig::Slack {
266 base_url,
267 access_token,
268 scope: SlackScope::Workspace { .. },
269 required_scopes,
270 ..
271 } => Ok(Box::new(
272 devboy_slack::SlackClient::new(access_token.clone())
273 .with_base_url(base_url)
274 .with_required_scopes(required_scopes.clone()),
275 )),
276 ProviderConfig::Telegram {
277 base_url,
278 access_token,
279 scope: TelegramScope::Bot { bot_username },
280 ..
281 } => Ok(Box::new(
282 devboy_telegram::TelegramClient::new(access_token.clone())
283 .with_base_url(base_url)
284 .with_bot_username(bot_username.clone()),
285 )),
286 other => Err(Error::ProviderUnsupported {
287 provider: other.provider_name().into(),
288 operation: "not a messenger provider".into(),
289 }),
290 }
291}
292
293pub fn create_enricher(
300 config: &ProviderConfig,
301 metadata: Option<&ProviderMetadata>,
302) -> Option<Box<dyn ToolEnricher>> {
303 match config {
304 ProviderConfig::GitLab { .. } => Some(Box::new(devboy_gitlab::GitLabSchemaEnricher)),
305 ProviderConfig::GitHub { .. } => Some(Box::new(devboy_github::GitHubSchemaEnricher)),
306 ProviderConfig::ClickUp { .. } => {
307 let meta = metadata?;
308 let clickup_meta: devboy_clickup::ClickUpMetadata =
309 serde_json::from_value(meta.data.clone()).ok()?;
310 Some(Box::new(devboy_clickup::ClickUpSchemaEnricher::new(
311 clickup_meta,
312 )))
313 }
314 ProviderConfig::Jira { .. } => {
315 let meta = metadata?;
316 let jira_meta: devboy_jira::JiraMetadata =
317 serde_json::from_value(meta.data.clone()).ok()?;
318 Some(Box::new(devboy_jira::JiraSchemaEnricher::new(jira_meta)))
319 }
320 ProviderConfig::Linear { .. } => {
321 if let Some(meta) = metadata {
322 let linear_meta: devboy_linear::LinearMetadata =
323 serde_json::from_value(meta.data.clone()).ok()?;
324 Some(Box::new(
325 devboy_linear::enricher::DynamicLinearSchemaEnricher::new(linear_meta),
326 ))
327 } else {
328 Some(Box::new(devboy_linear::LinearSchemaEnricher))
329 }
330 }
331 ProviderConfig::YouGile { .. } => {
332 let meta = metadata?;
333 let yougile_meta: devboy_yougile::YouGileMetadata =
334 serde_json::from_value(meta.data.clone()).ok()?;
335 Some(Box::new(devboy_yougile::YouGileSchemaEnricher::new(
336 yougile_meta,
337 )))
338 }
339 ProviderConfig::Confluence { .. } => None,
340 ProviderConfig::Fireflies { .. } => {
341 Some(Box::new(devboy_fireflies::FirefliesSchemaEnricher))
342 }
343 ProviderConfig::Slack { .. } => None,
344 ProviderConfig::Telegram { .. } => None,
345 ProviderConfig::Custom { .. } => None,
346 }
347}
348
349fn confluence_auth(auth: &ConfluenceAuthConfig) -> devboy_confluence::ConfluenceAuth {
350 match auth {
351 ConfluenceAuthConfig::BearerToken { token } => {
352 devboy_confluence::ConfluenceAuth::BearerToken(token.clone())
353 }
354 ConfluenceAuthConfig::Basic { username, password } => {
355 devboy_confluence::ConfluenceAuth::Basic {
356 username: username.clone(),
357 password: password.clone(),
358 }
359 }
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use crate::context::*;
367 use devboy_core::{IssueProvider, KnowledgeBaseProvider, MessengerProvider};
368 use httpmock::Method::GET;
369 use httpmock::MockServer;
370 use std::collections::HashMap;
371
372 #[test]
373 fn test_create_gitlab_project_provider() {
374 let config = ProviderConfig::GitLab {
375 base_url: "https://gitlab.com".into(),
376 access_token: "test-token".into(),
377 scope: GitLabScope::Project { id: "12345".into() },
378 extra: HashMap::new(),
379 };
380 let provider = create_provider(&config, None);
381 assert!(provider.is_ok());
382 assert_eq!(
383 IssueProvider::provider_name(provider.unwrap().as_ref()),
384 "gitlab"
385 );
386 }
387
388 #[test]
389 fn test_create_github_repo_provider() {
390 let config = ProviderConfig::GitHub {
391 base_url: "https://api.github.com".into(),
392 access_token: "ghp_test".into(),
393 scope: GitHubScope::Repository {
394 owner: "meteora-pro".into(),
395 repo: "devboy-tools".into(),
396 },
397 extra: HashMap::new(),
398 };
399 let provider = create_provider(&config, None);
400 assert!(provider.is_ok());
401 assert_eq!(
402 IssueProvider::provider_name(provider.unwrap().as_ref()),
403 "github"
404 );
405 }
406
407 #[test]
408 fn test_create_clickup_provider() {
409 let config = ProviderConfig::ClickUp {
410 access_token: "pk_test".into(),
411 scope: ClickUpScope::List {
412 id: "list123".into(),
413 team_id: Some("team456".into()),
414 },
415 extra: HashMap::new(),
416 };
417 let provider = create_provider(&config, None);
418 assert!(provider.is_ok());
419 assert_eq!(
420 IssueProvider::provider_name(provider.unwrap().as_ref()),
421 "clickup"
422 );
423 }
424
425 #[test]
426 fn test_create_telegram_provider() {
427 let config = ProviderConfig::Telegram {
428 base_url: "https://api.telegram.org".into(),
429 access_token: "bot-token".into(),
430 scope: TelegramScope::Bot {
431 bot_username: Some("devboy_bot".into()),
432 },
433 extra: HashMap::new(),
434 };
435 let provider = create_messenger_provider(&config);
436 assert!(provider.is_ok());
437 assert_eq!(
438 MessengerProvider::provider_name(provider.unwrap().as_ref()),
439 "telegram"
440 );
441 }
442
443 #[test]
444 fn test_create_jira_provider() {
445 let config = ProviderConfig::Jira {
446 base_url: "https://myorg.atlassian.net".into(),
447 access_token: "jira-token".into(),
448 email: "user@example.com".into(),
449 scope: JiraScope::Project { key: "PROJ".into() },
450 flavor: None,
451 extra: HashMap::new(),
452 };
453 let provider = create_provider(&config, None);
454 assert!(provider.is_ok());
455 assert_eq!(
456 IssueProvider::provider_name(provider.unwrap().as_ref()),
457 "jira"
458 );
459 }
460
461 #[test]
462 fn test_create_linear_provider() {
463 let config = ProviderConfig::Linear {
464 base_url: "https://api.linear.app/graphql".into(),
465 access_token: "lin_api_test".into(),
466 scope: LinearScope::Team {
467 id: "team-123".into(),
468 key: Some("ENG".into()),
469 },
470 extra: HashMap::new(),
471 };
472 let provider = create_provider(&config, None);
473 assert!(provider.is_ok());
474 assert_eq!(
475 IssueProvider::provider_name(provider.unwrap().as_ref()),
476 "linear"
477 );
478 }
479
480 #[test]
481 fn test_create_confluence_knowledge_base_provider() {
482 let config = ProviderConfig::Confluence {
483 base_url: "https://wiki.example.com".into(),
484 auth: ConfluenceAuthConfig::BearerToken {
485 token: "test-token".into(),
486 },
487 scope: ConfluenceScope::Space {
488 key: Some("ENG".into()),
489 },
490 flavor: None,
491 cloud_id: None,
492 api_version: Some("v1".into()),
493 extra: HashMap::new(),
494 };
495 let provider = create_knowledge_base_provider(&config, None);
496 assert!(provider.is_ok());
497 assert_eq!(
498 KnowledgeBaseProvider::provider_name(provider.unwrap().as_ref()),
499 "confluence"
500 );
501 }
502
503 #[test]
504 fn test_create_confluence_knowledge_base_enricher() {
505 let config = ProviderConfig::Confluence {
506 base_url: "https://wiki.example.com".into(),
507 auth: ConfluenceAuthConfig::BearerToken {
508 token: "test-token".into(),
509 },
510 scope: ConfluenceScope::Space {
511 key: Some("ENG".into()),
512 },
513 flavor: None,
514 cloud_id: None,
515 api_version: Some("v1".into()),
516 extra: HashMap::new(),
517 };
518 let enricher = create_knowledge_base_enricher(&config);
519 assert!(enricher.is_some());
520 assert_eq!(
521 enricher.unwrap().supported_categories(),
522 &[devboy_core::ToolCategory::KnowledgeBase]
523 );
524 }
525
526 #[test]
527 fn test_confluence_is_not_regular_provider() {
528 let config = ProviderConfig::Confluence {
529 base_url: "https://wiki.example.com".into(),
530 auth: ConfluenceAuthConfig::BearerToken {
531 token: "test-token".into(),
532 },
533 scope: ConfluenceScope::Space { key: None },
534 flavor: None,
535 cloud_id: None,
536 api_version: None,
537 extra: HashMap::new(),
538 };
539
540 let result = create_provider(&config, None);
541 assert!(matches!(
542 result,
543 Err(Error::ProviderUnsupported { provider, .. }) if provider == "confluence"
544 ));
545 }
546
547 #[test]
548 fn test_telegram_is_not_regular_provider() {
549 let config = ProviderConfig::Telegram {
550 base_url: "https://api.telegram.org".into(),
551 access_token: "bot-token".into(),
552 scope: TelegramScope::Bot {
553 bot_username: Some("devboy_bot".into()),
554 },
555 extra: HashMap::new(),
556 };
557
558 let result = create_provider(&config, None);
559 assert!(matches!(
560 result,
561 Err(Error::ProviderUnsupported { provider, .. }) if provider == "telegram"
562 ));
563 }
564
565 #[tokio::test]
566 async fn test_create_confluence_knowledge_base_provider_honors_api_version() {
567 let server = MockServer::start();
568 let mock = server.mock(|when, then| {
569 when.method(GET)
570 .path("/api/v2/space")
571 .query_param("limit", "100")
572 .query_param("type", "global,personal");
573 then.status(200)
574 .header("content-type", "application/json")
575 .body(r#"{"results":[],"start":0,"limit":100,"size":0,"_links":{}}"#);
576 });
577
578 let config = ProviderConfig::Confluence {
579 base_url: server.base_url(),
580 auth: ConfluenceAuthConfig::BearerToken {
581 token: "test-token".into(),
582 },
583 scope: ConfluenceScope::Space { key: None },
584 flavor: None,
585 cloud_id: None,
586 api_version: Some("v2".into()),
587 extra: HashMap::new(),
588 };
589
590 let provider = create_knowledge_base_provider(&config, None).unwrap();
591 let _ = provider.get_spaces().await.unwrap();
592
593 mock.assert();
594 }
595
596 #[tokio::test]
597 async fn test_create_confluence_knowledge_base_provider_honors_cloud_flavor() {
598 let server = MockServer::start();
599 let mock = server.mock(|when, then| {
600 when.method(GET)
601 .path("/wiki/rest/api/space")
605 .query_param("limit", "100")
606 .query_param("type", "global,personal");
607 then.status(200)
608 .header("content-type", "application/json")
609 .body(r#"{"results":[],"start":0,"limit":100,"size":0,"_links":{}}"#);
610 });
611
612 let config = ProviderConfig::Confluence {
613 base_url: server.base_url(),
614 auth: ConfluenceAuthConfig::BearerToken {
615 token: "test-token".into(),
616 },
617 scope: ConfluenceScope::Space { key: None },
618 flavor: Some(devboy_confluence::ConfluenceFlavor::Cloud),
619 cloud_id: Some("cloud-123".into()),
620 api_version: None,
621 extra: HashMap::new(),
622 };
623
624 let provider = create_knowledge_base_provider(&config, None).unwrap();
625 let _ = provider.get_spaces().await.unwrap();
626
627 mock.assert();
628 }
629
630 #[tokio::test]
631 async fn test_create_confluence_knowledge_base_provider_explicit_self_hosted_overrides_detection()
632 {
633 let server = MockServer::start();
634 let mock = server.mock(|when, then| {
635 when.method(GET)
636 .path("/rest/api/space")
637 .query_param("limit", "100")
638 .query_param("type", "global,personal");
639 then.status(200)
640 .header("content-type", "application/json")
641 .body(r#"{"results":[],"start":0,"limit":100,"size":0,"_links":{}}"#);
642 });
643
644 let config = ProviderConfig::Confluence {
645 base_url: "https://team.atlassian.net".into(),
646 auth: ConfluenceAuthConfig::BearerToken {
647 token: "test-token".into(),
648 },
649 scope: ConfluenceScope::Space { key: None },
650 flavor: Some(devboy_confluence::ConfluenceFlavor::SelfHosted),
651 cloud_id: None,
652 api_version: None,
653 extra: HashMap::new(),
654 };
655
656 let proxy = ProxyConfig {
657 url: server.base_url(),
658 headers: HashMap::new(),
659 };
660 let provider = create_knowledge_base_provider(&config, Some(&proxy)).unwrap();
661 let _ = provider.get_spaces().await.unwrap();
662
663 mock.assert();
664 }
665
666 #[test]
667 fn test_create_custom_provider_unsupported() {
668 let config = ProviderConfig::Custom {
669 name: "my-plugin".into(),
670 config: HashMap::new(),
671 };
672 let result = create_provider(&config, None);
673 assert!(result.is_err());
674 }
675
676 #[test]
677 fn test_gitlab_group_scope_unsupported() {
678 let config = ProviderConfig::GitLab {
679 base_url: "https://gitlab.com".into(),
680 access_token: "token".into(),
681 scope: GitLabScope::Group {
682 id: "group1".into(),
683 },
684 extra: HashMap::new(),
685 };
686 let result = create_provider(&config, None);
687 assert!(result.is_err());
688 }
689
690 #[test]
691 fn test_create_enricher_gitlab_static() {
692 let config = ProviderConfig::GitLab {
693 base_url: "https://gitlab.com".into(),
694 access_token: "token".into(),
695 scope: GitLabScope::Project { id: "123".into() },
696 extra: HashMap::new(),
697 };
698 let enricher = create_enricher(&config, None);
699 assert!(enricher.is_some());
700 }
701
702 #[test]
703 fn test_create_enricher_github_static() {
704 let config = ProviderConfig::GitHub {
705 base_url: "https://api.github.com".into(),
706 access_token: "token".into(),
707 scope: GitHubScope::Repository {
708 owner: "test".into(),
709 repo: "test".into(),
710 },
711 extra: HashMap::new(),
712 };
713 let enricher = create_enricher(&config, None);
714 assert!(enricher.is_some());
715 }
716
717 #[test]
718 fn test_create_enricher_clickup_needs_metadata() {
719 let config = ProviderConfig::ClickUp {
720 access_token: "token".into(),
721 scope: ClickUpScope::List {
722 id: "list1".into(),
723 team_id: None,
724 },
725 extra: HashMap::new(),
726 };
727 assert!(create_enricher(&config, None).is_none());
729
730 let meta = ProviderMetadata::new(serde_json::json!({
732 "statuses": [{ "name": "To Do" }],
733 "custom_fields": []
734 }));
735 assert!(create_enricher(&config, Some(&meta)).is_some());
736 }
737
738 #[test]
739 fn test_create_enricher_jira_needs_metadata() {
740 let config = ProviderConfig::Jira {
741 base_url: "https://test.atlassian.net".into(),
742 access_token: "token".into(),
743 email: "test@test.com".into(),
744 scope: JiraScope::Project { key: "PROJ".into() },
745 flavor: None,
746 extra: HashMap::new(),
747 };
748 assert!(create_enricher(&config, None).is_none());
750
751 let meta = ProviderMetadata::new(serde_json::json!({
753 "flavor": "cloud",
754 "projects": {
755 "PROJ": {
756 "issue_types": [],
757 "priorities": [],
758 "components": [],
759 "link_types": [],
760 "custom_fields": []
761 }
762 }
763 }));
764 assert!(create_enricher(&config, Some(&meta)).is_some());
765 }
766
767 #[test]
768 fn test_create_enricher_linear_uses_metadata_when_present() {
769 let config = ProviderConfig::Linear {
770 base_url: "https://api.linear.app/graphql".into(),
771 access_token: "token".into(),
772 scope: LinearScope::Team {
773 id: "team-1".into(),
774 key: Some("ENG".into()),
775 },
776 extra: HashMap::new(),
777 };
778
779 assert!(create_enricher(&config, None).is_some());
780
781 let meta = ProviderMetadata::new(serde_json::json!({
782 "statuses": [
783 { "id": "1", "name": "Backlog", "category": "backlog" },
784 { "id": "2", "name": "In Review", "category": "in_progress" }
785 ]
786 }));
787 assert!(create_enricher(&config, Some(&meta)).is_some());
788 }
789
790 #[test]
793 fn test_create_gitlab_provider_with_proxy() {
794 let config = ProviderConfig::GitLab {
795 base_url: "https://gitlab.internal.com".into(),
796 access_token: "test-token".into(),
797 scope: GitLabScope::Project { id: "99".into() },
798 extra: HashMap::new(),
799 };
800 let mut headers = HashMap::new();
801 headers.insert("X-Proxy-Auth".into(), "proxy-secret".into());
802 let proxy = ProxyConfig {
803 url: "https://proxy.example.com/gitlab".into(),
804 headers,
805 };
806 let provider = create_provider(&config, Some(&proxy));
807 assert!(provider.is_ok());
808 assert_eq!(
809 IssueProvider::provider_name(provider.unwrap().as_ref()),
810 "gitlab"
811 );
812 }
813
814 #[test]
815 fn test_create_jira_provider_with_proxy() {
816 let config = ProviderConfig::Jira {
817 base_url: "https://jira.mycompany.com".into(),
818 access_token: "jira-token".into(),
819 email: "dev@mycompany.com".into(),
820 scope: JiraScope::Project { key: "DEV".into() },
821 flavor: None,
822 extra: HashMap::new(),
823 };
824 let mut headers = HashMap::new();
825 headers.insert("X-Proxy-Auth".into(), "secret".into());
826 headers.insert("X-Route".into(), "jira-dc".into());
827 let proxy = ProxyConfig {
828 url: "https://proxy.internal/jira".into(),
829 headers,
830 };
831 let provider = create_provider(&config, Some(&proxy));
832 assert!(provider.is_ok());
833 assert_eq!(
834 IssueProvider::provider_name(provider.unwrap().as_ref()),
835 "jira"
836 );
837 }
838
839 #[test]
840 fn test_create_jira_provider_with_flavor_cloud() {
841 let config = ProviderConfig::Jira {
842 base_url: "https://myorg.atlassian.net".into(),
843 access_token: "tok".into(),
844 email: "a@b.com".into(),
845 scope: JiraScope::Project { key: "CLD".into() },
846 flavor: Some(devboy_jira::JiraFlavor::Cloud),
847 extra: HashMap::new(),
848 };
849 let provider = create_provider(&config, None);
850 assert!(provider.is_ok());
851 }
852
853 #[test]
854 fn test_create_jira_provider_with_flavor_self_hosted() {
855 let config = ProviderConfig::Jira {
856 base_url: "https://jira.local".into(),
857 access_token: "tok".into(),
858 email: "a@b.com".into(),
859 scope: JiraScope::Project { key: "SH".into() },
860 flavor: Some(devboy_jira::JiraFlavor::SelfHosted),
861 extra: HashMap::new(),
862 };
863 let provider = create_provider(&config, None);
864 assert!(provider.is_ok());
865 }
866
867 #[test]
868 fn test_create_jira_provider_with_proxy_and_flavor_override() {
869 let config = ProviderConfig::Jira {
870 base_url: "https://jira.dc.mycompany.com".into(),
871 access_token: "tok".into(),
872 email: "a@b.com".into(),
873 scope: JiraScope::Project { key: "DC".into() },
874 flavor: Some(devboy_jira::JiraFlavor::SelfHosted),
875 extra: HashMap::new(),
876 };
877 let proxy = ProxyConfig {
878 url: "https://proxy.internal/jira".into(),
879 headers: HashMap::new(),
880 };
881 let provider = create_provider(&config, Some(&proxy));
882 assert!(provider.is_ok());
883 }
884
885 #[test]
888 fn test_gitlab_global_scope_unsupported() {
889 let config = ProviderConfig::GitLab {
890 base_url: "https://gitlab.com".into(),
891 access_token: "tok".into(),
892 scope: GitLabScope::Global,
893 extra: HashMap::new(),
894 };
895 let result = create_provider(&config, None);
896 match result {
897 Err(e) => assert!(e.to_string().contains("global scope")),
898 Ok(_) => panic!("expected error for global scope"),
899 }
900 }
901
902 #[test]
903 fn test_github_organization_scope_unsupported() {
904 let config = ProviderConfig::GitHub {
905 base_url: "https://api.github.com".into(),
906 access_token: "tok".into(),
907 scope: GitHubScope::Organization {
908 name: "myorg".into(),
909 },
910 extra: HashMap::new(),
911 };
912 let result = create_provider(&config, None);
913 match result {
914 Err(e) => assert!(e.to_string().contains("organization scope")),
915 Ok(_) => panic!("expected error for organization scope"),
916 }
917 }
918
919 #[test]
920 fn test_github_global_scope_unsupported() {
921 let config = ProviderConfig::GitHub {
922 base_url: "https://api.github.com".into(),
923 access_token: "tok".into(),
924 scope: GitHubScope::Global,
925 extra: HashMap::new(),
926 };
927 let result = create_provider(&config, None);
928 assert!(result.is_err());
929 }
930
931 #[test]
932 fn test_jira_multi_project_scope_unsupported() {
933 let config = ProviderConfig::Jira {
934 base_url: "https://test.atlassian.net".into(),
935 access_token: "tok".into(),
936 email: "a@b.com".into(),
937 scope: JiraScope::MultiProject {
938 keys: vec!["A".into(), "B".into()],
939 },
940 flavor: None,
941 extra: HashMap::new(),
942 };
943 let result = create_provider(&config, None);
944 match result {
945 Err(e) => assert!(e.to_string().contains("multi-project")),
946 Ok(_) => panic!("expected error for multi-project scope"),
947 }
948 }
949
950 #[test]
953 fn test_create_clickup_provider_without_team_id() {
954 let config = ProviderConfig::ClickUp {
955 access_token: "pk_test".into(),
956 scope: ClickUpScope::List {
957 id: "list999".into(),
958 team_id: None,
959 },
960 extra: HashMap::new(),
961 };
962 let provider = create_provider(&config, None);
963 assert!(provider.is_ok());
964 assert_eq!(
965 IssueProvider::provider_name(provider.unwrap().as_ref()),
966 "clickup"
967 );
968 }
969
970 #[test]
971 fn test_create_yougile_provider() {
972 let config = ProviderConfig::YouGile {
973 base_url: "https://yougile.com/api-v2".into(),
974 access_token: "tok".into(),
975 scope: YouGileScope::Board {
976 id: "board-123".into(),
977 },
978 extra: HashMap::new(),
979 };
980 let provider = create_provider(&config, None);
981 assert!(provider.is_ok());
982 assert_eq!(
983 IssueProvider::provider_name(provider.unwrap().as_ref()),
984 "yougile"
985 );
986 }
987
988 #[test]
991 fn test_create_enricher_custom_returns_none() {
992 let config = ProviderConfig::Custom {
993 name: "custom-plugin".into(),
994 config: HashMap::new(),
995 };
996 assert!(create_enricher(&config, None).is_none());
997 }
998
999 #[test]
1000 fn test_create_enricher_telegram_returns_none() {
1001 let config = ProviderConfig::Telegram {
1002 base_url: "https://api.telegram.org".into(),
1003 access_token: "bot-token".into(),
1004 scope: TelegramScope::Bot {
1005 bot_username: Some("devboy_bot".into()),
1006 },
1007 extra: HashMap::new(),
1008 };
1009 assert!(create_enricher(&config, None).is_none());
1010 }
1011
1012 #[test]
1013 fn test_create_knowledge_base_enricher_non_kb_returns_none() {
1014 let config = ProviderConfig::GitHub {
1015 base_url: "https://api.github.com".into(),
1016 access_token: "tok".into(),
1017 scope: GitHubScope::Repository {
1018 owner: "test".into(),
1019 repo: "test".into(),
1020 },
1021 extra: HashMap::new(),
1022 };
1023 assert!(create_knowledge_base_enricher(&config).is_none());
1024 }
1025
1026 #[test]
1029 fn test_create_enricher_clickup_invalid_metadata_returns_none() {
1030 let config = ProviderConfig::ClickUp {
1031 access_token: "token".into(),
1032 scope: ClickUpScope::List {
1033 id: "list1".into(),
1034 team_id: None,
1035 },
1036 extra: HashMap::new(),
1037 };
1038 let meta = ProviderMetadata::new(serde_json::json!("invalid_data"));
1039 assert!(create_enricher(&config, Some(&meta)).is_none());
1040 }
1041
1042 #[test]
1043 fn test_create_enricher_jira_invalid_metadata_returns_none() {
1044 let config = ProviderConfig::Jira {
1045 base_url: "https://test.atlassian.net".into(),
1046 access_token: "token".into(),
1047 email: "a@b.com".into(),
1048 scope: JiraScope::Project { key: "X".into() },
1049 flavor: None,
1050 extra: HashMap::new(),
1051 };
1052 let meta = ProviderMetadata::new(serde_json::json!("not_valid"));
1053 assert!(create_enricher(&config, Some(&meta)).is_none());
1054 }
1055}