Skip to main content

busbar_sf_tooling/
types.rs

1//! Types for Salesforce Tooling API.
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5/// Deserialize a value that may be `null` or missing as `T::default()`.
6///
7/// Serde's `#[serde(default)]` only handles missing keys. This also handles
8/// explicit `null` values from the Salesforce API (e.g., `"parameters": null`
9/// instead of `"parameters": []`).
10fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
11where
12    D: Deserializer<'de>,
13    T: Default + Deserialize<'de>,
14{
15    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
16}
17
18// ============================================================================
19// Search Types
20// ============================================================================
21
22/// Result of a SOSL search query.
23#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct SearchResult<T> {
25    /// The search results.
26    #[serde(rename = "searchRecords")]
27    pub search_records: Vec<T>,
28}
29
30// ============================================================================
31// Execute Anonymous Types
32// ============================================================================
33
34/// Result of executing anonymous Apex.
35#[derive(Debug, Clone, Deserialize, Serialize)]
36pub struct ExecuteAnonymousResult {
37    /// Whether the code compiled successfully.
38    #[serde(default)]
39    pub compiled: bool,
40
41    /// Line number of compilation error (if any).
42    #[serde(rename = "compileProblem")]
43    pub compile_problem: Option<String>,
44
45    /// Whether the execution was successful.
46    #[serde(default)]
47    pub success: bool,
48
49    /// Line number where exception occurred.
50    #[serde(rename = "exceptionStackTrace")]
51    pub exception_stack_trace: Option<String>,
52
53    /// Exception message.
54    #[serde(rename = "exceptionMessage")]
55    pub exception_message: Option<String>,
56
57    /// The column number of the error.
58    pub column: Option<i32>,
59
60    /// The line number of the error.
61    pub line: Option<i32>,
62}
63
64// ============================================================================
65// Apex Class Types
66// ============================================================================
67
68/// ApexClass record from Tooling API.
69#[derive(Debug, Clone, Deserialize, Serialize)]
70pub struct ApexClass {
71    #[serde(rename = "Id")]
72    pub id: String,
73
74    #[serde(rename = "Name")]
75    pub name: String,
76
77    #[serde(rename = "Body")]
78    pub body: Option<String>,
79
80    #[serde(rename = "Status")]
81    pub status: Option<String>,
82
83    #[serde(rename = "IsValid")]
84    pub is_valid: Option<bool>,
85
86    #[serde(rename = "ApiVersion")]
87    pub api_version: Option<f64>,
88
89    #[serde(rename = "LengthWithoutComments")]
90    pub length_without_comments: Option<i32>,
91
92    #[serde(rename = "NamespacePrefix")]
93    pub namespace_prefix: Option<String>,
94
95    #[serde(rename = "CreatedDate")]
96    pub created_date: Option<String>,
97
98    #[serde(rename = "LastModifiedDate")]
99    pub last_modified_date: Option<String>,
100}
101
102/// ApexTrigger record from Tooling API.
103#[derive(Debug, Clone, Deserialize, Serialize)]
104pub struct ApexTrigger {
105    #[serde(rename = "Id")]
106    pub id: String,
107
108    #[serde(rename = "Name")]
109    pub name: String,
110
111    #[serde(rename = "Body")]
112    pub body: Option<String>,
113
114    #[serde(rename = "Status")]
115    pub status: Option<String>,
116
117    #[serde(rename = "IsValid")]
118    pub is_valid: Option<bool>,
119
120    #[serde(rename = "ApiVersion")]
121    pub api_version: Option<f64>,
122
123    #[serde(rename = "TableEnumOrId")]
124    pub table_enum_or_id: Option<String>,
125
126    #[serde(rename = "UsageBeforeInsert")]
127    pub usage_before_insert: Option<bool>,
128
129    #[serde(rename = "UsageAfterInsert")]
130    pub usage_after_insert: Option<bool>,
131
132    #[serde(rename = "UsageBeforeUpdate")]
133    pub usage_before_update: Option<bool>,
134
135    #[serde(rename = "UsageAfterUpdate")]
136    pub usage_after_update: Option<bool>,
137
138    #[serde(rename = "UsageBeforeDelete")]
139    pub usage_before_delete: Option<bool>,
140
141    #[serde(rename = "UsageAfterDelete")]
142    pub usage_after_delete: Option<bool>,
143
144    #[serde(rename = "UsageAfterUndelete")]
145    pub usage_after_undelete: Option<bool>,
146}
147
148// ============================================================================
149// Debug Log Types
150// ============================================================================
151
152/// ApexLog record from Tooling API.
153#[derive(Debug, Clone, Deserialize, Serialize)]
154pub struct ApexLog {
155    #[serde(rename = "Id")]
156    pub id: String,
157
158    #[serde(rename = "LogUser")]
159    pub log_user: Option<LogUser>,
160
161    #[serde(rename = "LogUserId")]
162    pub log_user_id: Option<String>,
163
164    #[serde(rename = "LogLength")]
165    pub log_length: Option<i64>,
166
167    #[serde(rename = "LastModifiedDate")]
168    pub last_modified_date: Option<String>,
169
170    #[serde(rename = "StartTime")]
171    pub start_time: Option<String>,
172
173    #[serde(rename = "Status")]
174    pub status: Option<String>,
175
176    #[serde(rename = "Operation")]
177    pub operation: Option<String>,
178
179    #[serde(rename = "Request")]
180    pub request: Option<String>,
181
182    #[serde(rename = "Application")]
183    pub application: Option<String>,
184
185    #[serde(rename = "DurationMilliseconds")]
186    pub duration_milliseconds: Option<i64>,
187
188    #[serde(rename = "Location")]
189    pub location: Option<String>,
190}
191
192/// LogUser reference.
193#[derive(Debug, Clone, Deserialize, Serialize)]
194pub struct LogUser {
195    #[serde(rename = "Name")]
196    pub name: Option<String>,
197}
198
199// ============================================================================
200// Trace Flag Types
201// ============================================================================
202
203/// TraceFlag record from Tooling API.
204#[derive(Debug, Clone, Deserialize, Serialize)]
205pub struct TraceFlag {
206    #[serde(rename = "Id")]
207    pub id: Option<String>,
208
209    #[serde(rename = "TracedEntityId")]
210    pub traced_entity_id: String,
211
212    #[serde(rename = "LogType")]
213    pub log_type: String,
214
215    #[serde(rename = "DebugLevelId")]
216    pub debug_level_id: String,
217
218    #[serde(rename = "StartDate")]
219    pub start_date: Option<String>,
220
221    #[serde(rename = "ExpirationDate")]
222    pub expiration_date: Option<String>,
223}
224
225/// DebugLevel record from Tooling API.
226#[derive(Debug, Clone, Deserialize, Serialize)]
227pub struct DebugLevel {
228    #[serde(rename = "Id")]
229    pub id: Option<String>,
230
231    #[serde(rename = "DeveloperName")]
232    pub developer_name: String,
233
234    #[serde(rename = "MasterLabel")]
235    pub master_label: String,
236
237    #[serde(rename = "ApexCode")]
238    pub apex_code: Option<String>,
239
240    #[serde(rename = "ApexProfiling")]
241    pub apex_profiling: Option<String>,
242
243    #[serde(rename = "Callout")]
244    pub callout: Option<String>,
245
246    #[serde(rename = "Database")]
247    pub database: Option<String>,
248
249    #[serde(rename = "System")]
250    pub system: Option<String>,
251
252    #[serde(rename = "Validation")]
253    pub validation: Option<String>,
254
255    #[serde(rename = "Visualforce")]
256    pub visualforce: Option<String>,
257
258    #[serde(rename = "Workflow")]
259    pub workflow: Option<String>,
260}
261
262/// Debug log level options.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum LogLevel {
265    None,
266    Error,
267    Warn,
268    Info,
269    Debug,
270    Fine,
271    Finer,
272    Finest,
273}
274
275impl std::fmt::Display for LogLevel {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        match self {
278            LogLevel::None => write!(f, "NONE"),
279            LogLevel::Error => write!(f, "ERROR"),
280            LogLevel::Warn => write!(f, "WARN"),
281            LogLevel::Info => write!(f, "INFO"),
282            LogLevel::Debug => write!(f, "DEBUG"),
283            LogLevel::Fine => write!(f, "FINE"),
284            LogLevel::Finer => write!(f, "FINER"),
285            LogLevel::Finest => write!(f, "FINEST"),
286        }
287    }
288}
289
290// ============================================================================
291// Code Coverage Types
292// ============================================================================
293
294/// ApexCodeCoverage record from Tooling API.
295#[derive(Debug, Clone, Deserialize, Serialize)]
296pub struct ApexCodeCoverage {
297    #[serde(rename = "Id")]
298    pub id: String,
299
300    #[serde(rename = "ApexClassOrTriggerId")]
301    pub apex_class_or_trigger_id: String,
302
303    #[serde(rename = "ApexClassOrTrigger")]
304    pub apex_class_or_trigger: Option<ApexClassOrTriggerRef>,
305
306    #[serde(rename = "TestMethodName")]
307    pub test_method_name: Option<String>,
308
309    #[serde(rename = "NumLinesCovered")]
310    pub num_lines_covered: Option<i32>,
311
312    #[serde(rename = "NumLinesUncovered")]
313    pub num_lines_uncovered: Option<i32>,
314
315    #[serde(rename = "Coverage")]
316    pub coverage: Option<CoverageDetail>,
317}
318
319/// Reference to ApexClass or ApexTrigger.
320#[derive(Debug, Clone, Deserialize, Serialize)]
321pub struct ApexClassOrTriggerRef {
322    #[serde(rename = "Name")]
323    pub name: Option<String>,
324}
325
326/// Coverage detail with covered and uncovered lines.
327#[derive(Debug, Clone, Deserialize, Serialize)]
328pub struct CoverageDetail {
329    #[serde(rename = "coveredLines", default)]
330    pub covered_lines: Vec<i32>,
331
332    #[serde(rename = "uncoveredLines", default)]
333    pub uncovered_lines: Vec<i32>,
334}
335
336/// ApexCodeCoverageAggregate record.
337#[derive(Debug, Clone, Deserialize, Serialize)]
338pub struct ApexCodeCoverageAggregate {
339    #[serde(rename = "Id")]
340    pub id: String,
341
342    #[serde(rename = "ApexClassOrTriggerId")]
343    pub apex_class_or_trigger_id: String,
344
345    #[serde(rename = "ApexClassOrTrigger")]
346    pub apex_class_or_trigger: Option<ApexClassOrTriggerRef>,
347
348    #[serde(rename = "NumLinesCovered")]
349    pub num_lines_covered: i32,
350
351    #[serde(rename = "NumLinesUncovered")]
352    pub num_lines_uncovered: i32,
353
354    #[serde(rename = "Coverage")]
355    pub coverage: Option<CoverageDetail>,
356}
357
358// ============================================================================
359// Test Execution Types
360// ============================================================================
361
362/// Request for running tests asynchronously.
363///
364/// The Salesforce `runTestsAsynchronous` endpoint accepts comma-separated
365/// strings for `classids`, `classNames`, `suiteids`, and `suiteNames`.
366#[derive(Debug, Clone, Default, Serialize, Deserialize)]
367pub struct RunTestsAsyncRequest {
368    /// Comma-separated list of test class IDs to run.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    #[serde(rename = "classids")]
371    pub class_ids: Option<String>,
372
373    /// Comma-separated list of test class names to run (alternative to class_ids).
374    #[serde(skip_serializing_if = "Option::is_none")]
375    #[serde(rename = "classNames")]
376    pub class_names: Option<String>,
377
378    /// Comma-separated list of test suite IDs to run.
379    #[serde(skip_serializing_if = "Option::is_none")]
380    #[serde(rename = "suiteids")]
381    pub suite_ids: Option<String>,
382
383    /// Comma-separated list of test suite names to run (alternative to suite_ids).
384    #[serde(skip_serializing_if = "Option::is_none")]
385    #[serde(rename = "suiteNames")]
386    pub suite_names: Option<String>,
387
388    /// Maximum number of failed tests before stopping (-1 for no limit).
389    #[serde(skip_serializing_if = "Option::is_none")]
390    #[serde(rename = "maxFailedTests")]
391    pub max_failed_tests: Option<i32>,
392
393    /// Test level: RunSpecifiedTests, RunLocalTests, RunAllTestsInOrg, etc.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    #[serde(rename = "testLevel")]
396    pub test_level: Option<String>,
397
398    /// Whether to skip code coverage calculation.
399    #[serde(skip_serializing_if = "Option::is_none")]
400    #[serde(rename = "skipCodeCoverage")]
401    pub skip_code_coverage: Option<bool>,
402}
403
404/// A test class descriptor for synchronous test execution.
405///
406/// Salesforce `runTestsSynchronous` expects objects with `className` and
407/// optional `testMethods`, not plain class name strings.
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct SyncTestItem {
410    /// The Apex test class name (required).
411    #[serde(rename = "className")]
412    pub class_name: String,
413
414    /// Specific test methods to run. If `None`, all test methods are executed.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    #[serde(rename = "testMethods")]
417    pub test_methods: Option<Vec<String>>,
418
419    /// Namespace prefix (for managed packages).
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub namespace: Option<String>,
422}
423
424/// Request for running tests synchronously.
425///
426/// The `tests` field is an array of [`SyncTestItem`] objects containing
427/// `className` and optional `testMethods`. Only one test class is allowed
428/// per synchronous request.
429#[derive(Debug, Clone, Default, Serialize, Deserialize)]
430pub struct RunTestsSyncRequest {
431    /// Test class descriptors to run.
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub tests: Option<Vec<SyncTestItem>>,
434
435    /// Maximum number of failed tests before stopping (-1 for no limit).
436    #[serde(skip_serializing_if = "Option::is_none")]
437    #[serde(rename = "maxFailedTests")]
438    pub max_failed_tests: Option<i32>,
439
440    /// Whether to skip code coverage calculation.
441    #[serde(skip_serializing_if = "Option::is_none")]
442    #[serde(rename = "skipCodeCoverage")]
443    pub skip_code_coverage: Option<bool>,
444}
445
446/// Result from running tests synchronously.
447#[derive(Debug, Clone, Default, Deserialize, Serialize)]
448#[serde(default)]
449pub struct RunTestsSyncResult {
450    #[serde(alias = "numTestsRun", default)]
451    pub num_tests_run: u32,
452
453    #[serde(alias = "numFailures", default)]
454    pub num_failures: u32,
455
456    #[serde(alias = "totalTime", default)]
457    pub total_time: f64,
458
459    #[serde(default)]
460    pub successes: Vec<TestSuccess>,
461
462    #[serde(default)]
463    pub failures: Vec<TestFailure>,
464
465    #[serde(alias = "codeCoverage", default)]
466    pub code_coverage: Vec<CodeCoverageResult>,
467
468    #[serde(alias = "codeCoverageWarnings", default)]
469    pub code_coverage_warnings: Vec<CodeCoverageWarning>,
470}
471
472/// Successful test result.
473#[derive(Debug, Clone, Default, Deserialize, Serialize)]
474#[serde(default)]
475pub struct TestSuccess {
476    #[serde(alias = "Id", alias = "id", default)]
477    pub id: String,
478
479    #[serde(alias = "MethodName", alias = "methodName", default)]
480    pub method_name: String,
481
482    #[serde(alias = "Name", alias = "name", default)]
483    pub name: String,
484
485    #[serde(alias = "NamespacePrefix", alias = "namespacePrefix")]
486    pub namespace_prefix: Option<String>,
487
488    #[serde(alias = "Time", alias = "time", default)]
489    pub time: f64,
490}
491
492/// Failed test result.
493#[derive(Debug, Clone, Default, Deserialize, Serialize)]
494#[serde(default)]
495pub struct TestFailure {
496    #[serde(alias = "Id", alias = "id", default)]
497    pub id: String,
498
499    #[serde(alias = "MethodName", alias = "methodName", default)]
500    pub method_name: String,
501
502    #[serde(alias = "Name", alias = "name", default)]
503    pub name: String,
504
505    #[serde(alias = "NamespacePrefix", alias = "namespacePrefix")]
506    pub namespace_prefix: Option<String>,
507
508    #[serde(alias = "Time", alias = "time", default)]
509    pub time: f64,
510
511    #[serde(alias = "Message", alias = "message", default)]
512    pub message: String,
513
514    #[serde(alias = "StackTrace", alias = "stackTrace")]
515    pub stack_trace: Option<String>,
516
517    #[serde(alias = "Type", rename = "type")]
518    pub failure_type: Option<String>,
519}
520
521/// Code coverage result for a single class or trigger.
522#[derive(Debug, Clone, Default, Deserialize, Serialize)]
523#[serde(default)]
524pub struct CodeCoverageResult {
525    #[serde(default)]
526    pub id: String,
527    #[serde(default)]
528    pub name: String,
529    pub namespace: Option<String>,
530
531    #[serde(alias = "numLocations", default)]
532    pub num_locations: u32,
533
534    #[serde(alias = "numLocationsNotCovered", default)]
535    pub num_locations_not_covered: u32,
536
537    #[serde(rename = "type", default)]
538    pub coverage_type: String,
539
540    #[serde(alias = "locationsNotCovered", default)]
541    pub locations_not_covered: Vec<CodeLocation>,
542}
543
544/// Code location (line number and column).
545#[derive(Debug, Clone, Default, Deserialize, Serialize)]
546#[serde(default)]
547pub struct CodeLocation {
548    #[serde(default)]
549    pub line: u32,
550    #[serde(default)]
551    pub column: u32,
552
553    #[serde(alias = "numExecutions", default)]
554    pub num_executions: u32,
555
556    #[serde(default)]
557    pub time: f64,
558}
559
560/// Code coverage warning.
561#[derive(Debug, Clone, Deserialize, Serialize)]
562pub struct CodeCoverageWarning {
563    pub message: String,
564    pub name: Option<String>,
565    pub namespace: Option<String>,
566}
567
568// ============================================================================
569// Test Discovery Types (v65.0+)
570// ============================================================================
571
572/// Result from Test Discovery API.
573#[derive(Debug, Clone, Deserialize, Serialize)]
574pub struct TestDiscoveryResult {
575    pub tests: Vec<TestItem>,
576}
577
578/// A single test item (Apex test or Flow test).
579#[derive(Debug, Clone, Deserialize, Serialize)]
580pub struct TestItem {
581    pub id: String,
582    pub name: String,
583
584    #[serde(rename = "className")]
585    pub class_name: Option<String>,
586
587    pub namespace: Option<String>,
588    pub category: String,
589}
590
591// ============================================================================
592// Test Runner Types (v65.0+)
593// ============================================================================
594
595/// Request for the unified Test Runner API (v65.0+).
596#[derive(Debug, Clone, Default, Serialize, Deserialize)]
597pub struct RunTestsRequest {
598    #[serde(skip_serializing_if = "Option::is_none")]
599    #[serde(rename = "classIds")]
600    pub class_ids: Option<Vec<String>>,
601
602    #[serde(skip_serializing_if = "Option::is_none")]
603    #[serde(rename = "suiteIds")]
604    pub suite_ids: Option<Vec<String>>,
605
606    #[serde(skip_serializing_if = "Option::is_none")]
607    #[serde(rename = "testIds")]
608    pub test_ids: Option<Vec<String>>,
609
610    #[serde(skip_serializing_if = "Option::is_none")]
611    #[serde(rename = "testLevel")]
612    pub test_level: Option<String>,
613
614    #[serde(skip_serializing_if = "Option::is_none")]
615    #[serde(rename = "skipCodeCoverage")]
616    pub skip_code_coverage: Option<bool>,
617}
618
619/// Response from the unified Test Runner API (v65.0+).
620#[derive(Debug, Clone, Deserialize, Serialize)]
621pub struct RunTestsResponse {
622    /// Test run ID for tracking results.
623    #[serde(rename = "testRunId")]
624    pub test_run_id: String,
625}
626
627// ============================================================================
628// Code Intelligence Types
629// ============================================================================
630
631/// Result from the completions endpoint.
632///
633/// The Salesforce completions API returns `{ "publicDeclarations": { "ClassName": [...], ... } }`
634/// where each key is a class/namespace name mapping to its members.
635#[derive(Debug, Clone, Deserialize, Serialize)]
636pub struct CompletionsResult {
637    #[serde(rename = "publicDeclarations")]
638    pub public_declarations: std::collections::HashMap<String, Vec<CompletionItem>>,
639}
640
641/// A single completion item.
642#[derive(Debug, Clone, Default, Deserialize, Serialize)]
643#[serde(default)]
644pub struct CompletionItem {
645    pub name: String,
646
647    #[serde(rename = "type")]
648    pub symbol_type: Option<String>,
649
650    pub namespace: Option<String>,
651
652    pub signature: Option<String>,
653
654    #[serde(rename = "returnType")]
655    pub return_type: Option<String>,
656
657    #[serde(default, deserialize_with = "null_as_default")]
658    pub parameters: Vec<Parameter>,
659
660    #[serde(default, deserialize_with = "null_as_default")]
661    pub references: Vec<Reference>,
662}
663
664/// Method parameter information.
665#[derive(Debug, Clone, Default, Deserialize, Serialize)]
666#[serde(default)]
667pub struct Parameter {
668    pub name: String,
669
670    #[serde(rename = "type")]
671    pub param_type: Option<String>,
672}
673
674/// Reference to documentation or related types.
675#[derive(Debug, Clone, Default, Deserialize, Serialize)]
676#[serde(default)]
677pub struct Reference {
678    pub name: String,
679
680    #[serde(rename = "type")]
681    pub ref_type: Option<String>,
682}
683
684// ============================================================================
685// Metadata Component Dependency (Beta)
686// ============================================================================
687
688#[cfg(feature = "dependencies")]
689pub use busbar_sf_client::MetadataComponentDependency;
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694
695    #[test]
696    fn test_execute_anonymous_result_deser() {
697        let json = r#"{
698            "compiled": true,
699            "success": true
700        }"#;
701
702        let result: ExecuteAnonymousResult = serde_json::from_str(json).unwrap();
703        assert!(result.compiled);
704        assert!(result.success);
705    }
706
707    #[test]
708    fn test_apex_class_deser() {
709        let json = r#"{
710            "Id": "01pxx00000000001AAA",
711            "Name": "TestClass",
712            "Status": "Active",
713            "IsValid": true,
714            "ApiVersion": 62.0
715        }"#;
716
717        let class: ApexClass = serde_json::from_str(json).unwrap();
718        assert_eq!(class.name, "TestClass");
719        assert_eq!(class.is_valid, Some(true));
720        assert_eq!(class.api_version, Some(62.0));
721    }
722
723    #[test]
724    fn test_apex_log_deser() {
725        let json = r#"{
726            "Id": "07Lxx00000000001AAA",
727            "LogLength": 12345,
728            "Status": "Success",
729            "Operation": "Apex",
730            "DurationMilliseconds": 150
731        }"#;
732
733        let log: ApexLog = serde_json::from_str(json).unwrap();
734        assert_eq!(log.id, "07Lxx00000000001AAA");
735        assert_eq!(log.log_length, Some(12345));
736        assert_eq!(log.duration_milliseconds, Some(150));
737    }
738
739    #[test]
740    fn test_log_level_display() {
741        assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
742        assert_eq!(LogLevel::Finest.to_string(), "FINEST");
743    }
744
745    #[test]
746    fn test_run_tests_async_request_ser() {
747        let req = RunTestsAsyncRequest {
748            class_names: Some("TestClass1".to_string()),
749            test_level: Some("RunSpecifiedTests".to_string()),
750            skip_code_coverage: Some(false),
751            ..Default::default()
752        };
753
754        let json = serde_json::to_string(&req).unwrap();
755        assert!(json.contains("classNames"));
756        assert!(json.contains("\"TestClass1\""));
757        assert!(json.contains("testLevel"));
758        assert!(json.contains("skipCodeCoverage"));
759        assert!(!json.contains("classids"));
760    }
761
762    #[test]
763    fn test_run_tests_async_request_multiple_classes() {
764        let req = RunTestsAsyncRequest {
765            class_names: Some("TestClass1,TestClass2,TestClass3".to_string()),
766            test_level: Some("RunSpecifiedTests".to_string()),
767            ..Default::default()
768        };
769
770        let json = serde_json::to_string(&req).unwrap();
771        assert!(json.contains("TestClass1,TestClass2,TestClass3"));
772    }
773
774    #[test]
775    fn test_run_tests_sync_request_ser() {
776        let req = RunTestsSyncRequest {
777            tests: Some(vec![SyncTestItem {
778                class_name: "MyTestClass".to_string(),
779                test_methods: Some(vec!["testMethod1".to_string()]),
780                namespace: None,
781            }]),
782            ..Default::default()
783        };
784
785        let json = serde_json::to_string(&req).unwrap();
786        assert!(json.contains("className"));
787        assert!(json.contains("MyTestClass"));
788        assert!(json.contains("testMethods"));
789        assert!(json.contains("testMethod1"));
790    }
791
792    #[test]
793    fn test_run_tests_sync_result_deser() {
794        let json = r#"{
795            "numTestsRun": 5,
796            "numFailures": 1,
797            "totalTime": 1234.5,
798            "successes": [
799                {
800                    "Id": "01p001",
801                    "MethodName": "testMethod1",
802                    "Name": "TestClass1",
803                    "NamespacePrefix": null,
804                    "Time": 100.0
805                }
806            ],
807            "failures": [
808                {
809                    "Id": "01p002",
810                    "MethodName": "testFailing",
811                    "Name": "TestClass2",
812                    "NamespacePrefix": null,
813                    "Time": 200.0,
814                    "Message": "Assertion failed",
815                    "StackTrace": "Class.TestClass2.testFailing: line 5"
816                }
817            ],
818            "codeCoverage": [],
819            "codeCoverageWarnings": []
820        }"#;
821
822        let result: RunTestsSyncResult = serde_json::from_str(json).unwrap();
823        assert_eq!(result.num_tests_run, 5);
824        assert_eq!(result.num_failures, 1);
825        assert_eq!(result.successes.len(), 1);
826        assert_eq!(result.successes[0].method_name, "testMethod1");
827        assert_eq!(result.failures.len(), 1);
828        assert_eq!(result.failures[0].message, "Assertion failed");
829    }
830
831    #[test]
832    fn test_test_discovery_result_deser() {
833        let json = r#"{
834            "tests": [
835                {
836                    "id": "01pxx00000000001",
837                    "name": "testMethod1",
838                    "className": "TestClass1",
839                    "namespace": null,
840                    "category": "apex"
841                },
842                {
843                    "id": "300xx00000000001",
844                    "name": "MyFlowTest",
845                    "namespace": null,
846                    "category": "flow"
847                }
848            ]
849        }"#;
850
851        let result: TestDiscoveryResult = serde_json::from_str(json).unwrap();
852        assert_eq!(result.tests.len(), 2);
853        assert_eq!(result.tests[0].category, "apex");
854        assert_eq!(result.tests[1].category, "flow");
855    }
856
857    #[test]
858    fn test_run_tests_request_ser() {
859        let req = RunTestsRequest {
860            test_ids: Some(vec!["01pxx00000000001".to_string()]),
861            test_level: Some("RunSpecifiedTests".to_string()),
862            skip_code_coverage: Some(true),
863            ..Default::default()
864        };
865
866        let json = serde_json::to_string(&req).unwrap();
867        assert!(json.contains("testIds"));
868        assert!(json.contains("skipCodeCoverage"));
869    }
870
871    #[test]
872    fn test_completions_result_deser() {
873        let json = r#"{
874            "publicDeclarations": {
875                "System": [
876                    {
877                        "name": "debug",
878                        "type": "Method",
879                        "namespace": "System",
880                        "returnType": "void",
881                        "parameters": [
882                            {
883                                "name": "message",
884                                "type": "Object"
885                            }
886                        ]
887                    }
888                ],
889                "Database": [
890                    {
891                        "name": "query",
892                        "type": "Method",
893                        "namespace": "Database"
894                    }
895                ]
896            }
897        }"#;
898
899        let result: CompletionsResult = serde_json::from_str(json).unwrap();
900        assert_eq!(result.public_declarations.len(), 2);
901        assert!(result.public_declarations.contains_key("System"));
902        assert!(result.public_declarations.contains_key("Database"));
903        let system_items = &result.public_declarations["System"];
904        assert_eq!(system_items.len(), 1);
905        assert_eq!(system_items[0].name, "debug");
906        assert_eq!(system_items[0].return_type, Some("void".to_string()));
907        assert_eq!(system_items[0].parameters.len(), 1);
908    }
909
910    #[test]
911    fn test_completions_result_null_fields() {
912        // Salesforce API can return null for arrays and missing fields
913        let json = r#"{
914            "publicDeclarations": {
915                "System": [
916                    {
917                        "name": "debug",
918                        "parameters": null,
919                        "references": null
920                    },
921                    {
922                        "name": "assert"
923                    }
924                ]
925            }
926        }"#;
927
928        let result: CompletionsResult = serde_json::from_str(json).unwrap();
929        let items = &result.public_declarations["System"];
930        assert_eq!(items.len(), 2);
931        assert_eq!(items[0].name, "debug");
932        assert!(items[0].parameters.is_empty());
933        assert!(items[0].references.is_empty());
934        assert_eq!(items[1].name, "assert");
935        assert!(items[1].parameters.is_empty());
936    }
937}