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  }