github.com/google/osv-scalibr@v0.4.1/veles/secrets/cratesioapitoken/validator_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cratesioapitoken_test
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"net/url"
    22  	"strings"
    23  	"testing"
    24  
    25  	"github.com/google/osv-scalibr/veles"
    26  	"github.com/google/osv-scalibr/veles/secrets/cratesioapitoken"
    27  )
    28  
    29  const validatorValidTestKey = "cioAbCdEfGhIjKlMnOpQrStUvWxYz123456"
    30  
    31  // mockTransport redirects requests to the test server
    32  type mockTransport struct {
    33  	testServer *httptest.Server
    34  }
    35  
    36  func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    37  	// Replace the original URL with our test server URL
    38  	if req.URL.Host == "crates.io" {
    39  		testURL, _ := url.Parse(m.testServer.URL)
    40  		req.URL.Scheme = testURL.Scheme
    41  		req.URL.Host = testURL.Host
    42  	}
    43  	return http.DefaultTransport.RoundTrip(req)
    44  }
    45  
    46  // mockCratesioServer creates a mock Crates.io API server for testing
    47  func mockCratesioServer(t *testing.T, expectedKey string) *httptest.Server {
    48  	t.Helper()
    49  
    50  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    51  		// Check if it's a PUT request to the expected crates endpoint
    52  		expectedPath := "/api/v1/crates/osvscalibr"
    53  		if r.Method != http.MethodPut || !strings.HasPrefix(r.URL.Path, expectedPath) {
    54  			t.Errorf("unexpected request: %s %s, expected: PUT %s", r.Method, r.URL.Path, expectedPath)
    55  			http.Error(w, "not found", http.StatusNotFound)
    56  			return
    57  		}
    58  
    59  		// Check Authorization header format
    60  		authHeader := r.Header.Get("Authorization")
    61  		if !strings.HasPrefix(authHeader, "Bearer ") {
    62  			w.WriteHeader(http.StatusInternalServerError)
    63  			return
    64  		}
    65  
    66  		w.Header().Set("Content-Type", "application/json")
    67  		// Check Authorization
    68  		if authHeader == "Bearer "+expectedKey {
    69  			// Valid token
    70  			w.WriteHeader(http.StatusNotFound)
    71  			_, _ = w.Write([]byte(`{"errors":[{"detail":"crate velesvalidationtestcrate  does not exist"}]}`))
    72  		} else {
    73  			w.WriteHeader(http.StatusForbidden)
    74  			_, _ = w.Write([]byte(`{"errors":[{"detail":"authentication failed"}]}`))
    75  			return
    76  		}
    77  	}))
    78  }
    79  
    80  func TestValidator(t *testing.T) {
    81  	cases := []struct {
    82  		name              string
    83  		key               string
    84  		serverExpectedKey string
    85  		want              veles.ValidationStatus
    86  		expectError       bool
    87  	}{
    88  		{
    89  			name:              "valid_key",
    90  			key:               validatorValidTestKey,
    91  			serverExpectedKey: validatorValidTestKey,
    92  			want:              veles.ValidationValid,
    93  		},
    94  		{
    95  			name:              "invalid_key_unauthorized",
    96  			key:               "random_string",
    97  			serverExpectedKey: validatorValidTestKey[3:],
    98  			want:              veles.ValidationInvalid,
    99  		},
   100  	}
   101  
   102  	for _, tc := range cases {
   103  		t.Run(tc.name, func(t *testing.T) {
   104  			// Create a mock server
   105  			server := mockCratesioServer(t, tc.serverExpectedKey)
   106  			defer server.Close()
   107  
   108  			// Create a client with custom transport
   109  			client := &http.Client{
   110  				Transport: &mockTransport{testServer: server},
   111  			}
   112  
   113  			// Create a validator with a mock client
   114  			validator := cratesioapitoken.NewValidator()
   115  			validator.HTTPC = client
   116  
   117  			// Create a test key
   118  			key := cratesioapitoken.CratesIOAPItoken{Token: tc.key}
   119  
   120  			// Test validation
   121  			got, err := validator.Validate(t.Context(), key)
   122  
   123  			// Check error expectation
   124  			if tc.expectError {
   125  				if err == nil {
   126  					t.Errorf("Validate() expected error, got nil")
   127  				}
   128  			} else {
   129  				if err != nil {
   130  					t.Errorf("Validate() unexpected error: %v", err)
   131  				}
   132  			}
   133  
   134  			// Check validation status
   135  			if got != tc.want {
   136  				t.Errorf("Validate() = %v, want %v", got, tc.want)
   137  			}
   138  		})
   139  	}
   140  }
   141  
   142  func TestValidator_ContextCancellation(t *testing.T) {
   143  	// Create a server that delays response
   144  	server := mockCratesioServer(t, "Bearer "+validatorValidTestKey)
   145  	defer server.Close()
   146  
   147  	// Create a client with custom transport
   148  	client := &http.Client{
   149  		Transport: &mockTransport{testServer: server},
   150  	}
   151  
   152  	validator := cratesioapitoken.NewValidator()
   153  	validator.HTTPC = client
   154  
   155  	key := cratesioapitoken.CratesIOAPItoken{Token: validatorValidTestKey}
   156  
   157  	// Create a cancelled context
   158  	ctx, cancel := context.WithCancel(t.Context())
   159  	cancel()
   160  
   161  	// Test validation with cancelled context
   162  	got, err := validator.Validate(ctx, key)
   163  
   164  	if err == nil {
   165  		t.Errorf("Validate() expected error due to context cancellation, got nil")
   166  	}
   167  	if got != veles.ValidationFailed {
   168  		t.Errorf("Validate() = %v, want %v", got, veles.ValidationFailed)
   169  	}
   170  }
   171  
   172  func TestValidator_InvalidRequest(t *testing.T) {
   173  	// Create a mock server that returns 403 Forbidden for invalid keys
   174  	server := mockCratesioServer(t, "invalid-key")
   175  	defer server.Close()
   176  
   177  	// Create a client with custom transport
   178  	client := &http.Client{
   179  		Transport: &mockTransport{testServer: server},
   180  	}
   181  
   182  	validator := cratesioapitoken.NewValidator()
   183  	validator.HTTPC = client
   184  
   185  	testCases := []struct {
   186  		name     string
   187  		key      string
   188  		expected veles.ValidationStatus
   189  		wantErr  bool
   190  	}{
   191  		{
   192  			name:     "empty_key",
   193  			key:      "",
   194  			expected: veles.ValidationFailed,
   195  			wantErr:  true,
   196  		},
   197  		{
   198  			name:     "invalid_key_format",
   199  			key:      "invalid-key-format",
   200  			expected: veles.ValidationInvalid,
   201  		},
   202  	}
   203  
   204  	for _, tc := range testCases {
   205  		t.Run(tc.name, func(t *testing.T) {
   206  			key := cratesioapitoken.CratesIOAPItoken{Token: tc.key}
   207  
   208  			got, err := validator.Validate(t.Context(), key)
   209  			if tc.wantErr {
   210  				if err == nil {
   211  					t.Errorf("Validate() expected error, got nil")
   212  				}
   213  			} else {
   214  				if err != nil {
   215  					t.Errorf("Validate() unexpected error for %s: %v", tc.name, err)
   216  				}
   217  			}
   218  
   219  			if got != tc.expected {
   220  				t.Errorf("Validate() = %v, want %v for %s", got, tc.expected, tc.name)
   221  			}
   222  		})
   223  	}
   224  }