@@ -647,6 +647,215 @@ fn test_load_many_custom_config_files() {
647647 assert_eq ! ( param_path, "custom value" ) ;
648648}
649649
650+ #[ derive( Deserialize , Debug , PartialEq ) ]
651+ struct NativeInnerConfig {
652+ a : usize ,
653+ b : String ,
654+ }
655+
656+ #[ derive( Deserialize , Debug , PartialEq ) ]
657+ struct NativeConfig {
658+ top : String ,
659+ inner : NativeInnerConfig ,
660+ }
661+
662+ // Mirrors the real secrets layout: dotted keys reach deep, real, nested leaves (e.g.
663+ // `consensus_manager_config.network_config.secret_key`), never single-token pointer targets.
664+ #[ derive( Deserialize , Debug , PartialEq ) ]
665+ struct NativeNetworkConfig {
666+ secret_key : String ,
667+ other_network_field : bool ,
668+ }
669+
670+ #[ derive( Deserialize , Debug , PartialEq ) ]
671+ struct NativeComponentConfig {
672+ network_config : NativeNetworkConfig ,
673+ other_component_field : u32 ,
674+ }
675+
676+ #[ derive( Deserialize , Debug , PartialEq ) ]
677+ struct NativeDeepConfig {
678+ component_config : NativeComponentConfig ,
679+ other_top_field : String ,
680+ }
681+
682+ // A `None` optional sub-config, mirroring a disabled component (e.g. `central_sync_client_config`).
683+ #[ derive( Deserialize , Debug , PartialEq ) ]
684+ struct NativeOptionalConfig {
685+ optional_component : Option < NativeComponentConfig > ,
686+ top_field : String ,
687+ }
688+
689+ // Loads a config in `--config_format native` mode. The schema file is unused by the native path but
690+ // still parsed, so an empty object is supplied as a valid (empty) schema.
691+ fn load_native_config < T : for < ' a > Deserialize < ' a > > (
692+ config_file_args : Vec < & str > ,
693+ ) -> Result < T , ConfigError > {
694+ let schema_file = NamedTempFile :: new ( ) . unwrap ( ) ;
695+ std:: fs:: write ( schema_file. path ( ) , json ! ( { } ) . to_string ( ) ) . unwrap ( ) ;
696+
697+ let args = chain ! ( [ "Testing" , "--config_format" , "native" ] , config_file_args)
698+ . map ( |arg| arg. to_owned ( ) )
699+ . collect ( ) ;
700+
701+ load_and_process_config :: < T > (
702+ File :: open ( schema_file. path ( ) ) . unwrap ( ) ,
703+ Command :: new ( "Program" ) ,
704+ args,
705+ false ,
706+ )
707+ }
708+
709+ #[ test]
710+ fn test_native_config_with_secret_overrides ( ) {
711+ let base_file = NamedTempFile :: new ( ) . unwrap ( ) ;
712+ let base_config = json ! ( { "top" : "base_top" , "inner" : { "a" : 1 , "b" : "base_b" } } ) ;
713+ std:: fs:: write ( base_file. path ( ) , base_config. to_string ( ) ) . unwrap ( ) ;
714+ // Secrets stay flat dotted-key and are applied onto the nested base.
715+ let secret_file = NamedTempFile :: new ( ) . unwrap ( ) ;
716+ std:: fs:: write ( secret_file. path ( ) , json ! ( { "inner.b" : "secret_b" } ) . to_string ( ) ) . unwrap ( ) ;
717+
718+ let loaded: NativeConfig = load_native_config ( vec ! [
719+ CONFIG_FILE_ARG ,
720+ base_file. path( ) . to_str( ) . unwrap( ) ,
721+ CONFIG_FILE_ARG ,
722+ secret_file. path( ) . to_str( ) . unwrap( ) ,
723+ ] )
724+ . unwrap ( ) ;
725+
726+ assert_eq ! (
727+ loaded,
728+ NativeConfig {
729+ top: "base_top" . to_owned( ) ,
730+ inner: NativeInnerConfig { a: 1 , b: "secret_b" . to_owned( ) } ,
731+ }
732+ ) ;
733+ }
734+
735+ #[ test]
736+ fn test_native_config_single_file ( ) {
737+ let base_file = NamedTempFile :: new ( ) . unwrap ( ) ;
738+ let base_config = json ! ( { "top" : "base_top" , "inner" : { "a" : 1 , "b" : "base_b" } } ) ;
739+ std:: fs:: write ( base_file. path ( ) , base_config. to_string ( ) ) . unwrap ( ) ;
740+
741+ let loaded: NativeConfig =
742+ load_native_config ( vec ! [ CONFIG_FILE_ARG , base_file. path( ) . to_str( ) . unwrap( ) ] ) . unwrap ( ) ;
743+
744+ assert_eq ! (
745+ loaded,
746+ NativeConfig {
747+ top: "base_top" . to_owned( ) ,
748+ inner: NativeInnerConfig { a: 1 , b: "base_b" . to_owned( ) } ,
749+ }
750+ ) ;
751+ }
752+
753+ #[ test]
754+ fn test_native_config_without_config_file ( ) {
755+ let result = load_native_config :: < NativeConfig > ( vec ! [ ] ) ;
756+ assert_matches ! ( result, Err ( ConfigError :: NativeModeRequiresConfigFile ) ) ;
757+ }
758+
759+ // Mirrors the production secrets file (crates/apollo_deployments/resources/testing_secrets.json):
760+ // each secret is a flat dotted key naming a deep, real nested leaf. Asserts the override lands at
761+ // exactly that leaf and every sibling at every level is preserved.
762+ #[ test]
763+ fn test_native_config_deep_secret_override_preserves_siblings ( ) {
764+ let base_file = NamedTempFile :: new ( ) . unwrap ( ) ;
765+ let base_config = json ! ( {
766+ "other_top_field" : "base_top" ,
767+ "component_config" : {
768+ "other_component_field" : 7 ,
769+ "network_config" : {
770+ "secret_key" : "base_secret" ,
771+ "other_network_field" : true
772+ }
773+ }
774+ } ) ;
775+ std:: fs:: write ( base_file. path ( ) , base_config. to_string ( ) ) . unwrap ( ) ;
776+
777+ let secret_file = NamedTempFile :: new ( ) . unwrap ( ) ;
778+ let secret_config = json ! ( { "component_config.network_config.secret_key" : "0xabc" } ) ;
779+ std:: fs:: write ( secret_file. path ( ) , secret_config. to_string ( ) ) . unwrap ( ) ;
780+
781+ let loaded: NativeDeepConfig = load_native_config ( vec ! [
782+ CONFIG_FILE_ARG ,
783+ base_file. path( ) . to_str( ) . unwrap( ) ,
784+ CONFIG_FILE_ARG ,
785+ secret_file. path( ) . to_str( ) . unwrap( ) ,
786+ ] )
787+ . unwrap ( ) ;
788+
789+ assert_eq ! (
790+ loaded,
791+ NativeDeepConfig {
792+ other_top_field: "base_top" . to_owned( ) ,
793+ component_config: NativeComponentConfig {
794+ other_component_field: 7 ,
795+ network_config: NativeNetworkConfig {
796+ secret_key: "0xabc" . to_owned( ) ,
797+ other_network_field: true ,
798+ } ,
799+ } ,
800+ }
801+ ) ;
802+ }
803+
804+ // A secret aimed at a child of a `None` (null) intermediate must be skipped, not vivified into a
805+ // partial object — otherwise deserialization of the (incomplete) sub-config would fail. The
806+ // disabled component stays `None`.
807+ #[ test]
808+ fn test_native_config_skips_secret_under_none_intermediate ( ) {
809+ let base_file = NamedTempFile :: new ( ) . unwrap ( ) ;
810+ let base_config = json ! ( { "optional_component" : null, "top_field" : "base" } ) ;
811+ std:: fs:: write ( base_file. path ( ) , base_config. to_string ( ) ) . unwrap ( ) ;
812+
813+ let secret_file = NamedTempFile :: new ( ) . unwrap ( ) ;
814+ let secret_config = json ! ( { "optional_component.network_config.secret_key" : "0xabc" } ) ;
815+ std:: fs:: write ( secret_file. path ( ) , secret_config. to_string ( ) ) . unwrap ( ) ;
816+
817+ let loaded: NativeOptionalConfig = load_native_config ( vec ! [
818+ CONFIG_FILE_ARG ,
819+ base_file. path( ) . to_str( ) . unwrap( ) ,
820+ CONFIG_FILE_ARG ,
821+ secret_file. path( ) . to_str( ) . unwrap( ) ,
822+ ] )
823+ . unwrap ( ) ;
824+
825+ assert_eq ! (
826+ loaded,
827+ NativeOptionalConfig { optional_component: None , top_field: "base" . to_owned( ) }
828+ ) ;
829+ }
830+
831+ // A secret naming a leaf that is absent from an existing parent object is created (the parent
832+ // exists, so the component is enabled and the override is relevant).
833+ #[ test]
834+ fn test_native_config_creates_absent_leaf_in_existing_parent ( ) {
835+ let base_file = NamedTempFile :: new ( ) . unwrap ( ) ;
836+ let base_config = json ! ( { "top" : "base_top" , "inner" : { "a" : 1 } } ) ;
837+ std:: fs:: write ( base_file. path ( ) , base_config. to_string ( ) ) . unwrap ( ) ;
838+
839+ let secret_file = NamedTempFile :: new ( ) . unwrap ( ) ;
840+ std:: fs:: write ( secret_file. path ( ) , json ! ( { "inner.b" : "created_b" } ) . to_string ( ) ) . unwrap ( ) ;
841+
842+ let loaded: NativeConfig = load_native_config ( vec ! [
843+ CONFIG_FILE_ARG ,
844+ base_file. path( ) . to_str( ) . unwrap( ) ,
845+ CONFIG_FILE_ARG ,
846+ secret_file. path( ) . to_str( ) . unwrap( ) ,
847+ ] )
848+ . unwrap ( ) ;
849+
850+ assert_eq ! (
851+ loaded,
852+ NativeConfig {
853+ top: "base_top" . to_owned( ) ,
854+ inner: NativeInnerConfig { a: 1 , b: "created_b" . to_owned( ) } ,
855+ }
856+ ) ;
857+ }
858+
650859// Make sure that if we have a field `foo_bar` and an optional field called `foo` with a value of
651860// None, we don't remove the foo_bar field from the config.
652861// This test was added following bug #37984 (see bug for more details).
0 commit comments