Skip to main content

bsv/wallet/
validation.rs

1//! Validation helpers for all wallet method arguments.
2//!
3//! Each validator returns `Result<(), WalletError>` using
4//! `WalletError::InvalidParameter` on bad input.
5//! Translated from TS SDK src/wallet/validationHelpers.ts.
6
7use crate::wallet::error::WalletError;
8use crate::wallet::interfaces::*;
9
10// ---------------------------------------------------------------------------
11// Shared validation helpers
12// ---------------------------------------------------------------------------
13
14fn invalid(field: &str, requirement: &str) -> WalletError {
15    WalletError::InvalidParameter(format!("{field}: must be {requirement}"))
16}
17
18fn validate_string_length(s: &str, name: &str, min: usize, max: usize) -> Result<(), WalletError> {
19    let len = s.len();
20    if len < min {
21        return Err(invalid(name, &format!("at least {min} bytes")));
22    }
23    if len > max {
24        return Err(invalid(name, &format!("no more than {max} bytes")));
25    }
26    Ok(())
27}
28
29fn validate_label(s: &str) -> Result<(), WalletError> {
30    validate_string_length(s, "label", 1, 300)
31}
32
33fn validate_tag(s: &str) -> Result<(), WalletError> {
34    validate_string_length(s, "tag", 1, 300)
35}
36
37fn validate_basket(s: &str) -> Result<(), WalletError> {
38    validate_basket_name(s)
39}
40
41fn validate_description(s: &str, name: &str) -> Result<(), WalletError> {
42    validate_string_length(s, name, 5, 2000)
43}
44
45fn validate_optional_limit(limit: Option<u32>) -> Result<(), WalletError> {
46    if let Some(v) = limit {
47        if !(1..=10000).contains(&v) {
48            return Err(invalid("limit", "between 1 and 10000"));
49        }
50    }
51    Ok(())
52}
53
54/// Normalize an identifier per BRC-100: trim whitespace, lowercase.
55/// Matches TS SDK `validateIdentifier` which does `trim().toLowerCase()`.
56pub fn normalize_identifier(s: &str) -> String {
57    s.trim().to_lowercase()
58}
59
60fn validate_protocol_id(protocol: &crate::wallet::types::Protocol) -> Result<(), WalletError> {
61    if protocol.security_level > 2 {
62        return Err(invalid("protocol_id.security_level", "0, 1, or 2"));
63    }
64    let normalized = normalize_identifier(&protocol.protocol);
65    if normalized.is_empty() {
66        return Err(invalid("protocol_id.protocol", "non-empty"));
67    }
68    validate_string_length(&normalized, "protocol_id.protocol", 1, 400)?;
69    // BRC-100: must not contain multiple consecutive spaces
70    if normalized.contains("  ") {
71        return Err(invalid(
72            "protocol_id.protocol",
73            "free of consecutive spaces",
74        ));
75    }
76    // BRC-100: must only contain lowercase letters, numbers, and spaces
77    if !normalized
78        .chars()
79        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == ' ')
80    {
81        return Err(invalid(
82            "protocol_id.protocol",
83            "only lowercase letters, numbers, and spaces",
84        ));
85    }
86    // BRC-100: must not end with "protocol"
87    if normalized.ends_with("protocol") {
88        return Err(invalid(
89            "protocol_id.protocol",
90            "not ending with 'protocol'",
91        ));
92    }
93    // The reserved namespace is the standalone token "p" — i.e. a name of the form
94    // `p <something>` — NOT every name that happens to begin with the letter p.
95    //
96    // This used to test `starts_with('p')`, a bare character check, which rejected
97    // ordinary names like "payment request auth" and "policy". Nothing else
98    // implements that: the reference BRC-43 `KeyDeriver` in the TS SDK validates
99    // length, charset, consecutive spaces, and the " protocol" suffix — and has no
100    // p-prefix rule at all. So the strict check was not enforcing the spec, it was
101    // making this SDK unable to speak the wire everyone else already speaks:
102    // `[2, "payment request auth"]` is the protocol the MessageBox payment-request
103    // HMAC is bound to, and no Rust client could produce it.
104    if normalized == "p" || normalized.starts_with("p ") {
105        return Err(invalid(
106            "protocol_id.protocol",
107            "not in the reserved 'p' namespace (a name of the form `p <...>`)",
108        ));
109    }
110    Ok(())
111}
112
113fn validate_basket_name(s: &str) -> Result<(), WalletError> {
114    let normalized = normalize_identifier(s);
115    validate_string_length(&normalized, "basket", 5, 300)?;
116    // BRC-100: must only contain lowercase letters, numbers, and spaces
117    if !normalized
118        .chars()
119        .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == ' ')
120    {
121        return Err(invalid(
122            "basket",
123            "only lowercase letters, numbers, and spaces",
124        ));
125    }
126    // BRC-100: must not contain consecutive spaces
127    if normalized.contains("  ") {
128        return Err(invalid("basket", "free of consecutive spaces"));
129    }
130    // BRC-100: must not end with "basket"
131    if normalized.ends_with("basket") {
132        return Err(invalid("basket", "not ending with 'basket'"));
133    }
134    // BRC-100: must not start with "admin"
135    if normalized.starts_with("admin") {
136        return Err(invalid("basket", "not starting with 'admin'"));
137    }
138    // BRC-100: must not be "default"
139    if normalized == "default" {
140        return Err(invalid("basket", "not 'default'"));
141    }
142    // Same correction as `validate_protocol_id`: the reserved namespace is the
143    // standalone token "p" (`p <something>`), not every basket whose name happens to
144    // begin with the letter p. The bare `starts_with('p')` check rejected ordinary
145    // names — "payments", "presigs" — for no reason any other implementation shares.
146    if normalized == "p" || normalized.starts_with("p ") {
147        return Err(invalid(
148            "basket",
149            "not in the reserved 'p' namespace (a name of the form `p <...>`)",
150        ));
151    }
152    Ok(())
153}
154
155fn validate_key_id(key_id: &str) -> Result<(), WalletError> {
156    if key_id.is_empty() {
157        return Err(invalid("key_id", "non-empty"));
158    }
159    validate_string_length(key_id, "key_id", 1, 800)
160}
161
162fn validate_privileged_reason(
163    privileged: bool,
164    reason: &Option<String>,
165) -> Result<(), WalletError> {
166    if privileged {
167        if let Some(r) = reason {
168            validate_string_length(r, "privileged_reason", 5, 50)?;
169        } else {
170            return Err(invalid(
171                "privileged_reason",
172                "provided when privileged is true",
173            ));
174        }
175    }
176    Ok(())
177}
178
179fn validate_optional_privileged_reason(
180    privileged: Option<bool>,
181    reason: &Option<String>,
182) -> Result<(), WalletError> {
183    if privileged.unwrap_or(false) {
184        if let Some(r) = reason {
185            validate_string_length(r, "privileged_reason", 5, 50)?;
186        } else {
187            return Err(invalid(
188                "privileged_reason",
189                "provided when privileged is true",
190            ));
191        }
192    }
193    Ok(())
194}
195
196// ---------------------------------------------------------------------------
197// Public validation functions -- one per wallet method's args
198// ---------------------------------------------------------------------------
199
200/// Validate CreateActionArgs.
201pub fn validate_create_action_args(args: &CreateActionArgs) -> Result<(), WalletError> {
202    validate_description(&args.description, "description")?;
203
204    for label in &args.labels {
205        validate_label(label)?;
206    }
207
208    for output in &args.outputs {
209        if output.locking_script.is_none() && output.output_description.is_empty() {
210            return Err(invalid(
211                "output",
212                "has locking_script or output_description",
213            ));
214        }
215        for tag in &output.tags {
216            validate_tag(tag)?;
217        }
218        if let Some(ref basket) = output.basket {
219            validate_basket(basket)?;
220        }
221    }
222
223    for input in &args.inputs {
224        if input.unlocking_script.is_none() && input.unlocking_script_length.is_none() {
225            return Err(invalid(
226                "input",
227                "has unlocking_script or unlocking_script_length",
228            ));
229        }
230    }
231
232    Ok(())
233}
234
235/// Validate SignActionArgs.
236pub fn validate_sign_action_args(args: &SignActionArgs) -> Result<(), WalletError> {
237    if args.reference.is_empty() {
238        return Err(invalid("reference", "non-empty"));
239    }
240    if args.spends.is_empty() {
241        return Err(invalid("spends", "at least one spend"));
242    }
243    for spend in args.spends.values() {
244        if spend.unlocking_script.is_empty() {
245            return Err(invalid("unlocking_script", "non-empty"));
246        }
247    }
248    Ok(())
249}
250
251/// Validate AbortActionArgs.
252pub fn validate_abort_action_args(args: &AbortActionArgs) -> Result<(), WalletError> {
253    if args.reference.is_empty() {
254        return Err(invalid("reference", "non-empty"));
255    }
256    Ok(())
257}
258
259/// Validate ListActionsArgs.
260pub fn validate_list_actions_args(args: &ListActionsArgs) -> Result<(), WalletError> {
261    if args.labels.is_empty() {
262        return Err(invalid("labels", "non-empty"));
263    }
264    for label in &args.labels {
265        validate_label(label)?;
266    }
267    validate_optional_limit(args.limit)?;
268    Ok(())
269}
270
271/// Validate InternalizeActionArgs.
272pub fn validate_internalize_action_args(args: &InternalizeActionArgs) -> Result<(), WalletError> {
273    if args.tx.is_empty() {
274        return Err(invalid("tx", "non-empty"));
275    }
276    if args.outputs.is_empty() {
277        return Err(invalid("outputs", "at least one output"));
278    }
279    validate_description(&args.description, "description")?;
280    for label in &args.labels {
281        validate_label(label)?;
282    }
283    // InternalizeOutput enum variants guarantee that the correct remittance
284    // data is always present -- impossible states are unrepresentable.
285    let _ = &args.outputs;
286    Ok(())
287}
288
289/// Validate ListOutputsArgs.
290pub fn validate_list_outputs_args(args: &ListOutputsArgs) -> Result<(), WalletError> {
291    validate_basket(&args.basket)?;
292    validate_optional_limit(args.limit)?;
293    for tag in &args.tags {
294        validate_tag(tag)?;
295    }
296    Ok(())
297}
298
299/// Validate RelinquishOutputArgs.
300pub fn validate_relinquish_output_args(args: &RelinquishOutputArgs) -> Result<(), WalletError> {
301    validate_basket(&args.basket)?;
302    if args.output.is_empty() {
303        return Err(invalid("output", "non-empty"));
304    }
305    Ok(())
306}
307
308/// Validate GetPublicKeyArgs.
309pub fn validate_get_public_key_args(args: &GetPublicKeyArgs) -> Result<(), WalletError> {
310    if !args.identity_key {
311        if args.protocol_id.is_none() {
312            return Err(invalid(
313                "protocol_id",
314                "provided when identity_key is false",
315            ));
316        }
317        if args.key_id.is_none() {
318            return Err(invalid("key_id", "provided when identity_key is false"));
319        }
320        if let Some(ref p) = args.protocol_id {
321            validate_protocol_id(p)?;
322        }
323        if let Some(ref k) = args.key_id {
324            validate_key_id(k)?;
325        }
326    }
327    Ok(())
328}
329
330/// Validate EncryptArgs.
331pub fn validate_encrypt_args(args: &EncryptArgs) -> Result<(), WalletError> {
332    validate_protocol_id(&args.protocol_id)?;
333    validate_key_id(&args.key_id)?;
334    if args.plaintext.is_empty() {
335        return Err(invalid("plaintext", "non-empty"));
336    }
337    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
338    Ok(())
339}
340
341/// Validate DecryptArgs.
342pub fn validate_decrypt_args(args: &DecryptArgs) -> Result<(), WalletError> {
343    validate_protocol_id(&args.protocol_id)?;
344    validate_key_id(&args.key_id)?;
345    if args.ciphertext.is_empty() {
346        return Err(invalid("ciphertext", "non-empty"));
347    }
348    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
349    Ok(())
350}
351
352/// Validate CreateHmacArgs.
353pub fn validate_create_hmac_args(args: &CreateHmacArgs) -> Result<(), WalletError> {
354    validate_protocol_id(&args.protocol_id)?;
355    validate_key_id(&args.key_id)?;
356    if args.data.is_empty() {
357        return Err(invalid("data", "non-empty"));
358    }
359    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
360    Ok(())
361}
362
363/// Validate VerifyHmacArgs.
364pub fn validate_verify_hmac_args(args: &VerifyHmacArgs) -> Result<(), WalletError> {
365    validate_protocol_id(&args.protocol_id)?;
366    validate_key_id(&args.key_id)?;
367    if args.data.is_empty() {
368        return Err(invalid("data", "non-empty"));
369    }
370    if args.hmac.is_empty() {
371        return Err(invalid("hmac", "non-empty"));
372    }
373    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
374    Ok(())
375}
376
377/// Validate CreateSignatureArgs.
378pub fn validate_create_signature_args(args: &CreateSignatureArgs) -> Result<(), WalletError> {
379    validate_protocol_id(&args.protocol_id)?;
380    validate_key_id(&args.key_id)?;
381    let has_data = args.data.as_ref().is_some_and(|d| !d.is_empty());
382    let has_hash = args
383        .hash_to_directly_sign
384        .as_ref()
385        .is_some_and(|h| !h.is_empty());
386    if !has_data && !has_hash {
387        return Err(invalid(
388            "data",
389            "provided (either data or hash_to_directly_sign)",
390        ));
391    }
392    if has_hash {
393        if let Some(ref h) = args.hash_to_directly_sign {
394            if h.len() != 32 {
395                return Err(invalid("hash_to_directly_sign", "exactly 32 bytes"));
396            }
397        }
398    }
399    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
400    Ok(())
401}
402
403/// Validate VerifySignatureArgs.
404pub fn validate_verify_signature_args(args: &VerifySignatureArgs) -> Result<(), WalletError> {
405    validate_protocol_id(&args.protocol_id)?;
406    validate_key_id(&args.key_id)?;
407    let has_data = args.data.as_ref().is_some_and(|d| !d.is_empty());
408    let has_hash = args
409        .hash_to_directly_verify
410        .as_ref()
411        .is_some_and(|h| !h.is_empty());
412    if !has_data && !has_hash {
413        return Err(invalid(
414            "data",
415            "provided (either data or hash_to_directly_verify)",
416        ));
417    }
418    if has_hash {
419        if let Some(ref h) = args.hash_to_directly_verify {
420            if h.len() != 32 {
421                return Err(invalid("hash_to_directly_verify", "exactly 32 bytes"));
422            }
423        }
424    }
425    if args.signature.is_empty() {
426        return Err(invalid("signature", "non-empty"));
427    }
428    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
429    Ok(())
430}
431
432/// Validate AcquireCertificateArgs.
433pub fn validate_acquire_certificate_args(args: &AcquireCertificateArgs) -> Result<(), WalletError> {
434    match args.acquisition_protocol {
435        AcquisitionProtocol::Direct => {
436            if args.serial_number.is_none() {
437                return Err(invalid("serial_number", "provided for direct acquisition"));
438            }
439            if args.signature.is_none() {
440                return Err(invalid("signature", "provided for direct acquisition"));
441            }
442            if args.revocation_outpoint.is_none() {
443                return Err(invalid(
444                    "revocation_outpoint",
445                    "provided for direct acquisition",
446                ));
447            }
448            if args.keyring_revealer.is_none() {
449                return Err(invalid(
450                    "keyring_revealer",
451                    "provided for direct acquisition",
452                ));
453            }
454            if args.keyring_for_subject.is_none() {
455                return Err(invalid(
456                    "keyring_for_subject",
457                    "provided for direct acquisition",
458                ));
459            }
460        }
461        AcquisitionProtocol::Issuance => {
462            if args.certifier_url.is_none() {
463                return Err(invalid(
464                    "certifier_url",
465                    "provided for issuance acquisition",
466                ));
467            }
468        }
469    }
470    validate_privileged_reason(args.privileged, &args.privileged_reason)?;
471    // Validate certificate field names
472    for field_name in args.fields.keys() {
473        validate_string_length(field_name, "field_name", 1, 50)?;
474    }
475    Ok(())
476}
477
478/// Validate ListCertificatesArgs.
479pub fn validate_list_certificates_args(args: &ListCertificatesArgs) -> Result<(), WalletError> {
480    validate_optional_limit(args.limit)?;
481    validate_optional_privileged_reason(args.privileged.0, &args.privileged_reason)?;
482    Ok(())
483}
484
485/// Validate ProveCertificateArgs.
486pub fn validate_prove_certificate_args(args: &ProveCertificateArgs) -> Result<(), WalletError> {
487    if args.fields_to_reveal.is_empty() {
488        return Err(invalid("fields_to_reveal", "non-empty"));
489    }
490    for field in &args.fields_to_reveal {
491        validate_string_length(field, "fields_to_reveal entry", 1, 50)?;
492    }
493    validate_optional_privileged_reason(args.privileged.0, &args.privileged_reason)?;
494    Ok(())
495}
496
497/// Validate RelinquishCertificateArgs.
498pub fn validate_relinquish_certificate_args(
499    args: &RelinquishCertificateArgs,
500) -> Result<(), WalletError> {
501    // cert_type, serial_number, certifier are required typed fields -- presence is enforced by struct.
502    let _ = args;
503    Ok(())
504}
505
506/// Validate DiscoverByIdentityKeyArgs.
507pub fn validate_discover_by_identity_key_args(
508    args: &DiscoverByIdentityKeyArgs,
509) -> Result<(), WalletError> {
510    // identity_key is a required typed field.
511    let _ = args;
512    Ok(())
513}
514
515/// Validate DiscoverByAttributesArgs.
516pub fn validate_discover_by_attributes_args(
517    args: &DiscoverByAttributesArgs,
518) -> Result<(), WalletError> {
519    if args.attributes.is_empty() {
520        return Err(invalid("attributes", "non-empty"));
521    }
522    Ok(())
523}
524
525/// Validate RevealCounterpartyKeyLinkageArgs.
526pub fn validate_reveal_counterparty_key_linkage_args(
527    args: &RevealCounterpartyKeyLinkageArgs,
528) -> Result<(), WalletError> {
529    validate_optional_privileged_reason(args.privileged, &args.privileged_reason)?;
530    Ok(())
531}
532
533/// Validate RevealSpecificKeyLinkageArgs.
534pub fn validate_reveal_specific_key_linkage_args(
535    args: &RevealSpecificKeyLinkageArgs,
536) -> Result<(), WalletError> {
537    validate_protocol_id(&args.protocol_id)?;
538    validate_key_id(&args.key_id)?;
539    validate_optional_privileged_reason(args.privileged, &args.privileged_reason)?;
540    Ok(())
541}
542
543/// Validate GetHeaderArgs.
544pub fn validate_get_header_args(args: &GetHeaderArgs) -> Result<(), WalletError> {
545    if args.height == 0 {
546        return Err(invalid("height", "greater than 0"));
547    }
548    Ok(())
549}
550
551// ===========================================================================
552// Tests
553// ===========================================================================
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558    use std::collections::HashMap;
559
560    use crate::primitives::private_key::PrivateKey;
561    use crate::wallet::types::{
562        BooleanDefaultFalse, BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol,
563    };
564
565    // ---- the reserved "p" NAMESPACE, not the letter p ----------------------
566    //
567    // These pin an interop fix. The check here used to be `starts_with('p')` — a
568    // bare character test — which rejected ordinary protocol and basket names.
569    // Nothing else in the ecosystem implements that: the reference BRC-43
570    // KeyDeriver (TS SDK) has no p-prefix rule at all. The practical consequence
571    // was that no Rust client could produce `[2, "payment request auth"]`, the
572    // protocol the MessageBox payment-request HMAC is bound to — so
573    // `request_payment` was unreachable from Rust entirely.
574    //
575    // If someone "tightens" this back to `starts_with('p')`, these go red.
576
577    fn proto(name: &str) -> Protocol {
578        Protocol {
579            security_level: 2,
580            protocol: name.to_string(),
581        }
582    }
583
584    /// THE REGRESSION THIS FIX EXISTS FOR: the MessageBox payment-request protocol.
585    #[test]
586    fn payment_request_auth_is_a_valid_protocol() {
587        validate_protocol_id(&proto("payment request auth"))
588            .expect("the wire protocol every other SDK speaks must validate here too");
589    }
590
591    /// Ordinary names that merely BEGIN with the letter p are fine.
592    #[test]
593    fn protocol_names_beginning_with_the_letter_p_are_allowed() {
594        for name in ["policy", "presign", "payments", "paragon vault"] {
595            validate_protocol_id(&proto(name)).unwrap_or_else(|e| {
596                panic!("{name:?} must be a valid protocol name, got: {e}");
597            });
598        }
599    }
600
601    /// The reserved namespace is `p` as a STANDALONE token — still rejected.
602    #[test]
603    fn the_reserved_p_namespace_is_still_rejected() {
604        assert!(validate_protocol_id(&proto("p foo")).is_err());
605        assert!(validate_protocol_id(&proto("p 1 bar")).is_err());
606    }
607
608    /// Same correction on baskets.
609    #[test]
610    fn basket_names_beginning_with_the_letter_p_are_allowed() {
611        for name in ["payments", "presigs", "paragon tokens"] {
612            validate_basket_name(name)
613                .unwrap_or_else(|e| panic!("{name:?} must be a valid basket, got: {e}"));
614        }
615        // ...and the reserved namespace still is not.
616        assert!(validate_basket_name("p foo").is_err());
617    }
618
619    fn test_pubkey() -> crate::primitives::public_key::PublicKey {
620        let pk = PrivateKey::from_bytes(&{
621            let mut buf = [0u8; 32];
622            buf[31] = 42;
623            buf
624        })
625        .unwrap();
626        pk.to_public_key()
627    }
628
629    fn test_counterparty() -> Counterparty {
630        Counterparty {
631            counterparty_type: CounterpartyType::Other,
632            public_key: Some(test_pubkey()),
633        }
634    }
635
636    fn test_protocol() -> Protocol {
637        Protocol {
638            security_level: 1,
639            protocol: "test signing".to_string(),
640        }
641    }
642
643    // ---- CreateActionArgs ----
644
645    #[test]
646    fn test_create_action_valid() {
647        let args = CreateActionArgs {
648            description: "Valid description text".to_string(),
649            input_beef: None,
650            inputs: vec![],
651            outputs: vec![],
652            lock_time: None,
653            version: None,
654            labels: vec![],
655            options: None,
656            reference: None,
657        };
658        assert!(validate_create_action_args(&args).is_ok());
659    }
660
661    #[test]
662    fn test_create_action_short_description() {
663        let args = CreateActionArgs {
664            description: "Hi".to_string(),
665            input_beef: None,
666            inputs: vec![],
667            outputs: vec![],
668            lock_time: None,
669            version: None,
670            labels: vec![],
671            options: None,
672            reference: None,
673        };
674        assert!(validate_create_action_args(&args).is_err());
675    }
676
677    #[test]
678    fn test_create_action_label_too_long() {
679        let args = CreateActionArgs {
680            description: "Valid description text".to_string(),
681            input_beef: None,
682            inputs: vec![],
683            outputs: vec![],
684            lock_time: None,
685            version: None,
686            labels: vec!["x".repeat(301)],
687            options: None,
688            reference: None,
689        };
690        assert!(validate_create_action_args(&args).is_err());
691    }
692
693    #[test]
694    fn test_create_action_input_needs_script_or_length() {
695        let args = CreateActionArgs {
696            description: "Valid description text".to_string(),
697            input_beef: None,
698            inputs: vec![CreateActionInput {
699                outpoint: "abc.0".to_string(),
700                input_description: "test input".to_string(),
701                unlocking_script: None,
702                unlocking_script_length: None,
703                sequence_number: None,
704            }],
705            outputs: vec![],
706            lock_time: None,
707            version: None,
708            labels: vec![],
709            options: None,
710            reference: None,
711        };
712        assert!(validate_create_action_args(&args).is_err());
713    }
714
715    // ---- SignActionArgs ----
716
717    #[test]
718    fn test_sign_action_valid() {
719        let mut spends = HashMap::new();
720        spends.insert(
721            0,
722            SignActionSpend {
723                unlocking_script: vec![1, 2, 3],
724                sequence_number: None,
725            },
726        );
727        let args = SignActionArgs {
728            reference: vec![1, 2, 3],
729            spends,
730            options: None,
731        };
732        assert!(validate_sign_action_args(&args).is_ok());
733    }
734
735    #[test]
736    fn test_sign_action_empty_reference() {
737        let mut spends = HashMap::new();
738        spends.insert(
739            0,
740            SignActionSpend {
741                unlocking_script: vec![1],
742                sequence_number: None,
743            },
744        );
745        let args = SignActionArgs {
746            reference: vec![],
747            spends,
748            options: None,
749        };
750        assert!(validate_sign_action_args(&args).is_err());
751    }
752
753    #[test]
754    fn test_sign_action_empty_spends() {
755        let args = SignActionArgs {
756            reference: vec![1, 2, 3],
757            spends: HashMap::new(),
758            options: None,
759        };
760        assert!(validate_sign_action_args(&args).is_err());
761    }
762
763    #[test]
764    fn test_sign_action_empty_unlocking_script() {
765        let mut spends = HashMap::new();
766        spends.insert(
767            0,
768            SignActionSpend {
769                unlocking_script: vec![],
770                sequence_number: None,
771            },
772        );
773        let args = SignActionArgs {
774            reference: vec![1, 2, 3],
775            spends,
776            options: None,
777        };
778        assert!(validate_sign_action_args(&args).is_err());
779    }
780
781    // ---- AbortActionArgs ----
782
783    #[test]
784    fn test_abort_action_valid() {
785        let args = AbortActionArgs {
786            reference: vec![1, 2, 3],
787        };
788        assert!(validate_abort_action_args(&args).is_ok());
789    }
790
791    #[test]
792    fn test_abort_action_empty_reference() {
793        let args = AbortActionArgs { reference: vec![] };
794        assert!(validate_abort_action_args(&args).is_err());
795    }
796
797    // ---- ListActionsArgs ----
798
799    #[test]
800    fn test_list_actions_valid() {
801        let args = ListActionsArgs {
802            labels: vec!["test".to_string()],
803            label_query_mode: None,
804            include_labels: BooleanDefaultFalse(None),
805            include_inputs: BooleanDefaultFalse(None),
806            include_input_source_locking_scripts: BooleanDefaultFalse(None),
807            include_input_unlocking_scripts: BooleanDefaultFalse(None),
808            include_outputs: BooleanDefaultFalse(None),
809            include_output_locking_scripts: BooleanDefaultFalse(None),
810            limit: Some(10),
811            offset: None,
812            seek_permission: BooleanDefaultTrue(None),
813        };
814        assert!(validate_list_actions_args(&args).is_ok());
815    }
816
817    #[test]
818    fn test_list_actions_empty_labels() {
819        let args = ListActionsArgs {
820            labels: vec![],
821            label_query_mode: None,
822            include_labels: BooleanDefaultFalse(None),
823            include_inputs: BooleanDefaultFalse(None),
824            include_input_source_locking_scripts: BooleanDefaultFalse(None),
825            include_input_unlocking_scripts: BooleanDefaultFalse(None),
826            include_outputs: BooleanDefaultFalse(None),
827            include_output_locking_scripts: BooleanDefaultFalse(None),
828            limit: None,
829            offset: None,
830            seek_permission: BooleanDefaultTrue(None),
831        };
832        assert!(validate_list_actions_args(&args).is_err());
833    }
834
835    #[test]
836    fn test_list_actions_limit_too_high() {
837        let args = ListActionsArgs {
838            labels: vec!["test".to_string()],
839            label_query_mode: None,
840            include_labels: BooleanDefaultFalse(None),
841            include_inputs: BooleanDefaultFalse(None),
842            include_input_source_locking_scripts: BooleanDefaultFalse(None),
843            include_input_unlocking_scripts: BooleanDefaultFalse(None),
844            include_outputs: BooleanDefaultFalse(None),
845            include_output_locking_scripts: BooleanDefaultFalse(None),
846            limit: Some(10001),
847            offset: None,
848            seek_permission: BooleanDefaultTrue(None),
849        };
850        assert!(validate_list_actions_args(&args).is_err());
851    }
852
853    #[test]
854    fn test_list_actions_limit_zero() {
855        let args = ListActionsArgs {
856            labels: vec!["test".to_string()],
857            label_query_mode: None,
858            include_labels: BooleanDefaultFalse(None),
859            include_inputs: BooleanDefaultFalse(None),
860            include_input_source_locking_scripts: BooleanDefaultFalse(None),
861            include_input_unlocking_scripts: BooleanDefaultFalse(None),
862            include_outputs: BooleanDefaultFalse(None),
863            include_output_locking_scripts: BooleanDefaultFalse(None),
864            limit: Some(0),
865            offset: None,
866            seek_permission: BooleanDefaultTrue(None),
867        };
868        assert!(validate_list_actions_args(&args).is_err());
869    }
870
871    // ---- InternalizeActionArgs ----
872
873    #[test]
874    fn test_internalize_action_valid() {
875        let args = InternalizeActionArgs {
876            tx: vec![1, 2, 3],
877            description: "Valid description text".to_string(),
878            labels: vec![],
879            seek_permission: BooleanDefaultTrue(None),
880            outputs: vec![InternalizeOutput::BasketInsertion {
881                output_index: 0,
882                insertion: BasketInsertion {
883                    basket: "test-basket".to_string(),
884                    custom_instructions: None,
885                    tags: vec![],
886                },
887            }],
888        };
889        assert!(validate_internalize_action_args(&args).is_ok());
890    }
891
892    #[test]
893    fn test_internalize_action_empty_tx() {
894        let args = InternalizeActionArgs {
895            tx: vec![],
896            description: "Valid description text".to_string(),
897            labels: vec![],
898            seek_permission: BooleanDefaultTrue(None),
899            outputs: vec![InternalizeOutput::BasketInsertion {
900                output_index: 0,
901                insertion: BasketInsertion {
902                    basket: "test".to_string(),
903                    custom_instructions: None,
904                    tags: vec![],
905                },
906            }],
907        };
908        assert!(validate_internalize_action_args(&args).is_err());
909    }
910
911    #[test]
912    fn test_internalize_action_empty_outputs() {
913        let args = InternalizeActionArgs {
914            tx: vec![1, 2, 3],
915            description: "Valid description text".to_string(),
916            labels: vec![],
917            seek_permission: BooleanDefaultTrue(None),
918            outputs: vec![],
919        };
920        assert!(validate_internalize_action_args(&args).is_err());
921    }
922
923    // ---- ListOutputsArgs ----
924
925    #[test]
926    fn test_list_outputs_valid() {
927        let args = ListOutputsArgs {
928            basket: "token store".to_string(),
929            tags: vec![],
930            tag_query_mode: None,
931            include: None,
932            include_custom_instructions: BooleanDefaultFalse(None),
933            include_tags: BooleanDefaultFalse(None),
934            include_labels: BooleanDefaultFalse(None),
935            limit: Some(10),
936            offset: None,
937            seek_permission: BooleanDefaultTrue(None),
938        };
939        assert!(validate_list_outputs_args(&args).is_ok());
940    }
941
942    #[test]
943    fn test_list_outputs_empty_basket() {
944        let args = ListOutputsArgs {
945            basket: "".to_string(),
946            tags: vec![],
947            tag_query_mode: None,
948            include: None,
949            include_custom_instructions: BooleanDefaultFalse(None),
950            include_tags: BooleanDefaultFalse(None),
951            include_labels: BooleanDefaultFalse(None),
952            limit: None,
953            offset: None,
954            seek_permission: BooleanDefaultTrue(None),
955        };
956        assert!(validate_list_outputs_args(&args).is_err());
957    }
958
959    #[test]
960    fn test_list_outputs_limit_too_high() {
961        let args = ListOutputsArgs {
962            basket: "token store".to_string(),
963            tags: vec![],
964            tag_query_mode: None,
965            include: None,
966            include_custom_instructions: BooleanDefaultFalse(None),
967            include_tags: BooleanDefaultFalse(None),
968            include_labels: BooleanDefaultFalse(None),
969            limit: Some(10001),
970            offset: None,
971            seek_permission: BooleanDefaultTrue(None),
972        };
973        assert!(validate_list_outputs_args(&args).is_err());
974    }
975
976    // ---- RelinquishOutputArgs ----
977
978    #[test]
979    fn test_relinquish_output_valid() {
980        let args = RelinquishOutputArgs {
981            basket: "token store".to_string(),
982            output: "abc123.0".to_string(),
983        };
984        assert!(validate_relinquish_output_args(&args).is_ok());
985    }
986
987    #[test]
988    fn test_relinquish_output_empty_basket() {
989        let args = RelinquishOutputArgs {
990            basket: "".to_string(),
991            output: "abc123.0".to_string(),
992        };
993        assert!(validate_relinquish_output_args(&args).is_err());
994    }
995
996    #[test]
997    fn test_relinquish_output_empty_output() {
998        let args = RelinquishOutputArgs {
999            basket: "token store".to_string(),
1000            output: "".to_string(),
1001        };
1002        assert!(validate_relinquish_output_args(&args).is_err());
1003    }
1004
1005    // ---- GetPublicKeyArgs ----
1006
1007    #[test]
1008    fn test_get_public_key_identity() {
1009        let args = GetPublicKeyArgs {
1010            identity_key: true,
1011            protocol_id: None,
1012            key_id: None,
1013            counterparty: None,
1014            privileged: false,
1015            privileged_reason: None,
1016            for_self: None,
1017            seek_permission: None,
1018        };
1019        assert!(validate_get_public_key_args(&args).is_ok());
1020    }
1021
1022    #[test]
1023    fn test_get_public_key_derived_valid() {
1024        let args = GetPublicKeyArgs {
1025            identity_key: false,
1026            protocol_id: Some(test_protocol()),
1027            key_id: Some("my-key".to_string()),
1028            counterparty: Some(test_counterparty()),
1029            privileged: false,
1030            privileged_reason: None,
1031            for_self: None,
1032            seek_permission: None,
1033        };
1034        assert!(validate_get_public_key_args(&args).is_ok());
1035    }
1036
1037    #[test]
1038    fn test_get_public_key_derived_missing_protocol() {
1039        let args = GetPublicKeyArgs {
1040            identity_key: false,
1041            protocol_id: None,
1042            key_id: Some("my-key".to_string()),
1043            counterparty: None,
1044            privileged: false,
1045            privileged_reason: None,
1046            for_self: None,
1047            seek_permission: None,
1048        };
1049        assert!(validate_get_public_key_args(&args).is_err());
1050    }
1051
1052    #[test]
1053    fn test_get_public_key_derived_missing_key_id() {
1054        let args = GetPublicKeyArgs {
1055            identity_key: false,
1056            protocol_id: Some(test_protocol()),
1057            key_id: None,
1058            counterparty: None,
1059            privileged: false,
1060            privileged_reason: None,
1061            for_self: None,
1062            seek_permission: None,
1063        };
1064        assert!(validate_get_public_key_args(&args).is_err());
1065    }
1066
1067    // ---- EncryptArgs ----
1068
1069    #[test]
1070    fn test_encrypt_valid() {
1071        let args = EncryptArgs {
1072            protocol_id: test_protocol(),
1073            key_id: "my-key".to_string(),
1074            counterparty: test_counterparty(),
1075            plaintext: vec![1, 2, 3],
1076            privileged: false,
1077            privileged_reason: None,
1078            seek_permission: None,
1079        };
1080        assert!(validate_encrypt_args(&args).is_ok());
1081    }
1082
1083    #[test]
1084    fn test_encrypt_empty_plaintext() {
1085        let args = EncryptArgs {
1086            protocol_id: test_protocol(),
1087            key_id: "my-key".to_string(),
1088            counterparty: test_counterparty(),
1089            plaintext: vec![],
1090            privileged: false,
1091            privileged_reason: None,
1092            seek_permission: None,
1093        };
1094        assert!(validate_encrypt_args(&args).is_err());
1095    }
1096
1097    #[test]
1098    fn test_encrypt_empty_protocol() {
1099        let args = EncryptArgs {
1100            protocol_id: Protocol {
1101                security_level: 1,
1102                protocol: "".to_string(),
1103            },
1104            key_id: "my-key".to_string(),
1105            counterparty: test_counterparty(),
1106            plaintext: vec![1, 2, 3],
1107            privileged: false,
1108            privileged_reason: None,
1109            seek_permission: None,
1110        };
1111        assert!(validate_encrypt_args(&args).is_err());
1112    }
1113
1114    #[test]
1115    fn test_encrypt_empty_key_id() {
1116        let args = EncryptArgs {
1117            protocol_id: test_protocol(),
1118            key_id: "".to_string(),
1119            counterparty: test_counterparty(),
1120            plaintext: vec![1, 2, 3],
1121            privileged: false,
1122            privileged_reason: None,
1123            seek_permission: None,
1124        };
1125        assert!(validate_encrypt_args(&args).is_err());
1126    }
1127
1128    #[test]
1129    fn test_encrypt_invalid_security_level() {
1130        let args = EncryptArgs {
1131            protocol_id: Protocol {
1132                security_level: 5,
1133                protocol: "test".to_string(),
1134            },
1135            key_id: "my-key".to_string(),
1136            counterparty: test_counterparty(),
1137            plaintext: vec![1, 2, 3],
1138            privileged: false,
1139            privileged_reason: None,
1140            seek_permission: None,
1141        };
1142        assert!(validate_encrypt_args(&args).is_err());
1143    }
1144
1145    // ---- DecryptArgs ----
1146
1147    #[test]
1148    fn test_decrypt_valid() {
1149        let args = DecryptArgs {
1150            protocol_id: test_protocol(),
1151            key_id: "my-key".to_string(),
1152            counterparty: test_counterparty(),
1153            ciphertext: vec![1, 2, 3],
1154            privileged: false,
1155            privileged_reason: None,
1156            seek_permission: None,
1157        };
1158        assert!(validate_decrypt_args(&args).is_ok());
1159    }
1160
1161    #[test]
1162    fn test_decrypt_empty_ciphertext() {
1163        let args = DecryptArgs {
1164            protocol_id: test_protocol(),
1165            key_id: "my-key".to_string(),
1166            counterparty: test_counterparty(),
1167            ciphertext: vec![],
1168            privileged: false,
1169            privileged_reason: None,
1170            seek_permission: None,
1171        };
1172        assert!(validate_decrypt_args(&args).is_err());
1173    }
1174
1175    // ---- CreateHmacArgs ----
1176
1177    #[test]
1178    fn test_create_hmac_valid() {
1179        let args = CreateHmacArgs {
1180            protocol_id: test_protocol(),
1181            key_id: "my-key".to_string(),
1182            counterparty: test_counterparty(),
1183            data: vec![1, 2, 3],
1184            privileged: false,
1185            privileged_reason: None,
1186            seek_permission: None,
1187        };
1188        assert!(validate_create_hmac_args(&args).is_ok());
1189    }
1190
1191    #[test]
1192    fn test_create_hmac_empty_data() {
1193        let args = CreateHmacArgs {
1194            protocol_id: test_protocol(),
1195            key_id: "my-key".to_string(),
1196            counterparty: test_counterparty(),
1197            data: vec![],
1198            privileged: false,
1199            privileged_reason: None,
1200            seek_permission: None,
1201        };
1202        assert!(validate_create_hmac_args(&args).is_err());
1203    }
1204
1205    // ---- VerifyHmacArgs ----
1206
1207    #[test]
1208    fn test_verify_hmac_valid() {
1209        let args = VerifyHmacArgs {
1210            protocol_id: test_protocol(),
1211            key_id: "my-key".to_string(),
1212            counterparty: test_counterparty(),
1213            data: vec![1, 2, 3],
1214            hmac: vec![4, 5, 6],
1215            privileged: false,
1216            privileged_reason: None,
1217            seek_permission: None,
1218        };
1219        assert!(validate_verify_hmac_args(&args).is_ok());
1220    }
1221
1222    #[test]
1223    fn test_verify_hmac_empty_hmac() {
1224        let args = VerifyHmacArgs {
1225            protocol_id: test_protocol(),
1226            key_id: "my-key".to_string(),
1227            counterparty: test_counterparty(),
1228            data: vec![1, 2, 3],
1229            hmac: vec![],
1230            privileged: false,
1231            privileged_reason: None,
1232            seek_permission: None,
1233        };
1234        assert!(validate_verify_hmac_args(&args).is_err());
1235    }
1236
1237    // ---- CreateSignatureArgs ----
1238
1239    #[test]
1240    fn test_create_signature_valid() {
1241        let args = CreateSignatureArgs {
1242            protocol_id: test_protocol(),
1243            key_id: "my-key".to_string(),
1244            counterparty: test_counterparty(),
1245            data: Some(vec![1, 2, 3]),
1246            hash_to_directly_sign: None,
1247            privileged: false,
1248            privileged_reason: None,
1249            seek_permission: None,
1250        };
1251        assert!(validate_create_signature_args(&args).is_ok());
1252    }
1253
1254    #[test]
1255    fn test_create_signature_empty_data() {
1256        let args = CreateSignatureArgs {
1257            protocol_id: test_protocol(),
1258            key_id: "my-key".to_string(),
1259            counterparty: test_counterparty(),
1260            data: Some(vec![]),
1261            hash_to_directly_sign: None,
1262            privileged: false,
1263            privileged_reason: None,
1264            seek_permission: None,
1265        };
1266        assert!(validate_create_signature_args(&args).is_err());
1267    }
1268
1269    // ---- VerifySignatureArgs ----
1270
1271    #[test]
1272    fn test_verify_signature_valid() {
1273        let args = VerifySignatureArgs {
1274            protocol_id: test_protocol(),
1275            key_id: "my-key".to_string(),
1276            counterparty: test_counterparty(),
1277            data: Some(vec![1, 2, 3]),
1278            hash_to_directly_verify: None,
1279            signature: vec![4, 5, 6],
1280            for_self: None,
1281            privileged: false,
1282            privileged_reason: None,
1283            seek_permission: None,
1284        };
1285        assert!(validate_verify_signature_args(&args).is_ok());
1286    }
1287
1288    #[test]
1289    fn test_verify_signature_empty_signature() {
1290        let args = VerifySignatureArgs {
1291            protocol_id: test_protocol(),
1292            key_id: "my-key".to_string(),
1293            counterparty: test_counterparty(),
1294            data: Some(vec![1, 2, 3]),
1295            hash_to_directly_verify: None,
1296            signature: vec![],
1297            for_self: None,
1298            privileged: false,
1299            privileged_reason: None,
1300            seek_permission: None,
1301        };
1302        assert!(validate_verify_signature_args(&args).is_err());
1303    }
1304
1305    // ---- AcquireCertificateArgs ----
1306
1307    #[test]
1308    fn test_acquire_certificate_direct_valid() {
1309        let args = AcquireCertificateArgs {
1310            cert_type: CertificateType([0u8; 32]),
1311            certifier: test_pubkey(),
1312            acquisition_protocol: AcquisitionProtocol::Direct,
1313            fields: HashMap::new(),
1314            serial_number: Some(SerialNumber([0u8; 32])),
1315            revocation_outpoint: Some("abc.0".to_string()),
1316            signature: Some(vec![1, 2, 3]),
1317            certifier_url: None,
1318            keyring_revealer: Some(KeyringRevealer::Certifier),
1319            keyring_for_subject: Some(HashMap::new()),
1320            privileged: false,
1321            privileged_reason: None,
1322        };
1323        assert!(validate_acquire_certificate_args(&args).is_ok());
1324    }
1325
1326    #[test]
1327    fn test_acquire_certificate_direct_missing_serial() {
1328        let args = AcquireCertificateArgs {
1329            cert_type: CertificateType([0u8; 32]),
1330            certifier: test_pubkey(),
1331            acquisition_protocol: AcquisitionProtocol::Direct,
1332            fields: HashMap::new(),
1333            serial_number: None,
1334            revocation_outpoint: Some("abc.0".to_string()),
1335            signature: Some(vec![1, 2, 3]),
1336            certifier_url: None,
1337            keyring_revealer: Some(KeyringRevealer::Certifier),
1338            keyring_for_subject: Some(HashMap::new()),
1339            privileged: false,
1340            privileged_reason: None,
1341        };
1342        assert!(validate_acquire_certificate_args(&args).is_err());
1343    }
1344
1345    #[test]
1346    fn test_acquire_certificate_issuance_valid() {
1347        let args = AcquireCertificateArgs {
1348            cert_type: CertificateType([0u8; 32]),
1349            certifier: test_pubkey(),
1350            acquisition_protocol: AcquisitionProtocol::Issuance,
1351            fields: HashMap::new(),
1352            serial_number: None,
1353            revocation_outpoint: None,
1354            signature: None,
1355            certifier_url: Some("https://example.com".to_string()),
1356            keyring_revealer: None,
1357            keyring_for_subject: None,
1358            privileged: false,
1359            privileged_reason: None,
1360        };
1361        assert!(validate_acquire_certificate_args(&args).is_ok());
1362    }
1363
1364    #[test]
1365    fn test_acquire_certificate_issuance_missing_url() {
1366        let args = AcquireCertificateArgs {
1367            cert_type: CertificateType([0u8; 32]),
1368            certifier: test_pubkey(),
1369            acquisition_protocol: AcquisitionProtocol::Issuance,
1370            fields: HashMap::new(),
1371            serial_number: None,
1372            revocation_outpoint: None,
1373            signature: None,
1374            certifier_url: None,
1375            keyring_revealer: None,
1376            keyring_for_subject: None,
1377            privileged: false,
1378            privileged_reason: None,
1379        };
1380        assert!(validate_acquire_certificate_args(&args).is_err());
1381    }
1382
1383    // ---- ListCertificatesArgs ----
1384
1385    #[test]
1386    fn test_list_certificates_valid() {
1387        let args = ListCertificatesArgs {
1388            certifiers: vec![],
1389            types: vec![],
1390            limit: Some(10),
1391            offset: None,
1392            privileged: BooleanDefaultFalse(None),
1393            privileged_reason: None,
1394            partial: None,
1395        };
1396        assert!(validate_list_certificates_args(&args).is_ok());
1397    }
1398
1399    #[test]
1400    fn test_list_certificates_limit_too_high() {
1401        let args = ListCertificatesArgs {
1402            certifiers: vec![],
1403            types: vec![],
1404            limit: Some(10001),
1405            offset: None,
1406            privileged: BooleanDefaultFalse(None),
1407            privileged_reason: None,
1408            partial: None,
1409        };
1410        assert!(validate_list_certificates_args(&args).is_err());
1411    }
1412
1413    // ---- ProveCertificateArgs ----
1414
1415    #[test]
1416    fn test_prove_certificate_valid() {
1417        let args = ProveCertificateArgs {
1418            certificate: Certificate {
1419                cert_type: CertificateType([0u8; 32]),
1420                serial_number: SerialNumber([0u8; 32]),
1421                subject: test_pubkey(),
1422                certifier: test_pubkey(),
1423                revocation_outpoint: None,
1424                fields: None,
1425                signature: None,
1426            }
1427            .into(),
1428            fields_to_reveal: vec!["name".to_string()],
1429            verifier: test_pubkey(),
1430            privileged: BooleanDefaultFalse(None),
1431            privileged_reason: None,
1432        };
1433        assert!(validate_prove_certificate_args(&args).is_ok());
1434    }
1435
1436    #[test]
1437    fn test_prove_certificate_empty_fields() {
1438        let args = ProveCertificateArgs {
1439            certificate: Certificate {
1440                cert_type: CertificateType([0u8; 32]),
1441                serial_number: SerialNumber([0u8; 32]),
1442                subject: test_pubkey(),
1443                certifier: test_pubkey(),
1444                revocation_outpoint: None,
1445                fields: None,
1446                signature: None,
1447            }
1448            .into(),
1449            fields_to_reveal: vec![],
1450            verifier: test_pubkey(),
1451            privileged: BooleanDefaultFalse(None),
1452            privileged_reason: None,
1453        };
1454        assert!(validate_prove_certificate_args(&args).is_err());
1455    }
1456
1457    // ---- RelinquishCertificateArgs ----
1458
1459    #[test]
1460    fn test_relinquish_certificate_valid() {
1461        let args = RelinquishCertificateArgs {
1462            cert_type: CertificateType([0u8; 32]),
1463            serial_number: SerialNumber([0u8; 32]),
1464            certifier: test_pubkey(),
1465        };
1466        assert!(validate_relinquish_certificate_args(&args).is_ok());
1467    }
1468
1469    // ---- DiscoverByIdentityKeyArgs ----
1470
1471    #[test]
1472    fn test_discover_by_identity_key_valid() {
1473        let args = DiscoverByIdentityKeyArgs {
1474            identity_key: test_pubkey(),
1475            limit: None,
1476            offset: None,
1477            seek_permission: None,
1478        };
1479        assert!(validate_discover_by_identity_key_args(&args).is_ok());
1480    }
1481
1482    // ---- DiscoverByAttributesArgs ----
1483
1484    #[test]
1485    fn test_discover_by_attributes_valid() {
1486        let mut attrs = HashMap::new();
1487        attrs.insert("name".to_string(), "Alice".to_string());
1488        let args = DiscoverByAttributesArgs {
1489            attributes: attrs,
1490            limit: None,
1491            offset: None,
1492            seek_permission: None,
1493        };
1494        assert!(validate_discover_by_attributes_args(&args).is_ok());
1495    }
1496
1497    #[test]
1498    fn test_discover_by_attributes_empty() {
1499        let args = DiscoverByAttributesArgs {
1500            attributes: HashMap::new(),
1501            limit: None,
1502            offset: None,
1503            seek_permission: None,
1504        };
1505        assert!(validate_discover_by_attributes_args(&args).is_err());
1506    }
1507
1508    // ---- RevealCounterpartyKeyLinkageArgs ----
1509
1510    #[test]
1511    fn test_reveal_counterparty_key_linkage_valid() {
1512        let args = RevealCounterpartyKeyLinkageArgs {
1513            counterparty: test_pubkey(),
1514            verifier: test_pubkey(),
1515            privileged: None,
1516            privileged_reason: None,
1517        };
1518        assert!(validate_reveal_counterparty_key_linkage_args(&args).is_ok());
1519    }
1520
1521    // ---- RevealSpecificKeyLinkageArgs ----
1522
1523    #[test]
1524    fn test_reveal_specific_key_linkage_valid() {
1525        let args = RevealSpecificKeyLinkageArgs {
1526            counterparty: test_counterparty(),
1527            verifier: test_pubkey(),
1528            protocol_id: test_protocol(),
1529            key_id: "my-key".to_string(),
1530            privileged: None,
1531            privileged_reason: None,
1532        };
1533        assert!(validate_reveal_specific_key_linkage_args(&args).is_ok());
1534    }
1535
1536    #[test]
1537    fn test_reveal_specific_key_linkage_empty_key_id() {
1538        let args = RevealSpecificKeyLinkageArgs {
1539            counterparty: test_counterparty(),
1540            verifier: test_pubkey(),
1541            protocol_id: test_protocol(),
1542            key_id: "".to_string(),
1543            privileged: None,
1544            privileged_reason: None,
1545        };
1546        assert!(validate_reveal_specific_key_linkage_args(&args).is_err());
1547    }
1548
1549    // ---- GetHeaderArgs ----
1550
1551    #[test]
1552    fn test_get_header_valid() {
1553        let args = GetHeaderArgs { height: 100 };
1554        assert!(validate_get_header_args(&args).is_ok());
1555    }
1556
1557    #[test]
1558    fn test_get_header_zero_height() {
1559        let args = GetHeaderArgs { height: 0 };
1560        assert!(validate_get_header_args(&args).is_err());
1561    }
1562
1563    // ---- Privileged reason validation ----
1564
1565    #[test]
1566    fn test_privileged_without_reason() {
1567        let args = EncryptArgs {
1568            protocol_id: test_protocol(),
1569            key_id: "my-key".to_string(),
1570            counterparty: test_counterparty(),
1571            plaintext: vec![1, 2, 3],
1572            privileged: true,
1573            privileged_reason: None,
1574            seek_permission: None,
1575        };
1576        assert!(validate_encrypt_args(&args).is_err());
1577    }
1578
1579    #[test]
1580    fn test_privileged_with_short_reason() {
1581        let args = EncryptArgs {
1582            protocol_id: test_protocol(),
1583            key_id: "my-key".to_string(),
1584            counterparty: test_counterparty(),
1585            plaintext: vec![1, 2, 3],
1586            privileged: true,
1587            privileged_reason: Some("ab".to_string()),
1588            seek_permission: None,
1589        };
1590        assert!(validate_encrypt_args(&args).is_err());
1591    }
1592
1593    #[test]
1594    fn test_privileged_with_valid_reason() {
1595        let args = EncryptArgs {
1596            protocol_id: test_protocol(),
1597            key_id: "my-key".to_string(),
1598            counterparty: test_counterparty(),
1599            plaintext: vec![1, 2, 3],
1600            privileged: true,
1601            privileged_reason: Some("Admin access required".to_string()),
1602            seek_permission: None,
1603        };
1604        assert!(validate_encrypt_args(&args).is_ok());
1605    }
1606}