| index cdf518660..5fd4e03f8 100644 |
| --- a/internal/edge/homemap_test.go |
| +++ b/internal/edge/homemap_test.go |
| @@ -1784,3 +1784,210 @@ func TestLocalWildcardYieldsToARemoteExactHost(t *testing.T) { |
| t.Fatalf("home edge saw %d request(s), want still 1", n) |
| } |
| } |
| + |
| +// ---- the lifetime pair (self-serve moves plan 1, Task 8) ------------------- |
| + |
| +// pairHost is the one name the pair-fold tests fold. |
| +const pairHost = "shop.example.com" |
| + |
| +// foldTwo folds a as annie's entry for pairHost and b as betty's, the |
| +// order the sorted file keys give, and returns the folded entry. |
| +func foldTwo(t *testing.T, a, b edgesHostView) homeEntry { |
| + t.Helper() |
| + files := map[string]edgesFileView{ |
| + "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{pairHost: a}), |
| + "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{pairHost: b}), |
| + } |
| + hosts, _ := foldHomes(files, "carol", false) |
| + return hosts[pairHost] |
| +} |
| + |
| +// winsBothOrders asserts that x takes the dial from y whichever file |
| +// sorts first. |
| +func winsBothOrders(t *testing.T, why string, x, y edgesHostView) { |
| + t.Helper() |
| + if got := foldTwo(t, x, y); !got.dial || got.box != "annie" { |
| + t.Errorf("%s: x first: dial %v box %q, want x (annie)", why, got.dial, got.box) |
| + } |
| + if got := foldTwo(t, y, x); !got.dial || got.box != "betty" { |
| + t.Errorf("%s: x second: dial %v box %q, want x (betty)", why, got.dial, got.box) |
| + } |
| +} |
| + |
| +// firstFileWins asserts today's rule: the earlier file keeps the dial, |
| +// whichever of x and y it holds. |
| +func firstFileWins(t *testing.T, why string, x, y edgesHostView) { |
| + t.Helper() |
| + if got := foldTwo(t, x, y); got.box != "annie" { |
| + t.Errorf("%s: x first: box %q, want the earlier file (annie)", why, got.box) |
| + } |
| + if got := foldTwo(t, y, x); got.box != "annie" { |
| + t.Errorf("%s: y first: box %q, want the earlier file (annie)", why, got.box) |
| + } |
| +} |
| + |
| +func TestFoldPrefersTheHigherPairOfOneLifetime(t *testing.T) { |
| + route := func(inc, seq uint64) edgesHostView { |
| + return edgesHostView{Role: "home", Incarnation: inc, ServingSeq: seq} |
| + } |
| + winsBothOrders(t, "open (1, 2) over the hold (1, 1)", route(1, 2), route(1, 1)) |
| + winsBothOrders(t, "a recreated host (2, 0) over lifetime 1", route(2, 0), route(1, 9)) |
| + winsBothOrders(t, "the first hold (1, 1) over a pair-less (0, 0)", route(1, 1), edgesHostView{Role: "home"}) |
| + firstFileWins(t, "equal pairs fall to the existing rule", route(1, 1), route(1, 1)) |
| + firstFileWins(t, "two pair-less entries fall to the existing rule", edgesHostView{Role: "home"}, edgesHostView{Role: "home"}) |
| + |
| + // The same rule read off the wire through a real Refresh. |
| + s := meshTestStore(t) |
| + putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.example.com":{"role":"home","incarnation":1,"serving_seq":1}}}`) |
| + putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.example.com":{"role":"home","incarnation":1,"serving_seq":2}}}`) |
| + m := newHomeMap(s, "carol") |
| + refreshHomeMap(t, m) |
| + if _, box, ok := m.Home(pairHost); !ok || box != "betty" { |
| + t.Fatalf("Home = %q %v, want betty: (1, 2) over (1, 1) off the wire", box, ok) |
| + } |
| +} |
| + |
| +func TestFoldStampBeatsPair(t *testing.T) { |
| + retained := edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 3} |
| + stamped := edgesHostView{Role: "home", Stamp: 5} |
| + winsBothOrders(t, "a stamped (0, 0) over an unstamped (1, 3)", stamped, retained) |
| + winsBothOrders(t, "the higher stamp whatever the pairs", |
| + edgesHostView{Role: "home", Stamp: 9}, edgesHostView{Role: "home", Stamp: 5, Incarnation: 3, ServingSeq: 3}) |
| + firstFileWins(t, "equal stamps keep the earlier file whatever the pairs", |
| + edgesHostView{Role: "home", Stamp: 5}, edgesHostView{Role: "home", Stamp: 5, Incarnation: 3, ServingSeq: 3}) |
| +} |
| + |
| +// The mixed-history statement's one case where the pair outranks an |
| +// unstamped claims-on publication: a retained claims-off file (its box's |
| +// file could not be refreshed after the deployment turned claims on) wins |
| +// until it refreshes. |
| +func TestFoldUnstampedClaimsOnAgainstRetainedPair(t *testing.T) { |
| + winsBothOrders(t, "a retained (1, 3) over an unstamped claims-on (0, 0)", |
| + edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 3}, edgesHostView{Role: "home"}) |
| +} |
| + |
| +func TestFoldKeepsRealRouteOverAHigherPairAlias(t *testing.T) { |
| + winsBothOrders(t, "a pair-less route over an alias at (5, 5)", |
| + edgesHostView{Role: "home"}, edgesHostView{Role: "home", Alias: true, Incarnation: 5, ServingSeq: 5, TargetHost: "bab.example.com"}) |
| + winsBothOrders(t, "a route at (1, 1) over an alias at (5, 5)", |
| + edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 1}, edgesHostView{Role: "home", Alias: true, Incarnation: 5, ServingSeq: 5, TargetHost: "bab.example.com"}) |
| +} |
| + |
| +func TestFoldComparesAliasesOnlyForOneTarget(t *testing.T) { |
| + alias := func(target string, inc, seq uint64) edgesHostView { |
| + return edgesHostView{Role: "home", Alias: true, TargetHost: target, Incarnation: inc, ServingSeq: seq} |
| + } |
| + winsBothOrders(t, "one target: the higher pair", alias("bab.example.com", 1, 2), alias("bab.example.com", 1, 1)) |
| + firstFileWins(t, "one target, equal pairs: the existing rule", alias("bab.example.com", 1, 1), alias("bab.example.com", 1, 1)) |
| + firstFileWins(t, "two targets: the existing rule whatever the pairs", alias("bab.example.com", 2, 0), alias("shop.example.net", 1, 1)) |
| + firstFileWins(t, "no target on either: the existing rule", alias("", 2, 0), alias("", 1, 1)) |
| + firstFileWins(t, "a target on one only: the existing rule", alias("bab.example.com", 2, 0), alias("", 1, 1)) |
| +} |
| + |
| +func TestFoldNeverDialsACertOnlyEntryForItsPair(t *testing.T) { |
| + const key = "*.shop.example.com" |
| + certOnly := edgesHostView{Role: "wildcard", Wildcard: true, Incarnation: 9, ServingSeq: 9} |
| + dial := edgesHostView{Role: "home", Wildcard: true, Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 1} |
| + for _, order := range [][2]edgesHostView{{certOnly, dial}, {dial, certOnly}} { |
| + files := map[string]edgesFileView{ |
| + "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: order[0]}), |
| + "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: order[1]}), |
| + } |
| + hosts, _ := foldHomes(files, "carol", false) |
| + want := "annie" |
| + if order[0].Role == "wildcard" { |
| + want = "betty" |
| + } |
| + if got := hosts[key]; !got.dial || got.box != want || !got.wildcard { |
| + t.Errorf("cert-only (9, 9) against a dial (1, 1): %+v, want the dial on %s", got, want) |
| + } |
| + } |
| + // A home entry with no dial address is folded cert-only: its pair |
| + // cannot make it a dial target either. |
| + files := map[string]edgesFileView{ |
| + "control/edges/annie.json": file("annie", "", map[string]edgesHostView{key: {Role: "home", Wildcard: true, Alias: true, TargetHost: "bab.example.com", Incarnation: 9, ServingSeq: 9}}), |
| + "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: dial}), |
| + } |
| + hosts, _ := foldHomes(files, "carol", false) |
| + if got := hosts[key]; !got.dial || got.box != "betty" { |
| + t.Errorf("an addressless (9, 9) against a dial (1, 1): %+v, want betty", got) |
| + } |
| + // A cert-only entry alone dials nothing, whatever its pair. |
| + files = map[string]edgesFileView{"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: certOnly})} |
| + hosts, _ = foldHomes(files, "carol", false) |
| + if got := hosts[key]; got.dial { |
| + t.Errorf("a cert-only entry alone = %+v, want no dial", got) |
| + } |
| +} |
| + |
| +func TestFoldCopiesTheWinnersFields(t *testing.T) { |
| + low := edgesHostView{Role: "home", Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 1} |
| + high := edgesHostView{Role: "home", Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 2} |
| + for _, got := range []homeEntry{foldTwo(t, low, high), foldTwo(t, high, low)} { |
| + if got.incarnation != 1 || got.servingSeq != 2 || got.targetHost != "bab.example.com" { |
| + t.Errorf("winner (1, 2): folded (%d, %d) %q", got.incarnation, got.servingSeq, got.targetHost) |
| + } |
| + } |
| + // The earlier file keeps the dial on two targets: its fields stay, the |
| + // loser's do not leak in. |
| + other := edgesHostView{Role: "home", Alias: true, TargetHost: "shop.example.net", Incarnation: 7, ServingSeq: 7} |
| + if got := foldTwo(t, low, other); got.box != "annie" || got.incarnation != 1 || got.servingSeq != 1 || got.targetHost != "bab.example.com" { |
| + t.Errorf("kept dial: %+v, want annie's (1, 1) bab.example.com", got) |
| + } |
| + // A route that takes the dial from an alias brings its own (here |
| + // empty) target and pair. |
| + route := edgesHostView{Role: "home", Incarnation: 0, ServingSeq: 0} |
| + if got := foldTwo(t, other, route); got.box != "betty" || got.alias || got.incarnation != 0 || got.servingSeq != 0 || got.targetHost != "" { |
| + t.Errorf("route over alias: %+v, want betty's pair-less route", got) |
| + } |
| + // One entry alone is copied whole. |
| + files := map[string]edgesFileView{"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{pairHost: high})} |
| + hosts, _ := foldHomes(files, "carol", false) |
| + if got := hosts[pairHost]; got.incarnation != 1 || got.servingSeq != 2 || got.targetHost != "bab.example.com" { |
| + t.Errorf("a lone entry folded (%d, %d) %q", got.incarnation, got.servingSeq, got.targetHost) |
| + } |
| +} |
| + |
| +// Every pair-less publication folds as it did before the pair existed: the |
| +// rules below are the ones dialBeats documented before Task 8, case by case. |
| +func TestFoldLegacyPublicationsUnchanged(t *testing.T) { |
| + route := edgesHostView{Role: "home"} |
| + alias := edgesHostView{Role: "home", Alias: true} |
| + cases := []struct { |
| + name string |
| + annie edgesHostView |
| + betty edgesHostView |
| + wantBox string |
| + wantDialed bool |
| + }{ |
| + {"two routes: the earlier file", route, route, "annie", true}, |
| + {"two aliases: the earlier file", alias, alias, "annie", true}, |
| + {"route over alias", alias, route, "betty", true}, |
| + {"route over alias, route first", route, alias, "annie", true}, |
| + {"stamped over unstamped", route, edgesHostView{Role: "home", Stamp: 3}, "betty", true}, |
| + {"higher stamp", edgesHostView{Role: "home", Stamp: 9}, edgesHostView{Role: "home", Stamp: 3}, "annie", true}, |
| + {"higher stamp second", edgesHostView{Role: "home", Stamp: 3}, edgesHostView{Role: "home", Stamp: 9}, "betty", true}, |
| + {"a stamped alias does not beat a route", route, edgesHostView{Role: "home", Alias: true, Stamp: 9}, "annie", true}, |
| + {"stamped alias over unstamped alias", alias, edgesHostView{Role: "home", Alias: true, Stamp: 2}, "betty", true}, |
| + {"a dial over a cert-only entry", edgesHostView{Role: "wildcard", Wildcard: true}, edgesHostView{Role: "home", Wildcard: true, Alias: true}, "betty", true}, |
| + {"cert-only twice dials nothing", edgesHostView{Role: "wildcard", Wildcard: true}, edgesHostView{Role: "wildcard", Wildcard: true}, "annie", false}, |
| + } |
| + for _, c := range cases { |
| + key := pairHost |
| + if c.annie.Wildcard || c.betty.Wildcard { |
| + key = "*." + pairHost |
| + } |
| + files := map[string]edgesFileView{ |
| + "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: c.annie}), |
| + "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: c.betty}), |
| + } |
| + hosts, _ := foldHomes(files, "carol", false) |
| + got := hosts[key] |
| + if got.box != c.wantBox || got.dial != c.wantDialed { |
| + t.Errorf("%s: box %q dial %v, want %q %v", c.name, got.box, got.dial, c.wantBox, c.wantDialed) |
| + } |
| + if got.incarnation != 0 || got.servingSeq != 0 || got.targetHost != "" { |
| + t.Errorf("%s: a pair-less fold carries (%d, %d) %q", c.name, got.incarnation, got.servingSeq, got.targetHost) |
| + } |
| + } |
| +} |