| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "fmt" |
| 7 | "io" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | type plan15BoundedStore struct { |
| 15 | *release.DirStore |
| 16 | oversized, conflict bool |
| 17 | read, closed, opens, gets int |
| 18 | } |
| 19 | |
| 20 | func (s *plan15BoundedStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 21 | if strings.Contains(key, "control/host-origins/") { |
| 22 | panic("unbounded node Get") |
| 23 | } |
| 24 | s.gets++ |
| 25 | return s.DirStore.Get(ctx, key) |
| 26 | } |
| 27 | func (s *plan15BoundedStore) Put(ctx context.Context, key string, b []byte, tag string) (string, error) { |
| 28 | if s.conflict && strings.Contains(key, "control/host-origins/") { |
| 29 | return "", release.ErrCASConflict |
| 30 | } |
| 31 | return s.DirStore.Put(ctx, key, b, tag) |
| 32 | } |
| 33 | |
| 34 | type plan15Reader struct { |
| 35 | io.Reader |
| 36 | store *plan15BoundedStore |
| 37 | } |
| 38 | |
| 39 | func (r plan15Reader) Read(b []byte) (int, error) { |
| 40 | n, e := r.Reader.Read(b) |
| 41 | r.store.read += n |
| 42 | return n, e |
| 43 | } |
| 44 | func (r plan15Reader) Close() error { r.store.closed++; return nil } |
| 45 | func (s *plan15BoundedStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) { |
| 46 | s.opens++ |
| 47 | if s.oversized { |
| 48 | return plan15Reader{strings.NewReader(strings.Repeat("x", 1<<20)), s}, "oversized", nil |
| 49 | } |
| 50 | return s.DirStore.Open(ctx, key) |
| 51 | } |
| 52 | func TestPlan15BoundedReachableAndConflicting(t *testing.T) { |
| 53 | for _, prefixed := range []bool{false, true} { |
| 54 | for _, conflict := range []bool{false, true} { |
| 55 | t.Run(fmt.Sprintf("prefix=%v/conflict=%v", prefixed, conflict), func(t *testing.T) { |
| 56 | plan15Enable(t) |
| 57 | counter := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())} |
| 58 | var s release.Store = counter |
| 59 | if prefixed { |
| 60 | s = release.NewPrefixStore(release.NewPrefixStore(s, "outer"), "inner") |
| 61 | } |
| 62 | o := servingClaim(t, t.Context(), s) |
| 63 | if !conflict { |
| 64 | o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "real", "ie")) |
| 65 | } |
| 66 | counter.oversized, counter.conflict = true, conflict |
| 67 | counter.opens, counter.closed, counter.read = 0, 0, 0 |
| 68 | var err error |
| 69 | if conflict { |
| 70 | _, err = Update(t.Context(), s, o, plan15Admit(o, "candidate", "ie")) |
| 71 | } else { |
| 72 | h := o.Claim.Holder |
| 73 | _, err = AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, "ie", "real") |
| 74 | } |
| 75 | if !errors.Is(err, ErrMalformed) || counter.read != 4097 || counter.closed != 1 || counter.opens != 1 { |
| 76 | t.Fatalf("err=%v bytes=%d closes=%d opens=%d", err, counter.read, counter.closed, counter.opens) |
| 77 | } |
| 78 | }) |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | func TestPlan15CacheBindings(t *testing.T) { |
| 83 | plan15Enable(t) |
| 84 | s := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())} |
| 85 | o := servingClaim(t, t.Context(), s) |
| 86 | o = updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "first", "ie")) |
| 87 | h := o.Claim.Holder |
| 88 | first := OriginTuple{h.Incarnation, h.Location, "ie", "first"} |
| 89 | cache := NewOriginProofCache(s) |
| 90 | prove := func(obs Observed, set []OriginTuple, want bool) { |
| 91 | t.Helper() |
| 92 | result, err := cache.Proven(t.Context(), obs, set) |
| 93 | if err != nil || result[set[0]] != want { |
| 94 | t.Fatalf("%v %v", result, err) |
| 95 | } |
| 96 | } |
| 97 | s.opens, s.gets = 0, 0 |
| 98 | prove(o, []OriginTuple{first, first}, true) |
| 99 | prove(o, []OriginTuple{first}, true) |
| 100 | if s.opens != int(o.Claim.Origins.Count) || s.gets != 2 { |
| 101 | t.Fatalf("cache opens=%d reads=%d", s.opens, s.gets) |
| 102 | } |
| 103 | different := first |
| 104 | different.Region = "uk" |
| 105 | prove(o, []OriginTuple{different}, false) |
| 106 | if s.opens != 2*int(o.Claim.Origins.Count) { |
| 107 | t.Fatal("tuple set reused") |
| 108 | } |
| 109 | other := &plan15BoundedStore{DirStore: release.NewDirStore(t.TempDir())} |
| 110 | key, _ := Key(o.Host) |
| 111 | body, _, err := s.Get(t.Context(), key) |
| 112 | if err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | if _, err := other.Put(t.Context(), key, body, ""); err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | if got, err := NewOriginProofCache(other).Proven(t.Context(), o, []OriginTuple{different}); got != nil || err == nil { |
| 119 | t.Fatal("foreign store reused") |
| 120 | } |
| 121 | prove(o, []OriginTuple{first}, true) |
| 122 | edited := o |
| 123 | c := *o.Claim |
| 124 | edited.Claim = &c |
| 125 | c.Origins = &OriginHead{Digest: strings.Repeat("f", 64), Count: o.Claim.Origins.Count} |
| 126 | if got, err := cache.Proven(t.Context(), edited, []OriginTuple{first}); got != nil || !errors.Is(err, ErrConflict) { |
| 127 | t.Fatal("edited head reused") |
| 128 | } |
| 129 | prove(o, []OriginTuple{first}, true) |
| 130 | newer := updateGenerationTest(t, t.Context(), s, o, plan15Admit(o, "second", "ie")) |
| 131 | if got, err := cache.Proven(t.Context(), o, []OriginTuple{first}); got != nil || !errors.Is(err, ErrConflict) { |
| 132 | t.Fatal("stale observation reused") |
| 133 | } |
| 134 | s.opens = 0 |
| 135 | prove(newer, []OriginTuple{first}, true) |
| 136 | if s.opens != int(newer.Claim.Origins.Count) { |
| 137 | t.Fatal("changed head did not traverse") |
| 138 | } |
| 139 | // No partial positives escape a failed batch, including a corrupt tail. |
| 140 | if err := s.Delete(t.Context(), originKey(o.Host, o.Claim.Origins.Digest)); err != nil { |
| 141 | t.Fatal(err) |
| 142 | } |
| 143 | second := first |
| 144 | second.Generation = "second" |
| 145 | if got, err := cache.Proven(t.Context(), newer, []OriginTuple{first, second}); got != nil || err == nil { |
| 146 | t.Fatal("partial proof escaped") |
| 147 | } |
| 148 | } |
| 149 | |