package appconfig import ( "context" "encoding/json" "sync" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // gatedStore holds every Get until want of them are in flight at once, or // a deadline passes. A loader that reads its layers one after another // waits out the deadline on each; one that reads them together does not. type gatedStore struct { release.Store want int mu sync.Mutex in int open chan struct{} } func (g *gatedStore) Get(ctx context.Context, key string) ([]byte, string, error) { g.mu.Lock() g.in++ if g.in == g.want { close(g.open) } g.mu.Unlock() select { case <-g.open: case <-time.After(300 * time.Millisecond): } return g.Store.Get(ctx, key) } func putManifest(t *testing.T, s release.Store, key string, m *Manifest) { t.Helper() raw, err := json.Marshal(m) if err != nil { t.Fatal(err) } if _, err := s.Put(context.Background(), key, raw, ""); err != nil { t.Fatal(err) } } // A wake's config admission reads three layers (the app's default bundle, // its environment, the host's overlay). They are independent objects, so // they are read together: one round trip, not three, before the lease. func TestLoadMergedHostVersionsReadsItsLayersTogether(t *testing.T) { dir := release.NewDirStore(t.TempDir()) putManifest(t, dir, Key("bab", "shop"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{"A": {Kind: "env", Value: "default"}, "B": {Kind: "env", Value: "default"}}}) putManifest(t, dir, EnvironmentKey("bab", "shop", "prod"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{"B": {Kind: "env", Value: "env"}, "C": {Kind: "env", Value: "env"}}}) putManifest(t, dir, InstanceConfigKey("bab", "shop", "one.example.com"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{"C": {Kind: "env", Value: "host"}}}) g := &gatedStore{Store: dir, want: 3, open: make(chan struct{})} start := time.Now() m, v, err := LoadMergedHostVersions(context.Background(), g, "bab", "shop", "prod", "one.example.com") if err != nil { t.Fatal(err) } if took := time.Since(start); took > 250*time.Millisecond { t.Errorf("took %s: the three layers were not read together", took) } if m.Vars["A"].Value != "default" || m.Vars["B"].Value != "env" || m.Vars["C"].Value != "host" { t.Errorf("merge = %+v, want default < environment < host", m.Vars) } if v.Default == "" || v.Environment == "" || v.Overlay == "" { t.Errorf("versions = %+v, want each layer's etag", v) } } // Reading together changes no answer: an environment the route is bound to // but nobody created is still the error, whatever the other reads found. func TestLoadMergedHostVersionsStillRefusesAMissingEnvironment(t *testing.T) { dir := release.NewDirStore(t.TempDir()) putManifest(t, dir, Key("bab", "shop"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{}}) putManifest(t, dir, InstanceConfigKey("bab", "shop", "one.example.com"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{}}) if _, _, err := LoadMergedHostVersions(context.Background(), dir, "bab", "shop", "prod", "one.example.com"); err == nil { t.Fatal("a missing environment was accepted") } }