| 1 | package activator |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | "net/http/httptest" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/instancekey" |
| 12 | "github.com/carlosframework/platform/internal/registry" |
| 13 | ) |
| 14 | |
| 15 | // adoptionFixture is one claim-launched process's control socket, served |
| 16 | // in memory: its launch state, and every record handed to the publisher. |
| 17 | type adoptionFixture struct { |
| 18 | mux *http.ServeMux |
| 19 | launch AdoptionLaunch |
| 20 | hasLaunch bool |
| 21 | published [][]byte |
| 22 | } |
| 23 | |
| 24 | func newAdoptionFixture(t *testing.T, rt registry.Route, tok, key, source string) *adoptionFixture { |
| 25 | t.Helper() |
| 26 | f := &adoptionFixture{hasLaunch: true} |
| 27 | f.launch = AdoptionLaunch{ |
| 28 | Writer: instancekey.Binding{Host: rt.Host, Incarnation: "inc-1", Session: 3, Generation: "g3"}, |
| 29 | KeySHA256: instancekey.Digest(key), Source: source, Dir: t.TempDir(), |
| 30 | Publish: func(rec []byte) { f.published = append(f.published, rec) }, |
| 31 | } |
| 32 | f.mux = http.NewServeMux() |
| 33 | (&ExecBackend{}).MountInstanceKeyAdoption(f.mux, rt, func() (string, error) { return tok, nil }, |
| 34 | func() (AdoptionLaunch, bool) { return f.launch, f.hasLaunch }) |
| 35 | return f |
| 36 | } |
| 37 | |
| 38 | func (f *adoptionFixture) put(token, body string) int { |
| 39 | req := httptest.NewRequest(http.MethodPut, instanceKeyAdoptionPath, strings.NewReader(body)) |
| 40 | if token != "" { |
| 41 | req.Header.Set("Authorization", "Bearer "+token) |
| 42 | } |
| 43 | w := httptest.NewRecorder() |
| 44 | f.mux.ServeHTTP(w, req) |
| 45 | return w.Code |
| 46 | } |
| 47 | |
| 48 | func adoptionBody(key, source string) string { |
| 49 | return `{"sha256":"` + instancekey.Digest(key) + `","source":"` + source + `"}` |
| 50 | } |
| 51 | |
| 52 | // Rulings 20 and 21: the agent records the launch's own writer and the key |
| 53 | // the launch delivered, from its own launch state. A request cannot name a |
| 54 | // writer, a digest or a source the launch did not have. |
| 55 | func TestInstanceKeyAdoptionRecordsTheLaunchesWriter(t *testing.T) { |
| 56 | tok := strings.Repeat("f", 64) |
| 57 | key := strings.Repeat("K", 43) + "=" |
| 58 | rt := registry.Route{Host: "shop.example.com", Kind: "instance"} |
| 59 | f := newAdoptionFixture(t, rt, tok, key, instancekey.SourcePlatform) |
| 60 | recPath := filepath.Join(f.launch.Dir, rt.Host+instancekey.AdoptionSuffix) |
| 61 | for _, tc := range []struct { |
| 62 | name, token, body string |
| 63 | want int |
| 64 | }{ |
| 65 | {"no bearer", "", adoptionBody(key, "platform"), http.StatusUnauthorized}, |
| 66 | {"another host's bearer", strings.Repeat("0", 64), adoptionBody(key, "platform"), http.StatusUnauthorized}, |
| 67 | {"not a digest", tok, `{"sha256":"abc","source":"platform"}`, http.StatusBadRequest}, |
| 68 | {"an unknown source", tok, adoptionBody(key, "own"), http.StatusBadRequest}, |
| 69 | {"a session in the request", tok, `{"sha256":"` + instancekey.Digest(key) + `","source":"platform","session":9}`, http.StatusBadRequest}, |
| 70 | // Astra's P3: the body is one object and nothing after it. A |
| 71 | // trailing } or ] passed Decoder.More; the second Decode must |
| 72 | // answer io.EOF. |
| 73 | {"a trailing }", tok, adoptionBody(key, "platform") + "}", http.StatusBadRequest}, |
| 74 | {"a trailing ]", tok, adoptionBody(key, "platform") + "]", http.StatusBadRequest}, |
| 75 | {"trailing garbage", tok, adoptionBody(key, "platform") + " xyz", http.StatusBadRequest}, |
| 76 | {"a second object", tok, adoptionBody(key, "platform") + adoptionBody(key, "platform"), http.StatusBadRequest}, |
| 77 | {"another key", tok, adoptionBody("some-other-key", "platform"), http.StatusConflict}, |
| 78 | {"another source", tok, adoptionBody(key, "config"), http.StatusConflict}, |
| 79 | } { |
| 80 | if got := f.put(tc.token, tc.body); got != tc.want { |
| 81 | t.Fatalf("%s: %d, want %d", tc.name, got, tc.want) |
| 82 | } |
| 83 | if _, err := os.Stat(recPath); !os.IsNotExist(err) || len(f.published) != 0 { |
| 84 | t.Fatalf("%s: a record was written or published", tc.name) |
| 85 | } |
| 86 | } |
| 87 | want := instancekey.AdoptionRecord(f.launch.Writer, instancekey.SourcePlatform, instancekey.Digest(key)) |
| 88 | for i := 0; i < 2; i++ { // the helper reports on every process start |
| 89 | if got := f.put(tok, adoptionBody(key, "platform")); got != http.StatusNoContent { |
| 90 | t.Fatalf("report %d: %d, want 204", i, got) |
| 91 | } |
| 92 | rec, err := os.ReadFile(recPath) |
| 93 | if err != nil || string(rec) != string(want) { |
| 94 | t.Fatalf("report %d: record is %d bytes, want %d, %v", i, len(rec), len(want), err) |
| 95 | } |
| 96 | if st, _ := os.Stat(recPath); st.Mode().Perm() != 0o600 { |
| 97 | t.Fatalf("record mode = %o, want 600", st.Mode().Perm()) |
| 98 | } |
| 99 | if len(f.published) != i+1 || string(f.published[i]) != string(want) { |
| 100 | t.Fatalf("report %d: published %d records", i, len(f.published)) |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | // A configured key is recorded the same way, with its source (ruling 20). |
| 105 | cfg := newAdoptionFixture(t, rt, tok, "from-config", instancekey.SourceConfig) |
| 106 | if got := cfg.put(tok, adoptionBody("from-config", "config")); got != http.StatusNoContent { |
| 107 | t.Fatalf("configured key: %d, want 204", got) |
| 108 | } |
| 109 | if a, err := instancekey.ParseAdoption(cfg.published[0]); err != nil || a.Source != instancekey.SourceConfig || !a.Binds(cfg.launch.Writer) { |
| 110 | t.Fatalf("configured key's record = %+v, %v", a, err) |
| 111 | } |
| 112 | |
| 113 | // No launch state (a legacy launch) is the 404 an agent without the |
| 114 | // verb answers; a replica refuses (ruling 26). |
| 115 | none := newAdoptionFixture(t, rt, tok, key, instancekey.SourcePlatform) |
| 116 | none.hasLaunch = false |
| 117 | replicaRT := rt |
| 118 | replicaRT.Replica = true |
| 119 | replica := newAdoptionFixture(t, replicaRT, tok, key, instancekey.SourcePlatform) |
| 120 | if got := none.put(tok, adoptionBody(key, "platform")); got != http.StatusNotFound { |
| 121 | t.Fatalf("no launch state: %d, want 404", got) |
| 122 | } |
| 123 | if got := replica.put(tok, adoptionBody(key, "platform")); got != http.StatusConflict { |
| 124 | t.Fatalf("replica: %d, want 409", got) |
| 125 | } |
| 126 | if len(none.published)+len(replica.published) != 0 { |
| 127 | t.Fatal("a record was published without a writer") |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // A launch state O6 built wrong (another host's writer, no publisher, or a |
| 132 | // writer binding the pinner would refuse) is a 500, with nothing written |
| 133 | // and nothing published: never a 204 for a record nobody can read. |
| 134 | func TestInstanceKeyAdoptionRefusesAnIncompleteLaunchState(t *testing.T) { |
| 135 | tok := strings.Repeat("f", 64) |
| 136 | key := strings.Repeat("K", 43) + "=" |
| 137 | rt := registry.Route{Host: "shop.example.com", Kind: "instance"} |
| 138 | for _, tc := range []struct { |
| 139 | name string |
| 140 | wreck func(*AdoptionLaunch) |
| 141 | }{ |
| 142 | {"another host's writer", func(l *AdoptionLaunch) { l.Writer.Host = "other.example.com" }}, |
| 143 | {"no publisher", func(l *AdoptionLaunch) { l.Publish = nil }}, |
| 144 | {"a zero session", func(l *AdoptionLaunch) { l.Writer.Session = 0 }}, |
| 145 | {"no incarnation", func(l *AdoptionLaunch) { l.Writer.Incarnation = "" }}, |
| 146 | {"no generation", func(l *AdoptionLaunch) { l.Writer.Generation = "" }}, |
| 147 | } { |
| 148 | f := newAdoptionFixture(t, rt, tok, key, instancekey.SourcePlatform) |
| 149 | tc.wreck(&f.launch) |
| 150 | if got := f.put(tok, adoptionBody(key, "platform")); got != http.StatusInternalServerError { |
| 151 | t.Fatalf("%s: %d, want 500", tc.name, got) |
| 152 | } |
| 153 | if _, err := os.Stat(filepath.Join(f.launch.Dir, rt.Host+instancekey.AdoptionSuffix)); !os.IsNotExist(err) || len(f.published) != 0 { |
| 154 | t.Fatalf("%s: a record was written or published", tc.name) |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | |