| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "strings" |
| 10 | "testing" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | "github.com/carlosframework/platform/internal/release" |
| 14 | ) |
| 15 | |
| 16 | type plan15StreamStore struct { |
| 17 | release.Store |
| 18 | bytes, closes, opens, puts int |
| 19 | } |
| 20 | |
| 21 | func (s *plan15StreamStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| 22 | if strings.Contains(key, "control/host-origins/") { |
| 23 | panic("unbounded origin Get") |
| 24 | } |
| 25 | return s.Store.Get(ctx, strings.TrimPrefix(key, "deployment/")) |
| 26 | } |
| 27 | func (s *plan15StreamStore) Put(ctx context.Context, key string, b []byte, tag string) (string, error) { |
| 28 | s.puts++ |
| 29 | return "", release.ErrCASConflict |
| 30 | } |
| 31 | func (s *plan15StreamStore) Open(context.Context, string) (io.ReadCloser, string, error) { |
| 32 | s.opens++ |
| 33 | return plan15StreamReader{s, strings.NewReader(strings.Repeat("x", 8192))}, "", nil |
| 34 | } |
| 35 | |
| 36 | type plan15StreamReader struct { |
| 37 | s *plan15StreamStore |
| 38 | r io.Reader |
| 39 | } |
| 40 | |
| 41 | func (r plan15StreamReader) Read(b []byte) (int, error) { |
| 42 | n, e := r.r.Read(b) |
| 43 | r.s.bytes += n |
| 44 | return n, e |
| 45 | } |
| 46 | func (r plan15StreamReader) Close() error { r.s.closes++; return nil } |
| 47 | |
| 48 | type plan15NoOpen struct{ release.Store } |
| 49 | |
| 50 | func TestPlan15StreamingCompositions(t *testing.T) { |
| 51 | for _, stream := range []bool{false, true} { |
| 52 | for _, boundary := range []bool{false, true} { |
| 53 | t.Run(fmt.Sprintf("stream=%v/boundary=%v", stream, boundary), func(t *testing.T) { |
| 54 | w, o, _ := wakeResetFixture(t) |
| 55 | o.Claim.Origins = &hostclaim.OriginHead{Digest: strings.Repeat("a", 64), Count: 1} |
| 56 | key, _ := hostclaim.Key(o.Host) |
| 57 | body, _ := json.Marshal(o.Claim) |
| 58 | var err error |
| 59 | o.ETag, err = w.raw.Put(t.Context(), key, body, o.ETag) |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | raw := &plan15StreamStore{Store: w.raw} |
| 64 | var s release.Store = raw |
| 65 | if !stream { |
| 66 | s = plan15NoOpen{s} |
| 67 | } |
| 68 | s = release.NewPrefixStore(s, "deployment") |
| 69 | s = conditionalRecordStore{raw: s} |
| 70 | if boundary { |
| 71 | s = &mutationBoundary{raw: s, check: func(context.Context, MutationTarget) error { return nil }} |
| 72 | } |
| 73 | h := o.Claim.Holder |
| 74 | if ok, err := hostclaim.AdmittedOrigin(t.Context(), s, o, h.Incarnation, h.Location, "ie", "real"); ok || !errors.Is(err, hostclaim.ErrMalformed) { |
| 75 | t.Fatalf("proof %v %v", ok, err) |
| 76 | } |
| 77 | if stream && (raw.bytes != 4097 || raw.opens != 1 || raw.closes != 1) { |
| 78 | t.Fatalf("bytes=%d opens=%d closes=%d", raw.bytes, raw.opens, raw.closes) |
| 79 | } |
| 80 | if !stream && (raw.bytes != 0 || raw.opens != 0) { |
| 81 | t.Fatal("nonstreaming store opened") |
| 82 | } |
| 83 | }) |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | func TestPlan15BoundaryRefusesCandidateBeforeRead(t *testing.T) { |
| 88 | for _, flag := range []string{"", "1"} { |
| 89 | t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", flag) |
| 90 | raw := &plan15StreamStore{} |
| 91 | s := &mutationBoundary{raw: conditionalRecordStore{raw: release.NewPrefixStore(raw, "deployment")}, check: func(context.Context, MutationTarget) error { t.Fatal("origin reached grant checker"); return nil }} |
| 92 | if _, err := s.Put(t.Context(), "control/host-origins/x.example/node.json", []byte("candidate"), ""); !errors.Is(err, ErrUnclassified) { |
| 93 | t.Fatal(err) |
| 94 | } |
| 95 | if raw.puts != 0 || raw.opens != 0 { |
| 96 | t.Fatal("candidate touched raw store") |
| 97 | } |
| 98 | } |
| 99 | } |
| 100 | |