github.com/release-engineering/exodus-rsync@v1.11.2/internal/gw/gw_helpers_test.go (about) 1 package gw 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 "testing" 11 12 "github.com/release-engineering/exodus-rsync/internal/args" 13 "github.com/release-engineering/exodus-rsync/internal/log" 14 ) 15 16 // A fake for the exodus-gw service. 17 type fakeGw struct { 18 t *testing.T 19 20 // List of IDs of publish objects to be created, or empty if creating publishes 21 // should fail 22 createPublishIds []string 23 24 // Existing publish objects 25 publishes publishMap 26 27 // If non-nil, forces next HTTP request to return this error. 28 nextHTTPError error 29 30 // If non-nil, forces next HTTP request to return this response 31 nextHTTPResponse *http.Response 32 } 33 34 type publishMap map[string]*fakePublish 35 36 type fakePublish struct { 37 id string 38 items []ItemInput 39 lastCommit string 40 41 // If publish is committed, then each time the task state is polled, 42 // we'll pop the next state from here. 43 taskStates []string 44 } 45 46 func (p *fakePublish) nextState() string { 47 out := p.taskStates[0] 48 p.taskStates = p.taskStates[1:] 49 return out 50 } 51 52 // Implement RoundTripper interface for fake handling of HTTP requests. 53 func (f *fakeGw) RoundTrip(r *http.Request) (*http.Response, error) { 54 if r.Body != nil { 55 defer r.Body.Close() 56 } 57 58 if f.nextHTTPError != nil { 59 err := f.nextHTTPError 60 f.nextHTTPError = nil 61 return nil, err 62 } 63 64 if f.nextHTTPResponse != nil { 65 out := f.nextHTTPResponse 66 f.nextHTTPResponse = nil 67 return out, nil 68 } 69 70 out := &http.Response{} 71 72 out.Status = "404 Not Found" 73 out.StatusCode = 404 74 // Body must not be nil, even for empty response. 75 out.Body = io.NopCloser(strings.NewReader("")) 76 77 path := strings.TrimPrefix(r.URL.Path, "/") 78 route := strings.Split(path, "/") 79 80 if len(route) == 2 && route[0] == "task" && r.Method == "GET" { 81 return f.getTask(route[1]), nil 82 } 83 84 // For every other route, path must be under /env/ suffix, bail out 85 // early if not 86 if route[0] != "env" { 87 f.t.Logf("unexpected request path %v", path) 88 return out, nil 89 } 90 route = route[1:] 91 92 if len(route) == 1 && route[0] == "publish" && r.Method == "POST" { 93 return f.createPublish(), nil 94 } 95 96 if len(route) == 2 && route[0] == "publish" && r.Method == "GET" { 97 return f.getPublish(route[1]), nil 98 } 99 100 if len(route) == 2 && route[0] == "publish" && r.Method == "PUT" { 101 return f.addPublishItems(r, route[1]), nil 102 } 103 104 if len(route) == 3 && route[0] == "publish" && route[2] == "commit" && r.Method == "POST" { 105 return f.commitPublish(route[1], r.URL.Query().Get("commit_mode")), nil 106 } 107 108 return out, nil 109 } 110 111 func newFakeGw(t *testing.T, c *client) *fakeGw { 112 out := &fakeGw{t: t, createPublishIds: make([]string, 0), publishes: make(publishMap)} 113 out.install(c) 114 return out 115 } 116 117 func (f *fakeGw) install(c *client) { 118 ctx := log.NewContext(context.Background(), log.Package.NewLogger(args.Config{})) 119 c.httpClient.Transport = retryTransport(ctx, c.cfg, f) 120 } 121 122 func (f *fakeGw) createPublish() *http.Response { 123 out := &http.Response{} 124 125 if len(f.createPublishIds) == 0 { 126 f.t.Log("out of publish IDs!") 127 out.Status = "500 Internal Server Error" 128 out.StatusCode = 500 129 return out 130 } 131 132 id := f.createPublishIds[0] 133 f.createPublishIds = f.createPublishIds[1:] 134 135 newPublish := &fakePublish{} 136 newPublish.id = id 137 // By default, any new publish will successfully commit. 138 newPublish.taskStates = []string{"NOT_STARTED", "IN_PROGRESS", "COMPLETE"} 139 140 f.publishes[id] = newPublish 141 142 content := fmt.Sprintf(`{ 143 "id": "%s", 144 "env": "env", 145 "state": "PENDING", 146 "links": { 147 "self": "/env/publish/%[1]s", 148 "commit": "/env/publish/%[1]s/commit" 149 }, 150 "items": [] 151 }`, id) 152 out.Body = io.NopCloser(strings.NewReader(content)) 153 154 out.Header = http.Header{} 155 out.Header.Add("Content-Type", "application/json") 156 out.Status = "200 OK" 157 out.StatusCode = 200 158 159 return out 160 } 161 162 func (f *fakeGw) addPublishItems(r *http.Request, id string) *http.Response { 163 out := &http.Response{} 164 165 publish, havePublish := f.publishes[id] 166 if !havePublish { 167 f.t.Logf("requested nonexistent publish %s", id) 168 out.Status = "404 Not Found" 169 out.StatusCode = 404 170 return out 171 } 172 173 dec := json.NewDecoder(r.Body) 174 requestItems := make([]map[string]string, 0) 175 176 err := dec.Decode(&requestItems) 177 if err != nil { 178 f.t.Logf("non-JSON request body? err = %v", err) 179 out.Status = "400 Bad Request" 180 out.StatusCode = 400 181 return out 182 } 183 184 for _, item := range requestItems { 185 publish.items = append(publish.items, ItemInput{item["web_uri"], item["object_key"], item["content_type"], item["link_to"]}) 186 } 187 188 out.Status = "200 OK" 189 out.StatusCode = 200 190 out.Body = io.NopCloser(strings.NewReader("{}")) 191 return out 192 } 193 194 func (f *fakeGw) commitPublish(id string, mode string) *http.Response { 195 out := &http.Response{} 196 197 publish, havePublish := f.publishes[id] 198 if !havePublish { 199 f.t.Logf("requested nonexistent publish %s", id) 200 out.Status = "404 Not Found" 201 out.StatusCode = 404 202 return out 203 } 204 205 if len(publish.taskStates) == 0 { 206 // test can set taskStates empty to force an error 207 out.Status = "500 Internal Server Error" 208 out.StatusCode = 500 209 return out 210 } 211 212 publish.lastCommit = mode 213 214 state := publish.nextState() 215 taskID := "task-" + publish.id 216 content := fmt.Sprintf(`{ 217 "id": "%s", 218 "publish_id": "%s", 219 "state": "%s", 220 "links": { 221 "self": "/task/%[1]s" 222 } 223 }`, taskID, id, state) 224 225 out.Status = "200 OK" 226 out.StatusCode = 200 227 out.Body = io.NopCloser(strings.NewReader(content)) 228 return out 229 } 230 231 func (f *fakeGw) getTask(id string) *http.Response { 232 out := &http.Response{} 233 234 if !strings.HasPrefix(id, "task-") { 235 f.t.Logf("requested non-task ID %s", id) 236 out.Status = "404 Not Found" 237 out.StatusCode = 404 238 return out 239 } 240 241 publishID := strings.TrimPrefix(id, "task-") 242 publish := f.publishes[publishID] 243 if len(publish.taskStates) == 0 { 244 out.Status = "500 Internal Server Error" 245 out.StatusCode = 500 246 return out 247 } 248 249 state := publish.nextState() 250 content := fmt.Sprintf(`{ 251 "id": "%s", 252 "publish_id": "%s", 253 "state": "%s", 254 "links": { 255 "self": "/task/%[1]s" 256 } 257 }`, id, publishID, state) 258 259 out.Status = "200 OK" 260 out.StatusCode = 200 261 out.Body = io.NopCloser(strings.NewReader(content)) 262 return out 263 } 264 265 func (f *fakeGw) getPublish(id string) *http.Response { 266 out := &http.Response{} 267 268 _, havePublish := f.publishes[id] 269 if !havePublish { 270 f.t.Logf("requested nonexistent publish %s", id) 271 out.Status = "404 Not Found" 272 out.StatusCode = 404 273 return out 274 } 275 276 content := fmt.Sprintf(`{ 277 "id": "%s", 278 "env": "env", 279 "links": { 280 "self": "/env/publish/%[1]s", 281 "commit": "/env/publish/%[1]s/commit" 282 }, 283 "items": [] 284 }`, id) 285 286 out.Status = "200 OK" 287 out.StatusCode = 200 288 out.Body = io.NopCloser(strings.NewReader(content)) 289 290 return out 291 }