| 1 | package appconfig |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "sync" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | // gatedStore holds every Get until want of them are in flight at once, or |
| 14 | // a deadline passes. A loader that reads its layers one after another |
| 15 | // waits out the deadline on each; one that reads them together does not. |
| 16 | type gatedStore struct { |
| 17 | release.Store |
| 18 | want int |
| 19 | mu sync.Mutex |
| 20 | in int |
| 21 | open chan struct{} |
| 22 | } |
| 23 | |
| 24 | func (g *gatedStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 25 | g.mu.Lock() |
| 26 | g.in++ |
| 27 | if g.in == g.want { |
| 28 | close(g.open) |
| 29 | } |
| 30 | g.mu.Unlock() |
| 31 | select { |
| 32 | case <-g.open: |
| 33 | case <-time.After(300 * time.Millisecond): |
| 34 | } |
| 35 | return g.Store.Get(ctx, key) |
| 36 | } |
| 37 | |
| 38 | func putManifest(t *testing.T, s release.Store, key string, m *Manifest) { |
| 39 | t.Helper() |
| 40 | raw, err := json.Marshal(m) |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | if _, err := s.Put(context.Background(), key, raw, ""); err != nil { |
| 45 | t.Fatal(err) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // A wake's config admission reads three layers (the app's default bundle, |
| 50 | // its environment, the host's overlay). They are independent objects, so |
| 51 | // they are read together: one round trip, not three, before the lease. |
| 52 | func TestLoadMergedHostVersionsReadsItsLayersTogether(t *testing.T) { |
| 53 | dir := release.NewDirStore(t.TempDir()) |
| 54 | 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"}}}) |
| 55 | 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"}}}) |
| 56 | putManifest(t, dir, InstanceConfigKey("bab", "shop", "one.example.com"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{"C": {Kind: "env", Value: "host"}}}) |
| 57 | g := &gatedStore{Store: dir, want: 3, open: make(chan struct{})} |
| 58 | start := time.Now() |
| 59 | m, v, err := LoadMergedHostVersions(context.Background(), g, "bab", "shop", "prod", "one.example.com") |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | if took := time.Since(start); took > 250*time.Millisecond { |
| 64 | t.Errorf("took %s: the three layers were not read together", took) |
| 65 | } |
| 66 | if m.Vars["A"].Value != "default" || m.Vars["B"].Value != "env" || m.Vars["C"].Value != "host" { |
| 67 | t.Errorf("merge = %+v, want default < environment < host", m.Vars) |
| 68 | } |
| 69 | if v.Default == "" || v.Environment == "" || v.Overlay == "" { |
| 70 | t.Errorf("versions = %+v, want each layer's etag", v) |
| 71 | } |
| 72 | } |
| 73 | |
| 74 | // Reading together changes no answer: an environment the route is bound to |
| 75 | // but nobody created is still the error, whatever the other reads found. |
| 76 | func TestLoadMergedHostVersionsStillRefusesAMissingEnvironment(t *testing.T) { |
| 77 | dir := release.NewDirStore(t.TempDir()) |
| 78 | putManifest(t, dir, Key("bab", "shop"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{}}) |
| 79 | putManifest(t, dir, InstanceConfigKey("bab", "shop", "one.example.com"), &Manifest{Schema: 1, App: "shop", Vars: map[string]Var{}}) |
| 80 | if _, _, err := LoadMergedHostVersions(context.Background(), dir, "bab", "shop", "prod", "one.example.com"); err == nil { |
| 81 | t.Fatal("a missing environment was accepted") |
| 82 | } |
| 83 | } |
| 84 | |