| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "encoding/json" |
| 7 | "errors" |
| 8 | "net/http" |
| 9 | "net/http/httptest" |
| 10 | "os" |
| 11 | "testing" |
| 12 | "time" |
| 13 | |
| 14 | "github.com/carlosframework/platform/internal/hostops" |
| 15 | "github.com/carlosframework/platform/internal/release" |
| 16 | ) |
| 17 | |
| 18 | type aliasWriteSpy struct { |
| 19 | release.Store |
| 20 | writes int |
| 21 | } |
| 22 | |
| 23 | func (s *aliasWriteSpy) Put(context.Context, string, []byte, string) (string, error) { |
| 24 | s.writes++ |
| 25 | return "changed", nil |
| 26 | } |
| 27 | func (s *aliasWriteSpy) Delete(context.Context, string) error { s.writes++; return nil } |
| 28 | func TestAliasOwnershipCannotRebaseOrBareDelete(t *testing.T) { |
| 29 | base := &aliasWriteSpy{} |
| 30 | guarded := claimAliasStore{data: base, key: "domains/alias.example.com.json", etag: "original"} |
| 31 | for _, etag := range []string{"", "successor"} { |
| 32 | if _, err := guarded.Put(context.Background(), guarded.key, []byte(`{}`), etag); !errors.Is(err, hostops.ErrPending) { |
| 33 | t.Fatalf("rebase %q: %v", etag, err) |
| 34 | } |
| 35 | } |
| 36 | if _, err := guarded.Put(context.Background(), "domains/other.example.com.json", []byte(`{}`), "original"); !errors.Is(err, hostops.ErrPending) { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | if err := guarded.Delete(context.Background(), guarded.key); !errors.Is(err, hostops.ErrContract) { |
| 40 | t.Fatal(err) |
| 41 | } |
| 42 | if base.writes != 0 { |
| 43 | t.Fatalf("unauthorized writes: %d", base.writes) |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | func (s *aliasWriteSpy) Get(context.Context, string) ([]byte, string, error) { |
| 48 | return nil, "", os.ErrNotExist |
| 49 | } |
| 50 | func TestAliasMarkerPutCannotStartAfterPurgeDrain(t *testing.T) { |
| 51 | ctx := context.Background() |
| 52 | base := &aliasWriteSpy{} |
| 53 | raw := release.NewDirStore(t.TempDir()) |
| 54 | pub, _, err := ed25519.GenerateKey(nil) |
| 55 | if err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | now := time.Now() |
| 59 | w, err := hostops.NewControlWriter(raw, pub, func() time.Time { return now }) |
| 60 | if err != nil { |
| 61 | t.Fatal(err) |
| 62 | } |
| 63 | open := hostops.AppAdmission{V: 1, AppID: "app-id", Revision: 1, Epoch: 1, State: "open", Registrations: map[string]hostops.Registration{}, Effects: map[string]hostops.Effect{}} |
| 64 | putAdmission := func(a hostops.AppAdmission) { |
| 65 | body, _ := json.Marshal(a) |
| 66 | _, tag, _ := raw.Get(ctx, "apps/bab/site/admission.json") |
| 67 | if _, err := raw.Put(ctx, "apps/bab/site/admission.json", body, tag); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | } |
| 71 | putAdmission(open) |
| 72 | owner := hostops.RegistrationOwner{Console: "console", Enrollment: "enrolled", Controller: "controller"} |
| 73 | h, err := w.RegisterStart(ctx, "bab", "site", "app-id", "old-registration", hostops.Registration{Kind: "create", Host: "alias.bab.example.com", Claim: hostops.SavedClaim{Absent: true}, Owner: owner, AppID: "app-id", Epoch: 1, CreatedAt: now}, now) |
| 74 | if err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | // The purge drained the registration and closed the gate. |
| 78 | drained := open |
| 79 | drained.State, drained.Revision = "closed", 2 |
| 80 | drained.Op = &hostops.AppOperation{ID: "purge", Kind: hostops.OpPurging, AppID: "app-id"} |
| 81 | putAdmission(drained) |
| 82 | guarded := claimAliasStore{data: base, key: "domains/alias.example.com.json", etag: "original", account: "bab", app: "site", acquisition: "acq", incarnation: "inc", effects: claimStoreEffects{OperationID: "old-registration", Runner: h.Runner(time.Now)}} |
| 83 | _, err = guarded.Put(ctx, guarded.key, []byte(`{"account":"bab","app":"site"}`), "original") |
| 84 | if !errors.Is(err, hostops.ErrPending) { |
| 85 | t.Fatalf("drained registration issued effect: %v", err) |
| 86 | } |
| 87 | if base.writes != 0 { |
| 88 | t.Fatalf("writes after drain: %d", base.writes) |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | func TestFlaggedDomainCapabilitiesRefuseBeforeBucketRead(t *testing.T) { |
| 93 | s := &Server{cfg: Config{HostClaim: true}} |
| 94 | w := httptest.NewRecorder() |
| 95 | yes := true |
| 96 | if s.setDomainCapabilities(w, httptest.NewRequest("POST", "/", nil), context.Background(), nil, "site", Principal{}, "alias.example.com", &yes, nil) { |
| 97 | t.Fatal("capability mutation admitted") |
| 98 | } |
| 99 | if w.Code != http.StatusConflict { |
| 100 | t.Fatalf("status=%d", w.Code) |
| 101 | } |
| 102 | } |
| 103 | |