| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/release" |
| 9 | ) |
| 10 | |
| 11 | func TestAppMutationRunnerOwnsTheWholeCall(t *testing.T) { |
| 12 | for _, kind := range []string{"joined", "validation", "ambiguous-bucket", "ambiguous-backend", "foreign", "nested"} { |
| 13 | t.Run(kind, func(t *testing.T) { |
| 14 | ctx := t.Context() |
| 15 | w, raw, _ := testWriter(t) |
| 16 | seedAdmission(t, raw, "bab", "demo", openAdmission("app")) |
| 17 | check, err := NewProductionMutationCheck(raw, w) |
| 18 | if err != nil { |
| 19 | t.Fatal(err) |
| 20 | } |
| 21 | data, err := NewMutationBoundary(true, raw, check) |
| 22 | if err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | owner := RegistrationOwner{Console: "console", Enrollment: "enrollment", Controller: "controller"} |
| 26 | if err = BindMutationRunner(data, w, owner, func(context.Context, string, string) (string, error) { return "app", nil }); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if kind == "foreign" { |
| 30 | _, err = w.BeginExclusive(ctx, "bab", "demo", "app", AppOperation{ID: "foreign", Kind: OpAppMutation, AppID: "app"}, owner) |
| 31 | if err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | } |
| 35 | before, _ := ReadAdmission(ctx, raw, "bab", "demo", "app") |
| 36 | called := false |
| 37 | failure := errors.New("joined callback failure") |
| 38 | var saved context.Context |
| 39 | err = release.WithAppMutation(ctx, data, "bab", "demo", "test", func(pctx context.Context) error { |
| 40 | called = true |
| 41 | saved = pctx |
| 42 | if err := RequireAppMutation(pctx, raw, "bab", "demo", "app"); err != nil { |
| 43 | return err |
| 44 | } |
| 45 | switch kind { |
| 46 | case "validation": |
| 47 | return failure |
| 48 | case "ambiguous-backend": |
| 49 | MarkAppMutationIssued(pctx) |
| 50 | return failure |
| 51 | case "nested": |
| 52 | a, _ := ReadAdmission(pctx, raw, "bab", "demo", "app") |
| 53 | return release.WithAppMutation(pctx, data, "bab", "demo", "nested", func(inner context.Context) error { |
| 54 | b, _ := ReadAdmission(inner, raw, "bab", "demo", "app") |
| 55 | if a.Value.Op.ID != b.Value.Op.ID { |
| 56 | t.Fatal("nested call acquired another operation") |
| 57 | } |
| 58 | _, err := data.Put(inner, "apps/bab/demo/test.json", []byte(`{}`), "") |
| 59 | return err |
| 60 | }) |
| 61 | } |
| 62 | _, err := data.Put(pctx, "apps/bab/demo/test.json", []byte(`{}`), "") |
| 63 | if err != nil { |
| 64 | return err |
| 65 | } |
| 66 | if kind == "ambiguous-bucket" { |
| 67 | return failure |
| 68 | } |
| 69 | return nil |
| 70 | }) |
| 71 | after, _ := ReadAdmission(ctx, raw, "bab", "demo", "app") |
| 72 | switch kind { |
| 73 | case "foreign": |
| 74 | if called || !errors.Is(err, ErrPending) || before.ETag != after.ETag { |
| 75 | t.Fatalf("crossed foreign op: called=%v err=%v", called, err) |
| 76 | } |
| 77 | case "validation": |
| 78 | if !errors.Is(err, failure) || after.Value.State != "open" || after.Value.Op != nil { |
| 79 | t.Fatalf("unissued failure retained authority: %v %+v", err, after.Value) |
| 80 | } |
| 81 | case "ambiguous-bucket", "ambiguous-backend": |
| 82 | if !errors.Is(err, failure) || after.Value.State != "closed" || after.Value.Op == nil { |
| 83 | t.Fatalf("ambiguous effect lost authority: %v %+v", err, after.Value) |
| 84 | } |
| 85 | default: |
| 86 | if err != nil || after.Value.State != "open" || after.Value.Op != nil { |
| 87 | t.Fatalf("joined call did not finish: %v %+v", err, after.Value) |
| 88 | } |
| 89 | } |
| 90 | if saved != nil && RequireAppMutation(saved, raw, "bab", "demo", "app") == nil { |
| 91 | t.Fatal("callback permit survived settlement") |
| 92 | } |
| 93 | }) |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | func TestRunAppMutationRequiresLocalIncarnation(t *testing.T) { |
| 98 | for _, local := range []string{"", "old", "app"} { |
| 99 | t.Run("local="+local, func(t *testing.T) { |
| 100 | w, raw, _ := testWriter(t) |
| 101 | seedAdmission(t, raw, "bab", "demo", openAdmission("app")) |
| 102 | check, err := NewProductionMutationCheck(raw, w) |
| 103 | if err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | data, err := NewMutationBoundary(true, raw, check) |
| 107 | if err != nil { |
| 108 | t.Fatal(err) |
| 109 | } |
| 110 | owner := RegistrationOwner{Console: "console", Enrollment: "enrollment", Controller: "controller"} |
| 111 | if err = BindMutationRunner(data, w, owner, func(context.Context, string, string) (string, error) { return local, nil }); err != nil { |
| 112 | t.Fatal(err) |
| 113 | } |
| 114 | before, err := ReadAdmission(t.Context(), raw, "bab", "demo", "app") |
| 115 | if err != nil { |
| 116 | t.Fatal(err) |
| 117 | } |
| 118 | called := false |
| 119 | err = release.WithAppMutation(t.Context(), data, "bab", "demo", "test", func(context.Context) error { called = true; return nil }) |
| 120 | after, e := ReadAdmission(t.Context(), raw, "bab", "demo", "app") |
| 121 | if e != nil { |
| 122 | t.Fatal(e) |
| 123 | } |
| 124 | if local == "app" { |
| 125 | if err != nil || !called { |
| 126 | t.Fatalf("matching identity refused: %v", err) |
| 127 | } |
| 128 | } else if err == nil || called || before.ETag != after.ETag { |
| 129 | t.Fatalf("local identity %q acquired authority: called=%v err=%v", local, called, err) |
| 130 | } |
| 131 | }) |
| 132 | } |
| 133 | } |
| 134 | |