github.com/hpcng/singularity@v3.1.1+incompatible/internal/pkg/build/remotebuilder/remotebuilder_test.go (about) 1 // Copyright (c) 2018, Sylabs Inc. All rights reserved. 2 // This software is licensed under a 3-clause BSD license. Please consult the 3 // LICENSE.md file distributed with the sources of this project regarding your 4 // rights to use or distribute this software. 5 6 package remotebuilder 7 8 import ( 9 "context" 10 "encoding/json" 11 "fmt" 12 "io/ioutil" 13 "net/http" 14 "net/http/httptest" 15 "net/url" 16 "os" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/globalsign/mgo/bson" 22 "github.com/gorilla/websocket" 23 "github.com/sylabs/json-resp" 24 "github.com/sylabs/singularity/internal/pkg/test" 25 "github.com/sylabs/singularity/pkg/build/types" 26 "github.com/sylabs/singularity/pkg/util/user-agent" 27 ) 28 29 const ( 30 authToken = "auth_token" 31 stdoutContents = "some_output" 32 imageContents = "image_contents" 33 buildPath = "/v1/build" 34 wsPath = "/v1/build-ws/" 35 imagePath = "/v1/image" 36 ) 37 38 type mockService struct { 39 t *testing.T 40 buildResponseCode int 41 wsResponseCode int 42 wsCloseCode int 43 statusResponseCode int 44 imageResponseCode int 45 httpAddr string 46 } 47 48 var upgrader = websocket.Upgrader{} 49 50 func TestMain(m *testing.M) { 51 useragent.InitValue("singularity", "3.0.0-alpha.1-303-gaed8d30-dirty") 52 53 os.Exit(m.Run()) 54 } 55 56 func newResponse(m *mockService, id bson.ObjectId, d types.Definition, libraryRef string) types.ResponseData { 57 wsURL := url.URL{ 58 Scheme: "ws", 59 Host: m.httpAddr, 60 Path: fmt.Sprintf("%s%s", wsPath, id.Hex()), 61 } 62 libraryURL := url.URL{ 63 Scheme: "http", 64 Host: m.httpAddr, 65 } 66 if libraryRef == "" { 67 libraryRef = "library://user/collection/image" 68 } 69 70 return types.ResponseData{ 71 ID: id, 72 Definition: d, 73 WSURL: wsURL.String(), 74 LibraryURL: libraryURL.String(), 75 LibraryRef: libraryRef, 76 IsComplete: true, 77 ImageSize: 1, 78 } 79 } 80 81 func (m *mockService) ServeHTTP(w http.ResponseWriter, r *http.Request) { 82 // Set the response body, depending on the type of operation 83 if r.Method == http.MethodPost && r.RequestURI == buildPath { 84 // Mock new build endpoint 85 var rd types.RequestData 86 if err := json.NewDecoder(r.Body).Decode(&rd); err != nil { 87 m.t.Fatalf("failed to parse request: %v", err) 88 } 89 if m.buildResponseCode == http.StatusCreated { 90 id := bson.NewObjectId() 91 jsonresp.WriteResponse(w, newResponse(m, id, rd.Definition, rd.LibraryRef), m.buildResponseCode) 92 } else { 93 jsonresp.WriteError(w, "", m.buildResponseCode) 94 } 95 } else if r.Method == http.MethodGet && strings.HasPrefix(r.RequestURI, buildPath) { 96 // Mock status endpoint 97 id := r.RequestURI[strings.LastIndexByte(r.RequestURI, '/')+1:] 98 if !bson.IsObjectIdHex(id) { 99 m.t.Fatalf("failed to parse ID '%v'", id) 100 } 101 if m.statusResponseCode == http.StatusOK { 102 jsonresp.WriteResponse(w, newResponse(m, bson.ObjectIdHex(id), types.Definition{}, ""), m.statusResponseCode) 103 } else { 104 jsonresp.WriteError(w, "", m.statusResponseCode) 105 } 106 } else if r.Method == http.MethodGet && strings.HasPrefix(r.RequestURI, imagePath) { 107 // Mock get image endpoint 108 if m.imageResponseCode == http.StatusOK { 109 if _, err := strings.NewReader(imageContents).WriteTo(w); err != nil { 110 m.t.Fatalf("failed to write image") 111 } 112 } else { 113 jsonresp.WriteError(w, "", m.imageResponseCode) 114 } 115 } else { 116 w.WriteHeader(http.StatusNotFound) 117 } 118 } 119 120 func (m *mockService) ServeWebsocket(w http.ResponseWriter, r *http.Request) { 121 if m.wsResponseCode != http.StatusOK { 122 w.WriteHeader(m.wsResponseCode) 123 } else { 124 ws, err := upgrader.Upgrade(w, r, nil) 125 if err != nil { 126 m.t.Fatalf("failed to upgrade websocket: %v", err) 127 } 128 defer ws.Close() 129 130 // Write some output and then cleanly close the connection 131 ws.WriteMessage(websocket.TextMessage, []byte(stdoutContents)) 132 ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(m.wsCloseCode, "")) 133 } 134 } 135 136 func TestBuild(t *testing.T) { 137 test.DropPrivilege(t) 138 defer test.ResetPrivilege(t) 139 140 // Craft an expired context 141 ctx, cancel := context.WithDeadline(context.Background(), time.Now()) 142 defer cancel() 143 144 // Create a temporary file for testing 145 f, err := ioutil.TempFile("/tmp", "TestBuild") 146 if err != nil { 147 t.Fatalf("failed to create temp file: %v", err) 148 } 149 f.Close() 150 defer os.Remove(f.Name()) 151 152 // Start a mock server 153 m := mockService{t: t} 154 mux := http.NewServeMux() 155 mux.HandleFunc("/", m.ServeHTTP) 156 mux.HandleFunc(wsPath, m.ServeWebsocket) 157 s := httptest.NewServer(mux) 158 defer s.Close() 159 160 // Mock server address is fixed for all tests 161 m.httpAddr = s.Listener.Addr().String() 162 163 // Table of tests to run 164 tests := []struct { 165 description string 166 expectSuccess bool 167 imagePath string 168 libraryURL string 169 buildResponseCode int 170 wsResponseCode int 171 wsCloseCode int 172 statusResponseCode int 173 imageResponseCode int 174 ctx context.Context 175 isDetached bool 176 }{ 177 {"SuccessAttached", true, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 178 {"SuccessDetached", true, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), true}, 179 {"SuccessLibraryRef", true, "library://user/collection/image", "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 180 {"SuccessLibraryRefURL", true, "library://user/collection/image", m.httpAddr, http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 181 {"BadImagePath", false, "/tmp/bad/", "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 182 {"BadLibraryRef", false, "library://bad", "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 183 {"AddBuildFailure", false, f.Name(), "", http.StatusUnauthorized, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 184 {"WebsocketFailure", false, f.Name(), "", http.StatusCreated, http.StatusUnauthorized, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 185 {"WebsocketAbnormalClosure", false, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseAbnormalClosure, http.StatusOK, http.StatusOK, context.Background(), false}, 186 {"GetStatusFailure", false, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusUnauthorized, http.StatusOK, context.Background(), false}, 187 {"GetImageFailure", false, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusUnauthorized, context.Background(), false}, 188 {"ContextExpired", false, f.Name(), "", http.StatusCreated, http.StatusOK, websocket.CloseNormalClosure, http.StatusOK, http.StatusOK, ctx, false}, 189 } 190 191 // Loop over test cases 192 for _, tt := range tests { 193 t.Run(tt.description, test.WithoutPrivilege(func(t *testing.T) { 194 rb, err := New(tt.imagePath, "", types.Definition{}, tt.isDetached, false, s.URL, authToken) 195 if err != nil { 196 t.Fatalf("failed to get new remote builder: %v", err) 197 } 198 rb.Force = true 199 200 // Set the response codes for each stage of the build 201 m.buildResponseCode = tt.buildResponseCode 202 m.wsResponseCode = tt.wsResponseCode 203 m.wsCloseCode = tt.wsCloseCode 204 m.statusResponseCode = tt.statusResponseCode 205 m.imageResponseCode = tt.imageResponseCode 206 207 // Do it! 208 err = rb.Build(tt.ctx) 209 210 if tt.expectSuccess { 211 // Ensure the handler returned no error, and the response is as expected 212 if err != nil { 213 t.Fatalf("unexpected failure: %v", err) 214 } 215 } else { 216 // Ensure the handler returned an error 217 if err == nil { 218 t.Fatalf("unexpected success") 219 } 220 } 221 })) 222 } 223 } 224 225 func TestDoBuildRequest(t *testing.T) { 226 // Craft an expired context 227 ctx, cancel := context.WithDeadline(context.Background(), time.Now()) 228 defer cancel() 229 230 // Table of tests to run 231 tests := []struct { 232 description string 233 expectSuccess bool 234 libraryRef string 235 responseCode int 236 ctx context.Context 237 }{ 238 {"SuccessAttached", true, "", http.StatusCreated, context.Background()}, 239 {"SuccessLibraryRef", true, "library://user/collection/image", http.StatusCreated, context.Background()}, 240 {"BadLibraryRef", false, "library://bad", http.StatusCreated, context.Background()}, 241 {"NotFoundAttached", false, "", http.StatusNotFound, context.Background()}, 242 {"ContextExpiredAttached", false, "", http.StatusCreated, ctx}, 243 } 244 245 // Start a mock server 246 m := mockService{t: t} 247 s := httptest.NewServer(&m) 248 defer s.Close() 249 250 // Enough of a struct to test with 251 url, err := url.Parse(s.URL) 252 if err != nil { 253 t.Fatalf("failed to parse URL: %v", err) 254 } 255 rb := RemoteBuilder{ 256 BuilderURL: url, 257 } 258 259 // Loop over test cases 260 for _, tt := range tests { 261 t.Run(tt.description, test.WithoutPrivilege(func(t *testing.T) { 262 m.buildResponseCode = tt.responseCode 263 264 // Call the handler 265 rd, err := rb.doBuildRequest(tt.ctx, types.Definition{}, tt.libraryRef) 266 267 if tt.expectSuccess { 268 // Ensure the handler returned no error, and the response is as expected 269 if err != nil { 270 t.Fatalf("unexpected failure: %v", err) 271 } 272 if !rd.ID.Valid() { 273 t.Fatalf("invalid ID") 274 } 275 if rd.WSURL == "" { 276 t.Errorf("empty websocket URL") 277 } 278 if rd.LibraryRef == "" { 279 t.Errorf("empty Library ref") 280 } 281 if rd.LibraryURL == "" { 282 t.Errorf("empty Library URL") 283 } 284 } else { 285 // Ensure the handler returned an error 286 if err == nil { 287 t.Fatalf("unexpected success") 288 } 289 } 290 })) 291 } 292 } 293 294 func TestDoStatusRequest(t *testing.T) { 295 // Craft an expired context 296 ctx, cancel := context.WithDeadline(context.Background(), time.Now()) 297 defer cancel() 298 299 // Table of tests to run 300 tests := []struct { 301 description string 302 expectSuccess bool 303 responseCode int 304 ctx context.Context 305 }{ 306 {"Success", true, http.StatusOK, context.Background()}, 307 {"NotFound", false, http.StatusNotFound, context.Background()}, 308 {"ContextExpired", false, http.StatusOK, ctx}, 309 } 310 311 // Start a mock server 312 m := mockService{t: t} 313 s := httptest.NewServer(&m) 314 defer s.Close() 315 316 // Enough of a struct to test with 317 url, err := url.Parse(s.URL) 318 if err != nil { 319 t.Fatalf("failed to parse URL: %v", err) 320 } 321 rb := RemoteBuilder{ 322 BuilderURL: url, 323 } 324 325 // ID to test with 326 id := bson.NewObjectId() 327 328 // Loop over test cases 329 for _, tt := range tests { 330 t.Run(tt.description, test.WithoutPrivilege(func(t *testing.T) { 331 m.statusResponseCode = tt.responseCode 332 333 // Call the handler 334 rd, err := rb.doStatusRequest(tt.ctx, id) 335 336 if tt.expectSuccess { 337 // Ensure the handler returned no error, and the response is as expected 338 if err != nil { 339 t.Fatalf("unexpected failure: %v", err) 340 } 341 if rd.ID != id { 342 t.Errorf("mismatched ID: %v/%v", rd.ID, id) 343 } 344 if rd.WSURL == "" { 345 t.Errorf("empty websocket URL") 346 } 347 if rd.LibraryRef == "" { 348 t.Errorf("empty Library ref") 349 } 350 if rd.LibraryURL == "" { 351 t.Errorf("empty Library URL") 352 } 353 } else { 354 // Ensure the handler returned an error 355 if err == nil { 356 t.Fatalf("unexpected success") 357 } 358 } 359 })) 360 } 361 }