carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3import (
4 "context"
5 "errors"
6 "net/http"
7 "os"
8 "sync/atomic"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/provision"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/seeds"
15)
16
17// afterRecordRead runs then after each of the first n reads of key: what
18// another request does between the delete door's import check and its
19// delete.
20type afterRecordRead struct {
21 release.Store
22 key string
23 n int32
24 reads atomic.Int32
25 then func()
26}
27
28func (s *afterRecordRead) Get(ctx context.Context, key string) ([]byte, string, error) {
29 raw, tag, err := s.Store.Get(ctx, key)
30 if key == s.key && s.reads.Add(1) <= s.n {
31 s.then()
32 }
33 return raw, tag, err
34}
35
36func (s *afterRecordRead) DeleteIfMatch(ctx context.Context, key, etag string) error {
37 return release.DeleteIfMatch(ctx, s.Store, key, etag)
38}
39
40// plainRecord writes a plain record of shop on host.
41func (f fromFixture) plainRecord(t *testing.T, host string) {
42 t.Helper()
43 if err := provision.CreateRecord(context.Background(), f.fx.rel, provision.Record{Schema: 1, Host: host, Slug: "old",
44 App: fromApp, Account: "bab", Channel: "stable", CreatedAt: time.Now().UTC()}); err != nil {
45 t.Fatal(err)
46 }
47}
48
49// Final review F7: the delete door's check finds no record, and an import
50// is begun before the delete would have run. Absence is the delete's own
51// idempotent answer: the ordinary delete is not run, and the import's
52// record stands.
53func TestImportDeleteOfAnAbsentHostLeavesAnImportBegunSince(t *testing.T) {
54 f := newImportFixture(t)
55 id := newSeedID(t)
56 key := provision.RecordKey("bab", fromApp, importHost)
57 f.fx.srv.cfg.Releases = &afterRecordRead{Store: f.fx.rel, key: key, n: 1, then: func() {
58 f.seedRecord(t, importHost, "", id, seeds.KindImport, seeds.StateAwaitingUpload)
59 }}
60 if code, out := deleteInstance(t, f, importHost); code != http.StatusOK || out["result"] != "deleted" {
61 t.Fatalf("delete of an absent host = %d %v, want the idempotent 200", code, out)
62 }
63 rec, err := f.record(t, importHost)
64 if err != nil {
65 t.Fatalf("the import begun since was deleted: %v", err)
66 }
67 requireImportState(t, rec, id, seeds.StateAwaitingUpload, "")
68}
69
70// The check reads a plain record; before the delete, that instance is
71// deleted and an import of the host begins and swaps. The ordinary delete
72// is bound to the plain record's etag, so it does not land: the door reads
73// the host again and answers as it answers for the import, SD-N57, with
74// the import's record standing.
75func TestImportDeleteReclassifiesARecordThatBecameAnImport(t *testing.T) {
76 f := newImportFixture(t)
77 f.plainRecord(t, importHost)
78 id := newSeedID(t)
79 key := provision.RecordKey("bab", fromApp, importHost)
80 f.fx.srv.cfg.Releases = &afterRecordRead{Store: f.fx.rel, key: key, n: 1, then: func() {
81 if err := f.fx.rel.Delete(context.Background(), key); err != nil {
82 t.Error(err)
83 }
84 f.seedRecord(t, importHost, "", id, seeds.KindImport, seeds.StateSwapped)
85 }}
86 if code, out := deleteInstance(t, f, importHost); code != http.StatusConflict || str(t, out, "error") != importAbortFirst(importHost) {
87 t.Fatalf("delete = %d %v, want 409 SD-N57", code, out)
88 }
89 rec, err := f.record(t, importHost)
90 if err != nil {
91 t.Fatalf("the import was deleted the ordinary way: %v", err)
92 }
93 requireImportState(t, rec, id, seeds.StateSwapped, "")
94 if rows := importAudit(t, f, "instance-delete"); len(rows) != 0 {
95 t.Fatalf("a refused delete audited: %q", rows)
96 }
97}
98
99// A record that changes under the delete twice is not chased: the second
100// conflict answers main's retry sentence, and the record stands.
101func TestImportDeleteGivesUpAfterASecondChange(t *testing.T) {
102 f := newImportFixture(t)
103 f.plainRecord(t, importHost)
104 key := provision.RecordKey("bab", fromApp, importHost)
105 f.fx.srv.cfg.Releases = &afterRecordRead{Store: f.fx.rel, key: key, n: 2, then: func() {
106 rec, etag, err := provision.GetRecordETag(context.Background(), f.fx.rel, "bab", fromApp, importHost)
107 if err != nil {
108 t.Error(err)
109 return
110 }
111 rec.Steering = map[string]string{"": "latency", "latency": ""}[rec.Steering]
112 if err := provision.UpdateRecord(context.Background(), f.fx.rel, *rec, etag); err != nil {
113 t.Error(err)
114 }
115 }}
116 if code, out := deleteInstance(t, f, importHost); code != http.StatusBadGateway || str(t, out, "error") != importDeleteRetry {
117 t.Fatalf("delete = %d %v, want 502 %q", code, out, importDeleteRetry)
118 }
119 if _, err := f.record(t, importHost); errors.Is(err, os.ErrNotExist) {
120 t.Fatal("a record that kept changing was deleted")
121 }
122}
123
124// The ordinary case is unchanged: a plain record the check read is
125// deleted, and a second delete of the gone host is the idempotent 200.
126func TestImportDeleteOfAPlainRecord(t *testing.T) {
127 f := newImportFixture(t)
128 f.plainRecord(t, importHost)
129 for range 2 {
130 if code, out := deleteInstance(t, f, importHost); code != http.StatusOK || out["result"] != "deleted" {
131 t.Fatalf("delete = %d %v", code, out)
132 }
133 f.requireRecordGone(t, importHost)
134 }
135 if rows := importAudit(t, f, "instance-delete"); len(rows) != 2 {
136 t.Fatalf("instance-delete rows = %q, want one per delete", rows)
137 }
138}
139