| 1 | package appconfig |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | |
| 6 | "github.com/carlosframework/platform/internal/registry" |
| 7 | ) |
| 8 | |
| 9 | // fpExecRoute is the TCP-addr exec shape (issue #18): carlos proxies to it |
| 10 | // but neither spawns nor owns the process. |
| 11 | func fpExecRoute(host, env string) registry.Route { |
| 12 | return registry.Route{Host: host, Kind: "instance", App: "titogo", |
| 13 | Account: "bab", Status: "ready", Addr: "127.0.0.1:9001", Environment: env} |
| 14 | } |
| 15 | |
| 16 | // fpUnitRoute is the unit-backed shape the ROOT pass delivers config for. |
| 17 | func fpUnitRoute(host, env string) registry.Route { |
| 18 | return registry.Route{Host: host, Kind: "instance", App: "keymail", |
| 19 | Account: "bae", Status: "ready", Addr: "127.0.0.1:9100", |
| 20 | Backing: registry.BackingUnit, Environment: env} |
| 21 | } |
| 22 | |
| 23 | func TestConfigRouteFingerprintsMoveOnRebinding(t *testing.T) { |
| 24 | before := []registry.Route{fpExecRoute("connect.go.tito.io", "")} |
| 25 | after := []registry.Route{fpExecRoute("connect.go.tito.io", "relay")} |
| 26 | be, bu := ConfigRouteFingerprints(before) |
| 27 | ae, au := ConfigRouteFingerprints(after) |
| 28 | if be == ae { |
| 29 | t.Fatal("binding an exec route to an environment must move the exec digest — this is the hour issue #196 cost") |
| 30 | } |
| 31 | if bu != au { |
| 32 | t.Fatal("an exec route's rebinding must not move the UNIT digest — that would ask for a root config pass for nothing") |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | func TestConfigRouteFingerprintsUnitHalfIsIndependent(t *testing.T) { |
| 37 | before := []registry.Route{fpUnitRoute("keymail.dev", "")} |
| 38 | after := []registry.Route{fpUnitRoute("keymail.dev", "staging")} |
| 39 | be, bu := ConfigRouteFingerprints(before) |
| 40 | ae, au := ConfigRouteFingerprints(after) |
| 41 | if bu == au { |
| 42 | t.Fatal("binding a unit-backed route to an environment must move the unit digest — the root pass resolves per environment too") |
| 43 | } |
| 44 | if be != ae { |
| 45 | t.Fatal("a unit route's rebinding must not move the exec digest") |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestConfigRouteFingerprintsIgnoreOrderAndIrrelevantColumns(t *testing.T) { |
| 50 | a := fpExecRoute("a.example", "") |
| 51 | b := fpExecRoute("b.example", "relay") |
| 52 | e1, _ := ConfigRouteFingerprints([]registry.Route{a, b}) |
| 53 | e2, _ := ConfigRouteFingerprints([]registry.Route{b, a}) |
| 54 | if e1 != e2 { |
| 55 | t.Fatal("registry order must not move the digest — it is not stable across passes") |
| 56 | } |
| 57 | // A column that cannot change which bytes belong in a route's config |
| 58 | // file must not move the digest, or every adopt would fire a pass. |
| 59 | c := fpExecRoute("a.example", "") |
| 60 | c.Version = "deadbeef" |
| 61 | c.PrevVersion = "cafebabe" |
| 62 | c.LastError = "whatever" |
| 63 | c.Hibernate = true |
| 64 | e3, _ := ConfigRouteFingerprints([]registry.Route{c, b}) |
| 65 | if e1 != e3 { |
| 66 | t.Fatalf("an irrelevant column moved the digest (%s -> %s)", e1, e3) |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | func TestConfigRouteFingerprintsMoveWhenARouteLeavesScope(t *testing.T) { |
| 71 | both := []registry.Route{fpExecRoute("a.example", ""), fpExecRoute("b.example", "")} |
| 72 | one := []registry.Route{fpExecRoute("a.example", "")} |
| 73 | e1, _ := ConfigRouteFingerprints(both) |
| 74 | e2, _ := ConfigRouteFingerprints(one) |
| 75 | if e1 == e2 { |
| 76 | t.Fatal("a route leaving the registry must move the digest — its file needs reaping") |
| 77 | } |
| 78 | // Leaving scope by STATUS, not by deletion, counts exactly the same: |
| 79 | // the eligibility predicate is what set membership is drawn from. |
| 80 | failed := []registry.Route{fpExecRoute("a.example", ""), fpExecRoute("b.example", "")} |
| 81 | failed[1].Status = "failed" |
| 82 | e3, _ := ConfigRouteFingerprints(failed) |
| 83 | if e1 == e3 { |
| 84 | t.Fatal("a route falling out of the eligibility predicate must move the digest") |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | func TestConfigRouteFingerprintsIgnoreOutOfScopeRoutes(t *testing.T) { |
| 89 | base := []registry.Route{fpExecRoute("a.example", "")} |
| 90 | withOthers := append([]registry.Route{}, base...) |
| 91 | withOthers = append(withOthers, |
| 92 | // A static route: no config is delivered to it by either half. |
| 93 | registry.Route{Host: "site.example", Kind: "static", App: "site", Account: "bab"}, |
| 94 | // A platform-minted row: excluded by execRouteBase and by |
| 95 | // release.Adoptable alike. |
| 96 | registry.Route{Host: "edge.example", Kind: "instance", App: "titogo", Account: "bab", |
| 97 | Status: "ready", Addr: "127.0.0.1:9500", Platform: true}, |
| 98 | ) |
| 99 | e1, u1 := ConfigRouteFingerprints(base) |
| 100 | e2, u2 := ConfigRouteFingerprints(withOthers) |
| 101 | if e1 != e2 || u1 != u2 { |
| 102 | t.Fatal("a route neither half can deliver config to must not move either digest") |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestConfigRouteFingerprintsEmptySetIsStable(t *testing.T) { |
| 107 | e1, u1 := ConfigRouteFingerprints(nil) |
| 108 | e2, u2 := ConfigRouteFingerprints([]registry.Route{}) |
| 109 | if e1 != e2 || u1 != u2 { |
| 110 | t.Fatal("nil and empty must digest identically — a box with no routes must not look like it changed every tick") |
| 111 | } |
| 112 | if e1 == "" || u1 == "" { |
| 113 | t.Fatal("the empty set must have a real digest, not the empty string: the fast loop tells 'never computed' from 'computed, and empty' by its own bool, not by this value") |
| 114 | } |
| 115 | } |
| 116 | |