| index e412673eb..08ef18226 100644 |
| --- a/internal/console/apiruns_test.go |
| +++ b/internal/console/apiruns_test.go |
| @@ -9,6 +9,7 @@ import ( |
| "io" |
| "mime/multipart" |
| "net/http" |
| + "net/http/httptest" |
| "os" |
| "strings" |
| "sync" |
| @@ -491,3 +492,76 @@ func TestAPIRunCreateAdmissionWhoseAnswerWasLost(t *testing.T) { |
| }) |
| } |
| } |
| + |
| +// leavingStore answers every call whose context has ended with that |
| +// context's error, as a real bucket client does, and ends the caller's |
| +// request once the admission's record write has landed: the client went |
| +// away right after its run was admitted. |
| +type leavingStore struct { |
| + release.Store |
| + recordKey string |
| + leave context.CancelFunc |
| +} |
| + |
| +func (l *leavingStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + if err := ctx.Err(); err != nil { |
| + return nil, "", err |
| + } |
| + return l.Store.Get(ctx, key) |
| +} |
| + |
| +func (l *leavingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { |
| + if err := ctx.Err(); err != nil { |
| + return "", err |
| + } |
| + etag, err := l.Store.Put(ctx, key, body, ifMatch) |
| + if err == nil && key == l.recordKey { |
| + l.leave() |
| + } |
| + return etag, err |
| +} |
| + |
| +// A run admitted for a client that then went away still bumps the serial, |
| +// so a box picks it up at once, and its run-start row still names the |
| +// version the instance follows: both run on the admission's own context. |
| +func TestAPIRunCreateFinishesTheAdmissionForAClientThatLeft(t *testing.T) { |
| + fx := runFixture(t) |
| + shipVersion(t, fx.consoleFixture, "shop", "v1") |
| + seedChannel(t, fx.consoleFixture, "shop", "stable", "v1") |
| + before, _, _ := release.ReadSerial(context.Background(), fx.rel) |
| + |
| + var buf bytes.Buffer |
| + mw := multipart.NewWriter(&buf) |
| + mp, _ := mw.CreateFormField("meta") |
| + if err := json.NewEncoder(mp).Encode(okMeta()); err != nil { |
| + t.Fatal(err) |
| + } |
| + mw.Close() |
| + ctx, leave := context.WithCancel(context.Background()) |
| + defer leave() |
| + fx.srv.cfg.Releases = &leavingStore{Store: fx.rel, recordKey: provision.RecordKey(release.OpsAccount, "shop", runTestHost), leave: leave} |
| + req := httptest.NewRequestWithContext(ctx, http.MethodPost, fx.runsURL(runTestHost), &buf) |
| + req.Header.Set("Content-Type", mw.FormDataContentType()) |
| + req.Header.Set("Authorization", "Bearer "+fx.member) |
| + w := httptest.NewRecorder() |
| + fx.srv.ServeHTTP(w, req) |
| + if w.Code != http.StatusAccepted { |
| + t.Fatalf("%d %s", w.Code, w.Body.String()) |
| + } |
| + if ctx.Err() == nil { |
| + t.Fatal("the client never left: the test proves nothing") |
| + } |
| + if after, _, err := release.ReadSerial(context.Background(), fx.rel); err != nil || after.Serial <= before.Serial { |
| + t.Errorf("serial %d -> %d, %v: not bumped", before.Serial, after.Serial, err) |
| + } |
| + events, _ := fx.st.Events(OpsAccountID, "shop", 10) |
| + found := false |
| + for _, e := range events { |
| + if e.Action == "run-start" && strings.Contains(e.Detail, `"version":"v1"`) { |
| + found = true |
| + } |
| + } |
| + if !found { |
| + t.Errorf("no run-start row naming v1 in %+v", events) |
| + } |
| +} |