From bd407f59f4ca3474b097bff5b94a8510792747c0 Mon Sep 17 00:00:00 2001 From: Srijan Saurav Date: Tue, 20 May 2025 21:09:02 +0530 Subject: [PATCH 1/6] feat: add OIDC support --- command/report/report.go | 25 +++++++ utils/fetch_oidc_token.go | 135 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 utils/fetch_oidc_token.go diff --git a/command/report/report.go b/command/report/report.go index 30bc954e..364c9dbc 100644 --- a/command/report/report.go +++ b/command/report/report.go @@ -25,6 +25,11 @@ type ReportOptions struct { ValueFile string SkipCertificateVerification bool DSN string + UseOIDC bool + OIDCRequestToken string // id token to manually get an OIDC token + OIDCRequestUrl string // url to manually get an OIDC token + DeepSourceHostEndpoint string // DeepSource host endpoint where the app is running. Defaults to the cloud endpoint https://app.deepsource.com + OIDCProvider string // OIDC provider to use for authentication } // NewCmdVersion returns the current version of cli being used @@ -67,6 +72,14 @@ func NewCmdReport() *cobra.Command { cmd.Flags().StringVar(&opts.ValueFile, "value-file", "", "path to the artifact value file") + cmd.Flags().BoolVar(&opts.UseOIDC, "use-oidc", false, "use OIDC to authenticate with DeepSource") + + cmd.Flags().StringVar(&opts.OIDCRequestToken, "oidc-request-token", "", "request ID token to fetch an OIDC token from OIDC provider") + + cmd.Flags().StringVar(&opts.OIDCRequestUrl, "oidc-request-url", "", "OIDC provider's request URL to fetch an OIDC token") + cmd.Flags().StringVar(&opts.DeepSourceHostEndpoint, "deepsource-host-endpoint", "https://app.deepsource.com", "DeepSource host endpoint where the app is running. Defaults to the cloud endpoint https://app.deepsource.com") + cmd.Flags().StringVar(&opts.OIDCProvider, "oidc-provider", "", "OIDC provider to use for authentication. Supported providers: github-actions") + // --skip-verify flag to skip SSL certificate verification while reporting test coverage data. cmd.Flags().BoolVar(&opts.SkipCertificateVerification, "skip-verify", false, "skip SSL certificate verification while sending the test coverage data") @@ -80,6 +93,9 @@ func (opts *ReportOptions) sanitize() { opts.Value = strings.TrimSpace(opts.Value) opts.ValueFile = strings.TrimSpace(opts.ValueFile) opts.DSN = strings.TrimSpace(os.Getenv("DEEPSOURCE_DSN")) + opts.OIDCRequestToken = strings.TrimSpace(opts.OIDCRequestToken) + opts.OIDCRequestUrl = strings.TrimSpace(opts.OIDCRequestUrl) + opts.DeepSourceHostEndpoint = strings.TrimSpace(opts.DeepSourceHostEndpoint) } func (opts *ReportOptions) validateKey() error { @@ -107,6 +123,15 @@ func (opts *ReportOptions) validateKey() error { func (opts *ReportOptions) Run() int { opts.sanitize() + if opts.UseOIDC { + dsn, err := utils.GetDSNFromOIDC(opts.OIDCRequestToken, opts.OIDCRequestUrl, opts.DeepSourceHostEndpoint, opts.OIDCProvider) + if err != nil { + fmt.Fprintln(os.Stderr, "DeepSource | Error | Failed to get DSN using OIDC:", err) + return 1 + } + opts.DSN = dsn + } + if opts.DSN == "" { fmt.Fprintln(os.Stderr, "DeepSource | Error | Environment variable DEEPSOURCE_DSN not set (or) is empty. You can find it under the repository settings page") return 1 diff --git a/utils/fetch_oidc_token.go b/utils/fetch_oidc_token.go new file mode 100644 index 00000000..4b72924b --- /dev/null +++ b/utils/fetch_oidc_token.go @@ -0,0 +1,135 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" + "os" +) + +var ( + DEEPSOURCE_AUDIENCE = "DeepSource" + ALLOWED_PROVIDERS = map[string]bool{ + "github-actions": true, + } +) + +// FetchOIDCTokenFromProvider fetches the OIDC token from the OIDC token provider. +// It takes the request ID and the request URL as input and returns the OIDC token as a string. +func FetchOIDCTokenFromProvider(requestId, requestUrl string) (string, error) { + // requestid is the bearer token that needs to be sent to the request url + req, err := http.NewRequest("GET", requestUrl, nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+requestId) + // set the expected audiences as the audience parameter + q := req.URL.Query() + q.Set("audience", DEEPSOURCE_AUDIENCE) + req.URL.RawQuery = q.Encode() + + // send the request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // check if the response is 200 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch OIDC token: %s", resp.Status) + } + + // extract the token from the json response. The token is sent under the key `value` + // and the response is a json object + var tokenResponse struct { + Value string `json:"value"` + } + if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { + return "", err + } + // check if the token is empty + if tokenResponse.Value == "" { + return "", fmt.Errorf("failed to fetch OIDC token: empty token") + } + // return the token + return tokenResponse.Value, nil +} + +// ExchangeOIDCTokenForTempDSN exchanges the OIDC token for a temporary DSN. +// It sends the OIDC token to the respective DeepSource API endpoint and returns the temp DSN as string. +func ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider string) (string, error) { + apiEndpoint := fmt.Sprintf("%s/services/oidc/%s/", dsEndpoint, provider) + req, err := http.NewRequest("POST", apiEndpoint, nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+oidcToken) + + type ExchangeResponse struct { + DSN string `json:"access_token"` + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to exchange OIDC token for DSN: %s", resp.Status) + } + var exchangeResponse ExchangeResponse + if err := json.NewDecoder(resp.Body).Decode(&exchangeResponse); err != nil { + return "", err + } + // check if the token is empty + if exchangeResponse.DSN == "" { + return "", fmt.Errorf("failed to exchange OIDC token for DSN: empty token") + } + // return the token + return exchangeResponse.DSN, nil +} + +func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, error) { + // infer provider from environment variables. + // Github actions sets the GITHUB_ACTIONS environment variable to true by default. + if os.Getenv("GITHUB_ACTIONS") == "true" { + provider = "github-actions" + } + + if dsEndpoint == "" { + return "", fmt.Errorf("--deepsource-host-endpoint can not be empty") + } + + if provider == "" { + return "", fmt.Errorf("--oidc-provider can not be empty") + } + + isSupported := ALLOWED_PROVIDERS[provider] + if !isSupported { + return "", fmt.Errorf("provider %s is not supported for OIDC Token exchange (Supported Providers: %v)", provider, ALLOWED_PROVIDERS) + } + if requestId == "" || requestUrl == "" { + var foundIDToken, foundRequestURL bool + // try to fetch the token from the environment variables. + // skipcq: CRT-A0014 + switch provider { + case "github-actions": + requestId, foundIDToken = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL") + if !foundIDToken || !foundRequestURL { + errMsg := `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN" and "ACTIONS_ID_TOKEN_REQUEST_URL" from environment variables. Please make sure you are running this in a GitHub Actions environment with the required permissions. Or, use '--oidc-request-token' and '--oidc-request-url' flags to pass the token and request URL` + return "", fmt.Errorf(errMsg) + } + } + } + oidcToken, err := FetchOIDCTokenFromProvider(requestId, requestUrl) + if err != nil { + return "", err + } + tempDSN, err := ExchangeOIDCTokenForTempDSN(oidcToken, dsEndpoint, provider) + if err != nil { + return "", err + } + return tempDSN, nil +} From 9553330c16d99588790f5459d52842d701e8df8a Mon Sep 17 00:00:00 2001 From: Srijan Saurav Date: Thu, 22 May 2025 12:47:55 +0530 Subject: [PATCH 2/6] chore: add oidc fetch tests --- utils/fetch_oidc_token_test.go | 363 +++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 utils/fetch_oidc_token_test.go diff --git a/utils/fetch_oidc_token_test.go b/utils/fetch_oidc_token_test.go new file mode 100644 index 00000000..24856a67 --- /dev/null +++ b/utils/fetch_oidc_token_test.go @@ -0,0 +1,363 @@ +package utils + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" +) + +const ( + testRequestID = "test-request-id" + testOidcToken = "test-oidc-token" + testDSN = "test-dsn" + testProvider = "github-actions" + testDsEndpoint = "http://localhost:12345" // Mock dsEndpoint +) + +// TestFetchOIDCTokenFromProvider tests the FetchOIDCTokenFromProvider function +func TestFetchOIDCTokenFromProvider(t *testing.T) { + t.Run("success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "GET" { + t.Errorf("Expected GET request, got %s", r.Method) + } + if auth := r.Header.Get("Authorization"); auth != "Bearer "+testRequestID { + t.Errorf("Expected Authorization header 'Bearer %s', got '%s'", testRequestID, auth) + } + if audience := r.URL.Query().Get("audience"); audience != DEEPSOURCE_AUDIENCE { + t.Errorf("Expected audience query param '%s', got '%s'", DEEPSOURCE_AUDIENCE, audience) + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + Value string `json:"value"` + }{Value: testOidcToken}) + })) + defer server.Close() + + token, err := FetchOIDCTokenFromProvider(testRequestID, server.URL) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if token != testOidcToken { + t.Errorf("Expected token '%s', got '%s'", testOidcToken, token) + } + }) + + t.Run("http_new_request_error", func(t *testing.T) { + _, err := FetchOIDCTokenFromProvider(testRequestID, "://invalid-url") + if err == nil { + t.Fatal("Expected error for invalid URL, got nil") + } + }) + + t.Run("client_do_error", func(t *testing.T) { + // No server running at this URL + _, err := FetchOIDCTokenFromProvider(testRequestID, "http://localhost:9999/unreachable") + if err == nil { + t.Fatal("Expected error for unreachable server, got nil") + } + }) + + t.Run("non_200_status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + _, err := FetchOIDCTokenFromProvider(testRequestID, server.URL) + if err == nil { + t.Fatal("Expected error for non-200 status, got nil") + } + expectedErrorMsg := "failed to fetch OIDC token: 404 Not Found" + if !strings.Contains(err.Error(), expectedErrorMsg) { + t.Errorf("Expected error message to contain '%s', got '%s'", expectedErrorMsg, err.Error()) + } + }) + + t.Run("json_decode_error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "not a json") + })) + defer server.Close() + + _, err := FetchOIDCTokenFromProvider(testRequestID, server.URL) + if err == nil { + t.Fatal("Expected error for invalid JSON response, got nil") + } + }) + + t.Run("empty_token_value", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + Value string `json:"value"` + }{Value: ""}) + })) + defer server.Close() + + _, err := FetchOIDCTokenFromProvider(testRequestID, server.URL) + if err == nil { + t.Fatal("Expected error for empty token value, got nil") + } + if !strings.Contains(err.Error(), "empty token") { + t.Errorf("Expected error message to contain 'empty token', got '%s'", err.Error()) + } + }) +} + +// TestExchangeOIDCTokenForTempDSN tests the ExchangeOIDCTokenForTempDSN function +func TestExchangeOIDCTokenForTempDSN(t *testing.T) { + t.Run("success", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + t.Errorf("Expected POST request, got %s", r.Method) + } + expectedPath := fmt.Sprintf("/services/oidc/%s/", testProvider) + if r.URL.Path != expectedPath { + t.Errorf("Expected path '%s', got '%s'", expectedPath, r.URL.Path) + } + if auth := r.Header.Get("Authorization"); auth != "Bearer "+testOidcToken { + t.Errorf("Expected Authorization header 'Bearer %s', got '%s'", testOidcToken, auth) + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + DSN string `json:"access_token"` + }{DSN: testDSN}) + })) + defer server.Close() + + dsn, err := ExchangeOIDCTokenForTempDSN(testOidcToken, server.URL, testProvider) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if dsn != testDSN { + t.Errorf("Expected DSN '%s', got '%s'", testDSN, dsn) + } + }) + + t.Run("http_new_request_error", func(t *testing.T) { + _, err := ExchangeOIDCTokenForTempDSN(testOidcToken, "://invalid-url", testProvider) + if err == nil { + t.Fatal("Expected error for invalid URL, got nil") + } + }) + + t.Run("client_do_error", func(t *testing.T) { + _, err := ExchangeOIDCTokenForTempDSN(testOidcToken, "http://localhost:9999/unreachable", testProvider) + if err == nil { + t.Fatal("Expected error for unreachable server, got nil") + } + }) + + t.Run("non_200_status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusForbidden) + })) + defer server.Close() + + _, err := ExchangeOIDCTokenForTempDSN(testOidcToken, server.URL, testProvider) + if err == nil { + t.Fatal("Expected error for non-200 status, got nil") + } + expectedErrorMsg := "failed to exchange OIDC token for DSN: 403 Forbidden" + if !strings.Contains(err.Error(), expectedErrorMsg) { + t.Errorf("Expected error message to contain '%s', got '%s'", expectedErrorMsg, err.Error()) + } + }) + + t.Run("json_decode_error", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, "not a json") + })) + defer server.Close() + + _, err := ExchangeOIDCTokenForTempDSN(testOidcToken, server.URL, testProvider) + if err == nil { + t.Fatal("Expected error for invalid JSON response, got nil") + } + }) + + t.Run("empty_dsn_value", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + DSN string `json:"access_token"` + }{DSN: ""}) + })) + defer server.Close() + + _, err := ExchangeOIDCTokenForTempDSN(testOidcToken, server.URL, testProvider) + if err == nil { + t.Fatal("Expected error for empty DSN value, got nil") + } + if !strings.Contains(err.Error(), "empty token") { // The error message is "empty token" + t.Errorf("Expected error message to contain 'empty token', got '%s'", err.Error()) + } + }) +} + +// TestGetDSNFromOIDC tests the GetDSNFromOIDC function +func TestGetDSNFromOIDC(t *testing.T) { + // Mock servers for FetchOIDCTokenFromProvider and ExchangeOIDCTokenForTempDSN + var mockTokenServerURL, mockDSNServerURL string + + setupServers := func( + tokenHandler http.HandlerFunc, + dsnHandler http.HandlerFunc, + ) { + tokenServer := httptest.NewServer(tokenHandler) + mockTokenServerURL = tokenServer.URL + t.Cleanup(tokenServer.Close) + + dsnServer := httptest.NewServer(dsnHandler) + mockDSNServerURL = dsnServer.URL + t.Cleanup(dsnServer.Close) + } + + defaultTokenHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + Value string `json:"value"` + }{Value: testOidcToken}) + }) + + defaultDSNHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(struct { + DSN string `json:"access_token"` + }{DSN: testDSN}) + }) + + t.Run("success_with_params", func(t *testing.T) { + setupServers(defaultTokenHandler, defaultDSNHandler) + dsn, err := GetDSNFromOIDC(testRequestID, mockTokenServerURL, mockDSNServerURL, testProvider) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if dsn != testDSN { + t.Errorf("Expected DSN '%s', got '%s'", testDSN, dsn) + } + }) + + t.Run("success_with_github_actions_env_vars", func(t *testing.T) { + setupServers(defaultTokenHandler, defaultDSNHandler) + t.Setenv("GITHUB_ACTIONS", "true") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", testRequestID) + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", mockTokenServerURL) + t.Cleanup(func() { + os.Unsetenv("GITHUB_ACTIONS") + os.Unsetenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + os.Unsetenv("ACTIONS_ID_TOKEN_REQUEST_URL") + }) + + dsn, err := GetDSNFromOIDC("", "", mockDSNServerURL, "") // provider will be inferred + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if dsn != testDSN { + t.Errorf("Expected DSN '%s', got '%s'", testDSN, dsn) + } + }) + + t.Run("error_empty_ds_endpoint", func(t *testing.T) { + _, err := GetDSNFromOIDC(testRequestID, "url", "", testProvider) + if err == nil { + t.Fatal("Expected error for empty dsEndpoint, got nil") + } + if !strings.Contains(err.Error(), "--deepsource-host-endpoint can not be empty") { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_empty_provider_no_env", func(t *testing.T) { + t.Setenv("GITHUB_ACTIONS", "false") // Ensure it's not inferred + t.Cleanup(func() { os.Unsetenv("GITHUB_ACTIONS") }) + _, err := GetDSNFromOIDC(testRequestID, "url", testDsEndpoint, "") + if err == nil { + t.Fatal("Expected error for empty provider, got nil") + } + if !strings.Contains(err.Error(), "--oidc-provider can not be empty") { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_unsupported_provider", func(t *testing.T) { + _, err := GetDSNFromOIDC(testRequestID, "url", testDsEndpoint, "unsupported") + if err == nil { + t.Fatal("Expected error for unsupported provider, got nil") + } + if !strings.Contains(err.Error(), "provider unsupported is not supported") { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_github_actions_env_vars_missing_token", func(t *testing.T) { + t.Setenv("GITHUB_ACTIONS", "true") + // ACTIONS_ID_TOKEN_REQUEST_TOKEN is missing + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_URL", "url") + t.Cleanup(func() { + os.Unsetenv("GITHUB_ACTIONS") + os.Unsetenv("ACTIONS_ID_TOKEN_REQUEST_URL") + }) + _, err := GetDSNFromOIDC("", "", testDsEndpoint, "") + if err == nil { + t.Fatal("Expected error for missing ACTIONS_ID_TOKEN_REQUEST_TOKEN, got nil") + } + if !strings.Contains(err.Error(), `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN"`) { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_github_actions_env_vars_missing_url", func(t *testing.T) { + t.Setenv("GITHUB_ACTIONS", "true") + t.Setenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "token") + // ACTIONS_ID_TOKEN_REQUEST_URL is missing + t.Cleanup(func() { + os.Unsetenv("GITHUB_ACTIONS") + os.Unsetenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + }) + _, err := GetDSNFromOIDC("", "", testDsEndpoint, "") + if err == nil { + t.Fatal("Expected error for missing ACTIONS_ID_TOKEN_REQUEST_URL, got nil") + } + if !strings.Contains(err.Error(), `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN"`) { // Error message covers both + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_fetch_oidc_token_fails", func(t *testing.T) { + tokenHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) // Cause FetchOIDCTokenFromProvider to fail + }) + setupServers(tokenHandler, defaultDSNHandler) + + _, err := GetDSNFromOIDC(testRequestID, mockTokenServerURL, mockDSNServerURL, testProvider) + if err == nil { + t.Fatal("Expected error when FetchOIDCTokenFromProvider fails, got nil") + } + if !strings.Contains(err.Error(), "failed to fetch OIDC token") { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) + + t.Run("error_exchange_oidc_token_fails", func(t *testing.T) { + dsnHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) // Cause ExchangeOIDCTokenForTempDSN to fail + }) + setupServers(defaultTokenHandler, dsnHandler) + + _, err := GetDSNFromOIDC(testRequestID, mockTokenServerURL, mockDSNServerURL, testProvider) + if err == nil { + t.Fatal("Expected error when ExchangeOIDCTokenForTempDSN fails, got nil") + } + if !strings.Contains(err.Error(), "failed to exchange OIDC token for DSN") { + t.Errorf("Unexpected error message: %s", err.Error()) + } + }) +} From df4cc16423c383021fe64eb47f7bbef98168f692 Mon Sep 17 00:00:00 2001 From: Srijan Saurav Date: Thu, 22 May 2025 12:55:13 +0530 Subject: [PATCH 3/6] fix test --- utils/fetch_oidc_token_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/fetch_oidc_token_test.go b/utils/fetch_oidc_token_test.go index 24856a67..d0334f77 100644 --- a/utils/fetch_oidc_token_test.go +++ b/utils/fetch_oidc_token_test.go @@ -288,12 +288,13 @@ func TestGetDSNFromOIDC(t *testing.T) { }) t.Run("error_unsupported_provider", func(t *testing.T) { + t.Setenv("GITHUB_ACTIONS", "false") // Ensure GITHUB_ACTIONS env does not interfere _, err := GetDSNFromOIDC(testRequestID, "url", testDsEndpoint, "unsupported") if err == nil { t.Fatal("Expected error for unsupported provider, got nil") } if !strings.Contains(err.Error(), "provider unsupported is not supported") { - t.Errorf("Unexpected error message: %s", err.Error()) + t.Errorf("Expected error message to contain 'provider unsupported is not supported', got '%s'", err.Error()) } }) From e7e29a050f9ae6de01e4a512afe28fc7eb0f3f18 Mon Sep 17 00:00:00 2001 From: Srijan Saurav Date: Thu, 22 May 2025 13:16:38 +0530 Subject: [PATCH 4/6] fix DeepSource issues --- utils/fetch_oidc_token.go | 2 +- utils/fetch_oidc_token_test.go | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/utils/fetch_oidc_token.go b/utils/fetch_oidc_token.go index 4b72924b..b70870fe 100644 --- a/utils/fetch_oidc_token.go +++ b/utils/fetch_oidc_token.go @@ -119,7 +119,7 @@ func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL") if !foundIDToken || !foundRequestURL { errMsg := `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN" and "ACTIONS_ID_TOKEN_REQUEST_URL" from environment variables. Please make sure you are running this in a GitHub Actions environment with the required permissions. Or, use '--oidc-request-token' and '--oidc-request-url' flags to pass the token and request URL` - return "", fmt.Errorf(errMsg) + return "", fmt.Errorf("%s", errMsg) } } } diff --git a/utils/fetch_oidc_token_test.go b/utils/fetch_oidc_token_test.go index d0334f77..c3ca3c80 100644 --- a/utils/fetch_oidc_token_test.go +++ b/utils/fetch_oidc_token_test.go @@ -63,7 +63,7 @@ func TestFetchOIDCTokenFromProvider(t *testing.T) { }) t.Run("non_200_status", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() @@ -79,7 +79,7 @@ func TestFetchOIDCTokenFromProvider(t *testing.T) { }) t.Run("json_decode_error", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "not a json") })) @@ -92,7 +92,7 @@ func TestFetchOIDCTokenFromProvider(t *testing.T) { }) t.Run("empty_token_value", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { Value string `json:"value"` @@ -155,7 +155,7 @@ func TestExchangeOIDCTokenForTempDSN(t *testing.T) { }) t.Run("non_200_status", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) })) defer server.Close() @@ -171,7 +171,7 @@ func TestExchangeOIDCTokenForTempDSN(t *testing.T) { }) t.Run("json_decode_error", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "not a json") })) @@ -184,7 +184,7 @@ func TestExchangeOIDCTokenForTempDSN(t *testing.T) { }) t.Run("empty_dsn_value", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { DSN string `json:"access_token"` @@ -203,6 +203,7 @@ func TestExchangeOIDCTokenForTempDSN(t *testing.T) { } // TestGetDSNFromOIDC tests the GetDSNFromOIDC function +// skipcq: GO-R1005 func TestGetDSNFromOIDC(t *testing.T) { // Mock servers for FetchOIDCTokenFromProvider and ExchangeOIDCTokenForTempDSN var mockTokenServerURL, mockDSNServerURL string @@ -220,14 +221,14 @@ func TestGetDSNFromOIDC(t *testing.T) { t.Cleanup(dsnServer.Close) } - defaultTokenHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defaultTokenHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { Value string `json:"value"` }{Value: testOidcToken}) }) - defaultDSNHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defaultDSNHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { DSN string `json:"access_token"` @@ -333,7 +334,7 @@ func TestGetDSNFromOIDC(t *testing.T) { }) t.Run("error_fetch_oidc_token_fails", func(t *testing.T) { - tokenHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tokenHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) // Cause FetchOIDCTokenFromProvider to fail }) setupServers(tokenHandler, defaultDSNHandler) @@ -348,7 +349,7 @@ func TestGetDSNFromOIDC(t *testing.T) { }) t.Run("error_exchange_oidc_token_fails", func(t *testing.T) { - dsnHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dsnHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) // Cause ExchangeOIDCTokenForTempDSN to fail }) setupServers(defaultTokenHandler, dsnHandler) From 6282e3fd612438f2441c28755f1fafe768ebd8df Mon Sep 17 00:00:00 2001 From: Srijan Saurav Date: Thu, 22 May 2025 14:35:29 +0530 Subject: [PATCH 5/6] suggestions from review --- utils/fetch_oidc_token.go | 2 +- utils/fetch_oidc_token_test.go | 21 +-------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/utils/fetch_oidc_token.go b/utils/fetch_oidc_token.go index b70870fe..b448cab6 100644 --- a/utils/fetch_oidc_token.go +++ b/utils/fetch_oidc_token.go @@ -117,7 +117,7 @@ func GetDSNFromOIDC(requestId, requestUrl, dsEndpoint, provider string) (string, case "github-actions": requestId, foundIDToken = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") requestUrl, foundRequestURL = os.LookupEnv("ACTIONS_ID_TOKEN_REQUEST_URL") - if !foundIDToken || !foundRequestURL { + if !(foundIDToken && foundRequestURL) { errMsg := `failed to fetch "ACTIONS_ID_TOKEN_REQUEST_TOKEN" and "ACTIONS_ID_TOKEN_REQUEST_URL" from environment variables. Please make sure you are running this in a GitHub Actions environment with the required permissions. Or, use '--oidc-request-token' and '--oidc-request-url' flags to pass the token and request URL` return "", fmt.Errorf("%s", errMsg) } diff --git a/utils/fetch_oidc_token_test.go b/utils/fetch_oidc_token_test.go index c3ca3c80..d493d411 100644 --- a/utils/fetch_oidc_token_test.go +++ b/utils/fetch_oidc_token_test.go @@ -21,16 +21,7 @@ const ( // TestFetchOIDCTokenFromProvider tests the FetchOIDCTokenFromProvider function func TestFetchOIDCTokenFromProvider(t *testing.T) { t.Run("success", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "GET" { - t.Errorf("Expected GET request, got %s", r.Method) - } - if auth := r.Header.Get("Authorization"); auth != "Bearer "+testRequestID { - t.Errorf("Expected Authorization header 'Bearer %s', got '%s'", testRequestID, auth) - } - if audience := r.URL.Query().Get("audience"); audience != DEEPSOURCE_AUDIENCE { - t.Errorf("Expected audience query param '%s', got '%s'", DEEPSOURCE_AUDIENCE, audience) - } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { Value string `json:"value"` @@ -114,16 +105,6 @@ func TestFetchOIDCTokenFromProvider(t *testing.T) { func TestExchangeOIDCTokenForTempDSN(t *testing.T) { t.Run("success", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - t.Errorf("Expected POST request, got %s", r.Method) - } - expectedPath := fmt.Sprintf("/services/oidc/%s/", testProvider) - if r.URL.Path != expectedPath { - t.Errorf("Expected path '%s', got '%s'", expectedPath, r.URL.Path) - } - if auth := r.Header.Get("Authorization"); auth != "Bearer "+testOidcToken { - t.Errorf("Expected Authorization header 'Bearer %s', got '%s'", testOidcToken, auth) - } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { DSN string `json:"access_token"` From 83c7c7370ab9907f68ede62971b62931890b02af Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 09:08:36 +0000 Subject: [PATCH 6/6] refactor: autofix issues in 1 file Resolved issues in utils/fetch_oidc_token_test.go with DeepSource Autofix --- utils/fetch_oidc_token_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/fetch_oidc_token_test.go b/utils/fetch_oidc_token_test.go index d493d411..84a5b8ae 100644 --- a/utils/fetch_oidc_token_test.go +++ b/utils/fetch_oidc_token_test.go @@ -104,7 +104,7 @@ func TestFetchOIDCTokenFromProvider(t *testing.T) { // TestExchangeOIDCTokenForTempDSN tests the ExchangeOIDCTokenForTempDSN function func TestExchangeOIDCTokenForTempDSN(t *testing.T) { t.Run("success", func(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(struct { DSN string `json:"access_token"`