github.com/alloyci/alloy-runner@v1.0.1-0.20180222164613-925503ccafd6/network/gitlab_test.go (about)

     1  package network
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	. "gitlab.com/gitlab-org/gitlab-runner/common"
    20  )
    21  
    22  var brokenCredentials = RunnerCredentials{
    23  	URL: "broken",
    24  }
    25  
    26  var brokenConfig = RunnerConfig{
    27  	RunnerCredentials: brokenCredentials,
    28  }
    29  
    30  func TestClients(t *testing.T) {
    31  	c := NewGitLabClient()
    32  	c1, _ := c.getClient(&RunnerCredentials{
    33  		URL: "http://test/",
    34  	})
    35  	c2, _ := c.getClient(&RunnerCredentials{
    36  		URL: "http://test2/",
    37  	})
    38  	c4, _ := c.getClient(&RunnerCredentials{
    39  		URL:       "http://test/",
    40  		TLSCAFile: "ca_file",
    41  	})
    42  	c5, _ := c.getClient(&RunnerCredentials{
    43  		URL:       "http://test/",
    44  		TLSCAFile: "ca_file",
    45  	})
    46  	c6, _ := c.getClient(&RunnerCredentials{
    47  		URL:         "http://test/",
    48  		TLSCAFile:   "ca_file",
    49  		TLSCertFile: "cert_file",
    50  		TLSKeyFile:  "key_file",
    51  	})
    52  	c7, _ := c.getClient(&RunnerCredentials{
    53  		URL:         "http://test/",
    54  		TLSCAFile:   "ca_file",
    55  		TLSCertFile: "cert_file",
    56  		TLSKeyFile:  "key_file2",
    57  	})
    58  	c8, c8err := c.getClient(&brokenCredentials)
    59  	assert.NotEqual(t, c1, c2)
    60  	assert.NotEqual(t, c1, c4)
    61  	assert.Equal(t, c4, c5)
    62  	assert.NotEqual(t, c5, c6)
    63  	assert.Equal(t, c6, c7)
    64  	assert.Nil(t, c8)
    65  	assert.Error(t, c8err)
    66  }
    67  
    68  func testRegisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
    69  	if r.URL.Path != "/api/v4/runners" {
    70  		w.WriteHeader(http.StatusNotFound)
    71  		return
    72  	}
    73  
    74  	if r.Method != "POST" {
    75  		w.WriteHeader(http.StatusNotAcceptable)
    76  		return
    77  	}
    78  
    79  	body, err := ioutil.ReadAll(r.Body)
    80  	assert.NoError(t, err)
    81  
    82  	var req map[string]interface{}
    83  	err = json.Unmarshal(body, &req)
    84  	assert.NoError(t, err)
    85  
    86  	res := make(map[string]interface{})
    87  
    88  	switch req["token"].(string) {
    89  	case "valid":
    90  		if req["description"].(string) != "test" {
    91  			w.WriteHeader(http.StatusBadRequest)
    92  			return
    93  		}
    94  
    95  		res["token"] = req["token"].(string)
    96  	case "invalid":
    97  		w.WriteHeader(http.StatusForbidden)
    98  		return
    99  	default:
   100  		w.WriteHeader(http.StatusBadRequest)
   101  		return
   102  	}
   103  
   104  	if r.Header.Get("Accept") != "application/json" {
   105  		w.WriteHeader(http.StatusBadRequest)
   106  		return
   107  	}
   108  
   109  	output, err := json.Marshal(res)
   110  	if err != nil {
   111  		w.WriteHeader(http.StatusInternalServerError)
   112  		return
   113  	}
   114  
   115  	w.Header().Set("Content-Type", "application/json")
   116  	w.WriteHeader(http.StatusCreated)
   117  	w.Write(output)
   118  }
   119  
   120  func TestRegisterRunner(t *testing.T) {
   121  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   122  		testRegisterRunnerHandler(w, r, t)
   123  	}))
   124  	defer s.Close()
   125  
   126  	validToken := RunnerCredentials{
   127  		URL:   s.URL,
   128  		Token: "valid",
   129  	}
   130  
   131  	invalidToken := RunnerCredentials{
   132  		URL:   s.URL,
   133  		Token: "invalid",
   134  	}
   135  
   136  	otherToken := RunnerCredentials{
   137  		URL:   s.URL,
   138  		Token: "other",
   139  	}
   140  
   141  	c := NewGitLabClient()
   142  
   143  	res := c.RegisterRunner(validToken, "test", "tags", true, true)
   144  	if assert.NotNil(t, res) {
   145  		assert.Equal(t, validToken.Token, res.Token)
   146  	}
   147  
   148  	res = c.RegisterRunner(validToken, "invalid description", "tags", true, true)
   149  	assert.Nil(t, res)
   150  
   151  	res = c.RegisterRunner(invalidToken, "test", "tags", true, true)
   152  	assert.Nil(t, res)
   153  
   154  	res = c.RegisterRunner(otherToken, "test", "tags", true, true)
   155  	assert.Nil(t, res)
   156  
   157  	res = c.RegisterRunner(brokenCredentials, "test", "tags", true, true)
   158  	assert.Nil(t, res)
   159  }
   160  
   161  func testUnregisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   162  	if r.URL.Path != "/api/v4/runners" {
   163  		w.WriteHeader(http.StatusNotFound)
   164  		return
   165  	}
   166  
   167  	if r.Method != "DELETE" {
   168  		w.WriteHeader(http.StatusNotAcceptable)
   169  		return
   170  	}
   171  
   172  	body, err := ioutil.ReadAll(r.Body)
   173  	assert.NoError(t, err)
   174  
   175  	var req map[string]interface{}
   176  	err = json.Unmarshal(body, &req)
   177  	assert.NoError(t, err)
   178  
   179  	switch req["token"].(string) {
   180  	case "valid":
   181  		w.WriteHeader(http.StatusNoContent)
   182  	case "invalid":
   183  		w.WriteHeader(http.StatusForbidden)
   184  	default:
   185  		w.WriteHeader(http.StatusBadRequest)
   186  	}
   187  }
   188  
   189  func TestUnregisterRunner(t *testing.T) {
   190  	handler := func(w http.ResponseWriter, r *http.Request) {
   191  		testUnregisterRunnerHandler(w, r, t)
   192  	}
   193  
   194  	s := httptest.NewServer(http.HandlerFunc(handler))
   195  	defer s.Close()
   196  
   197  	validToken := RunnerCredentials{
   198  		URL:   s.URL,
   199  		Token: "valid",
   200  	}
   201  
   202  	invalidToken := RunnerCredentials{
   203  		URL:   s.URL,
   204  		Token: "invalid",
   205  	}
   206  
   207  	otherToken := RunnerCredentials{
   208  		URL:   s.URL,
   209  		Token: "other",
   210  	}
   211  
   212  	c := NewGitLabClient()
   213  
   214  	state := c.UnregisterRunner(validToken)
   215  	assert.True(t, state)
   216  
   217  	state = c.UnregisterRunner(invalidToken)
   218  	assert.False(t, state)
   219  
   220  	state = c.UnregisterRunner(otherToken)
   221  	assert.False(t, state)
   222  
   223  	state = c.UnregisterRunner(brokenCredentials)
   224  	assert.False(t, state)
   225  }
   226  
   227  func testVerifyRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   228  	if r.URL.Path != "/api/v4/runners/verify" {
   229  		w.WriteHeader(http.StatusNotFound)
   230  		return
   231  	}
   232  
   233  	if r.Method != "POST" {
   234  		w.WriteHeader(http.StatusNotAcceptable)
   235  		return
   236  	}
   237  
   238  	body, err := ioutil.ReadAll(r.Body)
   239  	assert.NoError(t, err)
   240  
   241  	var req map[string]interface{}
   242  	err = json.Unmarshal(body, &req)
   243  	assert.NoError(t, err)
   244  
   245  	switch req["token"].(string) {
   246  	case "valid":
   247  		w.WriteHeader(http.StatusOK) // since the job id is broken, we should not find this job
   248  	case "invalid":
   249  		w.WriteHeader(http.StatusForbidden)
   250  	default:
   251  		w.WriteHeader(http.StatusBadRequest)
   252  	}
   253  }
   254  
   255  func TestVerifyRunner(t *testing.T) {
   256  	handler := func(w http.ResponseWriter, r *http.Request) {
   257  		testVerifyRunnerHandler(w, r, t)
   258  	}
   259  
   260  	s := httptest.NewServer(http.HandlerFunc(handler))
   261  	defer s.Close()
   262  
   263  	validToken := RunnerCredentials{
   264  		URL:   s.URL,
   265  		Token: "valid",
   266  	}
   267  
   268  	invalidToken := RunnerCredentials{
   269  		URL:   s.URL,
   270  		Token: "invalid",
   271  	}
   272  
   273  	otherToken := RunnerCredentials{
   274  		URL:   s.URL,
   275  		Token: "other",
   276  	}
   277  
   278  	c := NewGitLabClient()
   279  
   280  	state := c.VerifyRunner(validToken)
   281  	assert.True(t, state)
   282  
   283  	state = c.VerifyRunner(invalidToken)
   284  	assert.False(t, state)
   285  
   286  	state = c.VerifyRunner(otherToken)
   287  	assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's")
   288  
   289  	state = c.VerifyRunner(brokenCredentials)
   290  	assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's")
   291  }
   292  
   293  func getRequestJobResponse() (res map[string]interface{}) {
   294  	jobToken := "job-token"
   295  
   296  	res = make(map[string]interface{})
   297  	res["id"] = 10
   298  	res["token"] = jobToken
   299  	res["allow_git_fetch"] = false
   300  
   301  	jobInfo := make(map[string]interface{})
   302  	jobInfo["name"] = "test-job"
   303  	jobInfo["stage"] = "test"
   304  	jobInfo["project_id"] = 123
   305  	jobInfo["project_name"] = "test-project"
   306  	res["job_info"] = jobInfo
   307  
   308  	gitInfo := make(map[string]interface{})
   309  	gitInfo["repo_url"] = "https://gitlab-ci-token:testTokenHere1234@gitlab.example.com/test/test-project.git"
   310  	gitInfo["ref"] = "master"
   311  	gitInfo["sha"] = "abcdef123456"
   312  	gitInfo["before_sha"] = "654321fedcba"
   313  	gitInfo["ref_type"] = "branch"
   314  	res["git_info"] = gitInfo
   315  
   316  	runnerInfo := make(map[string]interface{})
   317  	runnerInfo["timeout"] = 3600
   318  	res["runner_info"] = runnerInfo
   319  
   320  	variables := make([]map[string]interface{}, 1)
   321  	variables[0] = make(map[string]interface{})
   322  	variables[0]["key"] = "CI_REF_NAME"
   323  	variables[0]["value"] = "master"
   324  	variables[0]["public"] = true
   325  	variables[0]["file"] = true
   326  	res["variables"] = variables
   327  
   328  	steps := make([]map[string]interface{}, 2)
   329  	steps[0] = make(map[string]interface{})
   330  	steps[0]["name"] = "script"
   331  	steps[0]["script"] = []string{"date", "ls -ls"}
   332  	steps[0]["timeout"] = 3600
   333  	steps[0]["when"] = "on_success"
   334  	steps[0]["allow_failure"] = false
   335  	steps[1] = make(map[string]interface{})
   336  	steps[1]["name"] = "after_script"
   337  	steps[1]["script"] = []string{"ls -ls"}
   338  	steps[1]["timeout"] = 3600
   339  	steps[1]["when"] = "always"
   340  	steps[1]["allow_failure"] = true
   341  	res["steps"] = steps
   342  
   343  	image := make(map[string]interface{})
   344  	image["name"] = "ruby:2.0"
   345  	image["entrypoint"] = []string{"/bin/sh"}
   346  	res["image"] = image
   347  
   348  	services := make([]map[string]interface{}, 2)
   349  	services[0] = make(map[string]interface{})
   350  	services[0]["name"] = "postgresql:9.5"
   351  	services[0]["entrypoint"] = []string{"/bin/sh"}
   352  	services[0]["command"] = []string{"sleep", "30"}
   353  	services[0]["alias"] = "db-pg"
   354  	services[1] = make(map[string]interface{})
   355  	services[1]["name"] = "mysql:5.6"
   356  	services[1]["alias"] = "db-mysql"
   357  	res["services"] = services
   358  
   359  	artifacts := make([]map[string]interface{}, 1)
   360  	artifacts[0] = make(map[string]interface{})
   361  	artifacts[0]["name"] = "artifact.zip"
   362  	artifacts[0]["untracked"] = false
   363  	artifacts[0]["paths"] = []string{"out/*"}
   364  	artifacts[0]["when"] = "always"
   365  	artifacts[0]["expire_in"] = "7d"
   366  	res["artifacts"] = artifacts
   367  
   368  	cache := make([]map[string]interface{}, 1)
   369  	cache[0] = make(map[string]interface{})
   370  	cache[0]["key"] = "$CI_COMMIT_REF"
   371  	cache[0]["untracked"] = false
   372  	cache[0]["paths"] = []string{"vendor/*"}
   373  	cache[0]["policy"] = "push"
   374  	res["cache"] = cache
   375  
   376  	credentials := make([]map[string]interface{}, 1)
   377  	credentials[0] = make(map[string]interface{})
   378  	credentials[0]["type"] = "Registry"
   379  	credentials[0]["url"] = "http://registry.gitlab.example.com/"
   380  	credentials[0]["username"] = "gitlab-ci-token"
   381  	credentials[0]["password"] = jobToken
   382  	res["credentials"] = credentials
   383  
   384  	dependencies := make([]map[string]interface{}, 1)
   385  	dependencies[0] = make(map[string]interface{})
   386  	dependencies[0]["id"] = 9
   387  	dependencies[0]["name"] = "other-job"
   388  	dependencies[0]["token"] = "other-job-token"
   389  	artifactsFile0 := make(map[string]interface{})
   390  	artifactsFile0["filename"] = "binaries.zip"
   391  	artifactsFile0["size"] = 13631488
   392  	dependencies[0]["artifacts_file"] = artifactsFile0
   393  	res["dependencies"] = dependencies
   394  
   395  	return
   396  }
   397  
   398  func testRequestJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   399  	if r.URL.Path != "/api/v4/jobs/request" {
   400  		w.WriteHeader(http.StatusNotFound)
   401  		return
   402  	}
   403  
   404  	if r.Method != "POST" {
   405  		w.WriteHeader(http.StatusNotAcceptable)
   406  		return
   407  	}
   408  
   409  	body, err := ioutil.ReadAll(r.Body)
   410  	assert.NoError(t, err)
   411  
   412  	var req map[string]interface{}
   413  	err = json.Unmarshal(body, &req)
   414  	assert.NoError(t, err)
   415  
   416  	switch req["token"].(string) {
   417  	case "valid":
   418  	case "no-jobs":
   419  		w.Header().Add("X-GitLab-Last-Update", "a nice timestamp")
   420  		w.WriteHeader(http.StatusNoContent)
   421  		return
   422  	case "invalid":
   423  		w.WriteHeader(http.StatusForbidden)
   424  		return
   425  	default:
   426  		w.WriteHeader(http.StatusBadRequest)
   427  		return
   428  	}
   429  
   430  	if r.Header.Get("Accept") != "application/json" {
   431  		w.WriteHeader(http.StatusBadRequest)
   432  		return
   433  	}
   434  
   435  	output, err := json.Marshal(getRequestJobResponse())
   436  	if err != nil {
   437  		w.WriteHeader(http.StatusInternalServerError)
   438  		return
   439  	}
   440  
   441  	w.Header().Set("Content-Type", "application/json")
   442  	w.WriteHeader(http.StatusCreated)
   443  	w.Write(output)
   444  	t.Logf("JobRequest response: %s\n", output)
   445  }
   446  
   447  func TestRequestJob(t *testing.T) {
   448  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   449  		testRequestJobHandler(w, r, t)
   450  	}))
   451  	defer s.Close()
   452  
   453  	validToken := RunnerConfig{
   454  		RunnerCredentials: RunnerCredentials{
   455  			URL:   s.URL,
   456  			Token: "valid",
   457  		},
   458  	}
   459  
   460  	noJobsToken := RunnerConfig{
   461  		RunnerCredentials: RunnerCredentials{
   462  			URL:   s.URL,
   463  			Token: "no-jobs",
   464  		},
   465  	}
   466  
   467  	invalidToken := RunnerConfig{
   468  		RunnerCredentials: RunnerCredentials{
   469  			URL:   s.URL,
   470  			Token: "invalid",
   471  		},
   472  	}
   473  
   474  	c := NewGitLabClient()
   475  
   476  	res, ok := c.RequestJob(validToken)
   477  	if assert.NotNil(t, res) {
   478  		assert.NotEmpty(t, res.ID)
   479  	}
   480  	assert.True(t, ok)
   481  
   482  	assert.Equal(t, "ruby:2.0", res.Image.Name)
   483  	assert.Equal(t, []string{"/bin/sh"}, res.Image.Entrypoint)
   484  	require.Len(t, res.Services, 2)
   485  	assert.Equal(t, "postgresql:9.5", res.Services[0].Name)
   486  	assert.Equal(t, []string{"/bin/sh"}, res.Services[0].Entrypoint)
   487  	assert.Equal(t, []string{"sleep", "30"}, res.Services[0].Command)
   488  	assert.Equal(t, "db-pg", res.Services[0].Alias)
   489  	assert.Equal(t, "mysql:5.6", res.Services[1].Name)
   490  	assert.Equal(t, "db-mysql", res.Services[1].Alias)
   491  
   492  	assert.Empty(t, c.getLastUpdate(&noJobsToken.RunnerCredentials), "Last-Update should not be set")
   493  	res, ok = c.RequestJob(noJobsToken)
   494  	assert.Nil(t, res)
   495  	assert.True(t, ok, "If no jobs, runner is healthy")
   496  	assert.Equal(t, c.getLastUpdate(&noJobsToken.RunnerCredentials), "a nice timestamp", "Last-Update should be set")
   497  
   498  	res, ok = c.RequestJob(invalidToken)
   499  	assert.Nil(t, res)
   500  	assert.False(t, ok, "If token is invalid, the runner is unhealthy")
   501  
   502  	res, ok = c.RequestJob(brokenConfig)
   503  	assert.Nil(t, res)
   504  	assert.False(t, ok)
   505  }
   506  
   507  func setStateForUpdateJobHandlerResponse(w http.ResponseWriter, req map[string]interface{}) {
   508  	switch req["state"].(string) {
   509  	case "running":
   510  		w.WriteHeader(http.StatusOK)
   511  	case "failed":
   512  		failureReason, ok := req["failure_reason"].(string)
   513  		if ok && (JobFailureReason(failureReason) == ScriptFailure ||
   514  			JobFailureReason(failureReason) == RunnerSystemFailure) {
   515  			w.WriteHeader(http.StatusOK)
   516  		} else {
   517  			w.WriteHeader(http.StatusBadRequest)
   518  		}
   519  	case "forbidden":
   520  		w.WriteHeader(http.StatusForbidden)
   521  	default:
   522  		w.WriteHeader(http.StatusBadRequest)
   523  	}
   524  }
   525  
   526  func testUpdateJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   527  	if r.URL.Path != "/api/v4/jobs/10" {
   528  		w.WriteHeader(http.StatusNotFound)
   529  		return
   530  	}
   531  
   532  	if r.Method != "PUT" {
   533  		w.WriteHeader(http.StatusNotAcceptable)
   534  		return
   535  	}
   536  
   537  	body, err := ioutil.ReadAll(r.Body)
   538  	assert.NoError(t, err)
   539  
   540  	var req map[string]interface{}
   541  	err = json.Unmarshal(body, &req)
   542  	assert.NoError(t, err)
   543  
   544  	assert.Equal(t, "token", req["token"])
   545  	assert.Equal(t, "trace", req["trace"])
   546  
   547  	setStateForUpdateJobHandlerResponse(w, req)
   548  }
   549  
   550  func TestUpdateJob(t *testing.T) {
   551  	handler := func(w http.ResponseWriter, r *http.Request) {
   552  		testUpdateJobHandler(w, r, t)
   553  	}
   554  
   555  	s := httptest.NewServer(http.HandlerFunc(handler))
   556  	defer s.Close()
   557  
   558  	config := RunnerConfig{
   559  		RunnerCredentials: RunnerCredentials{
   560  			URL: s.URL,
   561  		},
   562  	}
   563  
   564  	jobCredentials := &JobCredentials{
   565  		Token: "token",
   566  	}
   567  
   568  	trace := "trace"
   569  	c := NewGitLabClient()
   570  
   571  	var state UpdateState
   572  
   573  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "running", Trace: &trace, FailureReason: ""})
   574  	assert.Equal(t, UpdateSucceeded, state, "Update should continue when running")
   575  
   576  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "forbidden", Trace: &trace, FailureReason: ""})
   577  	assert.Equal(t, UpdateAbort, state, "Update should be aborted if the state is forbidden")
   578  
   579  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "other", Trace: &trace, FailureReason: ""})
   580  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   581  
   582  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 4, State: "state", Trace: &trace, FailureReason: ""})
   583  	assert.Equal(t, UpdateAbort, state, "Update should abort for unknown job")
   584  
   585  	state = c.UpdateJob(brokenConfig, jobCredentials, UpdateJobInfo{ID: 4, State: "state", Trace: &trace, FailureReason: ""})
   586  	assert.Equal(t, UpdateAbort, state)
   587  
   588  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", Trace: &trace, FailureReason: "script_failure"})
   589  	assert.Equal(t, UpdateSucceeded, state, "Update should continue when running")
   590  
   591  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", Trace: &trace, FailureReason: "unknown_failure_reason"})
   592  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   593  
   594  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", Trace: &trace, FailureReason: ""})
   595  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   596  }
   597  
   598  var patchToken = "token"
   599  var patchTraceString = "trace trace trace"
   600  
   601  func getPatchServer(t *testing.T, handler func(w http.ResponseWriter, r *http.Request, body string, offset, limit int)) (*httptest.Server, *GitLabClient, RunnerConfig) {
   602  	patchHandler := func(w http.ResponseWriter, r *http.Request) {
   603  		if r.URL.Path != "/api/v4/jobs/1/trace" {
   604  			w.WriteHeader(http.StatusNotFound)
   605  			return
   606  		}
   607  
   608  		if r.Method != "PATCH" {
   609  			w.WriteHeader(http.StatusNotAcceptable)
   610  			return
   611  		}
   612  
   613  		assert.Equal(t, patchToken, r.Header.Get("JOB-TOKEN"))
   614  
   615  		body, err := ioutil.ReadAll(r.Body)
   616  		assert.NoError(t, err)
   617  
   618  		contentRange := r.Header.Get("Content-Range")
   619  		ranges := strings.Split(contentRange, "-")
   620  
   621  		offset, err := strconv.Atoi(ranges[0])
   622  		assert.NoError(t, err)
   623  
   624  		limit, err := strconv.Atoi(ranges[1])
   625  		assert.NoError(t, err)
   626  
   627  		handler(w, r, string(body), offset, limit)
   628  	}
   629  
   630  	server := httptest.NewServer(http.HandlerFunc(patchHandler))
   631  
   632  	config := RunnerConfig{
   633  		RunnerCredentials: RunnerCredentials{
   634  			URL: server.URL,
   635  		},
   636  	}
   637  
   638  	return server, NewGitLabClient(), config
   639  }
   640  
   641  func getTracePatch(traceString string, offset int) *tracePatch {
   642  	trace := bytes.Buffer{}
   643  	trace.WriteString(traceString)
   644  	tracePatch, _ := newTracePatch(trace, offset)
   645  
   646  	return tracePatch
   647  }
   648  
   649  func TestUnknownPatchTrace(t *testing.T) {
   650  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   651  		w.WriteHeader(http.StatusNotFound)
   652  	}
   653  
   654  	server, client, config := getPatchServer(t, handler)
   655  	defer server.Close()
   656  
   657  	tracePatch := getTracePatch(patchTraceString, 0)
   658  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   659  	assert.Equal(t, UpdateNotFound, state)
   660  }
   661  
   662  func TestForbiddenPatchTrace(t *testing.T) {
   663  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   664  		w.WriteHeader(http.StatusForbidden)
   665  	}
   666  
   667  	server, client, config := getPatchServer(t, handler)
   668  	defer server.Close()
   669  
   670  	tracePatch := getTracePatch(patchTraceString, 0)
   671  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   672  	assert.Equal(t, UpdateAbort, state)
   673  }
   674  
   675  func TestPatchTrace(t *testing.T) {
   676  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   677  		assert.Equal(t, patchTraceString[offset:limit], body)
   678  
   679  		w.WriteHeader(http.StatusAccepted)
   680  	}
   681  
   682  	server, client, config := getPatchServer(t, handler)
   683  	defer server.Close()
   684  
   685  	tracePatch := getTracePatch(patchTraceString, 0)
   686  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   687  	assert.Equal(t, UpdateSucceeded, state)
   688  
   689  	tracePatch = getTracePatch(patchTraceString, 3)
   690  	state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   691  	assert.Equal(t, UpdateSucceeded, state)
   692  
   693  	tracePatch = getTracePatch(patchTraceString[:10], 3)
   694  	state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   695  	assert.Equal(t, UpdateSucceeded, state)
   696  }
   697  
   698  func TestRangeMismatchPatchTrace(t *testing.T) {
   699  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   700  		if offset > 10 {
   701  			w.Header().Set("Range", "0-10")
   702  			w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
   703  		}
   704  
   705  		w.WriteHeader(http.StatusAccepted)
   706  	}
   707  
   708  	server, client, config := getPatchServer(t, handler)
   709  	defer server.Close()
   710  
   711  	tracePatch := getTracePatch(patchTraceString, 11)
   712  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   713  	assert.Equal(t, UpdateRangeMismatch, state)
   714  
   715  	tracePatch = getTracePatch(patchTraceString, 15)
   716  	state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   717  	assert.Equal(t, UpdateRangeMismatch, state)
   718  
   719  	tracePatch = getTracePatch(patchTraceString, 5)
   720  	state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   721  	assert.Equal(t, UpdateSucceeded, state)
   722  }
   723  
   724  func TestResendPatchTrace(t *testing.T) {
   725  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   726  		if offset > 10 {
   727  			w.Header().Set("Range", "0-10")
   728  			w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
   729  		}
   730  
   731  		w.WriteHeader(http.StatusAccepted)
   732  	}
   733  
   734  	server, client, config := getPatchServer(t, handler)
   735  	defer server.Close()
   736  
   737  	tracePatch := getTracePatch(patchTraceString, 11)
   738  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   739  	assert.Equal(t, UpdateRangeMismatch, state)
   740  
   741  	state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   742  	assert.Equal(t, UpdateSucceeded, state)
   743  }
   744  
   745  // We've had a situation where the same job was triggered second time by GItLab. In GitLab the job trace
   746  // was 17041 bytes long while the repeated job trace was only 66 bytes long. We've had a `RangeMismatch`
   747  // response, so the offset was updated (to 17041) and `client.PatchTrace` was repeated, at it was planned.
   748  // Unfortunately the `tracePatch` struct was  not resistant to a situation when the offset is set to a
   749  // value bigger than trace's length. This test simulates such situation.
   750  func TestResendDoubledJobPatchTrace(t *testing.T) {
   751  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   752  		if offset > 10 {
   753  			w.Header().Set("Range", "0-100")
   754  			w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
   755  		}
   756  
   757  		w.WriteHeader(http.StatusAccepted)
   758  	}
   759  
   760  	server, client, config := getPatchServer(t, handler)
   761  	defer server.Close()
   762  
   763  	tracePatch := getTracePatch(patchTraceString, 11)
   764  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   765  	assert.Equal(t, UpdateRangeMismatch, state)
   766  	assert.False(t, tracePatch.ValidateRange())
   767  }
   768  
   769  func TestJobFailedStatePatchTrace(t *testing.T) {
   770  	handler := func(w http.ResponseWriter, r *http.Request, body string, offset, limit int) {
   771  		w.Header().Set("Job-Status", "failed")
   772  		w.WriteHeader(http.StatusAccepted)
   773  	}
   774  
   775  	server, client, config := getPatchServer(t, handler)
   776  	defer server.Close()
   777  
   778  	tracePatch := getTracePatch(patchTraceString, 0)
   779  	state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken}, tracePatch)
   780  	assert.Equal(t, UpdateAbort, state)
   781  }
   782  
   783  func testArtifactsUploadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   784  	if r.URL.Path != "/api/v4/jobs/10/artifacts" {
   785  		w.WriteHeader(http.StatusNotFound)
   786  		return
   787  	}
   788  
   789  	if r.Method != "POST" {
   790  		w.WriteHeader(http.StatusNotAcceptable)
   791  		return
   792  	}
   793  
   794  	if r.Header.Get("JOB-TOKEN") != "token" {
   795  		w.WriteHeader(http.StatusForbidden)
   796  		return
   797  	}
   798  
   799  	file, _, err := r.FormFile("file")
   800  	if err != nil {
   801  		w.WriteHeader(http.StatusBadRequest)
   802  		return
   803  	}
   804  
   805  	body, err := ioutil.ReadAll(file)
   806  	assert.NoError(t, err)
   807  
   808  	if string(body) != "content" {
   809  		w.WriteHeader(http.StatusRequestEntityTooLarge)
   810  	} else {
   811  		w.WriteHeader(http.StatusCreated)
   812  	}
   813  }
   814  
   815  func TestArtifactsUpload(t *testing.T) {
   816  	handler := func(w http.ResponseWriter, r *http.Request) {
   817  		testArtifactsUploadHandler(w, r, t)
   818  	}
   819  
   820  	s := httptest.NewServer(http.HandlerFunc(handler))
   821  	defer s.Close()
   822  
   823  	config := JobCredentials{
   824  		ID:    10,
   825  		URL:   s.URL,
   826  		Token: "token",
   827  	}
   828  	invalidToken := JobCredentials{
   829  		ID:    10,
   830  		URL:   s.URL,
   831  		Token: "invalid-token",
   832  	}
   833  
   834  	tempFile, err := ioutil.TempFile("", "artifacts")
   835  	assert.NoError(t, err)
   836  	defer tempFile.Close()
   837  	defer os.Remove(tempFile.Name())
   838  
   839  	c := NewGitLabClient()
   840  
   841  	fmt.Fprint(tempFile, "content")
   842  	state := c.UploadArtifacts(config, tempFile.Name())
   843  	assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded")
   844  
   845  	fmt.Fprint(tempFile, "too large")
   846  	state = c.UploadArtifacts(config, tempFile.Name())
   847  	assert.Equal(t, UploadTooLarge, state, "Artifacts should be not uploaded, because of too large archive")
   848  
   849  	state = c.UploadArtifacts(config, "not/existing/file")
   850  	assert.Equal(t, UploadFailed, state, "Artifacts should fail to be uploaded")
   851  
   852  	state = c.UploadArtifacts(invalidToken, tempFile.Name())
   853  	assert.Equal(t, UploadForbidden, state, "Artifacts should be rejected if invalid token")
   854  }
   855  
   856  func testArtifactsDownloadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   857  	if r.URL.Path != "/api/v4/jobs/10/artifacts" {
   858  		w.WriteHeader(http.StatusNotFound)
   859  		return
   860  	}
   861  
   862  	if r.Method != "GET" {
   863  		w.WriteHeader(http.StatusNotAcceptable)
   864  		return
   865  	}
   866  
   867  	if r.Header.Get("JOB-TOKEN") != "token" {
   868  		w.WriteHeader(http.StatusForbidden)
   869  		return
   870  	}
   871  
   872  	w.WriteHeader(http.StatusOK)
   873  	w.Write(bytes.NewBufferString("Test artifact file content").Bytes())
   874  }
   875  
   876  func TestArtifactsDownload(t *testing.T) {
   877  	handler := func(w http.ResponseWriter, r *http.Request) {
   878  		testArtifactsDownloadHandler(w, r, t)
   879  	}
   880  
   881  	s := httptest.NewServer(http.HandlerFunc(handler))
   882  	defer s.Close()
   883  
   884  	credentials := JobCredentials{
   885  		ID:    10,
   886  		URL:   s.URL,
   887  		Token: "token",
   888  	}
   889  	invalidTokenCredentials := JobCredentials{
   890  		ID:    10,
   891  		URL:   s.URL,
   892  		Token: "invalid-token",
   893  	}
   894  	fileNotFoundTokenCredentials := JobCredentials{
   895  		ID:    11,
   896  		URL:   s.URL,
   897  		Token: "token",
   898  	}
   899  
   900  	c := NewGitLabClient()
   901  
   902  	tempDir, err := ioutil.TempDir("", "artifacts")
   903  	assert.NoError(t, err)
   904  
   905  	artifactsFileName := filepath.Join(tempDir, "downloaded-artifact")
   906  	defer os.Remove(artifactsFileName)
   907  
   908  	state := c.DownloadArtifacts(credentials, artifactsFileName)
   909  	assert.Equal(t, DownloadSucceeded, state, "Artifacts should be downloaded")
   910  
   911  	state = c.DownloadArtifacts(invalidTokenCredentials, artifactsFileName)
   912  	assert.Equal(t, DownloadForbidden, state, "Artifacts should be not downloaded if invalid token is used")
   913  
   914  	state = c.DownloadArtifacts(fileNotFoundTokenCredentials, artifactsFileName)
   915  	assert.Equal(t, DownloadNotFound, state, "Artifacts should be bit downloaded if it's not found")
   916  }