carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package activator
2
3import (
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.
17type adoptionFixture struct {
18 mux *http.ServeMux
19 launch AdoptionLaunch
20 hasLaunch bool
21 published [][]byte
22}
23
24func 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
38func (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
48func 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.
55func 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.
134func 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