@@ -17,8 +17,10 @@ package agollo
1717import (
1818 "context"
1919 "encoding/json"
20+ "errors"
2021 "net/http"
2122 "net/http/httptest"
23+ "os"
2224 "regexp"
2325 "strings"
2426 "sync"
@@ -465,6 +467,40 @@ func TestApolloClientCloseRejectsNewStateAndSubscriptions(t *testing.T) {
465467 }
466468}
467469
470+ func TestApolloClientCloseWaitsForConfigSubscription (t * testing.T ) {
471+ client := newTestClient (t , "http://127.0.0.1" , ClientOptions {AppID : "sample" })
472+ state := newModernConfig (client , ConfigKey {AppID : "sample" , Cluster : "default" , Namespace : "application" , Format : ConfigFileFormatProperties })
473+ client .mu .Lock ()
474+ client .states [state .key ] = state
475+ client .mu .Unlock ()
476+
477+ started := make (chan struct {})
478+ release := make (chan struct {})
479+ state .Subscribe (func (ConfigChangeEvent ) {
480+ close (started )
481+ <- release
482+ })
483+ state .publish (ConfigSnapshot {Values : map [string ]interface {}{"key" : "value" }, Source : ConfigSourceRemote })
484+ <- started
485+
486+ closed := make (chan error , 1 )
487+ go func () { closed <- client .Close () }()
488+ select {
489+ case err := <- closed :
490+ t .Fatalf ("Close() returned before listener finished: %v" , err )
491+ case <- time .After (50 * time .Millisecond ):
492+ }
493+ close (release )
494+ select {
495+ case err := <- closed :
496+ if err != nil {
497+ t .Fatalf ("Close() error = %v" , err )
498+ }
499+ case <- time .After (time .Second ):
500+ t .Fatal ("Close() did not wait for listener completion" )
501+ }
502+ }
503+
468504func TestDecodeDiskSnapshotRejectsDifferentCluster (t * testing.T ) {
469505 t .Parallel ()
470506 body , err := json .Marshal (diskSnapshot {
@@ -590,6 +626,117 @@ func TestApolloClientOfflineLoadsConfigMapWithoutCacheDirectory(t *testing.T) {
590626 }
591627}
592628
629+ func TestApolloClientOfflineConfigMapDoesNotReadWorkingDirectoryCache (t * testing.T ) {
630+ workingDirectory , err := os .Getwd ()
631+ if err != nil {
632+ t .Fatalf ("Getwd() error = %v" , err )
633+ }
634+ temporaryDirectory := t .TempDir ()
635+ if err := os .Chdir (temporaryDirectory ); err != nil {
636+ t .Fatalf ("Chdir() error = %v" , err )
637+ }
638+ t .Cleanup (func () { _ = os .Chdir (workingDirectory ) })
639+ if err := os .WriteFile ("sample-application.json" , []byte (`{"appId":"sample","cluster":"default","namespaceName":"application","configurations":{"key":"disk"}}` ), 0o600 ); err != nil {
640+ t .Fatalf ("WriteFile() error = %v" , err )
641+ }
642+
643+ store := & memoryConfigMapStore {snapshot : ConfigSnapshot {
644+ Key : ConfigKey {AppID : "sample" , Cluster : "default" , Namespace : "application" , Format : ConfigFileFormatProperties },
645+ Values : map [string ]interface {}{"key" : "configmap" },
646+ }}
647+ client , err := NewClient (context .Background (), ClientOptions {AppID : "sample" , ConfigMapStore : store , Offline : true })
648+ if err != nil {
649+ t .Fatalf ("NewClient() error = %v" , err )
650+ }
651+ defer client .Close ()
652+ config , err := client .Config (context .Background (), "application" )
653+ if err != nil {
654+ t .Fatalf ("Config() error = %v" , err )
655+ }
656+ if got := config .String ("key" , "" ); got != "configmap" || config .Source () != ConfigSourceConfigMap {
657+ t .Fatalf ("offline ConfigMap value = %q from %s" , got , config .Source ())
658+ }
659+ }
660+
661+ func TestApolloClientLoadHonorsOperationAndLifecycleContexts (t * testing.T ) {
662+ t .Run ("operation context cancels ConfigMap load" , func (t * testing.T ) {
663+ store := & blockingConfigMapStore {started : make (chan struct {}, 1 )}
664+ client , err := NewClient (context .Background (), ClientOptions {AppID : "sample" , ConfigMapStore : store , Offline : true })
665+ if err != nil {
666+ t .Fatalf ("NewClient() error = %v" , err )
667+ }
668+ defer client .Close ()
669+ ctx , cancel := context .WithCancel (context .Background ())
670+ result := make (chan error , 1 )
671+ go func () {
672+ _ , err := client .Config (ctx , "application" )
673+ result <- err
674+ }()
675+ <- store .started
676+ cancel ()
677+ select {
678+ case err := <- result :
679+ if ! errors .Is (err , context .Canceled ) {
680+ t .Fatalf ("Config() error = %v, want context cancellation" , err )
681+ }
682+ case <- time .After (time .Second ):
683+ t .Fatal ("ConfigMap load did not observe operation cancellation" )
684+ }
685+ })
686+
687+ t .Run ("client lifecycle cancels remote load" , func (t * testing.T ) {
688+ started := make (chan struct {}, 1 )
689+ server := httptest .NewServer (http .HandlerFunc (func (_ http.ResponseWriter , request * http.Request ) {
690+ select {
691+ case started <- struct {}{}:
692+ default :
693+ }
694+ <- request .Context ().Done ()
695+ }))
696+ defer server .Close ()
697+ clientContext , cancelClient := context .WithCancel (context .Background ())
698+ client , err := NewClient (clientContext , ClientOptions {AppID : "sample" , ConfigServices : []string {server .URL }, DisableLongPolling : true })
699+ if err != nil {
700+ t .Fatalf ("NewClient() error = %v" , err )
701+ }
702+ defer client .Close ()
703+ result := make (chan error , 1 )
704+ go func () {
705+ _ , err := client .Config (context .Background (), "application" )
706+ result <- err
707+ }()
708+ <- started
709+ cancelClient ()
710+ select {
711+ case err := <- result :
712+ if ! errors .Is (err , context .Canceled ) {
713+ t .Fatalf ("Config() error = %v, want context cancellation" , err )
714+ }
715+ case <- time .After (time .Second ):
716+ t .Fatal ("remote load did not observe client cancellation" )
717+ }
718+ })
719+ }
720+
721+ func TestApolloClientIntSliceReadsNativeIntSlice (t * testing.T ) {
722+ store := & memoryConfigMapStore {snapshot : ConfigSnapshot {
723+ Key : ConfigKey {AppID : "sample" , Cluster : "default" , Namespace : "application" , Format : ConfigFileFormatProperties },
724+ Values : map [string ]interface {}{"ports" : []int {8080 , 9090 }},
725+ }}
726+ client , err := NewClient (context .Background (), ClientOptions {AppID : "sample" , ConfigMapStore : store , Offline : true })
727+ if err != nil {
728+ t .Fatalf ("NewClient() error = %v" , err )
729+ }
730+ defer client .Close ()
731+ config , err := client .Config (context .Background (), "application" )
732+ if err != nil {
733+ t .Fatalf ("Config() error = %v" , err )
734+ }
735+ if got := config .IntSlice ("ports" , nil ); len (got ) != 2 || got [0 ] != 8080 || got [1 ] != 9090 {
736+ t .Fatalf ("IntSlice(ports) = %v" , got )
737+ }
738+ }
739+
593740func TestApolloClientDiscoversConfigServiceFromMetaServer (t * testing.T ) {
594741 t .Parallel ()
595742 var server * httptest.Server
@@ -651,8 +798,29 @@ func TestApolloClientLongPollBuildsDataCenterAndRefreshes(t *testing.T) {
651798 }
652799 }))
653800 defer server .Close ()
801+ wrongServer := httptest .NewServer (http .HandlerFunc (func (_ http.ResponseWriter , request * http.Request ) {
802+ t .Fatalf ("selector was bypassed for %s" , request .URL .Path )
803+ }))
804+ defer wrongServer .Close ()
654805
655- client := newTestClient (t , server .URL , ClientOptions {AppID : "sample" , DataCenter : "sh" , ClientIP : "127.0.0.2" })
806+ var selectorCalls atomic.Int32
807+ client , err := NewClient (context .Background (), ClientOptions {
808+ AppID : "sample" ,
809+ DataCenter : "sh" ,
810+ ClientIP : "127.0.0.2" ,
811+ ConfigServices : []string {wrongServer .URL , server .URL },
812+ DisableLongPolling : true ,
813+ ConfigServiceSelector : func (appID string , services []string ) (string , error ) {
814+ if appID != "sample" || len (services ) != 2 {
815+ t .Fatalf ("selector input = %q, %v" , appID , services )
816+ }
817+ selectorCalls .Add (1 )
818+ return server .URL , nil
819+ },
820+ })
821+ if err != nil {
822+ t .Fatalf ("NewClient() error = %v" , err )
823+ }
656824 defer client .Close ()
657825 config , err := client .Config (context .Background (), "application" )
658826 if err != nil {
@@ -665,6 +833,9 @@ func TestApolloClientLongPollBuildsDataCenterAndRefreshes(t *testing.T) {
665833 if got := config .String ("key" , "" ); got != "after" {
666834 t .Fatalf ("long-poll refreshed value = %q" , got )
667835 }
836+ if got := selectorCalls .Load (); got != 3 {
837+ t .Fatalf ("selector calls = %d, want initial fetch, long-poll, and refresh selection" , got )
838+ }
668839}
669840
670841func TestApolloClientCloseCancelsLongPoll (t * testing.T ) {
@@ -761,6 +932,21 @@ type memoryConfigMapStore struct {
761932 saved chan ConfigSnapshot
762933}
763934
935+ type blockingConfigMapStore struct {
936+ started chan struct {}
937+ }
938+
939+ func (s * blockingConfigMapStore ) Load (ctx context.Context , _ ConfigKey ) (ConfigSnapshot , error ) {
940+ select {
941+ case s .started <- struct {}{}:
942+ default :
943+ }
944+ <- ctx .Done ()
945+ return ConfigSnapshot {}, ctx .Err ()
946+ }
947+
948+ func (s * blockingConfigMapStore ) Save (context.Context , ConfigSnapshot ) error { return nil }
949+
764950func (s * memoryConfigMapStore ) Load (ctx context.Context , key ConfigKey ) (ConfigSnapshot , error ) {
765951 s .mu .Lock ()
766952 defer s .mu .Unlock ()
0 commit comments