github.com/secure-build/gitlab-runner@v12.5.0+incompatible/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/mock"
    18  	"github.com/stretchr/testify/require"
    19  
    20  	. "gitlab.com/gitlab-org/gitlab-runner/common"
    21  )
    22  
    23  var brokenCredentials = RunnerCredentials{
    24  	URL: "broken",
    25  }
    26  
    27  var brokenConfig = RunnerConfig{
    28  	RunnerCredentials: brokenCredentials,
    29  }
    30  
    31  func TestClients(t *testing.T) {
    32  	c := NewGitLabClient()
    33  	c1, _ := c.getClient(&RunnerCredentials{
    34  		URL: "http://test/",
    35  	})
    36  	c2, _ := c.getClient(&RunnerCredentials{
    37  		URL: "http://test2/",
    38  	})
    39  	c4, _ := c.getClient(&RunnerCredentials{
    40  		URL:       "http://test/",
    41  		TLSCAFile: "ca_file",
    42  	})
    43  	c5, _ := c.getClient(&RunnerCredentials{
    44  		URL:       "http://test/",
    45  		TLSCAFile: "ca_file",
    46  	})
    47  	c6, _ := c.getClient(&RunnerCredentials{
    48  		URL:         "http://test/",
    49  		TLSCAFile:   "ca_file",
    50  		TLSCertFile: "cert_file",
    51  		TLSKeyFile:  "key_file",
    52  	})
    53  	c7, _ := c.getClient(&RunnerCredentials{
    54  		URL:         "http://test/",
    55  		TLSCAFile:   "ca_file",
    56  		TLSCertFile: "cert_file",
    57  		TLSKeyFile:  "key_file2",
    58  	})
    59  	c8, c8err := c.getClient(&brokenCredentials)
    60  	assert.NotEqual(t, c1, c2)
    61  	assert.NotEqual(t, c1, c4)
    62  	assert.Equal(t, c4, c5)
    63  	assert.NotEqual(t, c5, c6)
    64  	assert.Equal(t, c6, c7)
    65  	assert.Nil(t, c8)
    66  	assert.Error(t, c8err)
    67  }
    68  
    69  func testRegisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
    70  	if r.URL.Path != "/api/v4/runners" {
    71  		w.WriteHeader(http.StatusNotFound)
    72  		return
    73  	}
    74  
    75  	if r.Method != "POST" {
    76  		w.WriteHeader(http.StatusNotAcceptable)
    77  		return
    78  	}
    79  
    80  	body, err := ioutil.ReadAll(r.Body)
    81  	assert.NoError(t, err)
    82  
    83  	var req map[string]interface{}
    84  	err = json.Unmarshal(body, &req)
    85  	assert.NoError(t, err)
    86  
    87  	res := make(map[string]interface{})
    88  
    89  	switch req["token"].(string) {
    90  	case "valid":
    91  		if req["description"].(string) != "test" {
    92  			w.WriteHeader(http.StatusBadRequest)
    93  			return
    94  		}
    95  
    96  		res["token"] = req["token"].(string)
    97  	case "invalid":
    98  		w.WriteHeader(http.StatusForbidden)
    99  		return
   100  	default:
   101  		w.WriteHeader(http.StatusBadRequest)
   102  		return
   103  	}
   104  
   105  	if r.Header.Get("Accept") != "application/json" {
   106  		w.WriteHeader(http.StatusBadRequest)
   107  		return
   108  	}
   109  
   110  	output, err := json.Marshal(res)
   111  	if err != nil {
   112  		w.WriteHeader(http.StatusInternalServerError)
   113  		return
   114  	}
   115  
   116  	w.Header().Set("Content-Type", "application/json")
   117  	w.WriteHeader(http.StatusCreated)
   118  	w.Write(output)
   119  }
   120  
   121  func TestRegisterRunner(t *testing.T) {
   122  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   123  		testRegisterRunnerHandler(w, r, t)
   124  	}))
   125  	defer s.Close()
   126  
   127  	validToken := RunnerCredentials{
   128  		URL:   s.URL,
   129  		Token: "valid",
   130  	}
   131  
   132  	invalidToken := RunnerCredentials{
   133  		URL:   s.URL,
   134  		Token: "invalid",
   135  	}
   136  
   137  	otherToken := RunnerCredentials{
   138  		URL:   s.URL,
   139  		Token: "other",
   140  	}
   141  
   142  	c := NewGitLabClient()
   143  
   144  	res := c.RegisterRunner(validToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, Active: true})
   145  	if assert.NotNil(t, res) {
   146  		assert.Equal(t, validToken.Token, res.Token)
   147  	}
   148  
   149  	res = c.RegisterRunner(validToken, RegisterRunnerParameters{Description: "invalid description", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true})
   150  	assert.Nil(t, res)
   151  
   152  	res = c.RegisterRunner(invalidToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true})
   153  	assert.Nil(t, res)
   154  
   155  	res = c.RegisterRunner(otherToken, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true})
   156  	assert.Nil(t, res)
   157  
   158  	res = c.RegisterRunner(brokenCredentials, RegisterRunnerParameters{Description: "test", Tags: "tags", RunUntagged: true, Locked: true, AccessLevel: "not_protected", Active: true})
   159  	assert.Nil(t, res)
   160  }
   161  
   162  func testUnregisterRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   163  	if r.URL.Path != "/api/v4/runners" {
   164  		w.WriteHeader(http.StatusNotFound)
   165  		return
   166  	}
   167  
   168  	if r.Method != "DELETE" {
   169  		w.WriteHeader(http.StatusNotAcceptable)
   170  		return
   171  	}
   172  
   173  	body, err := ioutil.ReadAll(r.Body)
   174  	assert.NoError(t, err)
   175  
   176  	var req map[string]interface{}
   177  	err = json.Unmarshal(body, &req)
   178  	assert.NoError(t, err)
   179  
   180  	switch req["token"].(string) {
   181  	case "valid":
   182  		w.WriteHeader(http.StatusNoContent)
   183  	case "invalid":
   184  		w.WriteHeader(http.StatusForbidden)
   185  	default:
   186  		w.WriteHeader(http.StatusBadRequest)
   187  	}
   188  }
   189  
   190  func TestUnregisterRunner(t *testing.T) {
   191  	handler := func(w http.ResponseWriter, r *http.Request) {
   192  		testUnregisterRunnerHandler(w, r, t)
   193  	}
   194  
   195  	s := httptest.NewServer(http.HandlerFunc(handler))
   196  	defer s.Close()
   197  
   198  	validToken := RunnerCredentials{
   199  		URL:   s.URL,
   200  		Token: "valid",
   201  	}
   202  
   203  	invalidToken := RunnerCredentials{
   204  		URL:   s.URL,
   205  		Token: "invalid",
   206  	}
   207  
   208  	otherToken := RunnerCredentials{
   209  		URL:   s.URL,
   210  		Token: "other",
   211  	}
   212  
   213  	c := NewGitLabClient()
   214  
   215  	state := c.UnregisterRunner(validToken)
   216  	assert.True(t, state)
   217  
   218  	state = c.UnregisterRunner(invalidToken)
   219  	assert.False(t, state)
   220  
   221  	state = c.UnregisterRunner(otherToken)
   222  	assert.False(t, state)
   223  
   224  	state = c.UnregisterRunner(brokenCredentials)
   225  	assert.False(t, state)
   226  }
   227  
   228  func testVerifyRunnerHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   229  	if r.URL.Path != "/api/v4/runners/verify" {
   230  		w.WriteHeader(http.StatusNotFound)
   231  		return
   232  	}
   233  
   234  	if r.Method != "POST" {
   235  		w.WriteHeader(http.StatusNotAcceptable)
   236  		return
   237  	}
   238  
   239  	body, err := ioutil.ReadAll(r.Body)
   240  	assert.NoError(t, err)
   241  
   242  	var req map[string]interface{}
   243  	err = json.Unmarshal(body, &req)
   244  	assert.NoError(t, err)
   245  
   246  	switch req["token"].(string) {
   247  	case "valid":
   248  		w.WriteHeader(http.StatusOK) // since the job id is broken, we should not find this job
   249  	case "invalid":
   250  		w.WriteHeader(http.StatusForbidden)
   251  	default:
   252  		w.WriteHeader(http.StatusBadRequest)
   253  	}
   254  }
   255  
   256  func TestVerifyRunner(t *testing.T) {
   257  	handler := func(w http.ResponseWriter, r *http.Request) {
   258  		testVerifyRunnerHandler(w, r, t)
   259  	}
   260  
   261  	s := httptest.NewServer(http.HandlerFunc(handler))
   262  	defer s.Close()
   263  
   264  	validToken := RunnerCredentials{
   265  		URL:   s.URL,
   266  		Token: "valid",
   267  	}
   268  
   269  	invalidToken := RunnerCredentials{
   270  		URL:   s.URL,
   271  		Token: "invalid",
   272  	}
   273  
   274  	otherToken := RunnerCredentials{
   275  		URL:   s.URL,
   276  		Token: "other",
   277  	}
   278  
   279  	c := NewGitLabClient()
   280  
   281  	state := c.VerifyRunner(validToken)
   282  	assert.True(t, state)
   283  
   284  	state = c.VerifyRunner(invalidToken)
   285  	assert.False(t, state)
   286  
   287  	state = c.VerifyRunner(otherToken)
   288  	assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's")
   289  
   290  	state = c.VerifyRunner(brokenCredentials)
   291  	assert.True(t, state, "in other cases where we can't explicitly say that runner is valid we say that it's")
   292  }
   293  
   294  func getRequestJobResponse() (res map[string]interface{}) {
   295  	jobToken := "job-token"
   296  
   297  	res = make(map[string]interface{})
   298  	res["id"] = 10
   299  	res["token"] = jobToken
   300  	res["allow_git_fetch"] = false
   301  
   302  	jobInfo := make(map[string]interface{})
   303  	jobInfo["name"] = "test-job"
   304  	jobInfo["stage"] = "test"
   305  	jobInfo["project_id"] = 123
   306  	jobInfo["project_name"] = "test-project"
   307  	res["job_info"] = jobInfo
   308  
   309  	gitInfo := make(map[string]interface{})
   310  	gitInfo["repo_url"] = "https://gitlab-ci-token:testTokenHere1234@gitlab.example.com/test/test-project.git"
   311  	gitInfo["ref"] = "master"
   312  	gitInfo["sha"] = "abcdef123456"
   313  	gitInfo["before_sha"] = "654321fedcba"
   314  	gitInfo["ref_type"] = "branch"
   315  	res["git_info"] = gitInfo
   316  
   317  	runnerInfo := make(map[string]interface{})
   318  	runnerInfo["timeout"] = 3600
   319  	res["runner_info"] = runnerInfo
   320  
   321  	variables := make([]map[string]interface{}, 1)
   322  	variables[0] = make(map[string]interface{})
   323  	variables[0]["key"] = "CI_REF_NAME"
   324  	variables[0]["value"] = "master"
   325  	variables[0]["public"] = true
   326  	variables[0]["file"] = true
   327  	res["variables"] = variables
   328  
   329  	steps := make([]map[string]interface{}, 2)
   330  	steps[0] = make(map[string]interface{})
   331  	steps[0]["name"] = "script"
   332  	steps[0]["script"] = []string{"date", "ls -ls"}
   333  	steps[0]["timeout"] = 3600
   334  	steps[0]["when"] = "on_success"
   335  	steps[0]["allow_failure"] = false
   336  	steps[1] = make(map[string]interface{})
   337  	steps[1]["name"] = "after_script"
   338  	steps[1]["script"] = []string{"ls -ls"}
   339  	steps[1]["timeout"] = 3600
   340  	steps[1]["when"] = "always"
   341  	steps[1]["allow_failure"] = true
   342  	res["steps"] = steps
   343  
   344  	image := make(map[string]interface{})
   345  	image["name"] = "ruby:2.0"
   346  	image["entrypoint"] = []string{"/bin/sh"}
   347  	res["image"] = image
   348  
   349  	services := make([]map[string]interface{}, 2)
   350  	services[0] = make(map[string]interface{})
   351  	services[0]["name"] = "postgresql:9.5"
   352  	services[0]["entrypoint"] = []string{"/bin/sh"}
   353  	services[0]["command"] = []string{"sleep", "30"}
   354  	services[0]["alias"] = "db-pg"
   355  	services[1] = make(map[string]interface{})
   356  	services[1]["name"] = "mysql:5.6"
   357  	services[1]["alias"] = "db-mysql"
   358  	res["services"] = services
   359  
   360  	artifacts := make([]map[string]interface{}, 1)
   361  	artifacts[0] = make(map[string]interface{})
   362  	artifacts[0]["name"] = "artifact.zip"
   363  	artifacts[0]["untracked"] = false
   364  	artifacts[0]["paths"] = []string{"out/*"}
   365  	artifacts[0]["when"] = "always"
   366  	artifacts[0]["expire_in"] = "7d"
   367  	res["artifacts"] = artifacts
   368  
   369  	cache := make([]map[string]interface{}, 1)
   370  	cache[0] = make(map[string]interface{})
   371  	cache[0]["key"] = "$CI_COMMIT_SHA"
   372  	cache[0]["untracked"] = false
   373  	cache[0]["paths"] = []string{"vendor/*"}
   374  	cache[0]["policy"] = "push"
   375  	res["cache"] = cache
   376  
   377  	credentials := make([]map[string]interface{}, 1)
   378  	credentials[0] = make(map[string]interface{})
   379  	credentials[0]["type"] = "Registry"
   380  	credentials[0]["url"] = "http://registry.gitlab.example.com/"
   381  	credentials[0]["username"] = "gitlab-ci-token"
   382  	credentials[0]["password"] = jobToken
   383  	res["credentials"] = credentials
   384  
   385  	dependencies := make([]map[string]interface{}, 1)
   386  	dependencies[0] = make(map[string]interface{})
   387  	dependencies[0]["id"] = 9
   388  	dependencies[0]["name"] = "other-job"
   389  	dependencies[0]["token"] = "other-job-token"
   390  	artifactsFile0 := make(map[string]interface{})
   391  	artifactsFile0["filename"] = "binaries.zip"
   392  	artifactsFile0["size"] = 13631488
   393  	dependencies[0]["artifacts_file"] = artifactsFile0
   394  	res["dependencies"] = dependencies
   395  
   396  	return
   397  }
   398  
   399  func testRequestJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   400  	if r.URL.Path != "/api/v4/jobs/request" {
   401  		w.WriteHeader(http.StatusNotFound)
   402  		return
   403  	}
   404  
   405  	if r.Method != "POST" {
   406  		w.WriteHeader(http.StatusNotAcceptable)
   407  		return
   408  	}
   409  
   410  	body, err := ioutil.ReadAll(r.Body)
   411  	assert.NoError(t, err)
   412  
   413  	var req map[string]interface{}
   414  	err = json.Unmarshal(body, &req)
   415  	assert.NoError(t, err)
   416  
   417  	switch req["token"].(string) {
   418  	case "valid":
   419  	case "no-jobs":
   420  		w.Header().Add("X-GitLab-Last-Update", "a nice timestamp")
   421  		w.WriteHeader(http.StatusNoContent)
   422  		return
   423  	case "invalid":
   424  		w.WriteHeader(http.StatusForbidden)
   425  		return
   426  	default:
   427  		w.WriteHeader(http.StatusBadRequest)
   428  		return
   429  	}
   430  
   431  	if r.Header.Get("Accept") != "application/json" {
   432  		w.WriteHeader(http.StatusBadRequest)
   433  		return
   434  	}
   435  
   436  	output, err := json.Marshal(getRequestJobResponse())
   437  	if err != nil {
   438  		w.WriteHeader(http.StatusInternalServerError)
   439  		return
   440  	}
   441  
   442  	w.Header().Set("Content-Type", "application/json")
   443  	w.WriteHeader(http.StatusCreated)
   444  	w.Write(output)
   445  	t.Logf("JobRequest response: %s\n", output)
   446  }
   447  
   448  func TestRequestJob(t *testing.T) {
   449  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   450  		testRequestJobHandler(w, r, t)
   451  	}))
   452  	defer s.Close()
   453  
   454  	validToken := RunnerConfig{
   455  		RunnerCredentials: RunnerCredentials{
   456  			URL:   s.URL,
   457  			Token: "valid",
   458  		},
   459  	}
   460  
   461  	noJobsToken := RunnerConfig{
   462  		RunnerCredentials: RunnerCredentials{
   463  			URL:   s.URL,
   464  			Token: "no-jobs",
   465  		},
   466  	}
   467  
   468  	invalidToken := RunnerConfig{
   469  		RunnerCredentials: RunnerCredentials{
   470  			URL:   s.URL,
   471  			Token: "invalid",
   472  		},
   473  	}
   474  
   475  	c := NewGitLabClient()
   476  
   477  	res, ok := c.RequestJob(validToken, nil)
   478  	if assert.NotNil(t, res) {
   479  		assert.NotEmpty(t, res.ID)
   480  	}
   481  	assert.True(t, ok)
   482  
   483  	assert.Equal(t, "ruby:2.0", res.Image.Name)
   484  	assert.Equal(t, []string{"/bin/sh"}, res.Image.Entrypoint)
   485  	require.Len(t, res.Services, 2)
   486  	assert.Equal(t, "postgresql:9.5", res.Services[0].Name)
   487  	assert.Equal(t, []string{"/bin/sh"}, res.Services[0].Entrypoint)
   488  	assert.Equal(t, []string{"sleep", "30"}, res.Services[0].Command)
   489  	assert.Equal(t, "db-pg", res.Services[0].Alias)
   490  	assert.Equal(t, "mysql:5.6", res.Services[1].Name)
   491  	assert.Equal(t, "db-mysql", res.Services[1].Alias)
   492  
   493  	assert.Empty(t, c.getLastUpdate(&noJobsToken.RunnerCredentials), "Last-Update should not be set")
   494  	res, ok = c.RequestJob(noJobsToken, nil)
   495  	assert.Nil(t, res)
   496  	assert.True(t, ok, "If no jobs, runner is healthy")
   497  	assert.Equal(t, "a nice timestamp", c.getLastUpdate(&noJobsToken.RunnerCredentials), "Last-Update should be set")
   498  
   499  	res, ok = c.RequestJob(invalidToken, nil)
   500  	assert.Nil(t, res)
   501  	assert.False(t, ok, "If token is invalid, the runner is unhealthy")
   502  
   503  	res, ok = c.RequestJob(brokenConfig, nil)
   504  	assert.Nil(t, res)
   505  	assert.False(t, ok)
   506  }
   507  
   508  func setStateForUpdateJobHandlerResponse(w http.ResponseWriter, req map[string]interface{}) {
   509  	switch req["state"].(string) {
   510  	case "running":
   511  		w.WriteHeader(http.StatusOK)
   512  	case "failed":
   513  		failureReason, ok := req["failure_reason"].(string)
   514  		if ok && (JobFailureReason(failureReason) == ScriptFailure ||
   515  			JobFailureReason(failureReason) == RunnerSystemFailure) {
   516  			w.WriteHeader(http.StatusOK)
   517  		} else {
   518  			w.WriteHeader(http.StatusBadRequest)
   519  		}
   520  	case "forbidden":
   521  		w.WriteHeader(http.StatusForbidden)
   522  	default:
   523  		w.WriteHeader(http.StatusBadRequest)
   524  	}
   525  }
   526  
   527  func testUpdateJobHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   528  	if r.URL.Path != "/api/v4/jobs/10" {
   529  		w.WriteHeader(http.StatusNotFound)
   530  		return
   531  	}
   532  
   533  	if r.Method != "PUT" {
   534  		w.WriteHeader(http.StatusNotAcceptable)
   535  		return
   536  	}
   537  
   538  	body, err := ioutil.ReadAll(r.Body)
   539  	assert.NoError(t, err)
   540  
   541  	var req map[string]interface{}
   542  	err = json.Unmarshal(body, &req)
   543  	assert.NoError(t, err)
   544  
   545  	assert.Equal(t, "token", req["token"])
   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  	c := NewGitLabClient()
   569  
   570  	var state UpdateState
   571  
   572  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "running", FailureReason: ""})
   573  	assert.Equal(t, UpdateSucceeded, state, "Update should continue when running")
   574  
   575  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "forbidden", FailureReason: ""})
   576  	assert.Equal(t, UpdateAbort, state, "Update should be aborted if the state is forbidden")
   577  
   578  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "other", FailureReason: ""})
   579  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   580  
   581  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 4, State: "state", FailureReason: ""})
   582  	assert.Equal(t, UpdateAbort, state, "Update should abort for unknown job")
   583  
   584  	state = c.UpdateJob(brokenConfig, jobCredentials, UpdateJobInfo{ID: 4, State: "state", FailureReason: ""})
   585  	assert.Equal(t, UpdateAbort, state)
   586  
   587  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: "script_failure"})
   588  	assert.Equal(t, UpdateSucceeded, state, "Update should continue when running")
   589  
   590  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: "unknown_failure_reason"})
   591  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   592  
   593  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "failed", FailureReason: ""})
   594  	assert.Equal(t, UpdateFailed, state, "Update should fail for badly formatted request")
   595  }
   596  
   597  func testUpdateJobKeepAliveHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
   598  	if r.Method != "PUT" {
   599  		w.WriteHeader(http.StatusNotAcceptable)
   600  		return
   601  	}
   602  
   603  	switch r.URL.Path {
   604  	case "/api/v4/jobs/10":
   605  	case "/api/v4/jobs/11":
   606  		w.Header().Set("Job-Status", "canceled")
   607  	case "/api/v4/jobs/12":
   608  		w.Header().Set("Job-Status", "failed")
   609  	default:
   610  		w.WriteHeader(http.StatusNotFound)
   611  		return
   612  	}
   613  
   614  	body, err := ioutil.ReadAll(r.Body)
   615  	assert.NoError(t, err)
   616  
   617  	var req map[string]interface{}
   618  	err = json.Unmarshal(body, &req)
   619  	assert.NoError(t, err)
   620  
   621  	assert.Equal(t, "token", req["token"])
   622  
   623  	w.WriteHeader(http.StatusOK)
   624  }
   625  
   626  func TestUpdateJobAsKeepAlive(t *testing.T) {
   627  	handler := func(w http.ResponseWriter, r *http.Request) {
   628  		testUpdateJobKeepAliveHandler(w, r, t)
   629  	}
   630  
   631  	s := httptest.NewServer(http.HandlerFunc(handler))
   632  	defer s.Close()
   633  
   634  	config := RunnerConfig{
   635  		RunnerCredentials: RunnerCredentials{
   636  			URL: s.URL,
   637  		},
   638  	}
   639  
   640  	jobCredentials := &JobCredentials{
   641  		Token: "token",
   642  	}
   643  
   644  	c := NewGitLabClient()
   645  
   646  	var state UpdateState
   647  
   648  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 10, State: "running"})
   649  	assert.Equal(t, UpdateSucceeded, state, "Update should continue when running")
   650  
   651  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 11, State: "running"})
   652  	assert.Equal(t, UpdateAbort, state, "Update should be aborted when Job-Status=canceled")
   653  
   654  	state = c.UpdateJob(config, jobCredentials, UpdateJobInfo{ID: 12, State: "running"})
   655  	assert.Equal(t, UpdateAbort, state, "Update should continue when Job-Status=failed")
   656  }
   657  
   658  var patchToken = "token"
   659  var patchTraceContent = []byte("trace trace trace")
   660  
   661  func getPatchServer(t *testing.T, handler func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int)) (*httptest.Server, *GitLabClient, RunnerConfig) {
   662  	patchHandler := func(w http.ResponseWriter, r *http.Request) {
   663  		if r.URL.Path != "/api/v4/jobs/1/trace" {
   664  			w.WriteHeader(http.StatusNotFound)
   665  			return
   666  		}
   667  
   668  		if r.Method != "PATCH" {
   669  			w.WriteHeader(http.StatusNotAcceptable)
   670  			return
   671  		}
   672  
   673  		assert.Equal(t, patchToken, r.Header.Get("JOB-TOKEN"))
   674  
   675  		body, err := ioutil.ReadAll(r.Body)
   676  		assert.NoError(t, err)
   677  
   678  		contentRange := r.Header.Get("Content-Range")
   679  		ranges := strings.Split(contentRange, "-")
   680  
   681  		offset, err := strconv.Atoi(ranges[0])
   682  		assert.NoError(t, err)
   683  
   684  		limit, err := strconv.Atoi(ranges[1])
   685  		assert.NoError(t, err)
   686  
   687  		handler(w, r, body, offset, limit)
   688  	}
   689  
   690  	server := httptest.NewServer(http.HandlerFunc(patchHandler))
   691  
   692  	config := RunnerConfig{
   693  		RunnerCredentials: RunnerCredentials{
   694  			URL: server.URL,
   695  		},
   696  	}
   697  
   698  	return server, NewGitLabClient(), config
   699  }
   700  
   701  func TestUnknownPatchTrace(t *testing.T) {
   702  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   703  		w.WriteHeader(http.StatusNotFound)
   704  	}
   705  
   706  	server, client, config := getPatchServer(t, handler)
   707  	defer server.Close()
   708  
   709  	_, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   710  		patchTraceContent, 0)
   711  	assert.Equal(t, UpdateNotFound, state)
   712  }
   713  
   714  func TestForbiddenPatchTrace(t *testing.T) {
   715  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   716  		w.WriteHeader(http.StatusForbidden)
   717  	}
   718  
   719  	server, client, config := getPatchServer(t, handler)
   720  	defer server.Close()
   721  
   722  	_, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   723  		patchTraceContent, 0)
   724  	assert.Equal(t, UpdateAbort, state)
   725  }
   726  
   727  func TestPatchTrace(t *testing.T) {
   728  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   729  		assert.Equal(t, patchTraceContent[offset:limit+1], body)
   730  		w.WriteHeader(http.StatusAccepted)
   731  	}
   732  
   733  	server, client, config := getPatchServer(t, handler)
   734  	defer server.Close()
   735  
   736  	endOffset, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   737  		patchTraceContent, 0)
   738  	assert.Equal(t, UpdateSucceeded, state)
   739  	assert.Equal(t, len(patchTraceContent), endOffset)
   740  
   741  	endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   742  		patchTraceContent[3:], 3)
   743  	assert.Equal(t, UpdateSucceeded, state)
   744  	assert.Equal(t, len(patchTraceContent), endOffset)
   745  
   746  	endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   747  		patchTraceContent[3:10], 3)
   748  	assert.Equal(t, UpdateSucceeded, state)
   749  	assert.Equal(t, 10, endOffset)
   750  }
   751  
   752  func TestRangeMismatchPatchTrace(t *testing.T) {
   753  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   754  		if offset > 10 {
   755  			w.Header().Set("Range", "0-10")
   756  			w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
   757  		}
   758  
   759  		w.WriteHeader(http.StatusAccepted)
   760  	}
   761  
   762  	server, client, config := getPatchServer(t, handler)
   763  	defer server.Close()
   764  
   765  	endOffset, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   766  		patchTraceContent[11:], 11)
   767  	assert.Equal(t, UpdateRangeMismatch, state)
   768  	assert.Equal(t, 10, endOffset)
   769  
   770  	endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   771  		patchTraceContent[15:], 15)
   772  	assert.Equal(t, UpdateRangeMismatch, state)
   773  	assert.Equal(t, 10, endOffset)
   774  
   775  	endOffset, state = client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   776  		patchTraceContent[5:], 5)
   777  	assert.Equal(t, UpdateSucceeded, state)
   778  	assert.Equal(t, len(patchTraceContent), endOffset)
   779  }
   780  
   781  func TestJobFailedStatePatchTrace(t *testing.T) {
   782  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   783  		w.Header().Set("Job-Status", "failed")
   784  		w.WriteHeader(http.StatusAccepted)
   785  	}
   786  
   787  	server, client, config := getPatchServer(t, handler)
   788  	defer server.Close()
   789  
   790  	_, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   791  		patchTraceContent, 0)
   792  	assert.Equal(t, UpdateAbort, state)
   793  }
   794  
   795  func TestPatchTraceCantConnect(t *testing.T) {
   796  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {}
   797  
   798  	server, client, config := getPatchServer(t, handler)
   799  	server.Close()
   800  
   801  	_, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   802  		patchTraceContent, 0)
   803  	assert.Equal(t, UpdateFailed, state)
   804  }
   805  
   806  func TestPatchTraceUpdatedTrace(t *testing.T) {
   807  	sentTrace := 0
   808  	traceContent := []byte{}
   809  
   810  	updates := []struct {
   811  		traceUpdate             []byte
   812  		expectedContentRange    string
   813  		expectedContentLength   int64
   814  		expectedResult          UpdateState
   815  		shouldNotCallPatchTrace bool
   816  	}{
   817  		{
   818  			traceUpdate:           []byte("test"),
   819  			expectedContentRange:  "0-3",
   820  			expectedContentLength: 4,
   821  			expectedResult:        UpdateSucceeded,
   822  		},
   823  		{
   824  			traceUpdate:             []byte{},
   825  			expectedContentLength:   4,
   826  			expectedResult:          UpdateSucceeded,
   827  			shouldNotCallPatchTrace: true,
   828  		},
   829  		{
   830  			traceUpdate:          []byte(" "),
   831  			expectedContentRange: "4-4", expectedContentLength: 1,
   832  			expectedResult: UpdateSucceeded,
   833  		},
   834  		{
   835  			traceUpdate:          []byte("test"),
   836  			expectedContentRange: "5-8", expectedContentLength: 4,
   837  			expectedResult: UpdateSucceeded,
   838  		},
   839  	}
   840  
   841  	for id, update := range updates {
   842  		t.Run(fmt.Sprintf("patch-%d", id+1), func(t *testing.T) {
   843  			handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   844  				if update.shouldNotCallPatchTrace {
   845  					t.Error("PatchTrace endpoint should not be called")
   846  					return
   847  				}
   848  
   849  				if limit+1 <= len(traceContent) {
   850  					assert.Equal(t, traceContent[offset:limit+1], body)
   851  				}
   852  
   853  				assert.Equal(t, update.traceUpdate, body)
   854  				assert.Equal(t, update.expectedContentRange, r.Header.Get("Content-Range"))
   855  				assert.Equal(t, update.expectedContentLength, r.ContentLength)
   856  				w.WriteHeader(http.StatusAccepted)
   857  			}
   858  
   859  			server, client, config := getPatchServer(t, handler)
   860  			defer server.Close()
   861  
   862  			traceContent = append(traceContent, update.traceUpdate...)
   863  			endOffset, result := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   864  				traceContent[sentTrace:], sentTrace)
   865  			assert.Equal(t, update.expectedResult, result)
   866  
   867  			sentTrace = endOffset
   868  		})
   869  	}
   870  }
   871  
   872  func TestPatchTraceContentRangeAndLength(t *testing.T) {
   873  	tests := map[string]struct {
   874  		name                    string
   875  		trace                   []byte
   876  		expectedContentRange    string
   877  		expectedContentLength   int64
   878  		expectedResult          UpdateState
   879  		shouldNotCallPatchTrace bool
   880  	}{
   881  		"0 bytes": {
   882  			trace:                   []byte{},
   883  			expectedResult:          UpdateSucceeded,
   884  			shouldNotCallPatchTrace: true,
   885  		},
   886  		"1 byte": {
   887  			name:                    "1 byte",
   888  			trace:                   []byte("1"),
   889  			expectedContentRange:    "0-0",
   890  			expectedContentLength:   1,
   891  			expectedResult:          UpdateSucceeded,
   892  			shouldNotCallPatchTrace: false,
   893  		},
   894  		"2 bytes": {
   895  			trace:                   []byte("12"),
   896  			expectedContentRange:    "0-1",
   897  			expectedContentLength:   2,
   898  			expectedResult:          UpdateSucceeded,
   899  			shouldNotCallPatchTrace: false,
   900  		},
   901  	}
   902  
   903  	for name, test := range tests {
   904  		t.Run(name, func(t *testing.T) {
   905  			handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   906  				if test.shouldNotCallPatchTrace {
   907  					t.Error("PatchTrace endpoint should not be called")
   908  					return
   909  				}
   910  
   911  				assert.Equal(t, test.expectedContentRange, r.Header.Get("Content-Range"))
   912  				assert.Equal(t, test.expectedContentLength, r.ContentLength)
   913  				w.WriteHeader(http.StatusAccepted)
   914  			}
   915  
   916  			server, client, config := getPatchServer(t, handler)
   917  			defer server.Close()
   918  
   919  			_, result := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   920  				test.trace, 0)
   921  			assert.Equal(t, test.expectedResult, result)
   922  		})
   923  	}
   924  }
   925  
   926  func TestPatchTraceContentRangeHeaderValues(t *testing.T) {
   927  	handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   928  		contentRange := r.Header.Get("Content-Range")
   929  		bytes := strings.Split(contentRange, "-")
   930  
   931  		startByte, err := strconv.Atoi(bytes[0])
   932  		require.NoError(t, err, "Should not set error when parsing Content-Range startByte component")
   933  
   934  		endByte, err := strconv.Atoi(bytes[1])
   935  		require.NoError(t, err, "Should not set error when parsing Content-Range endByte component")
   936  
   937  		assert.Equal(t, 0, startByte, "Content-Range should contain start byte as first field")
   938  		assert.Equal(t, len(patchTraceContent)-1, endByte, "Content-Range should contain end byte as second field")
   939  
   940  		w.WriteHeader(http.StatusAccepted)
   941  	}
   942  
   943  	server, client, config := getPatchServer(t, handler)
   944  	defer server.Close()
   945  
   946  	client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   947  		patchTraceContent, 0)
   948  }
   949  
   950  func TestAbortedPatchTrace(t *testing.T) {
   951  	statuses := []string{"canceled", "failed"}
   952  
   953  	for _, status := range statuses {
   954  		t.Run(status, func(t *testing.T) {
   955  			handler := func(w http.ResponseWriter, r *http.Request, body []byte, offset, limit int) {
   956  				w.Header().Set("Job-Status", status)
   957  				w.WriteHeader(http.StatusAccepted)
   958  			}
   959  
   960  			server, client, config := getPatchServer(t, handler)
   961  			defer server.Close()
   962  
   963  			_, state := client.PatchTrace(config, &JobCredentials{ID: 1, Token: patchToken},
   964  				patchTraceContent, 0)
   965  			assert.Equal(t, UpdateAbort, state)
   966  		})
   967  	}
   968  }
   969  
   970  func checkTestArtifactsUploadHandlerContent(w http.ResponseWriter, r *http.Request, body string) {
   971  	switch body {
   972  	case "too-large":
   973  		w.WriteHeader(http.StatusRequestEntityTooLarge)
   974  		return
   975  
   976  	case "content":
   977  		w.WriteHeader(http.StatusCreated)
   978  		return
   979  
   980  	case "zip":
   981  		if r.FormValue("artifact_format") == "zip" {
   982  			w.WriteHeader(http.StatusCreated)
   983  			return
   984  		}
   985  
   986  	case "gzip":
   987  		if r.FormValue("artifact_format") == "gzip" {
   988  			w.WriteHeader(http.StatusCreated)
   989  			return
   990  		}
   991  
   992  	case "junit":
   993  		if r.FormValue("artifact_type") == "junit" {
   994  			w.WriteHeader(http.StatusCreated)
   995  			return
   996  		}
   997  	}
   998  
   999  	w.WriteHeader(http.StatusBadRequest)
  1000  }
  1001  
  1002  func testArtifactsUploadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
  1003  	if r.URL.Path != "/api/v4/jobs/10/artifacts" {
  1004  		w.WriteHeader(http.StatusNotFound)
  1005  		return
  1006  	}
  1007  
  1008  	if r.Method != "POST" {
  1009  		w.WriteHeader(http.StatusNotAcceptable)
  1010  		return
  1011  	}
  1012  
  1013  	if r.Header.Get("JOB-TOKEN") != "token" {
  1014  		w.WriteHeader(http.StatusForbidden)
  1015  		return
  1016  	}
  1017  
  1018  	file, _, err := r.FormFile("file")
  1019  	if err != nil {
  1020  		w.WriteHeader(http.StatusBadRequest)
  1021  		return
  1022  	}
  1023  
  1024  	body, err := ioutil.ReadAll(file)
  1025  	require.NoError(t, err)
  1026  
  1027  	checkTestArtifactsUploadHandlerContent(w, r, string(body))
  1028  }
  1029  
  1030  func uploadArtifacts(client *GitLabClient, config JobCredentials, artifactsFile, artifactType string, artifactFormat ArtifactFormat) UploadState {
  1031  	file, err := os.Open(artifactsFile)
  1032  	if err != nil {
  1033  		return UploadFailed
  1034  	}
  1035  	defer file.Close()
  1036  
  1037  	fi, err := file.Stat()
  1038  	if err != nil {
  1039  		return UploadFailed
  1040  	}
  1041  	if fi.IsDir() {
  1042  		return UploadFailed
  1043  	}
  1044  
  1045  	options := ArtifactsOptions{
  1046  		BaseName: filepath.Base(artifactsFile),
  1047  		Format:   artifactFormat,
  1048  		Type:     artifactType,
  1049  	}
  1050  	return client.UploadRawArtifacts(config, file, options)
  1051  }
  1052  func TestArtifactsUpload(t *testing.T) {
  1053  	handler := func(w http.ResponseWriter, r *http.Request) {
  1054  		testArtifactsUploadHandler(w, r, t)
  1055  	}
  1056  
  1057  	s := httptest.NewServer(http.HandlerFunc(handler))
  1058  	defer s.Close()
  1059  
  1060  	config := JobCredentials{
  1061  		ID:    10,
  1062  		URL:   s.URL,
  1063  		Token: "token",
  1064  	}
  1065  	invalidToken := JobCredentials{
  1066  		ID:    10,
  1067  		URL:   s.URL,
  1068  		Token: "invalid-token",
  1069  	}
  1070  
  1071  	tempFile, err := ioutil.TempFile("", "artifacts")
  1072  	assert.NoError(t, err)
  1073  	tempFile.Close()
  1074  	defer os.Remove(tempFile.Name())
  1075  
  1076  	c := NewGitLabClient()
  1077  
  1078  	ioutil.WriteFile(tempFile.Name(), []byte("content"), 0600)
  1079  	state := uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatDefault)
  1080  	assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded")
  1081  
  1082  	ioutil.WriteFile(tempFile.Name(), []byte("too-large"), 0600)
  1083  	state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatDefault)
  1084  	assert.Equal(t, UploadTooLarge, state, "Artifacts should be not uploaded, because of too large archive")
  1085  
  1086  	ioutil.WriteFile(tempFile.Name(), []byte("zip"), 0600)
  1087  	state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatZip)
  1088  	assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as zip")
  1089  
  1090  	ioutil.WriteFile(tempFile.Name(), []byte("gzip"), 0600)
  1091  	state = uploadArtifacts(c, config, tempFile.Name(), "", ArtifactFormatGzip)
  1092  	assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as gzip")
  1093  
  1094  	ioutil.WriteFile(tempFile.Name(), []byte("junit"), 0600)
  1095  	state = uploadArtifacts(c, config, tempFile.Name(), "junit", ArtifactFormatGzip)
  1096  	assert.Equal(t, UploadSucceeded, state, "Artifacts should be uploaded, as gzip")
  1097  
  1098  	state = uploadArtifacts(c, config, "not/existing/file", "", ArtifactFormatDefault)
  1099  	assert.Equal(t, UploadFailed, state, "Artifacts should fail to be uploaded")
  1100  
  1101  	state = uploadArtifacts(c, invalidToken, tempFile.Name(), "", ArtifactFormatDefault)
  1102  	assert.Equal(t, UploadForbidden, state, "Artifacts should be rejected if invalid token")
  1103  }
  1104  
  1105  func testArtifactsDownloadHandler(w http.ResponseWriter, r *http.Request, t *testing.T) {
  1106  	if r.URL.Path != "/api/v4/jobs/10/artifacts" {
  1107  		w.WriteHeader(http.StatusNotFound)
  1108  		return
  1109  	}
  1110  
  1111  	if r.Method != "GET" {
  1112  		w.WriteHeader(http.StatusNotAcceptable)
  1113  		return
  1114  	}
  1115  
  1116  	if r.Header.Get("JOB-TOKEN") != "token" {
  1117  		w.WriteHeader(http.StatusForbidden)
  1118  		return
  1119  	}
  1120  
  1121  	w.WriteHeader(http.StatusOK)
  1122  	w.Write(bytes.NewBufferString("Test artifact file content").Bytes())
  1123  }
  1124  
  1125  func TestArtifactsDownload(t *testing.T) {
  1126  	handler := func(w http.ResponseWriter, r *http.Request) {
  1127  		testArtifactsDownloadHandler(w, r, t)
  1128  	}
  1129  
  1130  	s := httptest.NewServer(http.HandlerFunc(handler))
  1131  	defer s.Close()
  1132  
  1133  	credentials := JobCredentials{
  1134  		ID:    10,
  1135  		URL:   s.URL,
  1136  		Token: "token",
  1137  	}
  1138  	invalidTokenCredentials := JobCredentials{
  1139  		ID:    10,
  1140  		URL:   s.URL,
  1141  		Token: "invalid-token",
  1142  	}
  1143  	fileNotFoundTokenCredentials := JobCredentials{
  1144  		ID:    11,
  1145  		URL:   s.URL,
  1146  		Token: "token",
  1147  	}
  1148  
  1149  	c := NewGitLabClient()
  1150  
  1151  	tempDir, err := ioutil.TempDir("", "artifacts")
  1152  	assert.NoError(t, err)
  1153  
  1154  	artifactsFileName := filepath.Join(tempDir, "downloaded-artifact")
  1155  	defer os.Remove(artifactsFileName)
  1156  
  1157  	state := c.DownloadArtifacts(credentials, artifactsFileName)
  1158  	assert.Equal(t, DownloadSucceeded, state, "Artifacts should be downloaded")
  1159  
  1160  	state = c.DownloadArtifacts(invalidTokenCredentials, artifactsFileName)
  1161  	assert.Equal(t, DownloadForbidden, state, "Artifacts should be not downloaded if invalid token is used")
  1162  
  1163  	state = c.DownloadArtifacts(fileNotFoundTokenCredentials, artifactsFileName)
  1164  	assert.Equal(t, DownloadNotFound, state, "Artifacts should be bit downloaded if it's not found")
  1165  }
  1166  
  1167  func TestRunnerVersion(t *testing.T) {
  1168  	c := NewGitLabClient()
  1169  	info := c.getRunnerVersion(RunnerConfig{
  1170  		RunnerSettings: RunnerSettings{
  1171  			Executor: "my-executor",
  1172  			Shell:    "my-shell",
  1173  		},
  1174  	})
  1175  
  1176  	assert.NotEmpty(t, info.Name)
  1177  	assert.NotEmpty(t, info.Version)
  1178  	assert.NotEmpty(t, info.Revision)
  1179  	assert.NotEmpty(t, info.Platform)
  1180  	assert.NotEmpty(t, info.Architecture)
  1181  	assert.Equal(t, "my-executor", info.Executor)
  1182  	assert.Equal(t, "my-shell", info.Shell)
  1183  }
  1184  
  1185  func TestRunnerVersionToGetExecutorAndShellFeaturesWithTheDefaultShell(t *testing.T) {
  1186  	executorProvider := MockExecutorProvider{}
  1187  	defer executorProvider.AssertExpectations(t)
  1188  	executorProvider.On("GetDefaultShell").Return("my-default-executor-shell").Twice()
  1189  	executorProvider.On("CanCreate").Return(true).Once()
  1190  	executorProvider.On("GetFeatures", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
  1191  		features := args[0].(*FeaturesInfo)
  1192  		features.Shared = true
  1193  	})
  1194  	RegisterExecutor("my-test-executor", &executorProvider)
  1195  
  1196  	shell := MockShell{}
  1197  	defer shell.AssertExpectations(t)
  1198  	shell.On("GetName").Return("my-default-executor-shell")
  1199  	shell.On("GetFeatures", mock.Anything).Return(nil).Run(func(args mock.Arguments) {
  1200  		features := args[0].(*FeaturesInfo)
  1201  		features.Variables = true
  1202  	})
  1203  	RegisterShell(&shell)
  1204  
  1205  	c := NewGitLabClient()
  1206  	info := c.getRunnerVersion(RunnerConfig{
  1207  		RunnerSettings: RunnerSettings{
  1208  			Executor: "my-test-executor",
  1209  			Shell:    "",
  1210  		},
  1211  	})
  1212  
  1213  	assert.Equal(t, "my-test-executor", info.Executor)
  1214  	assert.Equal(t, "my-default-executor-shell", info.Shell)
  1215  	assert.False(t, info.Features.Artifacts, "dry-run that this is not enabled")
  1216  	assert.True(t, info.Features.Shared, "feature is enabled by executor")
  1217  	assert.True(t, info.Features.Variables, "feature is enabled by shell")
  1218  }