From e9e5ca328274a17c5982bd02597fe6fb9755795b Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Fri, 31 Oct 2025 12:00:11 +0000 Subject: [PATCH] Consolidate tests in a single file (#6) --- actions_server_test.go | 113 -- byte_order_mark_test.go | 60 - client_generate_jit_test.go | 60 - client_proxy_test.go | 38 - client_runner_scale_set_message_test.go | 247 ---- client_runner_scale_set_session_test.go | 219 --- client_runner_scale_set_test.go | 423 ------ client_runner_test.go | 217 --- client_test.go | 1710 +++++++++++++++++++++++ client_tls_test.go | 177 --- github_api_request_test.go | 247 ---- internal/testdata/generate.sh | 29 + internal/testdata/intermediate.crt | 19 + internal/testdata/leaf.crt | 20 + internal/testdata/leaf.key | 28 + internal/testdata/rootCA.crt | 19 + internal/testdata/server.crt | 20 + internal/testdata/server.key | 28 + user_agent_test.go | 24 - 19 files changed, 1873 insertions(+), 1825 deletions(-) delete mode 100644 actions_server_test.go delete mode 100644 byte_order_mark_test.go delete mode 100644 client_generate_jit_test.go delete mode 100644 client_proxy_test.go delete mode 100644 client_runner_scale_set_message_test.go delete mode 100644 client_runner_scale_set_session_test.go delete mode 100644 client_runner_scale_set_test.go delete mode 100644 client_runner_test.go create mode 100644 client_test.go delete mode 100644 client_tls_test.go delete mode 100644 github_api_request_test.go create mode 100644 internal/testdata/generate.sh create mode 100644 internal/testdata/intermediate.crt create mode 100644 internal/testdata/leaf.crt create mode 100644 internal/testdata/leaf.key create mode 100644 internal/testdata/rootCA.crt create mode 100644 internal/testdata/server.crt create mode 100644 internal/testdata/server.key delete mode 100644 user_agent_test.go diff --git a/actions_server_test.go b/actions_server_test.go deleted file mode 100644 index 3d92180..0000000 --- a/actions_server_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package scaleset - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/golang-jwt/jwt/v4" - "github.com/stretchr/testify/require" -) - -// newActionsServer returns a new httptest.Server that handles the -// authentication requests neeeded to create a new client. Any requests not -// made to the /actions/runners/registration-token or -// /actions/runner-registration endpoints will be handled by the provided -// handler. The returned server is started and will be automatically closed -// when the test ends. -func newActionsServer(t *testing.T, handler http.Handler, options ...actionsServerOption) *actionsServer { - s := httptest.NewServer(nil) - server := &actionsServer{ - Server: s, - } - t.Cleanup(func() { - server.Close() - }) - - for _, option := range options { - option(server) - } - - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // handle getRunnerRegistrationToken - if strings.HasSuffix(r.URL.Path, "/runners/registration-token") { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"token":"token"}`)) - return - } - - // handle getActionsServiceAdminConnection - if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") { - if server.token == "" { - server.token = defaultActionsToken(t) - } - - w.Write([]byte(`{"url":"` + s.URL + `/tenant/123/","token":"` + server.token + `"}`)) - return - } - - handler.ServeHTTP(w, r) - }) - - server.Config.Handler = h - - return server -} - -type actionsServerOption func(*actionsServer) - -type actionsServer struct { - *httptest.Server - - token string -} - -func (s *actionsServer) configURLForOrg(org string) string { - return s.URL + "/" + org -} - -func defaultActionsToken(t *testing.T) string { - claims := &jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute)), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), - Issuer: "123", - } - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey)) - require.NoError(t, err) - tokenString, err := token.SignedString(privateKey) - require.NoError(t, err) - return tokenString -} - -const samplePrivateKey = `-----BEGIN PRIVATE KEY----- -MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC7tgquvNIp+Ik3 -rRVZ9r0zJLsSzTHqr2dA6EUUmpRiQ25MzjMqKqu0OBwvh/pZyfjSIkKrhIridNK4 -DWnPfPWHE2K3Muh0X2sClxtqiiFmXsvbiTzhUm5a+zCcv0pJCWYnKi0HmyXpAXjJ -iN8mWliZN896verVYXWrod7EaAnuST4TiJeqZYW4bBBG81fPNc/UP4j6CKAW8nx9 -HtcX6ApvlHeCLZUTW/qhGLO0nLKoEOr3tXCPW5VjKzlm134Dl+8PN6f1wv6wMAoA -lo7Ha5+c74jhPL6gHXg7cRaHQmuJCJrtl8qbLkFAulfkBixBw/6i11xoM/MOC64l -TWmXqrxTAgMBAAECgf9zYlxfL+rdHRXCoOm7pUeSPL0dWaPFP12d/Z9LSlDAt/h6 -Pd+eqYEwhf795SAbJuzNp51Ls6LUGnzmLOdojKwfqJ51ahT1qbcBcMZNOcvtGqZ9 -xwLG993oyR49C361Lf2r8mKrdrR5/fW0B1+1s6A+eRFivqFOtsOc4V4iMeHYsCVJ -hM7yMu0UfpolDJA/CzopsoGq3UuQlibUEUxKULza06aDjg/gBH3PnP+fQ1m0ovDY -h0pX6SCq5fXVJFS+Pbpu7j2ePNm3mr0qQhrUONZq0qhGN/piCbBZe1CqWApyO7nA -B95VChhL1eYs1BKvQePh12ap83woIUcW2mJF2F0CgYEA+aERTuKWEm+zVNKS9t3V -qNhecCOpayKM9OlALIK/9W6KBS+pDsjQQteQAUAItjvLiDjd5KsrtSgjbSgr66IP -b615Pakywe5sdnVGzSv+07KMzuFob9Hj6Xv9als9Y2geVhUZB2Frqve/UCjmC56i -zuQTSele5QKCSSTFBV3423cCgYEAwIBv9ChsI+mse6vPaqSPpZ2n237anThMcP33 -aS0luYXqMWXZ0TQ/uSmCElY4G3xqNo8szzfy6u0HpldeUsEUsIcBNUV5kIIb8wKu -Zmgcc8gBIjJkyUJI4wuz9G/fegEUj3u6Cttmmj4iWLzCRscRJdfGpqwRIhOGyXb9 -2Rur5QUCgYAGWIPaH4R1H4XNiDTYNbdyvV1ZOG7cHFq89xj8iK5cjNzRWO7RQ2WX -7WbpwTj3ePmpktiBMaDA0C5mXfkP2mTOD/jfCmgR6f+z2zNbj9zAgO93at9+yDUl -AFPm2j7rQgBTa+HhACb+h6HDZebDMNsuqzmaTWZuJ+wr89VWV5c17QKBgH3jwNNQ -mCAIUidynaulQNfTOZIe7IMC7WK7g9CBmPkx7Y0uiXr6C25hCdJKFllLTP6vNWOy -uCcQqf8LhgDiilBDifO3op9xpyuOJlWMYocJVkxx3l2L/rSU07PYcbKNAFAxXuJ4 -xym51qZnkznMN5ei/CPFxVKeqHgaXDpekVStAoGAV3pSWAKDXY/42XEHixrCTqLW -kBxfaf3g7iFnl3u8+7Z/7Cb4ZqFcw0bRJseKuR9mFvBhcZxSErbMDEYrevefU9aM -APeCxEyw6hJXgbWKoG7Fw2g2HP3ytCJ4YzH0zNitHjk/1h4BG7z8cEQILCSv5mN2 -etFcaQuTHEZyRhhJ4BU= ------END PRIVATE KEY-----` diff --git a/byte_order_mark_test.go b/byte_order_mark_test.go deleted file mode 100644 index 0615586..0000000 --- a/byte_order_mark_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package scaleset - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestClient_Do(t *testing.T) { - t.Run("trims byte order mark from response if present", func(t *testing.T) { - t.Run("when there is no body", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })) - defer server.Close() - - client, err := NewClient("https://localhost/org/repo", &ActionsAuth{Token: "token"}) - require.NoError(t, err) - - req, err := http.NewRequest("GET", server.URL, nil) - require.NoError(t, err) - - resp, err := client.do(req) - require.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - assert.Empty(t, string(body)) - }) - - responses := []string{ - "\xef\xbb\xbf{\"foo\":\"bar\"}", - "{\"foo\":\"bar\"}", - } - - for _, response := range responses { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(response)) - })) - defer server.Close() - - client, err := NewClient("https://localhost/org/repo", &ActionsAuth{Token: "token"}) - require.NoError(t, err) - - req, err := http.NewRequest("GET", server.URL, nil) - require.NoError(t, err) - - resp, err := client.do(req) - require.NoError(t, err) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - assert.Equal(t, "{\"foo\":\"bar\"}", string(body)) - } - }) -} diff --git a/client_generate_jit_test.go b/client_generate_jit_test.go deleted file mode 100644 index 9f32225..0000000 --- a/client_generate_jit_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package scaleset - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGenerateJitRunnerConfig(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Get JIT Config for Runner", func(t *testing.T) { - want := &RunnerScaleSetJitRunnerConfig{} - response := []byte(`{"count":1,"value":[{"id":1,"name":"scale-set-name"}]}`) - - runnerSettings := &RunnerScaleSetJitRunnerSetting{} - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(response) - })) - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GenerateJitRunnerConfig(ctx, runnerSettings, 1) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - runnerSettings := &RunnerScaleSetJitRunnerSetting{} - - retryMax := 1 - actualRetry := 0 - expectedRetry := retryMax + 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(1), - WithRetryWaitMax(1*time.Millisecond), - ) - require.NoError(t, err) - - _, err = client.GenerateJitRunnerConfig(ctx, runnerSettings, 1) - assert.NotNil(t, err) - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) -} diff --git a/client_proxy_test.go b/client_proxy_test.go deleted file mode 100644 index 8cfe855..0000000 --- a/client_proxy_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package scaleset - -import ( - "net/http" - "net/url" - "testing" - - "github.com/actions/scaleset/testserver" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "golang.org/x/net/http/httpproxy" -) - -func TestClientProxy(t *testing.T) { - serverCalled := false - - proxy := testserver.New(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - serverCalled = true - })) - - proxyConfig := &httpproxy.Config{ - HTTPProxy: proxy.URL, - } - proxyFunc := func(req *http.Request) (*url.URL, error) { - return proxyConfig.ProxyFunc()(req.URL) - } - - c, err := NewClient("http://github.com/org/repo", nil, WithProxy(proxyFunc)) - require.NoError(t, err) - - req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) - require.NoError(t, err) - - _, err = c.do(req) - require.NoError(t, err) - - assert.True(t, serverCalled) -} diff --git a/client_runner_scale_set_message_test.go b/client_runner_scale_set_message_test.go deleted file mode 100644 index 5618d6f..0000000 --- a/client_runner_scale_set_message_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package scaleset - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetMessage(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE" - runnerScaleSetMessage := &RunnerScaleSetMessage{ - MessageID: 1, - MessageType: "rssType", - } - - t.Run("Get Runner Scale Set Message", func(t *testing.T) { - want := runnerScaleSetMessage - response := []byte(`{"messageId":1,"messageType":"rssType"}`) - s := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(response) - })) - - client, err := NewClient(s.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetMessage(ctx, s.URL, token, 0, 10) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("GetMessage sets the last message id if not 0", func(t *testing.T) { - want := runnerScaleSetMessage - response := []byte(`{"messageId":1,"messageType":"rssType"}`) - s := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - q := r.URL.Query() - assert.Equal(t, "1", q.Get("lastMessageId")) - w.Write(response) - })) - - client, err := NewClient(s.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetMessage(ctx, s.URL, token, 1, 10) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - retryMax := 1 - - actualRetry := 0 - expectedRetry := retryMax + 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(1*time.Millisecond), - ) - require.NoError(t, err) - - _, err = client.GetMessage(ctx, server.URL, token, 0, 10) - assert.NotNil(t, err) - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) - - t.Run("Message token expired", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetMessage(ctx, server.URL, token, 0, 10) - require.NotNil(t, err) - - var expectedErr *MessageQueueTokenExpiredError - require.True(t, errors.As(err, &expectedErr)) - }) - - t.Run("Status code not found", func(t *testing.T) { - want := ActionsError{ - Err: errors.New("unknown exception"), - StatusCode: 404, - } - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetMessage(ctx, server.URL, token, 0, 10) - require.NotNil(t, err) - assert.Equal(t, want.Error(), err.Error()) - }) - - t.Run("Error when Content-Type is text/plain", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetMessage(ctx, server.URL, token, 0, 10) - assert.NotNil(t, err) - }) - - t.Run("Capacity error handling", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - hc := r.Header.Get(HeaderScaleSetMaxCapacity) - c, err := strconv.Atoi(hc) - require.NoError(t, err) - assert.GreaterOrEqual(t, c, 0) - - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetMessage(ctx, server.URL, token, 0, -1) - require.Error(t, err) - // Ensure we don't send requests with negative capacity - assert.False(t, errors.Is(err, &ActionsError{})) - - _, err = client.GetMessage(ctx, server.URL, token, 0, 0) - assert.Error(t, err) - var expectedErr *ActionsError - assert.ErrorAs(t, err, &expectedErr) - assert.Equal(t, http.StatusBadRequest, expectedErr.StatusCode) - }) -} - -func TestDeleteMessage(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE" - runnerScaleSetMessage := &RunnerScaleSetMessage{ - MessageID: 1, - MessageType: "rssType", - } - - t.Run("Delete existing message", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) - assert.Nil(t, err) - }) - - t.Run("Message token expired", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteMessage(ctx, server.URL, token, 0) - require.NotNil(t, err) - var expectedErr *MessageQueueTokenExpiredError - assert.True(t, errors.As(err, &expectedErr)) - }) - - t.Run("Error when Content-Type is text/plain", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) - require.NotNil(t, err) - var expectedErr *ActionsError - assert.True(t, errors.As(err, &expectedErr)) - }, - ) - - t.Run("Default retries on server error", func(t *testing.T) { - actualRetry := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - retryMax := 1 - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(1*time.Nanosecond), - ) - require.NoError(t, err) - err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) - assert.NotNil(t, err) - expectedRetry := retryMax + 1 - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) - - t.Run("No message found", func(t *testing.T) { - want := (*RunnerScaleSetMessage)(nil) - rsl, err := json.Marshal(want) - require.NoError(t, err) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(rsl) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID+1) - var expectedErr *ActionsError - require.True(t, errors.As(err, &expectedErr)) - }) -} diff --git a/client_runner_scale_set_session_test.go b/client_runner_scale_set_session_test.go deleted file mode 100644 index c69589e..0000000 --- a/client_runner_scale_set_session_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package scaleset - -import ( - "context" - "errors" - "net/http" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const exampleRequestID = "5ddf2050-dae0-013c-9159-04421ad31b68" - -func TestCreateMessageSession(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("CreateMessageSession unmarshals correctly", func(t *testing.T) { - owner := "foo" - runnerScaleSet := RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - RunnerSetting: RunnerSetting{}, - } - - want := &RunnerScaleSetSession{ - OwnerName: "foo", - RunnerScaleSet: &RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - }, - MessageQueueURL: "http://fake.github.com/123", - MessageQueueAccessToken: "fake.jwt.here", - } - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - resp := []byte(`{ - "ownerName": "foo", - "runnerScaleSet": { - "id": 1, - "name": "ScaleSet" - }, - "messageQueueUrl": "http://fake.github.com/123", - "messageQueueAccessToken": "fake.jwt.here" - }`) - w.Write(resp) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) { - owner := "foo" - runnerScaleSet := RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - RunnerSetting: RunnerSetting{}, - } - - want := &ActionsError{ - ActivityID: exampleRequestID, - StatusCode: http.StatusBadRequest, - Err: &ActionsExceptionError{ - ExceptionName: "CSharpExceptionNameHere", - Message: "could not do something", - }, - } - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set(headerActionsActivityID, exampleRequestID) - w.WriteHeader(http.StatusBadRequest) - resp := []byte(`{"typeName": "CSharpExceptionNameHere","message": "could not do something"}`) - w.Write(resp) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) - require.NotNil(t, err) - - errorTypeForComparison := &ActionsError{} - assert.True( - t, - errors.As(err, &errorTypeForComparison), - "CreateMessageSession expected to be able to parse the error into ActionsError type: %v", - err, - ) - - assert.Equal(t, want, errorTypeForComparison) - }) - - t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) { - owner := "foo" - runnerScaleSet := RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - RunnerSetting: RunnerSetting{}, - } - - gotRetries := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - gotRetries++ - })) - - retryMax := 3 - retryWaitMax := 1 * time.Microsecond - - wantRetries := retryMax + 1 - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - _, err = client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) - assert.NotNil(t, err) - assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) - }) -} - -func TestDeleteMessageSession(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("DeleteMessageSession call is retried the correct amount of times", func(t *testing.T) { - runnerScaleSet := RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - RunnerSetting: RunnerSetting{}, - } - - gotRetries := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - gotRetries++ - })) - - retryMax := 3 - retryWaitMax := 1 * time.Microsecond - - wantRetries := retryMax + 1 - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - sessionID := uuid.New() - - err = client.DeleteMessageSession(ctx, runnerScaleSet.ID, sessionID) - assert.NotNil(t, err) - assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) - }) -} - -func TestRefreshMessageSession(t *testing.T) { - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("RefreshMessageSession call is retried the correct amount of times", func(t *testing.T) { - runnerScaleSet := RunnerScaleSet{ - ID: 1, - Name: "ScaleSet", - CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), - RunnerSetting: RunnerSetting{}, - } - - gotRetries := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - gotRetries++ - })) - - retryMax := 3 - retryWaitMax := 1 * time.Microsecond - - wantRetries := retryMax + 1 - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - sessionID := uuid.New() - - _, err = client.RefreshMessageSession(context.Background(), runnerScaleSet.ID, sessionID) - assert.NotNil(t, err) - assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) - }) -} diff --git a/client_runner_scale_set_test.go b/client_runner_scale_set_test.go deleted file mode 100644 index 82e2543..0000000 --- a/client_runner_scale_set_test.go +++ /dev/null @@ -1,423 +0,0 @@ -package scaleset - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetRunnerScaleSet(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - scaleSetName := "ScaleSet" - runnerScaleSet := RunnerScaleSet{ID: 1, Name: scaleSetName} - - t.Run("Get existing scale set", func(t *testing.T) { - want := &runnerScaleSet - runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(runnerScaleSetsResp) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("GetRunnerScaleSet calls correct url", func(t *testing.T) { - runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`) - url := url.URL{} - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(runnerScaleSetsResp) - url = *r.URL - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) - require.NoError(t, err) - - expectedPath := "/tenant/123/_apis/runtime/runnerscalesets" - assert.Equal(t, expectedPath, url.Path) - assert.Equal(t, scaleSetName, url.Query().Get("name")) - assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) - }) - - t.Run("Status code not found", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) - assert.NotNil(t, err) - }) - - t.Run("Error when Content-Type is text/plain", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) - assert.NotNil(t, err) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - actualRetry := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - retryMax := 1 - retryWaitMax := 1 * time.Microsecond - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) - assert.NotNil(t, err) - expectedRetry := retryMax + 1 - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) - - t.Run("RunnerScaleSet count is zero", func(t *testing.T) { - want := (*RunnerScaleSet)(nil) - runnerScaleSetsResp := []byte(`{"count":0,"value":[{"id":1,"name":"ScaleSet"}]}`) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(runnerScaleSetsResp) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Multiple runner scale sets found", func(t *testing.T) { - reqID := uuid.NewString() - wantErr := &ActionsError{ - StatusCode: http.StatusOK, - ActivityID: reqID, - Err: fmt.Errorf("multiple runner scale sets found with name %q", scaleSetName), - } - runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set(headerActionsActivityID, reqID) - w.Write(runnerScaleSetsResp) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) - require.NotNil(t, err) - assert.Equal(t, wantErr.Error(), err.Error()) - }) -} - -func TestGetRunnerScaleSetByID(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} - - t.Run("Get existing scale set by Id", func(t *testing.T) { - want := &runnerScaleSet - rsl, err := json.Marshal(want) - require.NoError(t, err) - sservere := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(rsl) - })) - - client, err := NewClient(sservere.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("GetRunnerScaleSetByID calls correct url", func(t *testing.T) { - rsl, err := json.Marshal(&runnerScaleSet) - require.NoError(t, err) - - url := url.URL{} - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(rsl) - url = *r.URL - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - require.NoError(t, err) - - expectedPath := fmt.Sprintf("/tenant/123/_apis/runtime/runnerscalesets/%d", runnerScaleSet.ID) - assert.Equal(t, expectedPath, url.Path) - assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) - }) - - t.Run("Status code not found", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - assert.NotNil(t, err) - }) - - t.Run("Error when Content-Type is text/plain", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - assert.NotNil(t, err) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - actualRetry := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - retryMax := 1 - retryWaitMax := 1 * time.Microsecond - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - require.NotNil(t, err) - expectedRetry := retryMax + 1 - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) - - t.Run("No RunnerScaleSet found", func(t *testing.T) { - want := (*RunnerScaleSet)(nil) - rsl, err := json.Marshal(want) - require.NoError(t, err) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(rsl) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) - require.NoError(t, err) - assert.Equal(t, want, got) - }) -} - -func TestCreateRunnerScaleSet(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} - - t.Run("Create runner scale set", func(t *testing.T) { - want := &runnerScaleSet - rsl, err := json.Marshal(want) - require.NoError(t, err) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(rsl) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.CreateRunnerScaleSet(ctx, &runnerScaleSet) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("CreateRunnerScaleSet calls correct url", func(t *testing.T) { - rsl, err := json.Marshal(&runnerScaleSet) - require.NoError(t, err) - url := url.URL{} - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(rsl) - url = *r.URL - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) - require.NoError(t, err) - - expectedPath := "/tenant/123/_apis/runtime/runnerscalesets" - assert.Equal(t, expectedPath, url.Path) - assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) - }) - - t.Run("Error when Content-Type is text/plain", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "text/plain") - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) - require.NotNil(t, err) - var expectedErr *ActionsError - assert.True(t, errors.As(err, &expectedErr)) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - actualRetry := 0 - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - retryMax := 1 - retryWaitMax := 1 * time.Microsecond - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) - require.NotNil(t, err) - expectedRetry := retryMax + 1 - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) -} - -func TestUpdateRunnerScaleSet(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) - runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", RunnerGroupID: 1, RunnerGroupName: "group", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} - - t.Run("Update runner scale set", func(t *testing.T) { - want := &runnerScaleSet - rsl, err := json.Marshal(want) - require.NoError(t, err) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Write(rsl) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.UpdateRunnerScaleSet(ctx, 1, &RunnerScaleSet{RunnerGroupID: 1}) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) { - rsl, err := json.Marshal(&runnerScaleSet) - require.NoError(t, err) - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - expectedPath := "/tenant/123/_apis/runtime/runnerscalesets/1" - assert.Equal(t, expectedPath, r.URL.Path) - assert.Equal(t, http.MethodPatch, r.Method) - assert.Equal(t, "6.0-preview", r.URL.Query().Get("api-version")) - - w.Write(rsl) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - _, err = client.UpdateRunnerScaleSet(ctx, 1, &runnerScaleSet) - require.NoError(t, err) - }) -} - -func TestDeleteRunnerScaleSet(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Delete runner scale set", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "DELETE", r.Method) - assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview") - w.WriteHeader(http.StatusNoContent) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteRunnerScaleSet(ctx, 10) - assert.NoError(t, err) - }) - - t.Run("Delete calls with error", func(t *testing.T) { - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "DELETE", r.Method) - assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview") - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(`{"message": "test error"}`)) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.DeleteRunnerScaleSet(ctx, 10) - assert.ErrorContains(t, err, "test error") - }) -} diff --git a/client_runner_test.go b/client_runner_test.go deleted file mode 100644 index 42dcef2..0000000 --- a/client_runner_test.go +++ /dev/null @@ -1,217 +0,0 @@ -package scaleset - -import ( - "context" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetRunner(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Get Runner", func(t *testing.T) { - var runnerID int64 = 1 - want := &RunnerReference{ - ID: int(runnerID), - Name: "self-hosted-ubuntu", - } - response := []byte(`{"id": 1, "name": "self-hosted-ubuntu"}`) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(response) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunner(ctx, runnerID) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - var runnerID int64 = 1 - retryWaitMax := 1 * time.Millisecond - retryMax := 1 - - actualRetry := 0 - expectedRetry := retryMax + 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) - require.NoError(t, err) - - _, err = client.GetRunner(ctx, runnerID) - require.Error(t, err) - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) -} - -func TestGetRunnerByName(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Get Runner by Name", func(t *testing.T) { - var runnerID int64 = 1 - runnerName := "self-hosted-ubuntu" - want := &RunnerReference{ - ID: int(runnerID), - Name: runnerName, - } - response := []byte(`{"count": 1, "value": [{"id": 1, "name": "self-hosted-ubuntu"}]}`) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(response) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerByName(ctx, runnerName) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Get Runner by name with not exist runner", func(t *testing.T) { - runnerName := "self-hosted-ubuntu" - response := []byte(`{"count": 0, "value": []}`) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(response) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerByName(ctx, runnerName) - require.NoError(t, err) - assert.Nil(t, got) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - runnerName := "self-hosted-ubuntu" - - retryWaitMax := 1 * time.Millisecond - retryMax := 1 - - actualRetry := 0 - expectedRetry := retryMax + 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) - require.NoError(t, err) - - _, err = client.GetRunnerByName(ctx, runnerName) - require.Error(t, err) - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) -} - -func TestDeleteRunner(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Delete Runner", func(t *testing.T) { - var runnerID int64 = 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - err = client.RemoveRunner(ctx, runnerID) - assert.NoError(t, err) - }) - - t.Run("Default retries on server error", func(t *testing.T) { - var runnerID int64 = 1 - - retryWaitMax := 1 * time.Millisecond - retryMax := 1 - - actualRetry := 0 - expectedRetry := retryMax + 1 - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusServiceUnavailable) - actualRetry++ - })) - - client, err := NewClient( - server.configURLForOrg("my-org"), - auth, - WithRetryMax(retryMax), - WithRetryWaitMax(retryWaitMax), - ) - require.NoError(t, err) - - err = client.RemoveRunner(ctx, runnerID) - require.Error(t, err) - assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) - }) -} - -func TestGetRunnerGroupByName(t *testing.T) { - ctx := context.Background() - auth := &ActionsAuth{ - Token: "token", - } - - t.Run("Get RunnerGroup by Name", func(t *testing.T) { - runnerGroupID := 1 - runnerGroupName := "test-runner-group" - want := &RunnerGroup{ - ID: runnerGroupID, - Name: runnerGroupName, - } - response := []byte(`{"count": 1, "value": [{"id": 1, "name": "test-runner-group"}]}`) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(response) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) - require.NoError(t, err) - assert.Equal(t, want, got) - }) - - t.Run("Get RunnerGroup by name with not exist runner group", func(t *testing.T) { - runnerGroupName := "test-runner-group" - response := []byte(`{"count": 0, "value": []}`) - - server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(response) - })) - - client, err := NewClient(server.configURLForOrg("my-org"), auth) - require.NoError(t, err) - - got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) - assert.ErrorContains(t, err, "no runner group found with name") - assert.Nil(t, got) - }) -} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..a40ca3c --- /dev/null +++ b/client_test.go @@ -0,0 +1,1710 @@ +package scaleset + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "testing" + "time" + + "github.com/actions/scaleset/testserver" + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/http/httpproxy" +) + +const exampleRequestID = "5ddf2050-dae0-013c-9159-04421ad31b68" + +var testUserAgent = UserAgentInfo{ + Version: "test", + CommitSHA: "test", + ScaleSetID: 1, +} + +func TestNewGitHubAPIRequest(t *testing.T) { + ctx := context.Background() + + t.Run("uses the right host/path prefix", func(t *testing.T) { + scenarios := []struct { + configURL string + path string + expected string + }{ + { + configURL: "https://github.com/org/repo", + path: "/app/installations/123/access_tokens", + expected: "https://api.github.com/app/installations/123/access_tokens", + }, + { + configURL: "https://www.github.com/org/repo", + path: "/app/installations/123/access_tokens", + expected: "https://api.github.com/app/installations/123/access_tokens", + }, + { + configURL: "http://github.localhost/org/repo", + path: "/app/installations/123/access_tokens", + expected: "http://api.github.localhost/app/installations/123/access_tokens", + }, + { + configURL: "https://my-instance.com/org/repo", + path: "/app/installations/123/access_tokens", + expected: "https://my-instance.com/api/v3/app/installations/123/access_tokens", + }, + { + configURL: "http://localhost/org/repo", + path: "/app/installations/123/access_tokens", + expected: "http://localhost/api/v3/app/installations/123/access_tokens", + }, + } + + for _, scenario := range scenarios { + client, err := NewClient(scenario.configURL, nil) + require.NoError(t, err) + + req, err := client.newGitHubAPIRequest(ctx, http.MethodGet, scenario.path, nil) + require.NoError(t, err) + assert.Equal(t, scenario.expected, req.URL.String()) + } + }) + + t.Run("sets user agent header if present", func(t *testing.T) { + client, err := NewClient("http://localhost/my-org", nil) + require.NoError(t, err) + + client.SetUserAgent(testUserAgent) + + req, err := client.newGitHubAPIRequest(ctx, http.MethodGet, "/app/installations/123/access_tokens", nil) + require.NoError(t, err) + + assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) + }) + + t.Run("sets the body we pass", func(t *testing.T) { + client, err := NewClient("http://localhost/my-org", nil) + require.NoError(t, err) + + req, err := client.newGitHubAPIRequest( + ctx, + http.MethodGet, + "/app/installations/123/access_tokens", + strings.NewReader("the-body"), + ) + require.NoError(t, err) + + b, err := io.ReadAll(req.Body) + require.NoError(t, err) + assert.Equal(t, "the-body", string(b)) + }) +} + +func TestNewActionsServiceRequest(t *testing.T) { + ctx := context.Background() + defaultCreds := &ActionsAuth{Token: "token"} + + t.Run("manages authentication", func(t *testing.T) { + t.Run("client is brand new", func(t *testing.T) { + token := defaultActionsToken(t) + server := testserver.New(t, nil, testserver.WithActionsToken(token)) + + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + + req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) + require.NoError(t, err) + + assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization")) + }) + + t.Run("admin token is about to expire", func(t *testing.T) { + newToken := defaultActionsToken(t) + server := testserver.New(t, nil, testserver.WithActionsToken(newToken)) + + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + client.actionsServiceAdminToken = "expiring-token" + client.actionsServiceAdminTokenExpiresAt = time.Now().Add(59 * time.Second) + + req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) + require.NoError(t, err) + + assert.Equal(t, "Bearer "+newToken, req.Header.Get("Authorization")) + }) + + t.Run("admin token refresh failure", func(t *testing.T) { + newToken := defaultActionsToken(t) + errMessage := `{"message":"test"}` + unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(errMessage)) + } + server := testserver.New( + t, + nil, + testserver.WithActionsToken("random-token"), + testserver.WithActionsToken(newToken), + testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler), + ) + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + expiringToken := "expiring-token" + expiresAt := time.Now().Add(59 * time.Second) + client.actionsServiceAdminToken = expiringToken + client.actionsServiceAdminTokenExpiresAt = expiresAt + _, err = client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) + require.Error(t, err) + assert.Contains(t, err.Error(), errMessage) + assert.Equal(t, client.actionsServiceAdminToken, expiringToken) + assert.Equal(t, client.actionsServiceAdminTokenExpiresAt, expiresAt) + }) + + t.Run("admin token refresh retry", func(t *testing.T) { + newToken := defaultActionsToken(t) + errMessage := `{"message":"test"}` + + srv := "http://github.com/my-org" + resp := &ActionsServiceAdminConnection{ + AdminToken: &newToken, + ActionsServiceURL: &srv, + } + failures := 0 + unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) { + if failures < 5 { + failures++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(errMessage)) + return + } + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(resp) + } + server := testserver.New(t, nil, testserver.WithActionsToken("random-token"), testserver.WithActionsToken(newToken), testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler)) + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + expiringToken := "expiring-token" + expiresAt := time.Now().Add(59 * time.Second) + client.actionsServiceAdminToken = expiringToken + client.actionsServiceAdminTokenExpiresAt = expiresAt + + _, err = client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) + require.NoError(t, err) + assert.Equal(t, client.actionsServiceAdminToken, newToken) + assert.Equal(t, client.actionsServiceURL, srv) + assert.NotEqual(t, client.actionsServiceAdminTokenExpiresAt, expiresAt) + }) + + t.Run("token is currently valid", func(t *testing.T) { + tokenThatShouldNotBeFetched := defaultActionsToken(t) + server := testserver.New(t, nil, testserver.WithActionsToken(tokenThatShouldNotBeFetched)) + + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + client.actionsServiceAdminToken = "healthy-token" + client.actionsServiceAdminTokenExpiresAt = time.Now().Add(1 * time.Hour) + + req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) + require.NoError(t, err) + + assert.Equal(t, "Bearer healthy-token", req.Header.Get("Authorization")) + }) + }) + + t.Run("builds the right URL including api version", func(t *testing.T) { + server := testserver.New(t, nil) + + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + + req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "/my/path?name=banana", nil) + require.NoError(t, err) + + serverURL, err := url.Parse(server.URL) + require.NoError(t, err) + + result := req.URL + assert.Equal(t, serverURL.Host, result.Host) + assert.Equal(t, "/tenant/123/my/path", result.Path) + assert.Equal(t, "banana", result.Query().Get("name")) + assert.Equal(t, "6.0-preview", result.Query().Get("api-version")) + }) + + t.Run("populates header", func(t *testing.T) { + server := testserver.New(t, nil) + + client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) + require.NoError(t, err) + + client.SetUserAgent(testUserAgent) + + req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "/my/path", nil) + require.NoError(t, err) + + assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) + }) +} + +func TestGetRunner(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Get Runner", func(t *testing.T) { + var runnerID int64 = 1 + want := &RunnerReference{ + ID: int(runnerID), + Name: "self-hosted-ubuntu", + } + response := []byte(`{"id": 1, "name": "self-hosted-ubuntu"}`) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(response) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunner(ctx, runnerID) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + var runnerID int64 = 1 + retryWaitMax := 1 * time.Millisecond + retryMax := 1 + + actualRetry := 0 + expectedRetry := retryMax + 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) + require.NoError(t, err) + + _, err = client.GetRunner(ctx, runnerID) + require.Error(t, err) + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) +} + +func TestGetRunnerByName(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Get Runner by Name", func(t *testing.T) { + var runnerID int64 = 1 + runnerName := "self-hosted-ubuntu" + want := &RunnerReference{ + ID: int(runnerID), + Name: runnerName, + } + response := []byte(`{"count": 1, "value": [{"id": 1, "name": "self-hosted-ubuntu"}]}`) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(response) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerByName(ctx, runnerName) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Get Runner by name with not exist runner", func(t *testing.T) { + runnerName := "self-hosted-ubuntu" + response := []byte(`{"count": 0, "value": []}`) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(response) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerByName(ctx, runnerName) + require.NoError(t, err) + assert.Nil(t, got) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + runnerName := "self-hosted-ubuntu" + + retryWaitMax := 1 * time.Millisecond + retryMax := 1 + + actualRetry := 0 + expectedRetry := retryMax + 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth, WithRetryMax(retryMax), WithRetryWaitMax(retryWaitMax)) + require.NoError(t, err) + + _, err = client.GetRunnerByName(ctx, runnerName) + require.Error(t, err) + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) +} + +func TestDeleteRunner(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Delete Runner", func(t *testing.T) { + var runnerID int64 = 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.RemoveRunner(ctx, runnerID) + assert.NoError(t, err) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + var runnerID int64 = 1 + + retryWaitMax := 1 * time.Millisecond + retryMax := 1 + + actualRetry := 0 + expectedRetry := retryMax + 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + err = client.RemoveRunner(ctx, runnerID) + require.Error(t, err) + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) +} + +func TestGetRunnerGroupByName(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Get RunnerGroup by Name", func(t *testing.T) { + runnerGroupID := 1 + runnerGroupName := "test-runner-group" + want := &RunnerGroup{ + ID: runnerGroupID, + Name: runnerGroupName, + } + response := []byte(`{"count": 1, "value": [{"id": 1, "name": "test-runner-group"}]}`) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(response) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Get RunnerGroup by name with not exist runner group", func(t *testing.T) { + runnerGroupName := "test-runner-group" + response := []byte(`{"count": 0, "value": []}`) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(response) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerGroupByName(ctx, runnerGroupName) + assert.ErrorContains(t, err, "no runner group found with name") + assert.Nil(t, got) + }) +} + +func TestGetRunnerScaleSet(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + scaleSetName := "ScaleSet" + runnerScaleSet := RunnerScaleSet{ID: 1, Name: scaleSetName} + + t.Run("Get existing scale set", func(t *testing.T) { + want := &runnerScaleSet + runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(runnerScaleSetsResp) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("GetRunnerScaleSet calls correct url", func(t *testing.T) { + runnerScaleSetsResp := []byte(`{"count":1,"value":[{"id":1,"name":"ScaleSet"}]}`) + url := url.URL{} + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(runnerScaleSetsResp) + url = *r.URL + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) + require.NoError(t, err) + + expectedPath := "/tenant/123/_apis/runtime/runnerscalesets" + assert.Equal(t, expectedPath, url.Path) + assert.Equal(t, scaleSetName, url.Query().Get("name")) + assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) + }) + + t.Run("Status code not found", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) + assert.NotNil(t, err) + }) + + t.Run("Error when Content-Type is text/plain", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) + assert.NotNil(t, err) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + actualRetry := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + retryMax := 1 + retryWaitMax := 1 * time.Microsecond + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) + assert.NotNil(t, err) + expectedRetry := retryMax + 1 + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) + + t.Run("RunnerScaleSet count is zero", func(t *testing.T) { + want := (*RunnerScaleSet)(nil) + runnerScaleSetsResp := []byte(`{"count":0,"value":[{"id":1,"name":"ScaleSet"}]}`) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(runnerScaleSetsResp) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerScaleSet(ctx, 1, scaleSetName) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Multiple runner scale sets found", func(t *testing.T) { + reqID := uuid.NewString() + wantErr := &ActionsError{ + StatusCode: http.StatusOK, + ActivityID: reqID, + Err: fmt.Errorf("multiple runner scale sets found with name %q", scaleSetName), + } + runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set(headerActionsActivityID, reqID) + w.Write(runnerScaleSetsResp) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName) + require.NotNil(t, err) + assert.Equal(t, wantErr.Error(), err.Error()) + }) +} + +func TestGetRunnerScaleSetByID(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} + + t.Run("Get existing scale set by Id", func(t *testing.T) { + want := &runnerScaleSet + rsl, err := json.Marshal(want) + require.NoError(t, err) + sservere := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(rsl) + })) + + client, err := NewClient(sservere.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("GetRunnerScaleSetByID calls correct url", func(t *testing.T) { + rsl, err := json.Marshal(&runnerScaleSet) + require.NoError(t, err) + + url := url.URL{} + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(rsl) + url = *r.URL + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + require.NoError(t, err) + + expectedPath := fmt.Sprintf("/tenant/123/_apis/runtime/runnerscalesets/%d", runnerScaleSet.ID) + assert.Equal(t, expectedPath, url.Path) + assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) + }) + + t.Run("Status code not found", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + assert.NotNil(t, err) + }) + + t.Run("Error when Content-Type is text/plain", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + assert.NotNil(t, err) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + actualRetry := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + retryMax := 1 + retryWaitMax := 1 * time.Microsecond + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + _, err = client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + require.NotNil(t, err) + expectedRetry := retryMax + 1 + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) + + t.Run("No RunnerScaleSet found", func(t *testing.T) { + want := (*RunnerScaleSet)(nil) + rsl, err := json.Marshal(want) + require.NoError(t, err) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(rsl) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetRunnerScaleSetByID(ctx, runnerScaleSet.ID) + require.NoError(t, err) + assert.Equal(t, want, got) + }) +} + +func TestCreateRunnerScaleSet(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} + + t.Run("Create runner scale set", func(t *testing.T) { + want := &runnerScaleSet + rsl, err := json.Marshal(want) + require.NoError(t, err) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(rsl) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.CreateRunnerScaleSet(ctx, &runnerScaleSet) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("CreateRunnerScaleSet calls correct url", func(t *testing.T) { + rsl, err := json.Marshal(&runnerScaleSet) + require.NoError(t, err) + url := url.URL{} + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(rsl) + url = *r.URL + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) + require.NoError(t, err) + + expectedPath := "/tenant/123/_apis/runtime/runnerscalesets" + assert.Equal(t, expectedPath, url.Path) + assert.Equal(t, "6.0-preview", url.Query().Get("api-version")) + }) + + t.Run("Error when Content-Type is text/plain", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) + require.NotNil(t, err) + var expectedErr *ActionsError + assert.True(t, errors.As(err, &expectedErr)) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + actualRetry := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + retryMax := 1 + retryWaitMax := 1 * time.Microsecond + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + _, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet) + require.NotNil(t, err) + expectedRetry := retryMax + 1 + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) +} + +func TestUpdateRunnerScaleSet(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + scaleSetCreationDateTime := time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC) + runnerScaleSet := RunnerScaleSet{ID: 1, Name: "ScaleSet", RunnerGroupID: 1, RunnerGroupName: "group", CreatedOn: scaleSetCreationDateTime, RunnerSetting: RunnerSetting{}} + + t.Run("Update runner scale set", func(t *testing.T) { + want := &runnerScaleSet + rsl, err := json.Marshal(want) + require.NoError(t, err) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(rsl) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.UpdateRunnerScaleSet(ctx, 1, &RunnerScaleSet{RunnerGroupID: 1}) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("UpdateRunnerScaleSet calls correct url", func(t *testing.T) { + rsl, err := json.Marshal(&runnerScaleSet) + require.NoError(t, err) + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/tenant/123/_apis/runtime/runnerscalesets/1" + assert.Equal(t, expectedPath, r.URL.Path) + assert.Equal(t, http.MethodPatch, r.Method) + assert.Equal(t, "6.0-preview", r.URL.Query().Get("api-version")) + + w.Write(rsl) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.UpdateRunnerScaleSet(ctx, 1, &runnerScaleSet) + require.NoError(t, err) + }) +} + +func TestDeleteRunnerScaleSet(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Delete runner scale set", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview") + w.WriteHeader(http.StatusNoContent) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteRunnerScaleSet(ctx, 10) + assert.NoError(t, err) + }) + + t.Run("Delete calls with error", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + assert.Contains(t, r.URL.String(), "/_apis/runtime/runnerscalesets/10?api-version=6.0-preview") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"message": "test error"}`)) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteRunnerScaleSet(ctx, 10) + assert.ErrorContains(t, err, "test error") + }) +} + +func TestCreateMessageSession(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("CreateMessageSession unmarshals correctly", func(t *testing.T) { + owner := "foo" + runnerScaleSet := RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + RunnerSetting: RunnerSetting{}, + } + + want := &RunnerScaleSetSession{ + OwnerName: "foo", + RunnerScaleSet: &RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + }, + MessageQueueURL: "http://fake.github.com/123", + MessageQueueAccessToken: "fake.jwt.here", + } + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + resp := []byte(`{ + "ownerName": "foo", + "runnerScaleSet": { + "id": 1, + "name": "ScaleSet" + }, + "messageQueueUrl": "http://fake.github.com/123", + "messageQueueAccessToken": "fake.jwt.here" + }`) + w.Write(resp) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) { + owner := "foo" + runnerScaleSet := RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + RunnerSetting: RunnerSetting{}, + } + + want := &ActionsError{ + ActivityID: exampleRequestID, + StatusCode: http.StatusBadRequest, + Err: &ActionsExceptionError{ + ExceptionName: "CSharpExceptionNameHere", + Message: "could not do something", + }, + } + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set(headerActionsActivityID, exampleRequestID) + w.WriteHeader(http.StatusBadRequest) + resp := []byte(`{"typeName": "CSharpExceptionNameHere","message": "could not do something"}`) + w.Write(resp) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) + require.NotNil(t, err) + + errorTypeForComparison := &ActionsError{} + assert.True( + t, + errors.As(err, &errorTypeForComparison), + "CreateMessageSession expected to be able to parse the error into ActionsError type: %v", + err, + ) + + assert.Equal(t, want, errorTypeForComparison) + }) + + t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) { + owner := "foo" + runnerScaleSet := RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + RunnerSetting: RunnerSetting{}, + } + + gotRetries := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + gotRetries++ + })) + + retryMax := 3 + retryWaitMax := 1 * time.Microsecond + + wantRetries := retryMax + 1 + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + _, err = client.CreateMessageSession(ctx, runnerScaleSet.ID, owner) + assert.NotNil(t, err) + assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) + }) +} + +func TestDeleteMessageSession(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("DeleteMessageSession call is retried the correct amount of times", func(t *testing.T) { + runnerScaleSet := RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + RunnerSetting: RunnerSetting{}, + } + + gotRetries := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + gotRetries++ + })) + + retryMax := 3 + retryWaitMax := 1 * time.Microsecond + + wantRetries := retryMax + 1 + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + sessionID := uuid.New() + + err = client.DeleteMessageSession(ctx, runnerScaleSet.ID, sessionID) + assert.NotNil(t, err) + assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) + }) +} + +func TestRefreshMessageSession(t *testing.T) { + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("RefreshMessageSession call is retried the correct amount of times", func(t *testing.T) { + runnerScaleSet := RunnerScaleSet{ + ID: 1, + Name: "ScaleSet", + CreatedOn: time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), + RunnerSetting: RunnerSetting{}, + } + + gotRetries := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + gotRetries++ + })) + + retryMax := 3 + retryWaitMax := 1 * time.Microsecond + + wantRetries := retryMax + 1 + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(retryWaitMax), + ) + require.NoError(t, err) + + sessionID := uuid.New() + + _, err = client.RefreshMessageSession(context.Background(), runnerScaleSet.ID, sessionID) + assert.NotNil(t, err) + assert.Equalf(t, gotRetries, wantRetries, "CreateMessageSession got unexpected retry count: got=%v, want=%v", gotRetries, wantRetries) + }) +} + +func TestGetMessage(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE" + runnerScaleSetMessage := &RunnerScaleSetMessage{ + MessageID: 1, + MessageType: "rssType", + } + + t.Run("Get Runner Scale Set Message", func(t *testing.T) { + want := runnerScaleSetMessage + response := []byte(`{"messageId":1,"messageType":"rssType"}`) + s := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(response) + })) + + client, err := NewClient(s.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetMessage(ctx, s.URL, token, 0, 10) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("GetMessage sets the last message id if not 0", func(t *testing.T) { + want := runnerScaleSetMessage + response := []byte(`{"messageId":1,"messageType":"rssType"}`) + s := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + assert.Equal(t, "1", q.Get("lastMessageId")) + w.Write(response) + })) + + client, err := NewClient(s.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GetMessage(ctx, s.URL, token, 1, 10) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + retryMax := 1 + + actualRetry := 0 + expectedRetry := retryMax + 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(1*time.Millisecond), + ) + require.NoError(t, err) + + _, err = client.GetMessage(ctx, server.URL, token, 0, 10) + assert.NotNil(t, err) + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) + + t.Run("Message token expired", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetMessage(ctx, server.URL, token, 0, 10) + require.NotNil(t, err) + + var expectedErr *MessageQueueTokenExpiredError + require.True(t, errors.As(err, &expectedErr)) + }) + + t.Run("Status code not found", func(t *testing.T) { + want := ActionsError{ + Err: errors.New("unknown exception"), + StatusCode: 404, + } + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetMessage(ctx, server.URL, token, 0, 10) + require.NotNil(t, err) + assert.Equal(t, want.Error(), err.Error()) + }) + + t.Run("Error when Content-Type is text/plain", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetMessage(ctx, server.URL, token, 0, 10) + assert.NotNil(t, err) + }) + + t.Run("Capacity error handling", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hc := r.Header.Get(HeaderScaleSetMaxCapacity) + c, err := strconv.Atoi(hc) + require.NoError(t, err) + assert.GreaterOrEqual(t, c, 0) + + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + _, err = client.GetMessage(ctx, server.URL, token, 0, -1) + require.Error(t, err) + // Ensure we don't send requests with negative capacity + assert.False(t, errors.Is(err, &ActionsError{})) + + _, err = client.GetMessage(ctx, server.URL, token, 0, 0) + assert.Error(t, err) + var expectedErr *ActionsError + assert.ErrorAs(t, err, &expectedErr) + assert.Equal(t, http.StatusBadRequest, expectedErr.StatusCode) + }) +} + +func TestDeleteMessage(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjI1MTYyMzkwMjJ9.tlrHslTmDkoqnc4Kk9ISoKoUNDfHo-kjlH-ByISBqzE" + runnerScaleSetMessage := &RunnerScaleSetMessage{ + MessageID: 1, + MessageType: "rssType", + } + + t.Run("Delete existing message", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) + assert.Nil(t, err) + }) + + t.Run("Message token expired", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteMessage(ctx, server.URL, token, 0) + require.NotNil(t, err) + var expectedErr *MessageQueueTokenExpiredError + assert.True(t, errors.As(err, &expectedErr)) + }) + + t.Run("Error when Content-Type is text/plain", func(t *testing.T) { + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", "text/plain") + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) + require.NotNil(t, err) + var expectedErr *ActionsError + assert.True(t, errors.As(err, &expectedErr)) + }, + ) + + t.Run("Default retries on server error", func(t *testing.T) { + actualRetry := 0 + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + retryMax := 1 + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(retryMax), + WithRetryWaitMax(1*time.Nanosecond), + ) + require.NoError(t, err) + err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID) + assert.NotNil(t, err) + expectedRetry := retryMax + 1 + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) + + t.Run("No message found", func(t *testing.T) { + want := (*RunnerScaleSetMessage)(nil) + rsl, err := json.Marshal(want) + require.NoError(t, err) + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(rsl) + })) + + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + err = client.DeleteMessage(ctx, server.URL, token, runnerScaleSetMessage.MessageID+1) + var expectedErr *ActionsError + require.True(t, errors.As(err, &expectedErr)) + }) +} + +func TestClientProxy(t *testing.T) { + serverCalled := false + + proxy := testserver.New(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + serverCalled = true + })) + + proxyConfig := &httpproxy.Config{ + HTTPProxy: proxy.URL, + } + proxyFunc := func(req *http.Request) (*url.URL, error) { + return proxyConfig.ProxyFunc()(req.URL) + } + + c, err := NewClient("http://github.com/org/repo", nil, WithProxy(proxyFunc)) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + require.NoError(t, err) + + _, err = c.do(req) + require.NoError(t, err) + + assert.True(t, serverCalled) +} + +func TestGenerateJitRunnerConfig(t *testing.T) { + ctx := context.Background() + auth := &ActionsAuth{ + Token: "token", + } + + t.Run("Get JIT Config for Runner", func(t *testing.T) { + want := &RunnerScaleSetJitRunnerConfig{} + response := []byte(`{"count":1,"value":[{"id":1,"name":"scale-set-name"}]}`) + + runnerSettings := &RunnerScaleSetJitRunnerSetting{} + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(response) + })) + client, err := NewClient(server.configURLForOrg("my-org"), auth) + require.NoError(t, err) + + got, err := client.GenerateJitRunnerConfig(ctx, runnerSettings, 1) + require.NoError(t, err) + assert.Equal(t, want, got) + }) + + t.Run("Default retries on server error", func(t *testing.T) { + runnerSettings := &RunnerScaleSetJitRunnerSetting{} + + retryMax := 1 + actualRetry := 0 + expectedRetry := retryMax + 1 + + server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + actualRetry++ + })) + + client, err := NewClient( + server.configURLForOrg("my-org"), + auth, + WithRetryMax(1), + WithRetryWaitMax(1*time.Millisecond), + ) + require.NoError(t, err) + + _, err = client.GenerateJitRunnerConfig(ctx, runnerSettings, 1) + assert.NotNil(t, err) + assert.Equalf(t, actualRetry, expectedRetry, "A retry was expected after the first request but got: %v", actualRetry) + }) +} + +func TestClient_Do(t *testing.T) { + t.Run("trims byte order mark from response if present", func(t *testing.T) { + t.Run("when there is no body", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + defer server.Close() + + client, err := NewClient("https://localhost/org/repo", &ActionsAuth{Token: "token"}) + require.NoError(t, err) + + req, err := http.NewRequest("GET", server.URL, nil) + require.NoError(t, err) + + resp, err := client.do(req) + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Empty(t, string(body)) + }) + + responses := []string{ + "\xef\xbb\xbf{\"foo\":\"bar\"}", + "{\"foo\":\"bar\"}", + } + + for _, response := range responses { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(response)) + })) + defer server.Close() + + client, err := NewClient("https://localhost/org/repo", &ActionsAuth{Token: "token"}) + require.NoError(t, err) + + req, err := http.NewRequest("GET", server.URL, nil) + require.NoError(t, err) + + resp, err := client.do(req) + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "{\"foo\":\"bar\"}", string(body)) + } + }) +} + +// newActionsServer returns a new httptest.Server that handles the +// authentication requests neeeded to create a new client. Any requests not +// made to the /actions/runners/registration-token or +// /actions/runner-registration endpoints will be handled by the provided +// handler. The returned server is started and will be automatically closed +// when the test ends. +func newActionsServer(t *testing.T, handler http.Handler, options ...actionsServerOption) *actionsServer { + s := httptest.NewServer(nil) + server := &actionsServer{ + Server: s, + } + t.Cleanup(func() { + server.Close() + }) + + for _, option := range options { + option(server) + } + + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // handle getRunnerRegistrationToken + if strings.HasSuffix(r.URL.Path, "/runners/registration-token") { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"token":"token"}`)) + return + } + + // handle getActionsServiceAdminConnection + if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") { + if server.token == "" { + server.token = defaultActionsToken(t) + } + + w.Write([]byte(`{"url":"` + s.URL + `/tenant/123/","token":"` + server.token + `"}`)) + return + } + + handler.ServeHTTP(w, r) + }) + + server.Config.Handler = h + + return server +} + +type actionsServerOption func(*actionsServer) + +type actionsServer struct { + *httptest.Server + + token string +} + +func (s *actionsServer) configURLForOrg(org string) string { + return s.URL + "/" + org +} + +func defaultActionsToken(t *testing.T) string { + claims := &jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now().Add(-10 * time.Minute)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(10 * time.Minute)), + Issuer: "123", + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey)) + require.NoError(t, err) + tokenString, err := token.SignedString(privateKey) + require.NoError(t, err) + return tokenString +} + +func TestServerWithSelfSignedCertificates(t *testing.T) { + ctx := context.Background() + // this handler is a very very barebones replica of actions api + // used during the creation of a a new client + var u string + h := func(w http.ResponseWriter, r *http.Request) { + // handle get registration token + if strings.HasSuffix(r.URL.Path, "/runners/registration-token") { + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{"token":"token"}`)) + return + } + + // handle getActionsServiceAdminConnection + if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") { + claims := &jwt.RegisteredClaims{ + IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)), + Issuer: "123", + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey)) + require.NoError(t, err) + tokenString, err := token.SignedString(privateKey) + require.NoError(t, err) + w.Write([]byte(`{"url":"` + u + `","token":"` + tokenString + `"}`)) + return + } + + // default happy response for RemoveRunner + w.WriteHeader(http.StatusNoContent) + } + + certPath := filepath.Join("testdata", "server.crt") + keyPath := filepath.Join("testdata", "server.key") + + t.Run("client without ca certs", func(t *testing.T) { + server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h)) + u = server.URL + configURL := server.URL + "/my-org" + + auth := &ActionsAuth{ + Token: "token", + } + client, err := NewClient(configURL, auth) + require.NoError(t, err) + require.NotNil(t, client) + + err = client.RemoveRunner(ctx, 1) + require.NotNil(t, err) + + if runtime.GOOS == "linux" { + assert.True(t, errors.As(err, &x509.UnknownAuthorityError{})) + } + + // on macOS we only get an untyped error from the system verifying the + // certificate + if runtime.GOOS == "darwin" { + assert.True(t, strings.HasSuffix(err.Error(), "certificate is not trusted")) + } + }) + + t.Run("client with ca certs", func(t *testing.T) { + server := startNewTLSTestServer( + t, + certPath, + keyPath, + http.HandlerFunc(h), + ) + u = server.URL + configURL := server.URL + "/my-org" + + auth := &ActionsAuth{ + Token: "token", + } + + cert, err := os.ReadFile(filepath.Join("testdata", "rootCA.crt")) + require.NoError(t, err) + + pool := x509.NewCertPool() + require.True(t, pool.AppendCertsFromPEM(cert)) + + client, err := NewClient( + configURL, + auth, + WithRootCAs(pool), + ) + require.NoError(t, err) + assert.NotNil(t, client) + + err = client.RemoveRunner(ctx, 1) + assert.NoError(t, err) + }) + + t.Run("client with ca chain certs", func(t *testing.T) { + server := startNewTLSTestServer( + t, + filepath.Join("testdata", "leaf.crt"), + filepath.Join("testdata", "leaf.key"), + http.HandlerFunc(h), + ) + u = server.URL + configURL := server.URL + "/my-org" + + auth := &ActionsAuth{ + Token: "token", + } + + cert, err := os.ReadFile(filepath.Join("testdata", "intermediate.crt")) + require.NoError(t, err) + + pool := x509.NewCertPool() + require.True(t, pool.AppendCertsFromPEM(cert)) + + client, err := NewClient( + configURL, + auth, + WithRootCAs(pool), + WithRetryMax(0), + ) + require.NoError(t, err) + require.NotNil(t, client) + + err = client.RemoveRunner(ctx, 1) + assert.NoError(t, err) + }) + + t.Run("client skipping tls verification", func(t *testing.T) { + server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h)) + configURL := server.URL + "/my-org" + + auth := &ActionsAuth{ + Token: "token", + } + + client, err := NewClient(configURL, auth, WithoutTLSVerify()) + require.NoError(t, err) + assert.NotNil(t, client) + }) +} + +func startNewTLSTestServer(t *testing.T, certPath, keyPath string, handler http.Handler) *httptest.Server { + server := httptest.NewUnstartedServer(handler) + t.Cleanup(func() { + server.Close() + }) + + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + require.NoError(t, err) + + server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} + server.StartTLS() + + return server +} + +func TestUserAgentInfoString(t *testing.T) { + userAgentInfo := UserAgentInfo{ + System: "actions-runner-controller", + Version: "0.1.0", + CommitSHA: "1234567890abcdef", + ScaleSetID: 10, + HasProxy: true, + Subsystem: "test", + } + + userAgent := userAgentInfo.String() + expectedProduct := "actions-runner-controller/0.1.0 (1234567890abcdef; test)" + assert.Contains(t, userAgent, expectedProduct) + expectedScaleSet := "ScaleSetID/10 (Proxy/enabled)" + assert.Contains(t, userAgent, expectedScaleSet) +} + +const samplePrivateKey = `-----BEGIN PRIVATE KEY----- +MIIEugIBADANBgkqhkiG9w0BAQEFAASCBKQwggSgAgEAAoIBAQC7tgquvNIp+Ik3 +rRVZ9r0zJLsSzTHqr2dA6EUUmpRiQ25MzjMqKqu0OBwvh/pZyfjSIkKrhIridNK4 +DWnPfPWHE2K3Muh0X2sClxtqiiFmXsvbiTzhUm5a+zCcv0pJCWYnKi0HmyXpAXjJ +iN8mWliZN896verVYXWrod7EaAnuST4TiJeqZYW4bBBG81fPNc/UP4j6CKAW8nx9 +HtcX6ApvlHeCLZUTW/qhGLO0nLKoEOr3tXCPW5VjKzlm134Dl+8PN6f1wv6wMAoA +lo7Ha5+c74jhPL6gHXg7cRaHQmuJCJrtl8qbLkFAulfkBixBw/6i11xoM/MOC64l +TWmXqrxTAgMBAAECgf9zYlxfL+rdHRXCoOm7pUeSPL0dWaPFP12d/Z9LSlDAt/h6 +Pd+eqYEwhf795SAbJuzNp51Ls6LUGnzmLOdojKwfqJ51ahT1qbcBcMZNOcvtGqZ9 +xwLG993oyR49C361Lf2r8mKrdrR5/fW0B1+1s6A+eRFivqFOtsOc4V4iMeHYsCVJ +hM7yMu0UfpolDJA/CzopsoGq3UuQlibUEUxKULza06aDjg/gBH3PnP+fQ1m0ovDY +h0pX6SCq5fXVJFS+Pbpu7j2ePNm3mr0qQhrUONZq0qhGN/piCbBZe1CqWApyO7nA +B95VChhL1eYs1BKvQePh12ap83woIUcW2mJF2F0CgYEA+aERTuKWEm+zVNKS9t3V +qNhecCOpayKM9OlALIK/9W6KBS+pDsjQQteQAUAItjvLiDjd5KsrtSgjbSgr66IP +b615Pakywe5sdnVGzSv+07KMzuFob9Hj6Xv9als9Y2geVhUZB2Frqve/UCjmC56i +zuQTSele5QKCSSTFBV3423cCgYEAwIBv9ChsI+mse6vPaqSPpZ2n237anThMcP33 +aS0luYXqMWXZ0TQ/uSmCElY4G3xqNo8szzfy6u0HpldeUsEUsIcBNUV5kIIb8wKu +Zmgcc8gBIjJkyUJI4wuz9G/fegEUj3u6Cttmmj4iWLzCRscRJdfGpqwRIhOGyXb9 +2Rur5QUCgYAGWIPaH4R1H4XNiDTYNbdyvV1ZOG7cHFq89xj8iK5cjNzRWO7RQ2WX +7WbpwTj3ePmpktiBMaDA0C5mXfkP2mTOD/jfCmgR6f+z2zNbj9zAgO93at9+yDUl +AFPm2j7rQgBTa+HhACb+h6HDZebDMNsuqzmaTWZuJ+wr89VWV5c17QKBgH3jwNNQ +mCAIUidynaulQNfTOZIe7IMC7WK7g9CBmPkx7Y0uiXr6C25hCdJKFllLTP6vNWOy +uCcQqf8LhgDiilBDifO3op9xpyuOJlWMYocJVkxx3l2L/rSU07PYcbKNAFAxXuJ4 +xym51qZnkznMN5ei/CPFxVKeqHgaXDpekVStAoGAV3pSWAKDXY/42XEHixrCTqLW +kBxfaf3g7iFnl3u8+7Z/7Cb4ZqFcw0bRJseKuR9mFvBhcZxSErbMDEYrevefU9aM +APeCxEyw6hJXgbWKoG7Fw2g2HP3ytCJ4YzH0zNitHjk/1h4BG7z8cEQILCSv5mN2 +etFcaQuTHEZyRhhJ4BU= +-----END PRIVATE KEY-----` diff --git a/client_tls_test.go b/client_tls_test.go deleted file mode 100644 index 95ada82..0000000 --- a/client_tls_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package scaleset - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" - - "github.com/golang-jwt/jwt/v4" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestServerWithSelfSignedCertificates(t *testing.T) { - ctx := context.Background() - // this handler is a very very barebones replica of actions api - // used during the creation of a a new client - var u string - h := func(w http.ResponseWriter, r *http.Request) { - // handle get registration token - if strings.HasSuffix(r.URL.Path, "/runners/registration-token") { - w.WriteHeader(http.StatusCreated) - w.Write([]byte(`{"token":"token"}`)) - return - } - - // handle getActionsServiceAdminConnection - if strings.HasSuffix(r.URL.Path, "/actions/runner-registration") { - claims := &jwt.RegisteredClaims{ - IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Minute)), - ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)), - Issuer: "123", - } - - token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - privateKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(samplePrivateKey)) - require.NoError(t, err) - tokenString, err := token.SignedString(privateKey) - require.NoError(t, err) - w.Write([]byte(`{"url":"` + u + `","token":"` + tokenString + `"}`)) - return - } - - // default happy response for RemoveRunner - w.WriteHeader(http.StatusNoContent) - } - - certPath := filepath.Join("testdata", "server.crt") - keyPath := filepath.Join("testdata", "server.key") - - t.Run("client without ca certs", func(t *testing.T) { - server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h)) - u = server.URL - configURL := server.URL + "/my-org" - - auth := &ActionsAuth{ - Token: "token", - } - client, err := NewClient(configURL, auth) - require.NoError(t, err) - require.NotNil(t, client) - - err = client.RemoveRunner(ctx, 1) - require.NotNil(t, err) - - if runtime.GOOS == "linux" { - assert.True(t, errors.As(err, &x509.UnknownAuthorityError{})) - } - - // on macOS we only get an untyped error from the system verifying the - // certificate - if runtime.GOOS == "darwin" { - assert.True(t, strings.HasSuffix(err.Error(), "certificate is not trusted")) - } - }) - - t.Run("client with ca certs", func(t *testing.T) { - server := startNewTLSTestServer( - t, - certPath, - keyPath, - http.HandlerFunc(h), - ) - u = server.URL - configURL := server.URL + "/my-org" - - auth := &ActionsAuth{ - Token: "token", - } - - cert, err := os.ReadFile(filepath.Join("testdata", "rootCA.crt")) - require.NoError(t, err) - - pool := x509.NewCertPool() - require.True(t, pool.AppendCertsFromPEM(cert)) - - client, err := NewClient( - configURL, - auth, - WithRootCAs(pool), - ) - require.NoError(t, err) - assert.NotNil(t, client) - - err = client.RemoveRunner(ctx, 1) - assert.NoError(t, err) - }) - - t.Run("client with ca chain certs", func(t *testing.T) { - server := startNewTLSTestServer( - t, - filepath.Join("testdata", "leaf.crt"), - filepath.Join("testdata", "leaf.key"), - http.HandlerFunc(h), - ) - u = server.URL - configURL := server.URL + "/my-org" - - auth := &ActionsAuth{ - Token: "token", - } - - cert, err := os.ReadFile(filepath.Join("testdata", "intermediate.crt")) - require.NoError(t, err) - - pool := x509.NewCertPool() - require.True(t, pool.AppendCertsFromPEM(cert)) - - client, err := NewClient( - configURL, - auth, - WithRootCAs(pool), - WithRetryMax(0), - ) - require.NoError(t, err) - require.NotNil(t, client) - - err = client.RemoveRunner(ctx, 1) - assert.NoError(t, err) - }) - - t.Run("client skipping tls verification", func(t *testing.T) { - server := startNewTLSTestServer(t, certPath, keyPath, http.HandlerFunc(h)) - configURL := server.URL + "/my-org" - - auth := &ActionsAuth{ - Token: "token", - } - - client, err := NewClient(configURL, auth, WithoutTLSVerify()) - require.NoError(t, err) - assert.NotNil(t, client) - }) -} - -func startNewTLSTestServer(t *testing.T, certPath, keyPath string, handler http.Handler) *httptest.Server { - server := httptest.NewUnstartedServer(handler) - t.Cleanup(func() { - server.Close() - }) - - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - require.NoError(t, err) - - server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}} - server.StartTLS() - - return server -} diff --git a/github_api_request_test.go b/github_api_request_test.go deleted file mode 100644 index 93fe778..0000000 --- a/github_api_request_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package scaleset - -import ( - "context" - "encoding/json" - "io" - "net/http" - "net/url" - "strings" - "testing" - "time" - - "github.com/actions/scaleset/testserver" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var testUserAgent = UserAgentInfo{ - Version: "test", - CommitSHA: "test", - ScaleSetID: 1, -} - -func TestNewGitHubAPIRequest(t *testing.T) { - ctx := context.Background() - - t.Run("uses the right host/path prefix", func(t *testing.T) { - scenarios := []struct { - configURL string - path string - expected string - }{ - { - configURL: "https://github.com/org/repo", - path: "/app/installations/123/access_tokens", - expected: "https://api.github.com/app/installations/123/access_tokens", - }, - { - configURL: "https://www.github.com/org/repo", - path: "/app/installations/123/access_tokens", - expected: "https://api.github.com/app/installations/123/access_tokens", - }, - { - configURL: "http://github.localhost/org/repo", - path: "/app/installations/123/access_tokens", - expected: "http://api.github.localhost/app/installations/123/access_tokens", - }, - { - configURL: "https://my-instance.com/org/repo", - path: "/app/installations/123/access_tokens", - expected: "https://my-instance.com/api/v3/app/installations/123/access_tokens", - }, - { - configURL: "http://localhost/org/repo", - path: "/app/installations/123/access_tokens", - expected: "http://localhost/api/v3/app/installations/123/access_tokens", - }, - } - - for _, scenario := range scenarios { - client, err := NewClient(scenario.configURL, nil) - require.NoError(t, err) - - req, err := client.newGitHubAPIRequest(ctx, http.MethodGet, scenario.path, nil) - require.NoError(t, err) - assert.Equal(t, scenario.expected, req.URL.String()) - } - }) - - t.Run("sets user agent header if present", func(t *testing.T) { - client, err := NewClient("http://localhost/my-org", nil) - require.NoError(t, err) - - client.SetUserAgent(testUserAgent) - - req, err := client.newGitHubAPIRequest(ctx, http.MethodGet, "/app/installations/123/access_tokens", nil) - require.NoError(t, err) - - assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) - }) - - t.Run("sets the body we pass", func(t *testing.T) { - client, err := NewClient("http://localhost/my-org", nil) - require.NoError(t, err) - - req, err := client.newGitHubAPIRequest( - ctx, - http.MethodGet, - "/app/installations/123/access_tokens", - strings.NewReader("the-body"), - ) - require.NoError(t, err) - - b, err := io.ReadAll(req.Body) - require.NoError(t, err) - assert.Equal(t, "the-body", string(b)) - }) -} - -func TestNewActionsServiceRequest(t *testing.T) { - ctx := context.Background() - defaultCreds := &ActionsAuth{Token: "token"} - - t.Run("manages authentication", func(t *testing.T) { - t.Run("client is brand new", func(t *testing.T) { - token := defaultActionsToken(t) - server := testserver.New(t, nil, testserver.WithActionsToken(token)) - - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - - req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) - require.NoError(t, err) - - assert.Equal(t, "Bearer "+token, req.Header.Get("Authorization")) - }) - - t.Run("admin token is about to expire", func(t *testing.T) { - newToken := defaultActionsToken(t) - server := testserver.New(t, nil, testserver.WithActionsToken(newToken)) - - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - client.actionsServiceAdminToken = "expiring-token" - client.actionsServiceAdminTokenExpiresAt = time.Now().Add(59 * time.Second) - - req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) - require.NoError(t, err) - - assert.Equal(t, "Bearer "+newToken, req.Header.Get("Authorization")) - }) - - t.Run("admin token refresh failure", func(t *testing.T) { - newToken := defaultActionsToken(t) - errMessage := `{"message":"test"}` - unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(errMessage)) - } - server := testserver.New( - t, - nil, - testserver.WithActionsToken("random-token"), - testserver.WithActionsToken(newToken), - testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler), - ) - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - expiringToken := "expiring-token" - expiresAt := time.Now().Add(59 * time.Second) - client.actionsServiceAdminToken = expiringToken - client.actionsServiceAdminTokenExpiresAt = expiresAt - _, err = client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) - require.Error(t, err) - assert.Contains(t, err.Error(), errMessage) - assert.Equal(t, client.actionsServiceAdminToken, expiringToken) - assert.Equal(t, client.actionsServiceAdminTokenExpiresAt, expiresAt) - }) - - t.Run("admin token refresh retry", func(t *testing.T) { - newToken := defaultActionsToken(t) - errMessage := `{"message":"test"}` - - srv := "http://github.com/my-org" - resp := &ActionsServiceAdminConnection{ - AdminToken: &newToken, - ActionsServiceURL: &srv, - } - failures := 0 - unauthorizedHandler := func(w http.ResponseWriter, r *http.Request) { - if failures < 5 { - failures++ - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(errMessage)) - return - } - - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(resp) - } - server := testserver.New(t, nil, testserver.WithActionsToken("random-token"), testserver.WithActionsToken(newToken), testserver.WithActionsRegistrationTokenHandler(unauthorizedHandler)) - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - expiringToken := "expiring-token" - expiresAt := time.Now().Add(59 * time.Second) - client.actionsServiceAdminToken = expiringToken - client.actionsServiceAdminTokenExpiresAt = expiresAt - - _, err = client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) - require.NoError(t, err) - assert.Equal(t, client.actionsServiceAdminToken, newToken) - assert.Equal(t, client.actionsServiceURL, srv) - assert.NotEqual(t, client.actionsServiceAdminTokenExpiresAt, expiresAt) - }) - - t.Run("token is currently valid", func(t *testing.T) { - tokenThatShouldNotBeFetched := defaultActionsToken(t) - server := testserver.New(t, nil, testserver.WithActionsToken(tokenThatShouldNotBeFetched)) - - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - client.actionsServiceAdminToken = "healthy-token" - client.actionsServiceAdminTokenExpiresAt = time.Now().Add(1 * time.Hour) - - req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil) - require.NoError(t, err) - - assert.Equal(t, "Bearer healthy-token", req.Header.Get("Authorization")) - }) - }) - - t.Run("builds the right URL including api version", func(t *testing.T) { - server := testserver.New(t, nil) - - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - - req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "/my/path?name=banana", nil) - require.NoError(t, err) - - serverURL, err := url.Parse(server.URL) - require.NoError(t, err) - - result := req.URL - assert.Equal(t, serverURL.Host, result.Host) - assert.Equal(t, "/tenant/123/my/path", result.Path) - assert.Equal(t, "banana", result.Query().Get("name")) - assert.Equal(t, "6.0-preview", result.Query().Get("api-version")) - }) - - t.Run("populates header", func(t *testing.T) { - server := testserver.New(t, nil) - - client, err := NewClient(server.ConfigURLForOrg("my-org"), defaultCreds) - require.NoError(t, err) - - client.SetUserAgent(testUserAgent) - - req, err := client.newActionsServiceRequest(ctx, http.MethodGet, "/my/path", nil) - require.NoError(t, err) - - assert.Equal(t, testUserAgent.String(), req.Header.Get("User-Agent")) - assert.Equal(t, "application/json", req.Header.Get("Content-Type")) - }) -} diff --git a/internal/testdata/generate.sh b/internal/testdata/generate.sh new file mode 100644 index 0000000..7790be4 --- /dev/null +++ b/internal/testdata/generate.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Generate Root CA +openssl genrsa -out rootCA.key 2048 +openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt -subj "/CN=Test Root CA" \ + -addext "basicConstraints = critical, CA:TRUE" \ + -addext "keyUsage = critical, keyCertSign, cRLSign" + +# Generate Intermediate Certificate +openssl genrsa -out intermediate.key 2048 +openssl req -new -key intermediate.key -out intermediate.csr -subj "/CN=Test Intermediate CA" +openssl x509 -req -in intermediate.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out intermediate.crt -days 1000 -sha256 \ + -extfile <(echo -e "basicConstraints = critical, CA:TRUE, pathlen:0\nkeyUsage = critical, keyCertSign, cRLSign") + +# Generate Leaf Certificate +openssl genrsa -out leaf.key 2048 +openssl req -new -key leaf.key -out leaf.csr -subj "/CN=localhost" \ + -addext "subjectAltName = IP:127.0.0.1" +openssl x509 -req -in leaf.csr -CA intermediate.crt -CAkey intermediate.key -CAcreateserial -out leaf.crt -days 500 -sha256 \ + -extfile <(echo -e "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=IP:127.0.0.1") + +# Generate Leaf Certificate +openssl genrsa -out server.key 2048 +openssl req -new -key server.key -out server.csr -subj "/CN=localhost" \ + -addext "subjectAltName = IP:127.0.0.1" +openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 500 -sha256 \ + -extfile <(echo -e "authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, keyEncipherment\nextendedKeyUsage=serverAuth\nsubjectAltName=IP:127.0.0.1") + +rm rootCA.key intermediate.key *.csr *.srl diff --git a/internal/testdata/intermediate.crt b/internal/testdata/intermediate.crt new file mode 100644 index 0000000..476da8e --- /dev/null +++ b/internal/testdata/intermediate.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDKjCCAhKgAwIBAgIUQr7R8yN5+2and6ucUOPF6oIbD44wDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEzMFoXDTI3 +MTEyNTEyMDEzMFowHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDOGvN95wCkYO35qyJnf/RwTiDb +oEVaefKnZZny1JrO34MFjlAz8C/P5WwxNUzzbQLTPh5iTqFRU+vis6HPvV0HJEoI +wTfgBCZxcdY8fEIY96FGHLju3PzfxeJaVHyi+2cTtzU+oNp4OFF8huApjYXjaV4y +pAirPbiiP/cgtcT4L5WErQi0aGZkq+1YqY2duNFNIGPTEcXV4iN4IhuD9dpqdKFg +H0wmZDgH+VE/5ACXovU8j5cxCKOJGxTVMKVZlvxPH3w69Z85x3o5AAnyxwo8E2zo +TC1FJ1eFLsmYLZki6cGBzSkIl5QlLGHakWYh+JLu/pkfTL8t+AkY3hZJM96ZAgMB +AAGjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSmAyntm95+KoyL3ffLBXZKSpk1VTAfBgNVHSMEGDAWgBR93+rQFjh+RUFX +df4dbNcfS2hbTzANBgkqhkiG9w0BAQsFAAOCAQEABopVFLGQf/LFH+OKVCOT8FCC +y/+o1B/U5jXVvbfwlSGScaiJGQ94FsuH59XJCGySQj77ZVTeElBtntoLXmOCFjyF +jKHCDfUpB4nzeqNMvTDzuoYyPS8DhoGfEnaCgJyKf6GU4p41502gH8mQRB7azzL7 +5jW0aFatCA6G6T1oogHZpHf0ice80C2JkFbWHSE9JxqARbTc06wCDBiSBFTGZQDO +JaBIbn6FL3zSkKcpwgJEqDRavVuoDUlJPDqtTzjf/fMQGGR2LUFkceJpsQqf1jrF +1yTtEZ8gjR2g2Vj6IszUAgbc87xR0AgyGDVckiUdhlX2Y6KCqo2cl9LfSVpqtw== +-----END CERTIFICATE----- diff --git a/internal/testdata/leaf.crt b/internal/testdata/leaf.crt new file mode 100644 index 0000000..1d05047 --- /dev/null +++ b/internal/testdata/leaf.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQjCCAiqgAwIBAgIUHT3JtqsYKs7NHv1LNyS9RYC7vsAwDQYJKoZIhvcNAQEL +BQAwHzEdMBsGA1UEAwwUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMjUwMjI4MTIw +MTMwWhcNMjYwNzEzMTIwMTMwWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHwI/xSLgCuQrx+WsvupA8w4eMdSef +WGw523OJYPJkDYJGgSGsjVb9htba6vXYbGNohuluEAZIyT7GvmPezTokeVMkuSYT +lSV8xplFEtDlQhTzaI/cofbi7qtT91/5zS/w0JSaNosThGtZg/M4ZOiMj04m0NGK +Zz56l9Lpe/yM7fPda++D9xYEGSSdwK9CqqwF+cXN09d6IK1VINIIjT3Sdb9Sssok +GWmD7UUPLvwZ5379+HRs1K8AFXqvbkeWVYtrJwJMxJGVnNSeiqKGSmMEpP7tVNHl +s4V7oyQXd8KX+HpziiayjGy9giVteJJi/bAmUp+0+hTHBes5fOWI5JyDAgMBAAGj +gYAwfjAfBgNVHSMEGDAWgBSmAyntm95+KoyL3ffLBXZKSpk1VTAJBgNVHRMEAjAA +MAsGA1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHREECDAGhwR/ +AAABMB0GA1UdDgQWBBTVdJE2lkGsNIU3LlEf3rN5fyaRkjANBgkqhkiG9w0BAQsF +AAOCAQEAo1klH9WMsPWTN9qN3tdud07eatulEKo/0okaph6MJ59ozseOzxrfpwL0 +67Nr8yl+VwZqrRTBurp0n6G+n0j8UHfWjSrAqN4yUHl+heT0HpnLR2FE9YgZEmxR +bPfVbPBef/eJeE7/U6imfBYzzMajua+hg05sVHUNNdPaFOP+Xj47x8uQmf9w5/kf +MrylRUSgH5RRge4+2T5hmNM9tHfF6OfDHitrXnl+X6h/x/tkBvDcUXtKa5xuEcSg +WpmJKl3pKfXvdmCIrj9Vca+UD2Bntkk2jgDTLEPJAxMgrsQRhnUJclaunnd1NQbc +FmjFW7iaNvDVKt+vYqH8ff8U9iCB2g== +-----END CERTIFICATE----- diff --git a/internal/testdata/leaf.key b/internal/testdata/leaf.key new file mode 100644 index 0000000..0fee211 --- /dev/null +++ b/internal/testdata/leaf.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHwI/xSLgCuQrx ++WsvupA8w4eMdSefWGw523OJYPJkDYJGgSGsjVb9htba6vXYbGNohuluEAZIyT7G +vmPezTokeVMkuSYTlSV8xplFEtDlQhTzaI/cofbi7qtT91/5zS/w0JSaNosThGtZ +g/M4ZOiMj04m0NGKZz56l9Lpe/yM7fPda++D9xYEGSSdwK9CqqwF+cXN09d6IK1V +INIIjT3Sdb9SssokGWmD7UUPLvwZ5379+HRs1K8AFXqvbkeWVYtrJwJMxJGVnNSe +iqKGSmMEpP7tVNHls4V7oyQXd8KX+HpziiayjGy9giVteJJi/bAmUp+0+hTHBes5 +fOWI5JyDAgMBAAECggEADanzbrrds3n68LByD5LAeRea9xWwfyrqRE7pqVUTX5q4 +9Z+xsP7+G1uU6Oa3qHVJm4XXA+tesq3peGjfpgb92i7ebB2qKB7EsLNZGqt91KDf +lALsDFib7cwLtjOuwgSyKdPqxl3Cx7QAL+Bhy9LDQZIv82HHY6NKV9J43/XWQcGK +KNZyS1o0vBWvt135YeE3qfQA9Ww8GI3jWyk49QDOVtVNZ1HRQpXPkt5exAyU8JpW +Y3Y5VqyEcKPBRlw/scEc5CRuzIP3P06Y+NEuuvnlnDt/BR/wyyuPiZoIqGXhXUKe +oDzEmtVrvB5RmhRc1PoS1l1GBfBfVqwkLUmeSitn8QKBgQD1pC6Ukiqtm7vyhirU +ynE+Dik/gHBBW6fQalD7yZwStid1+HfIvoU+RB0wyPUVwbu7eOoMnjG2ChLYX7cV +UhAcu5ZWFhlc8OS5bGGMI99d4ueBqTYONqnCV3DhiWZhs8OesiF3hohE6jV3G5xC +ra8DzImMpujyMIWXvOwnPLRWCQKBgQDQLPwTvx472XbY1aWviQkzxMgxP8l9D+lO +nBaybarLxDa+89RMWidgJX2kGM7i6FgicGgSNpNDIYNLrVZYAz11PP6o2Oqa9ZaR +5IvnfhP1iOwSgIoC8weNSE+Y0Lw/w0IOW71+XsfgswhJG45eXV8hRqYpUEkjjl1x +nQM0hCshKwKBgCQDFfEiHK+nDT7Y/J6Fr2Rxnwp4QfzS+x9K9uRzAjacDdz1uFnt +1Ir0YXMtgwDVjjhF2cpPxunxQCIIpkax6TrNJZUpWD6P8nhcs1BgUfbptRcFP6+F +xA2B1EK8ag4Y1K0HYHCtgHzZ+Uyk95uu6uGbsu6z6aLYCj3crKJz+9xBAoGACOdT +pLiQ33hul9mTa42N4jPxaAHVaU7r6JvOcLU2D98FhGdDVjyo4HjaBdG1z4imdFqg +aN8Cr2VYiz0Pq1YAI+qG7cvRRO1qEjVXMoB29BJ2Hlh3Dqc8VHOaS+vpkUSVp62O +zj/ZhqfBm/bcwPZ3YiH2a1/usOGe54QSpgVdHt8CgYBr6xpKQSrQvgtiuv9kCzI+ +WQYP2Xxj+zsQb29hagXY/JllKOl5aDGz2qMV2RgOWZabB/xxkAeTF4A26wWrmcq0 +wsv96jEl1MtI3lB3bi/8Y/tctkNsp0drvDZdfnGMpzxDafECKdxdFvhO63p7yBOA +LG1OvFTywkxBuOUKsNsErQ== +-----END PRIVATE KEY----- diff --git a/internal/testdata/rootCA.crt b/internal/testdata/rootCA.crt new file mode 100644 index 0000000..a6b6bda --- /dev/null +++ b/internal/testdata/rootCA.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHzCCAgegAwIBAgIUUmc9nWf4fhGFNd0oCNE0CzOXMaEwDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEyOVoXDTI3 +MTIxOTEyMDEyOVowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5ZFoDGTL0YyrwEA7qTu32cur0sQXDV86Xwl +G0ilk2DXXH4F70ruTTW3NG0Rniw/rt2jzJADo1Tlosq9eJKQGQKAr21N5kjhlU3J +8nFBK+1WJyG27EvyeqZOCucXOJaAm0HSbhlT0MYpZ4kzuxmOUPmTsJmt2BtK+uRU +3LlXtzyZnJo53azQuLZz26tBGd9LXsBUMi+KJ0eX1HPluIT3o+nslnJZaqGySLKm +cJnLf9hio+rAwFBb8sgDdzeI7jqZ2bmAGPJBYpIT/dIxuZUkgTfX+OMp2g3RnQea +M0w0UjhbbQeAJONH9HGREDdp7tYtuyuBbE4miNTyjSsouqk6AwIDAQABo2MwYTAd +BgNVHQ4EFgQUfd/q0BY4fkVBV3X+HWzXH0toW08wHwYDVR0jBBgwFoAUfd/q0BY4 +fkVBV3X+HWzXH0toW08wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQELBQADggEBAGLUya6xwaCwUPWHNOjlvGBGzGwAmSstJzh5o77O +XTTvyCwb0p80AnS9XoX3An5e4ePzw69mEw6RzfVLIex7fCRDekqPFuSWjVagKGJc +G7nvCqdHoCh2z1Jkb9gFpYPd6p45dtLWBw9e9/t9cFHtDR6stC16/Hy8cLzEIr0c +EWxCNdJdZW+soJivaZQeVWtlMXxVpGIs8i33CAFYufZCTKMgyYRegZuMQ676OcDE +9VSi2vJnnhdn7OBip82xX3NDQrwVt60fvFMr25cPOlzhXRY4mQLslGOleqT3sSPV +DVJnOBBmdjgFQQ8BO7rFUNGGOaUcEZp0HLRwxPZyc6OBCIg= +-----END CERTIFICATE----- diff --git a/internal/testdata/server.crt b/internal/testdata/server.crt new file mode 100644 index 0000000..6093005 --- /dev/null +++ b/internal/testdata/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDOjCCAiKgAwIBAgIUQr7R8yN5+2and6ucUOPF6oIbD48wDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTI1MDIyODEyMDEzMFoXDTI2 +MDcxMzEyMDEzMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4oL2hAPQlDVaNJru5fIstkpoVSuam0vpswC7ciRc +XQRjF3q8kjtIA7+jdySsKJqOLGnybDX3awvRyKMEjq11IfnZLjZc+FzTlA+x4z0h +MHb0GiBFXKNzrExGI9F0KEPtFxcMIqZ119LY2ReexxWkZBQYlgTepaevp71za4c2 +n4Zy1+0iS5+uklZ4ANKMTBGlN76Qgt530VnpNiIeUbiUzY58Vx4q7kFcUv/oSz8p +rbXr+/GGpAjrOc6/JsezRE8YK2po60dvV80TJ2Jt6pduvF7OSQnq/v4mJl1xuXKl +Byo9HLbeu3BuVRWQs2/EwEzx5kX3Ugysl9Bm44K2yKe9/QIDAQABo4GAMH4wHwYD +VR0jBBgwFoAUfd/q0BY4fkVBV3X+HWzXH0toW08wCQYDVR0TBAIwADALBgNVHQ8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0RBAgwBocEfwAAATAdBgNV +HQ4EFgQUe0rTTfWjho3hgeLTnajTCpddo2MwDQYJKoZIhvcNAQELBQADggEBAIR2 +5zkA7rPnddxCunsz8Jjq3wyhR/KiAFz+RGeFeiXDkF2fWr7QIQ9KbFbv8tpfXR7P +B75bY0sXwutHMB2sZDi92cH5sthNBfp19fI35cxcU4oTPxp4UZJKEiA3Qx8y73CX +NJu1009nPdOJNlIboDGAFdZ5SH6RCh+YcQZ68kjHPWBIpXxLbs9FN3QmpbAvtLh1 +PoPaSy7IjKmxm1u+Lf6tyIn2IiB3MiynaB3OKvbkLCseM/5SZKMk6WKSDWopOCJr +xciPOc+yeLz5I2Omn0uViOIIciqjlgxncWAyNtDgvJcecwqB2cPiIhk6GY0QZ1uM +e7KoqGzWXvWLqJ13a9U= +-----END CERTIFICATE----- diff --git a/internal/testdata/server.key b/internal/testdata/server.key new file mode 100644 index 0000000..064c6c1 --- /dev/null +++ b/internal/testdata/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDigvaEA9CUNVo0 +mu7l8iy2SmhVK5qbS+mzALtyJFxdBGMXerySO0gDv6N3JKwomo4safJsNfdrC9HI +owSOrXUh+dkuNlz4XNOUD7HjPSEwdvQaIEVco3OsTEYj0XQoQ+0XFwwipnXX0tjZ +F57HFaRkFBiWBN6lp6+nvXNrhzafhnLX7SJLn66SVngA0oxMEaU3vpCC3nfRWek2 +Ih5RuJTNjnxXHiruQVxS/+hLPymttev78YakCOs5zr8mx7NETxgramjrR29XzRMn +Ym3ql268Xs5JCer+/iYmXXG5cqUHKj0ctt67cG5VFZCzb8TATPHmRfdSDKyX0Gbj +grbIp739AgMBAAECggEADgUIbbAFbJbyHV1q5Jqc/9oSeRW40lyG0Mh+fEMZ4Gam +x3ZA+QAS+1W/hV6ktTf+YsCv+4NKQWWQN3iM41PYcyDmu1XWt/Hu5TQk0NQgxhd8 +EP3nAnkvbf5OkmWiveHuaRvJFCqfZ/Cp8U3lSvHg+edwhMs1CKXHWSeAXwBrIMEb +ajpxuD3B/NT/CGmKnj3cgAuIbvNHVIcwu8ACbpczDL++vi7KrWmOJn1QzSlUlNFi +fsgnF0heO5Uff4vkjXU84INQxOP3tbvXcDNiwDewZy75h2d3Pv+ku8GoZYWFUXSJ +yKtafJMJUD0kJMuKhkzrwYcQGY6ioSYisPK+JoungQKBgQD8fWmuHwCXbM4Ckyns +Wg4f+kG8d+wypgIs6ENmgr9UnNB0N6n7nO7v/4l/l7IN9CQQmdtSvek2ytk5rGBM +XUAWxZaokE3MecxR0EUJx42k/k3dN4XgU/YNk4D6/wpEsyUATE4nIFDVjxE+Jc07 +CZ2CUWKyxTPGz2kfHnEQ0vFiYQKBgQDlqRiGlJ6c99zTas2wrvr+50aQhn6BryDK +kjGM6woPnnwMq+Jy6vum3o0cU+iNNeFAijShXo2XR3iZJcoJ2sPhy3dRWdBNdFyy +hwxgD0cXzEjQL0M03DPDykTnM7ZvE6KUZjnxJZkytJHLKapoGzxBH9656zx5qnuH +MPYwTWg5HQKBgDKBD4OBtgeT/v0q3KbnOI4S69U8E6Xp6ON8rgayPn05RMUKYVjw +AidFcQZxnG8IF7KuY92AGUcZeiv8G+MKgAhOC526B6XP8xumUjjrjpyjNYX7Vi8R +/FSo3ZLXMwGc59jQao2O/DxLesJ4oz2c5cGsb9acdYfd8wQDfdBEsX3hAoGAc4Pu +NiMi9MknZZ/e/fPFg9lIgQFlOE2iLMID8mF2mgyZULZUHIFdOr3ONGVwHzbuqcva +VSB+D41/d2iuiu5igHwa8+w8/fh9d7691sNYevvh0/Ux1LC9yMlAhxpXtN8nc4VH +t6e1uu9gNdQrRloMoKUrHlDYBkpd/838xqbouXECgYEArwn+eXKD5zgNN4jEbNBp +ygIp+Oh2abt+CNQjfLUa+qon5ziH53mHixJ2hpaOa6Rxxu9R2ZgNLtbodm+ccD8z +ZNA7Z0rApAwfuhD8zIzkZ4HuARN8eopYmTubpzDkAcfRWhw1EBDQc0V6trl+EJsK +xfbmGepRVWXw2dLmxhA9/zM= +-----END PRIVATE KEY----- diff --git a/user_agent_test.go b/user_agent_test.go deleted file mode 100644 index 4096299..0000000 --- a/user_agent_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package scaleset - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestUserAgentInfoString(t *testing.T) { - userAgentInfo := UserAgentInfo{ - System: "actions-runner-controller", - Version: "0.1.0", - CommitSHA: "1234567890abcdef", - ScaleSetID: 10, - HasProxy: true, - Subsystem: "test", - } - - userAgent := userAgentInfo.String() - expectedProduct := "actions-runner-controller/0.1.0 (1234567890abcdef; test)" - assert.Contains(t, userAgent, expectedProduct) - expectedScaleSet := "ScaleSetID/10 (Proxy/enabled)" - assert.Contains(t, userAgent, expectedScaleSet) -}