geneva_uploader_ffi/
lib.rs

1//! C-compatible FFI bindings for geneva-uploader
2
3// Allow #[repr(C)] and other FFI attributes without wrapping in unsafe blocks (standard FFI practice)
4#![allow(unsafe_attr_outside_unsafe)]
5
6use std::ffi::CStr;
7use std::os::raw::{c_char, c_uint};
8use std::ptr;
9use std::sync::OnceLock;
10use tokio::runtime::Runtime;
11
12use geneva_uploader::client::{EncodedBatch, GenevaClient, GenevaClientConfig};
13use geneva_uploader::AuthMethod;
14use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
15use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
16use prost::Message;
17use std::path::PathBuf;
18
19/// Magic number for handle validation
20const GENEVA_HANDLE_MAGIC: u64 = 0xFEED_BEEF;
21
22/// Shared Tokio runtime for async operations
23/// TODO: Consider making runtime configurable via FFI in the future:
24/// - Thread count configuration (currently uses available_parallelism())
25/// - Runtime type selection (multi_thread vs current_thread)
26/// - Per-client runtimes vs shared global runtime
27/// - External runtime integration (accept user-provided runtime handle)
28/// - Runtime lifecycle management for FFI (shutdown, cleanup)
29static RUNTIME: OnceLock<Runtime> = OnceLock::new(); // TODO - Consider using LazyLock once msrv is 1.80.
30
31fn runtime() -> &'static Runtime {
32    RUNTIME.get_or_init(|| {
33        tokio::runtime::Builder::new_multi_thread()
34            .worker_threads(
35                std::thread::available_parallelism()
36                    .map(|n| n.get())
37                    .unwrap_or(4),
38            )
39            .thread_name("geneva-ffi-worker")
40            .enable_time()
41            .enable_io() // Only enable time + I/O for Geneva's needs
42            .build()
43            .expect("Failed to create Tokio runtime for Geneva FFI")
44    })
45}
46
47/// Trait for handles that support validation
48trait ValidatedHandle {
49    fn magic(&self) -> u64;
50    fn set_magic(&mut self, magic: u64);
51}
52
53/// Generic validation function that works for any ValidatedHandle
54unsafe fn validate_handle<T: ValidatedHandle>(handle: *const T) -> GenevaError {
55    if handle.is_null() {
56        return GenevaError::NullPointer;
57    }
58
59    let handle_ref = unsafe { handle.as_ref().unwrap() };
60
61    if handle_ref.magic() != GENEVA_HANDLE_MAGIC {
62        return GenevaError::InvalidHandle;
63    }
64
65    GenevaError::Success
66}
67
68/// Generic function to clear magic number on free
69unsafe fn clear_handle_magic<T: ValidatedHandle>(handle: *mut T) {
70    if !handle.is_null() {
71        if let Some(h) = unsafe { handle.as_mut() } {
72            h.set_magic(0);
73        }
74    }
75}
76
77/// Opaque handle for GenevaClient
78pub struct GenevaClientHandle {
79    magic: u64, // Magic number for handle validation
80    client: GenevaClient,
81}
82
83impl ValidatedHandle for GenevaClientHandle {
84    fn magic(&self) -> u64 {
85        self.magic
86    }
87
88    fn set_magic(&mut self, magic: u64) {
89        self.magic = magic;
90    }
91}
92
93/// Opaque handle holding encoded batches
94pub struct EncodedBatchesHandle {
95    magic: u64,
96    batches: Vec<EncodedBatch>,
97}
98
99impl ValidatedHandle for EncodedBatchesHandle {
100    fn magic(&self) -> u64 {
101        self.magic
102    }
103
104    fn set_magic(&mut self, magic: u64) {
105        self.magic = magic;
106    }
107}
108
109/// Configuration for certificate auth (valid only when auth_method == 1)
110#[repr(C)]
111#[derive(Copy, Clone)]
112pub struct GenevaCertAuthConfig {
113    pub cert_path: *const c_char,     // Path to certificate file
114    pub cert_password: *const c_char, // Certificate password
115}
116
117/// Configuration for Workload Identity auth (valid only when auth_method == 2)
118#[repr(C)]
119#[derive(Copy, Clone)]
120pub struct GenevaWorkloadIdentityAuthConfig {
121    pub resource: *const c_char, // Azure AD resource URI (e.g., "https://monitor.azure.com")
122}
123
124/// Configuration for User-assigned Managed Identity by client ID (valid only when auth_method == 3)
125#[repr(C)]
126#[derive(Copy, Clone)]
127pub struct GenevaUserManagedIdentityAuthConfig {
128    pub client_id: *const c_char, // Azure AD client ID
129}
130
131/// Configuration for User-assigned Managed Identity by object ID (valid only when auth_method == 4)
132#[repr(C)]
133#[derive(Copy, Clone)]
134pub struct GenevaUserManagedIdentityByObjectIdAuthConfig {
135    pub object_id: *const c_char, // Azure AD object ID
136}
137
138/// Configuration for User-assigned Managed Identity by resource ID (valid only when auth_method == 5)
139#[repr(C)]
140#[derive(Copy, Clone)]
141pub struct GenevaUserManagedIdentityByResourceIdAuthConfig {
142    pub resource_id: *const c_char, // Azure resource ID
143}
144
145#[repr(C)]
146pub union GenevaAuthConfig {
147    pub cert: GenevaCertAuthConfig, // Valid when auth_method == 1
148    pub workload_identity: GenevaWorkloadIdentityAuthConfig, // Valid when auth_method == 2
149    pub user_msi: GenevaUserManagedIdentityAuthConfig, // Valid when auth_method == 3
150    pub user_msi_objid: GenevaUserManagedIdentityByObjectIdAuthConfig, // Valid when auth_method == 4
151    pub user_msi_resid: GenevaUserManagedIdentityByResourceIdAuthConfig, // Valid when auth_method == 5
152}
153
154/// Configuration structure for Geneva client (C-compatible, tagged union)
155///
156/// # Auth Methods
157/// - 0 = SystemManagedIdentity (auto-detected VM/AKS system-assigned identity)
158/// - 1 = Certificate (mTLS with PKCS#12 certificate)
159/// - 2 = WorkloadIdentity (explicit Azure Workload Identity for AKS)
160/// - 3 = UserManagedIdentity (by client ID)
161/// - 4 = UserManagedIdentityByObjectId (by object ID)
162/// - 5 = UserManagedIdentityByResourceId (by resource ID)
163///
164/// # Resource Configuration
165/// Different auth methods require different resource configuration:
166/// - **Auth methods 0, 3, 4, 5 (MSI variants)**: Use the `msi_resource` field to specify the Azure AD resource URI
167/// - **Auth method 2 (WorkloadIdentity)**: Use `auth.workload_identity.resource` field
168/// - **Auth method 1 (Certificate)**: No resource needed
169///
170/// The `msi_resource` field specifies the Azure AD resource URI for token acquisition
171/// (e.g., <https://monitor.azure.com>). For user-assigned identities (3, 4, 5), the
172/// auth union specifies WHICH identity to use, while `msi_resource` specifies WHAT
173/// Azure resource to request tokens FOR. These are orthogonal concerns.
174#[repr(C)]
175pub struct GenevaConfig {
176    pub endpoint: *const c_char,
177    pub environment: *const c_char,
178    pub account: *const c_char,
179    pub namespace_name: *const c_char,
180    pub region: *const c_char,
181    pub config_major_version: c_uint,
182    pub auth_method: c_uint,
183    pub tenant: *const c_char,
184    pub role_name: *const c_char,
185    pub role_instance: *const c_char,
186    pub auth: GenevaAuthConfig, // Active member selected by auth_method
187    pub msi_resource: *const c_char, // Azure AD resource URI for MSI auth (auth methods 0, 3, 4, 5). Not used for auth methods 1, 2. Nullable.
188}
189
190/// Error codes returned by FFI functions
191/// TODO: Use cbindgen to auto-generate geneva_errors.h from this enum to eliminate duplication
192#[repr(C)]
193#[derive(PartialEq)]
194pub enum GenevaError {
195    // Base codes (stable)
196    Success = 0,
197    InvalidConfig = 1,
198    InitializationFailed = 2,
199    UploadFailed = 3,
200    InvalidData = 4,
201    InternalError = 5,
202
203    // Granular argument/data errors (used)
204    NullPointer = 100,
205    EmptyInput = 101,
206    DecodeFailed = 102,
207    IndexOutOfRange = 103,
208    InvalidHandle = 104,
209
210    // Granular config/auth errors (used)
211    InvalidAuthMethod = 110,
212    InvalidCertConfig = 111,
213    InvalidWorkloadIdentityConfig = 112,
214    InvalidUserMsiConfig = 113,
215    InvalidUserMsiByObjectIdConfig = 114,
216    InvalidUserMsiByResourceIdConfig = 115,
217
218    // Missing required config (granular INVALID_CONFIG)
219    MissingEndpoint = 130,
220    MissingEnvironment = 131,
221    MissingAccount = 132,
222    MissingNamespace = 133,
223    MissingRegion = 134,
224    MissingTenant = 135,
225    MissingRoleName = 136,
226    MissingRoleInstance = 137,
227}
228
229/// Safely converts a C string to Rust String
230unsafe fn c_str_to_string(ptr: *const c_char, field_name: &str) -> Result<String, String> {
231    if ptr.is_null() {
232        return Err(format!("Field '{field_name}' is null"));
233    }
234
235    match unsafe { CStr::from_ptr(ptr) }.to_str() {
236        Ok(s) => Ok(s.to_string()),
237        Err(_) => Err(format!("Invalid UTF-8 in field '{field_name}'")),
238    }
239}
240
241/// Writes error message to caller-provided buffer if available
242///
243/// This function has zero allocation cost when err_msg_out is NULL or err_msg_len is 0.
244/// Only allocates (via Display::to_string) when caller requests error details.
245unsafe fn write_error_if_provided(
246    err_msg_out: *mut c_char,
247    err_msg_len: usize,
248    error: &impl std::fmt::Display,
249) {
250    if !err_msg_out.is_null() && err_msg_len > 0 {
251        let error_string = error.to_string();
252        let bytes_to_copy = error_string.len().min(err_msg_len - 1);
253        if bytes_to_copy > 0 {
254            unsafe {
255                std::ptr::copy_nonoverlapping(
256                    error_string.as_ptr() as *const c_char,
257                    err_msg_out,
258                    bytes_to_copy,
259                );
260            }
261        }
262        // Always null-terminate if we have space
263        unsafe {
264            *err_msg_out.add(bytes_to_copy) = 0;
265        }
266    }
267}
268
269/// Creates a new Geneva client with explicit result semantics (no TLS needed).
270///
271/// On success: returns GenevaError::Success and writes a non-null handle into *out_handle.
272/// On failure: returns an error code and writes a diagnostic message into err_msg_out if provided.
273///
274/// # Safety
275/// - config must be a valid pointer to a GenevaConfig struct
276/// - out_handle must be a valid pointer to receive the client handle
277/// - err_msg_out: optional buffer to receive error message (can be NULL)
278/// - err_msg_len: size of err_msg_out buffer
279/// - caller must eventually call geneva_client_free on the returned handle
280#[no_mangle]
281pub unsafe extern "C" fn geneva_client_new(
282    config: *const GenevaConfig,
283    out_handle: *mut *mut GenevaClientHandle,
284    err_msg_out: *mut c_char,
285    err_msg_len: usize,
286) -> GenevaError {
287    // Validate pointers
288    if config.is_null() || out_handle.is_null() {
289        return GenevaError::NullPointer;
290    }
291    unsafe { *out_handle = ptr::null_mut() };
292
293    let config = unsafe { config.as_ref().unwrap() };
294
295    // Validate required fields with granular error codes
296    if config.endpoint.is_null() {
297        return GenevaError::MissingEndpoint;
298    }
299    if config.environment.is_null() {
300        return GenevaError::MissingEnvironment;
301    }
302    if config.account.is_null() {
303        return GenevaError::MissingAccount;
304    }
305    if config.namespace_name.is_null() {
306        return GenevaError::MissingNamespace;
307    }
308    if config.region.is_null() {
309        return GenevaError::MissingRegion;
310    }
311    if config.tenant.is_null() {
312        return GenevaError::MissingTenant;
313    }
314    if config.role_name.is_null() {
315        return GenevaError::MissingRoleName;
316    }
317    if config.role_instance.is_null() {
318        return GenevaError::MissingRoleInstance;
319    }
320
321    // Convert C strings to Rust strings
322    let endpoint = match unsafe { c_str_to_string(config.endpoint, "endpoint") } {
323        Ok(s) => s,
324        Err(e) => {
325            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
326            return GenevaError::InvalidConfig;
327        }
328    };
329    let environment = match unsafe { c_str_to_string(config.environment, "environment") } {
330        Ok(s) => s,
331        Err(e) => {
332            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
333            return GenevaError::InvalidConfig;
334        }
335    };
336    let account = match unsafe { c_str_to_string(config.account, "account") } {
337        Ok(s) => s,
338        Err(e) => {
339            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
340            return GenevaError::InvalidConfig;
341        }
342    };
343    let namespace = match unsafe { c_str_to_string(config.namespace_name, "namespace_name") } {
344        Ok(s) => s,
345        Err(e) => {
346            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
347            return GenevaError::InvalidConfig;
348        }
349    };
350    let region = match unsafe { c_str_to_string(config.region, "region") } {
351        Ok(s) => s,
352        Err(e) => {
353            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
354            return GenevaError::InvalidConfig;
355        }
356    };
357    let tenant = match unsafe { c_str_to_string(config.tenant, "tenant") } {
358        Ok(s) => s,
359        Err(e) => {
360            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
361            return GenevaError::InvalidConfig;
362        }
363    };
364    let role_name = match unsafe { c_str_to_string(config.role_name, "role_name") } {
365        Ok(s) => s,
366        Err(e) => {
367            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
368            return GenevaError::InvalidConfig;
369        }
370    };
371    let role_instance = match unsafe { c_str_to_string(config.role_instance, "role_instance") } {
372        Ok(s) => s,
373        Err(e) => {
374            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
375            return GenevaError::InvalidConfig;
376        }
377    };
378
379    // Auth method conversion
380    let auth_method = match config.auth_method {
381        0 => {
382            // System-assigned Managed Identity
383            AuthMethod::SystemManagedIdentity
384        }
385
386        1 => {
387            // Certificate authentication: read fields from tagged union
388            let cert = unsafe { config.auth.cert };
389            if cert.cert_path.is_null() {
390                return GenevaError::InvalidCertConfig;
391            }
392            if cert.cert_password.is_null() {
393                return GenevaError::InvalidCertConfig;
394            }
395            let cert_path = match unsafe { c_str_to_string(cert.cert_path, "cert_path") } {
396                Ok(s) => PathBuf::from(s),
397                Err(e) => {
398                    unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
399                    return GenevaError::InvalidConfig;
400                }
401            };
402            let cert_password =
403                match unsafe { c_str_to_string(cert.cert_password, "cert_password") } {
404                    Ok(s) => s,
405                    Err(e) => {
406                        unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
407                        return GenevaError::InvalidConfig;
408                    }
409                };
410            AuthMethod::Certificate {
411                path: cert_path,
412                password: cert_password,
413            }
414        }
415
416        2 => {
417            // Workload Identity authentication
418            let workload_identity = unsafe { config.auth.workload_identity };
419            if workload_identity.resource.is_null() {
420                return GenevaError::InvalidWorkloadIdentityConfig;
421            }
422            let resource = match unsafe { c_str_to_string(workload_identity.resource, "resource") }
423            {
424                Ok(s) => s,
425                Err(e) => {
426                    unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
427                    return GenevaError::InvalidConfig;
428                }
429            };
430            AuthMethod::WorkloadIdentity { resource }
431        }
432
433        3 => {
434            // User-assigned Managed Identity by client ID
435            let user_msi = unsafe { config.auth.user_msi };
436            if user_msi.client_id.is_null() {
437                return GenevaError::InvalidUserMsiConfig;
438            }
439            let client_id = match unsafe { c_str_to_string(user_msi.client_id, "client_id") } {
440                Ok(s) => s,
441                Err(e) => {
442                    unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
443                    return GenevaError::InvalidConfig;
444                }
445            };
446            AuthMethod::UserManagedIdentity { client_id }
447        }
448
449        4 => {
450            // User-assigned Managed Identity by object ID
451            let user_msi_objid = unsafe { config.auth.user_msi_objid };
452            if user_msi_objid.object_id.is_null() {
453                return GenevaError::InvalidUserMsiByObjectIdConfig;
454            }
455            let object_id = match unsafe { c_str_to_string(user_msi_objid.object_id, "object_id") }
456            {
457                Ok(s) => s,
458                Err(e) => {
459                    unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
460                    return GenevaError::InvalidConfig;
461                }
462            };
463            AuthMethod::UserManagedIdentityByObjectId { object_id }
464        }
465
466        5 => {
467            // User-assigned Managed Identity by resource ID
468            let user_msi_resid = unsafe { config.auth.user_msi_resid };
469            if user_msi_resid.resource_id.is_null() {
470                return GenevaError::InvalidUserMsiByResourceIdConfig;
471            }
472            let resource_id =
473                match unsafe { c_str_to_string(user_msi_resid.resource_id, "resource_id") } {
474                    Ok(s) => s,
475                    Err(e) => {
476                        unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
477                        return GenevaError::InvalidConfig;
478                    }
479                };
480            AuthMethod::UserManagedIdentityByResourceId { resource_id }
481        }
482
483        _ => {
484            return GenevaError::InvalidAuthMethod;
485        }
486    };
487
488    // Parse optional MSI resource
489    let msi_resource = if !config.msi_resource.is_null() {
490        match unsafe { c_str_to_string(config.msi_resource, "msi_resource") } {
491            Ok(s) => Some(s),
492            Err(e) => {
493                unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
494                return GenevaError::InvalidConfig;
495            }
496        }
497    } else {
498        None
499    };
500
501    // Build client config
502    let geneva_config = GenevaClientConfig {
503        endpoint,
504        environment,
505        account,
506        namespace,
507        region,
508        config_major_version: config.config_major_version,
509        auth_method,
510        tenant,
511        role_name,
512        role_instance,
513        msi_resource,
514    };
515
516    // Create client
517    let client = match GenevaClient::new(geneva_config) {
518        Ok(client) => client,
519        Err(e) => {
520            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
521            return GenevaError::InitializationFailed;
522        }
523    };
524
525    let handle = GenevaClientHandle {
526        magic: GENEVA_HANDLE_MAGIC,
527        client,
528    };
529    unsafe { *out_handle = Box::into_raw(Box::new(handle)) };
530    GenevaError::Success
531}
532
533/// Encode and compress logs into batches (synchronous)
534///
535/// # Safety
536/// - handle must be a valid pointer returned by geneva_client_new
537/// - data must be a valid pointer to protobuf-encoded ExportLogsServiceRequest
538/// - data_len must be the correct length of the data
539/// - out_batches must be non-null; on success it receives a non-null pointer the caller must free with geneva_batches_free
540/// - err_msg_out: optional buffer to receive error message (can be NULL)
541/// - err_msg_len: size of err_msg_out buffer
542#[no_mangle]
543pub unsafe extern "C" fn geneva_encode_and_compress_logs(
544    handle: *mut GenevaClientHandle,
545    data: *const u8,
546    data_len: usize,
547    out_batches: *mut *mut EncodedBatchesHandle,
548    err_msg_out: *mut c_char,
549    err_msg_len: usize,
550) -> GenevaError {
551    if out_batches.is_null() {
552        return GenevaError::NullPointer;
553    }
554    unsafe { *out_batches = ptr::null_mut() };
555
556    if handle.is_null() {
557        return GenevaError::NullPointer;
558    }
559    if data.is_null() {
560        return GenevaError::NullPointer;
561    }
562    if data_len == 0 {
563        return GenevaError::EmptyInput;
564    }
565
566    // Validate handle first
567    let validation_result = unsafe { validate_handle(handle) };
568    if validation_result != GenevaError::Success {
569        return validation_result;
570    }
571
572    let handle_ref = unsafe { handle.as_ref().unwrap() };
573    let data_slice = unsafe { std::slice::from_raw_parts(data, data_len) };
574
575    let logs_data: ExportLogsServiceRequest = match Message::decode(data_slice) {
576        Ok(data) => data,
577        Err(e) => {
578            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
579            return GenevaError::DecodeFailed;
580        }
581    };
582
583    let resource_logs = logs_data.resource_logs;
584    match handle_ref.client.encode_and_compress_logs(&resource_logs) {
585        Ok(batches) => {
586            let h = EncodedBatchesHandle {
587                magic: GENEVA_HANDLE_MAGIC,
588                batches,
589            };
590            unsafe { *out_batches = Box::into_raw(Box::new(h)) };
591            GenevaError::Success
592        }
593        Err(e) => {
594            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
595            GenevaError::InternalError
596        }
597    }
598}
599
600/// Encode and compress spans into batches (synchronous)
601///
602/// # Safety
603/// - handle must be a valid pointer returned by geneva_client_new
604/// - data must be a valid pointer to protobuf-encoded ExportTraceServiceRequest
605/// - data_len must be the correct length of the data
606/// - out_batches must be non-null; on success it receives a non-null pointer the caller must free with geneva_batches_free
607/// - err_msg_out: optional buffer to receive error message (can be NULL)
608/// - err_msg_len: size of err_msg_out buffer
609#[no_mangle]
610pub unsafe extern "C" fn geneva_encode_and_compress_spans(
611    handle: *mut GenevaClientHandle,
612    data: *const u8,
613    data_len: usize,
614    out_batches: *mut *mut EncodedBatchesHandle,
615    err_msg_out: *mut c_char,
616    err_msg_len: usize,
617) -> GenevaError {
618    if out_batches.is_null() {
619        return GenevaError::NullPointer;
620    }
621    unsafe { *out_batches = ptr::null_mut() };
622
623    if handle.is_null() {
624        return GenevaError::NullPointer;
625    }
626    if data.is_null() {
627        return GenevaError::NullPointer;
628    }
629    if data_len == 0 {
630        return GenevaError::EmptyInput;
631    }
632
633    // Validate handle first
634    let validation_result = unsafe { validate_handle(handle) };
635    if validation_result != GenevaError::Success {
636        return validation_result;
637    }
638
639    let handle_ref = unsafe { handle.as_ref().unwrap() };
640    let data_slice = unsafe { std::slice::from_raw_parts(data, data_len) };
641
642    let spans_data: ExportTraceServiceRequest = match Message::decode(data_slice) {
643        Ok(data) => data,
644        Err(e) => {
645            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
646            return GenevaError::DecodeFailed;
647        }
648    };
649
650    let resource_spans = spans_data.resource_spans;
651    match handle_ref.client.encode_and_compress_spans(&resource_spans) {
652        Ok(batches) => {
653            let h = EncodedBatchesHandle {
654                magic: GENEVA_HANDLE_MAGIC,
655                batches,
656            };
657            unsafe { *out_batches = Box::into_raw(Box::new(h)) };
658            GenevaError::Success
659        }
660        Err(e) => {
661            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
662            GenevaError::InternalError
663        }
664    }
665}
666
667/// Returns the number of batches in the encoded batches handle
668///
669/// # Safety
670/// - batches must be a valid pointer returned by geneva_encode_and_compress_logs, or null
671#[no_mangle]
672pub unsafe extern "C" fn geneva_batches_len(batches: *const EncodedBatchesHandle) -> usize {
673    // Validate batches
674    match unsafe { validate_handle(batches) } {
675        GenevaError::Success => {
676            // Safe to dereference after validation
677            let batches_ref = unsafe { batches.as_ref().unwrap() };
678            batches_ref.batches.len()
679        }
680        _ => 0, // Return 0 for invalid handles
681    }
682}
683
684/// Uploads a specific batch synchronously
685///
686/// # Safety
687/// - handle must be a valid pointer returned by geneva_client_new
688/// - batches must be a valid pointer returned by geneva_encode_and_compress_logs
689/// - index must be less than the value returned by geneva_batches_len
690/// - err_msg_out: optional buffer to receive error message (can be NULL)
691/// - err_msg_len: size of err_msg_out buffer
692#[no_mangle]
693pub unsafe extern "C" fn geneva_upload_batch_sync(
694    handle: *mut GenevaClientHandle,
695    batches: *const EncodedBatchesHandle,
696    index: usize,
697    err_msg_out: *mut c_char,
698    err_msg_len: usize,
699) -> GenevaError {
700    // Validate client handle
701    match unsafe { validate_handle(handle) } {
702        GenevaError::Success => {}
703        error => return error,
704    }
705    // validate batches
706    match unsafe { validate_handle(batches) } {
707        GenevaError::Success => {}
708        error => return error,
709    }
710
711    // Now we know both handles are valid, safe to dereference
712    let handle_ref = unsafe { handle.as_ref().unwrap() };
713    let batches_ref = unsafe { batches.as_ref().unwrap() };
714
715    if index >= batches_ref.batches.len() {
716        return GenevaError::IndexOutOfRange;
717    }
718
719    let batch = &batches_ref.batches[index];
720    let client = &handle_ref.client;
721    let res = runtime().block_on(async move { client.upload_batch(batch).await });
722    match res {
723        Ok(_) => GenevaError::Success,
724        Err(e) => {
725            unsafe { write_error_if_provided(err_msg_out, err_msg_len, &e) };
726            GenevaError::UploadFailed
727        }
728    }
729}
730
731/// Frees encoded batches handle
732///
733/// # Safety
734/// - batches must be a valid pointer returned by geneva_encode_and_compress_logs, or null
735/// - batches must not be used after calling this function
736#[no_mangle]
737pub unsafe extern "C" fn geneva_batches_free(batches: *mut EncodedBatchesHandle) {
738    if !batches.is_null() {
739        unsafe { clear_handle_magic(batches) };
740        let _ = unsafe { Box::from_raw(batches) };
741    }
742}
743
744// Frees a Geneva client handle
745///
746/// # Safety
747/// - client handle must be a valid pointer returned by geneva_client_new
748/// - client handle must not be used after calling this function
749#[no_mangle]
750pub unsafe extern "C" fn geneva_client_free(handle: *mut GenevaClientHandle) {
751    if !handle.is_null() {
752        unsafe { clear_handle_magic(handle) };
753        let _ = unsafe { Box::from_raw(handle) };
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760    use std::ffi::CString;
761
762    // Build a minimal unsigned JWT with the Endpoint claim and an exp. Matches what extract_endpoint_from_token expects.
763    #[allow(dead_code)]
764    fn generate_mock_jwt_and_expiry(endpoint: &str, ttl_secs: i64) -> (String, String) {
765        use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
766        use chrono::{Duration, Utc};
767
768        let header = r#"{"alg":"none","typ":"JWT"}"#;
769        let exp = Utc::now() + Duration::seconds(ttl_secs);
770        let payload = format!(r#"{{"Endpoint":"{endpoint}","exp":{}}}"#, exp.timestamp());
771
772        let header_b64 = URL_SAFE_NO_PAD.encode(header.as_bytes());
773        let payload_b64 = URL_SAFE_NO_PAD.encode(payload.as_bytes());
774        let token = format!("{}.{}.{sig}", header_b64, payload_b64, sig = "dummy");
775
776        (token, exp.to_rfc3339())
777    }
778
779    #[test]
780    fn test_geneva_client_new_with_null_config() {
781        unsafe {
782            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
783            let rc = geneva_client_new(std::ptr::null(), &mut out, ptr::null_mut(), 0);
784            assert_eq!(rc as u32, GenevaError::NullPointer as u32);
785            assert!(out.is_null());
786        }
787    }
788
789    #[test]
790    fn test_upload_batch_sync_with_nulls() {
791        unsafe {
792            let result =
793                geneva_upload_batch_sync(ptr::null_mut(), ptr::null(), 0, ptr::null_mut(), 0);
794            assert_eq!(result as u32, GenevaError::NullPointer as u32);
795        }
796    }
797
798    #[test]
799    fn test_encode_with_nulls() {
800        unsafe {
801            let mut out: *mut EncodedBatchesHandle = std::ptr::null_mut();
802            let rc = geneva_encode_and_compress_logs(
803                ptr::null_mut(),
804                ptr::null(),
805                0,
806                &mut out,
807                ptr::null_mut(),
808                0,
809            );
810            assert_eq!(rc as u32, GenevaError::NullPointer as u32);
811            assert!(out.is_null());
812        }
813    }
814
815    #[test]
816    fn test_geneva_client_free_with_null() {
817        unsafe {
818            // Should not crash
819            geneva_client_free(ptr::null_mut());
820        }
821    }
822
823    #[test]
824    fn test_null_field_validation() {
825        unsafe {
826            // Test with missing endpoint
827            let environment = CString::new("test").unwrap();
828            let account = CString::new("testaccount").unwrap();
829            let namespace = CString::new("testns").unwrap();
830            let region = CString::new("testregion").unwrap();
831            let tenant = CString::new("testtenant").unwrap();
832            let role_name = CString::new("testrole").unwrap();
833            let role_instance = CString::new("testinstance").unwrap();
834
835            let config = GenevaConfig {
836                endpoint: ptr::null(), // Missing endpoint should cause failure
837                environment: environment.as_ptr(),
838                account: account.as_ptr(),
839                namespace_name: namespace.as_ptr(),
840                region: region.as_ptr(),
841                config_major_version: 1,
842                auth_method: 0, // SystemManagedIdentity - union not used
843                tenant: tenant.as_ptr(),
844                role_name: role_name.as_ptr(),
845                role_instance: role_instance.as_ptr(),
846                // SAFETY: GenevaAuthConfig only contains raw pointers (*const c_char).
847                // Zero-initializing raw pointers creates null pointers, which is valid.
848                // The union is never accessed for SystemManagedIdentity (auth_method 0).
849                auth: std::mem::zeroed(),
850                msi_resource: ptr::null(),
851            };
852
853            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
854            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
855            assert_eq!(rc as u32, GenevaError::MissingEndpoint as u32);
856            assert!(out.is_null());
857        }
858    }
859
860    #[test]
861    fn test_invalid_auth_method() {
862        unsafe {
863            let endpoint = CString::new("https://test.geneva.com").unwrap();
864            let environment = CString::new("test").unwrap();
865            let account = CString::new("testaccount").unwrap();
866            let namespace = CString::new("testns").unwrap();
867            let region = CString::new("testregion").unwrap();
868            let tenant = CString::new("testtenant").unwrap();
869            let role_name = CString::new("testrole").unwrap();
870            let role_instance = CString::new("testinstance").unwrap();
871
872            let config = GenevaConfig {
873                endpoint: endpoint.as_ptr(),
874                environment: environment.as_ptr(),
875                account: account.as_ptr(),
876                namespace_name: namespace.as_ptr(),
877                region: region.as_ptr(),
878                config_major_version: 1,
879                auth_method: 99, // Invalid auth method - union not used
880                tenant: tenant.as_ptr(),
881                role_name: role_name.as_ptr(),
882                role_instance: role_instance.as_ptr(),
883                auth: std::mem::zeroed(), // Union not accessed for invalid auth method
884                msi_resource: ptr::null(),
885            };
886
887            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
888            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
889            assert_eq!(rc as u32, GenevaError::InvalidAuthMethod as u32);
890            assert!(out.is_null());
891        }
892    }
893
894    #[test]
895    fn test_certificate_auth_missing_cert_path() {
896        unsafe {
897            let endpoint = CString::new("https://test.geneva.com").unwrap();
898            let environment = CString::new("test").unwrap();
899            let account = CString::new("testaccount").unwrap();
900            let namespace = CString::new("testns").unwrap();
901            let region = CString::new("testregion").unwrap();
902            let tenant = CString::new("testtenant").unwrap();
903            let role_name = CString::new("testrole").unwrap();
904            let role_instance = CString::new("testinstance").unwrap();
905
906            let config = GenevaConfig {
907                endpoint: endpoint.as_ptr(),
908                environment: environment.as_ptr(),
909                account: account.as_ptr(),
910                namespace_name: namespace.as_ptr(),
911                region: region.as_ptr(),
912                config_major_version: 1,
913                auth_method: 1, // Certificate auth
914                tenant: tenant.as_ptr(),
915                role_name: role_name.as_ptr(),
916                role_instance: role_instance.as_ptr(),
917                auth: GenevaAuthConfig {
918                    cert: GenevaCertAuthConfig {
919                        cert_path: ptr::null(),
920                        cert_password: ptr::null(),
921                    },
922                },
923                msi_resource: ptr::null(),
924            };
925
926            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
927            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
928            assert_eq!(rc as u32, GenevaError::InvalidCertConfig as u32);
929            assert!(out.is_null());
930        }
931    }
932
933    #[test]
934    fn test_workload_identity_auth_missing_resource() {
935        unsafe {
936            let endpoint = CString::new("https://test.geneva.com").unwrap();
937            let environment = CString::new("test").unwrap();
938            let account = CString::new("testaccount").unwrap();
939            let namespace = CString::new("testns").unwrap();
940            let region = CString::new("testregion").unwrap();
941            let tenant = CString::new("testtenant").unwrap();
942            let role_name = CString::new("testrole").unwrap();
943            let role_instance = CString::new("testinstance").unwrap();
944
945            let config = GenevaConfig {
946                endpoint: endpoint.as_ptr(),
947                environment: environment.as_ptr(),
948                account: account.as_ptr(),
949                namespace_name: namespace.as_ptr(),
950                region: region.as_ptr(),
951                config_major_version: 1,
952                auth_method: 2, // Workload Identity
953                tenant: tenant.as_ptr(),
954                role_name: role_name.as_ptr(),
955                role_instance: role_instance.as_ptr(),
956                auth: GenevaAuthConfig {
957                    workload_identity: GenevaWorkloadIdentityAuthConfig {
958                        resource: ptr::null(),
959                    },
960                },
961                msi_resource: ptr::null(),
962            };
963
964            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
965            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
966            assert_eq!(rc as u32, GenevaError::InvalidWorkloadIdentityConfig as u32);
967            assert!(out.is_null());
968        }
969    }
970
971    #[test]
972    fn test_user_msi_auth_missing_client_id() {
973        unsafe {
974            let endpoint = CString::new("https://test.geneva.com").unwrap();
975            let environment = CString::new("test").unwrap();
976            let account = CString::new("testaccount").unwrap();
977            let namespace = CString::new("testns").unwrap();
978            let region = CString::new("testregion").unwrap();
979            let tenant = CString::new("testtenant").unwrap();
980            let role_name = CString::new("testrole").unwrap();
981            let role_instance = CString::new("testinstance").unwrap();
982
983            let config = GenevaConfig {
984                endpoint: endpoint.as_ptr(),
985                environment: environment.as_ptr(),
986                account: account.as_ptr(),
987                namespace_name: namespace.as_ptr(),
988                region: region.as_ptr(),
989                config_major_version: 1,
990                auth_method: 3, // User Managed Identity by client ID
991                tenant: tenant.as_ptr(),
992                role_name: role_name.as_ptr(),
993                role_instance: role_instance.as_ptr(),
994                auth: GenevaAuthConfig {
995                    user_msi: GenevaUserManagedIdentityAuthConfig {
996                        client_id: ptr::null(),
997                    },
998                },
999                msi_resource: ptr::null(),
1000            };
1001
1002            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1003            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1004            assert_eq!(rc as u32, GenevaError::InvalidUserMsiConfig as u32);
1005            assert!(out.is_null());
1006        }
1007    }
1008
1009    #[test]
1010    fn test_user_msi_auth_by_object_id_missing() {
1011        unsafe {
1012            let endpoint = CString::new("https://test.geneva.com").unwrap();
1013            let environment = CString::new("test").unwrap();
1014            let account = CString::new("testaccount").unwrap();
1015            let namespace = CString::new("testns").unwrap();
1016            let region = CString::new("testregion").unwrap();
1017            let tenant = CString::new("testtenant").unwrap();
1018            let role_name = CString::new("testrole").unwrap();
1019            let role_instance = CString::new("testinstance").unwrap();
1020
1021            let config = GenevaConfig {
1022                endpoint: endpoint.as_ptr(),
1023                environment: environment.as_ptr(),
1024                account: account.as_ptr(),
1025                namespace_name: namespace.as_ptr(),
1026                region: region.as_ptr(),
1027                config_major_version: 1,
1028                auth_method: 4, // User Managed Identity by object ID
1029                tenant: tenant.as_ptr(),
1030                role_name: role_name.as_ptr(),
1031                role_instance: role_instance.as_ptr(),
1032                auth: GenevaAuthConfig {
1033                    user_msi_objid: GenevaUserManagedIdentityByObjectIdAuthConfig {
1034                        object_id: ptr::null(),
1035                    },
1036                },
1037                msi_resource: ptr::null(),
1038            };
1039
1040            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1041            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1042            assert_eq!(
1043                rc as u32,
1044                GenevaError::InvalidUserMsiByObjectIdConfig as u32
1045            );
1046            assert!(out.is_null());
1047        }
1048    }
1049
1050    #[test]
1051    fn test_user_msi_auth_by_resource_id_missing() {
1052        unsafe {
1053            let endpoint = CString::new("https://test.geneva.com").unwrap();
1054            let environment = CString::new("test").unwrap();
1055            let account = CString::new("testaccount").unwrap();
1056            let namespace = CString::new("testns").unwrap();
1057            let region = CString::new("testregion").unwrap();
1058            let tenant = CString::new("testtenant").unwrap();
1059            let role_name = CString::new("testrole").unwrap();
1060            let role_instance = CString::new("testinstance").unwrap();
1061
1062            let config = GenevaConfig {
1063                endpoint: endpoint.as_ptr(),
1064                environment: environment.as_ptr(),
1065                account: account.as_ptr(),
1066                namespace_name: namespace.as_ptr(),
1067                region: region.as_ptr(),
1068                config_major_version: 1,
1069                auth_method: 5, // User Managed Identity by resource ID
1070                tenant: tenant.as_ptr(),
1071                role_name: role_name.as_ptr(),
1072                role_instance: role_instance.as_ptr(),
1073                auth: GenevaAuthConfig {
1074                    user_msi_resid: GenevaUserManagedIdentityByResourceIdAuthConfig {
1075                        resource_id: ptr::null(),
1076                    },
1077                },
1078                msi_resource: ptr::null(),
1079            };
1080
1081            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1082            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1083            assert_eq!(
1084                rc as u32,
1085                GenevaError::InvalidUserMsiByResourceIdConfig as u32
1086            );
1087            assert!(out.is_null());
1088        }
1089    }
1090
1091    #[test]
1092    fn test_certificate_auth_missing_cert_password() {
1093        unsafe {
1094            let endpoint = CString::new("https://test.geneva.com").unwrap();
1095            let environment = CString::new("test").unwrap();
1096            let account = CString::new("testaccount").unwrap();
1097            let namespace = CString::new("testns").unwrap();
1098            let region = CString::new("testregion").unwrap();
1099            let tenant = CString::new("testtenant").unwrap();
1100            let role_name = CString::new("testrole").unwrap();
1101            let role_instance = CString::new("testinstance").unwrap();
1102            let cert_path = CString::new("/path/to/cert.p12").unwrap();
1103
1104            let config = GenevaConfig {
1105                endpoint: endpoint.as_ptr(),
1106                environment: environment.as_ptr(),
1107                account: account.as_ptr(),
1108                namespace_name: namespace.as_ptr(),
1109                region: region.as_ptr(),
1110                config_major_version: 1,
1111                auth_method: 1, // Certificate auth
1112                tenant: tenant.as_ptr(),
1113                role_name: role_name.as_ptr(),
1114                role_instance: role_instance.as_ptr(),
1115                auth: GenevaAuthConfig {
1116                    cert: GenevaCertAuthConfig {
1117                        cert_path: cert_path.as_ptr(),
1118                        cert_password: ptr::null(),
1119                    },
1120                },
1121                msi_resource: ptr::null(),
1122            };
1123
1124            let mut out: *mut GenevaClientHandle = std::ptr::null_mut();
1125            let rc = geneva_client_new(&config, &mut out, ptr::null_mut(), 0);
1126            assert_eq!(rc as u32, GenevaError::InvalidCertConfig as u32);
1127            assert!(out.is_null());
1128        }
1129    }
1130
1131    #[test]
1132    fn test_batches_len_with_null() {
1133        unsafe {
1134            let n = geneva_batches_len(ptr::null());
1135            assert_eq!(n, 0, "batches_len should return 0 for null pointer");
1136        }
1137    }
1138
1139    #[test]
1140    fn test_batches_free_with_null() {
1141        unsafe {
1142            geneva_batches_free(ptr::null_mut());
1143        }
1144    }
1145
1146    // Integration-style test: encode via FFI then upload via FFI using MockAuth + Wiremock server.
1147    // Uses otlp_builder to construct an ExportLogsServiceRequest payload.
1148    #[test]
1149    #[cfg(feature = "mock_auth")]
1150    fn test_encode_and_upload_with_mock_server() {
1151        use otlp_builder::builder::build_otlp_logs_minimal;
1152        use wiremock::matchers::method;
1153        use wiremock::{Mock, MockServer, ResponseTemplate};
1154
1155        // Start mock server on the shared runtime used by the FFI code
1156        let mock_server = runtime().block_on(async { MockServer::start().await });
1157        let ingestion_endpoint = mock_server.uri();
1158
1159        // Build JWT dynamically so the Endpoint claim matches the mock server, and compute a fresh expiry
1160        let (auth_token, auth_token_expiry) =
1161            generate_mock_jwt_and_expiry(&ingestion_endpoint, 24 * 3600);
1162
1163        // Mock config service (GET)
1164        runtime().block_on(async {
1165            Mock::given(method("GET"))
1166                .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1167                    r#"{{
1168                        "IngestionGatewayInfo": {{
1169                            "Endpoint": "{ingestion_endpoint}",
1170                            "AuthToken": "{auth_token}",
1171                            "AuthTokenExpiryTime": "{auth_token_expiry}"
1172                        }},
1173                        "StorageAccountKeys": [{{
1174                            "AccountMonikerName": "testdiagaccount",
1175                            "AccountGroupName": "testgroup",
1176                            "IsPrimaryMoniker": true
1177                        }}],
1178                        "TagId": "test"
1179                    }}"#
1180                )))
1181                .mount(&mock_server)
1182                .await;
1183
1184            // Mock ingestion service (POST)
1185            Mock::given(method("POST"))
1186                .respond_with(
1187                    ResponseTemplate::new(202).set_body_string(r#"{"ticket":"accepted"}"#),
1188                )
1189                .mount(&mock_server)
1190                .await;
1191        });
1192
1193        // Build a real GenevaClient using MockAuth (no mTLS), then wrap it in the FFI handle.
1194        let cfg = GenevaClientConfig {
1195            endpoint: mock_server.uri(),
1196            environment: "test".to_string(),
1197            account: "test".to_string(),
1198            namespace: "testns".to_string(),
1199            region: "testregion".to_string(),
1200            config_major_version: 1,
1201            auth_method: AuthMethod::MockAuth,
1202            tenant: "testtenant".to_string(),
1203            role_name: "testrole".to_string(),
1204            role_instance: "testinstance".to_string(),
1205            msi_resource: None,
1206        };
1207        let client = GenevaClient::new(cfg).expect("failed to create GenevaClient with MockAuth");
1208
1209        // Wrap into an FFI-compatible handle
1210        let handle = GenevaClientHandle {
1211            magic: GENEVA_HANDLE_MAGIC,
1212            client,
1213        };
1214        // Keep the boxed handle alive until we explicitly free it via FFI
1215        let mut handle_box = Box::new(handle);
1216        let handle_ptr: *mut GenevaClientHandle = &mut *handle_box;
1217
1218        // Build minimal OTLP logs payload bytes using the test helper
1219        let bytes = build_otlp_logs_minimal("TestEvent", "hello-world", Some(("rk", "rv")));
1220
1221        // Encode via FFI
1222        let mut batches_ptr: *mut EncodedBatchesHandle = std::ptr::null_mut();
1223        let rc = unsafe {
1224            geneva_encode_and_compress_logs(
1225                handle_ptr,
1226                bytes.as_ptr(),
1227                bytes.len(),
1228                &mut batches_ptr,
1229                ptr::null_mut(),
1230                0,
1231            )
1232        };
1233        assert_eq!(rc as u32, GenevaError::Success as u32, "encode failed");
1234        assert!(
1235            !batches_ptr.is_null(),
1236            "out_batches should be non-null on success"
1237        );
1238
1239        // Validate number of batches and upload first batch via FFI (sync)
1240        let len = unsafe { geneva_batches_len(batches_ptr) };
1241        assert!(len >= 1, "expected at least one encoded batch");
1242
1243        // Attempt upload (ignore return code; we will assert via recorded requests)
1244        let _ = unsafe {
1245            geneva_upload_batch_sync(handle_ptr, batches_ptr as *const _, 0, ptr::null_mut(), 0)
1246        };
1247
1248        // Cleanup: free batches and client
1249        unsafe {
1250            geneva_batches_free(batches_ptr);
1251        }
1252        // Transfer ownership of handle_box to the FFI free function
1253        let raw_handle = Box::into_raw(handle_box);
1254        unsafe {
1255            geneva_client_free(raw_handle);
1256        }
1257
1258        // Keep mock_server in scope until end of test
1259        drop(mock_server);
1260    }
1261
1262    // Verifies batching groups by LogRecord.event_name:
1263    // multiple different event_names in one request produce multiple batches,
1264    // and each batch upload hits ingestion with the corresponding event query param.
1265    #[test]
1266    #[cfg(feature = "mock_auth")]
1267    fn test_encode_batching_by_event_name_and_upload() {
1268        use wiremock::http::Method;
1269        use wiremock::matchers::method;
1270        use wiremock::{Mock, MockServer, ResponseTemplate};
1271
1272        // Start mock server
1273        let mock_server = runtime().block_on(async { MockServer::start().await });
1274        let ingestion_endpoint = mock_server.uri();
1275        let (auth_token, auth_token_expiry) =
1276            generate_mock_jwt_and_expiry(&ingestion_endpoint, 24 * 3600);
1277
1278        // Mock Geneva Config (GET) and Ingestion (POST)
1279        runtime().block_on(async {
1280            Mock::given(method("GET"))
1281                .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1282                    r#"{{
1283                        "IngestionGatewayInfo": {{
1284                            "Endpoint": "{ingestion_endpoint}",
1285                            "AuthToken": "{auth_token}",
1286                            "AuthTokenExpiryTime": "{auth_token_expiry}"
1287                        }},
1288                        "StorageAccountKeys": [{{
1289                            "AccountMonikerName": "testdiagaccount",
1290                            "AccountGroupName": "testgroup",
1291                            "IsPrimaryMoniker": true
1292                        }}],
1293                        "TagId": "test"
1294                    }}"#
1295                )))
1296                .mount(&mock_server)
1297                .await;
1298
1299            Mock::given(method("POST"))
1300                .respond_with(
1301                    ResponseTemplate::new(202).set_body_string(r#"{"ticket":"accepted"}"#),
1302                )
1303                .mount(&mock_server)
1304                .await;
1305        });
1306
1307        // Build client with MockAuth
1308        let cfg = GenevaClientConfig {
1309            endpoint: mock_server.uri(),
1310            environment: "test".to_string(),
1311            account: "test".to_string(),
1312            namespace: "testns".to_string(),
1313            region: "testregion".to_string(),
1314            config_major_version: 1,
1315            auth_method: AuthMethod::MockAuth,
1316            tenant: "testtenant".to_string(),
1317            role_name: "testrole".to_string(),
1318            role_instance: "testinstance".to_string(),
1319            msi_resource: None,
1320        };
1321        let client = GenevaClient::new(cfg).expect("failed to create GenevaClient with MockAuth");
1322
1323        // Wrap client into FFI handle
1324        let mut handle_box = Box::new(GenevaClientHandle {
1325            magic: GENEVA_HANDLE_MAGIC,
1326            client,
1327        });
1328        let handle_ptr: *mut GenevaClientHandle = &mut *handle_box;
1329
1330        // Build ExportLogsServiceRequest with two different event_names
1331        let log1 = opentelemetry_proto::tonic::logs::v1::LogRecord {
1332            observed_time_unix_nano: 1_700_000_000_000_000_001,
1333            event_name: "EventA".to_string(),
1334            severity_number: 9,
1335            ..Default::default()
1336        };
1337        let log2 = opentelemetry_proto::tonic::logs::v1::LogRecord {
1338            observed_time_unix_nano: 1_700_000_000_000_000_002,
1339            event_name: "EventB".to_string(),
1340            severity_number: 10,
1341            ..Default::default()
1342        };
1343        let scope_logs = opentelemetry_proto::tonic::logs::v1::ScopeLogs {
1344            log_records: vec![log1, log2],
1345            ..Default::default()
1346        };
1347        let resource_logs = opentelemetry_proto::tonic::logs::v1::ResourceLogs {
1348            scope_logs: vec![scope_logs],
1349            ..Default::default()
1350        };
1351        let req = ExportLogsServiceRequest {
1352            resource_logs: vec![resource_logs],
1353        };
1354        let bytes = req.encode_to_vec();
1355
1356        // Encode via FFI
1357        let mut batches_ptr: *mut EncodedBatchesHandle = std::ptr::null_mut();
1358        let rc = unsafe {
1359            geneva_encode_and_compress_logs(
1360                handle_ptr,
1361                bytes.as_ptr(),
1362                bytes.len(),
1363                &mut batches_ptr,
1364                ptr::null_mut(),
1365                0,
1366            )
1367        };
1368        assert_eq!(rc as u32, GenevaError::Success as u32, "encode failed");
1369        assert!(!batches_ptr.is_null());
1370
1371        // Expect 2 batches (EventA, EventB)
1372        let len = unsafe { geneva_batches_len(batches_ptr) };
1373        assert_eq!(len, 2, "expected 2 batches grouped by event_name");
1374
1375        // Upload all batches
1376        for i in 0..len {
1377            let _ = unsafe {
1378                geneva_upload_batch_sync(handle_ptr, batches_ptr as *const _, i, ptr::null_mut(), 0)
1379            };
1380        }
1381
1382        // Verify requests contain event=EventA and event=EventB in their URLs
1383        // Poll until both POSTs appear or timeout to avoid flakiness
1384        let (urls, has_a, has_b) = runtime().block_on(async {
1385            use tokio::time::{sleep, Duration};
1386            let mut last_urls: Vec<String> = Vec::new();
1387            for _ in 0..200 {
1388                let reqs = mock_server.received_requests().await.unwrap();
1389                let posts: Vec<String> = reqs
1390                    .iter()
1391                    .filter(|r| r.method == Method::Post)
1392                    .map(|r| r.url.to_string())
1393                    .collect();
1394
1395                let has_a = posts.iter().any(|u| u.contains("event=EventA"));
1396                let has_b = posts.iter().any(|u| u.contains("event=EventB"));
1397                if has_a && has_b {
1398                    return (posts, true, true);
1399                }
1400
1401                if !posts.is_empty() {
1402                    last_urls = posts.clone();
1403                }
1404
1405                sleep(Duration::from_millis(20)).await;
1406            }
1407
1408            if last_urls.is_empty() {
1409                let reqs = mock_server.received_requests().await.unwrap();
1410                last_urls = reqs.into_iter().map(|r| r.url.to_string()).collect();
1411            }
1412            let has_a = last_urls.iter().any(|u| u.contains("event=EventA"));
1413            let has_b = last_urls.iter().any(|u| u.contains("event=EventB"));
1414            (last_urls, has_a, has_b)
1415        });
1416        assert!(
1417            has_a,
1418            "Expected request containing event=EventA; got: {urls:?}"
1419        );
1420        assert!(
1421            has_b,
1422            "Expected request containing event=EventB; got: {urls:?}"
1423        );
1424
1425        // Cleanup
1426        unsafe { geneva_batches_free(batches_ptr) };
1427        let raw_handle = Box::into_raw(handle_box);
1428        unsafe { geneva_client_free(raw_handle) };
1429        drop(mock_server);
1430    }
1431}