github.com/google/osv-scalibr@v0.4.1/veles/secrets/pypiapitoken/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 pypiapitoken_test
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/osv-scalibr/veles"
    25  	"github.com/google/osv-scalibr/veles/secrets/pypiapitoken"
    26  )
    27  
    28  const validatorTestKey = `pypi-AgEIc433aS5vcmcffDgyZDA0MzFkLWMzZjEtNDlhNy1iOWQwLfflMjE5NmNkMjhjNQACKlszLCI22UBiYzQ2Yi05YjNhhTQ5NmItYWIxMHYhMGI3MmEyOWI5MzYiXQAABiCJBI80LFFz0JvS6UIj2LzgV9N-BQnBAD2123Dyu9xs33`
    29  
    30  // mockPyPIServer creates a mock PyPI API server for testing
    31  func mockPyPIServer(t *testing.T, expectedKey string, serverResponseCode int) *httptest.Server {
    32  	t.Helper()
    33  
    34  	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    35  		// Check if it's a GET request to the expected endpoint
    36  		if r.Method != http.MethodPost || r.URL.Path != "/legacy/" {
    37  			t.Errorf("unexpected request: %s %s, expected: POST /legacy/", r.Method, r.URL.Path)
    38  			http.Error(w, "not found", http.StatusNotFound)
    39  			return
    40  		}
    41  
    42  		// Check Authorization header
    43  		authHeader := r.Header.Get("Authorization")
    44  		if !strings.Contains(authHeader, expectedKey) {
    45  			w.Header().Set("Content-Type", "application/json")
    46  			w.WriteHeader(http.StatusForbidden)
    47  			return
    48  		}
    49  
    50  		// Set response
    51  		w.Header().Set("Content-Type", "application/json")
    52  		w.WriteHeader(serverResponseCode)
    53  	}))
    54  }
    55  
    56  func TestValidator(t *testing.T) {
    57  	cases := []struct {
    58  		name               string
    59  		key                string
    60  		serverExpectedKey  string
    61  		serverResponseCode int
    62  		want               veles.ValidationStatus
    63  		expectError        bool
    64  	}{
    65  		{
    66  			name:               "valid_key",
    67  			key:                validatorTestKey,
    68  			serverExpectedKey:  validatorTestKey,
    69  			serverResponseCode: http.StatusBadRequest,
    70  			want:               veles.ValidationValid,
    71  		},
    72  		{
    73  			name:               "invalid_key_unauthorized",
    74  			key:                "random_string",
    75  			serverExpectedKey:  validatorTestKey,
    76  			serverResponseCode: http.StatusUnauthorized,
    77  			want:               veles.ValidationInvalid,
    78  		},
    79  		{
    80  			name:               "server_error",
    81  			serverResponseCode: http.StatusInternalServerError,
    82  			want:               veles.ValidationFailed,
    83  			expectError:        true,
    84  		},
    85  		{
    86  			name:               "bad_gateway",
    87  			serverResponseCode: http.StatusBadGateway,
    88  			want:               veles.ValidationFailed,
    89  			expectError:        true,
    90  		},
    91  	}
    92  
    93  	for _, tc := range cases {
    94  		t.Run(tc.name, func(t *testing.T) {
    95  			// Create a mock server
    96  			server := mockPyPIServer(t, tc.serverExpectedKey, tc.serverResponseCode)
    97  			defer server.Close()
    98  
    99  			validator := pypiapitoken.NewValidator()
   100  			validator.HTTPC = server.Client()
   101  			validator.Endpoint = server.URL + "/legacy/"
   102  
   103  			// Create a test key
   104  			key := pypiapitoken.PyPIAPIToken{Token: tc.key}
   105  
   106  			// Test validation
   107  			got, err := validator.Validate(context.Background(), key)
   108  
   109  			// Check error expectation
   110  			if tc.expectError {
   111  				if err == nil {
   112  					t.Errorf("Validate() expected error, got nil")
   113  				}
   114  			} else {
   115  				if err != nil {
   116  					t.Errorf("Validate() unexpected error: %v", err)
   117  				}
   118  			}
   119  
   120  			// Check validation status
   121  			if got != tc.want {
   122  				t.Errorf("Validate() = %v, want %v", got, tc.want)
   123  			}
   124  		})
   125  	}
   126  }
   127  
   128  func TestValidator_ContextCancellation(t *testing.T) {
   129  	// Create a server that delays response
   130  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   131  		w.WriteHeader(http.StatusOK)
   132  	}))
   133  	defer server.Close()
   134  
   135  	validator := pypiapitoken.NewValidator()
   136  	validator.HTTPC = server.Client()
   137  	validator.Endpoint = server.URL + "/legacy/"
   138  
   139  	key := pypiapitoken.PyPIAPIToken{Token: validatorTestKey}
   140  
   141  	ctx, cancel := context.WithCancel(t.Context())
   142  	cancel()
   143  
   144  	// Test validation with cancelled context
   145  	got, err := validator.Validate(ctx, key)
   146  
   147  	if err == nil {
   148  		t.Errorf("Validate() expected error due to context cancellation, got nil")
   149  	}
   150  	if got != veles.ValidationFailed {
   151  		t.Errorf("Validate() = %v, want %v", got, veles.ValidationFailed)
   152  	}
   153  }
   154  
   155  func TestValidator_InvalidRequest(t *testing.T) {
   156  	// Create a mock server that returns 401 Unauthorized
   157  	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   158  		w.WriteHeader(http.StatusUnauthorized)
   159  	}))
   160  	defer server.Close()
   161  
   162  	validator := pypiapitoken.NewValidator()
   163  	validator.HTTPC = server.Client()
   164  	validator.Endpoint = server.URL + "/legacy/"
   165  
   166  	testCases := []struct {
   167  		name        string
   168  		key         string
   169  		expected    veles.ValidationStatus
   170  		expectError bool
   171  	}{
   172  		{
   173  			name:        "empty_key",
   174  			key:         "",
   175  			expected:    veles.ValidationFailed,
   176  			expectError: true,
   177  		},
   178  		{
   179  			name:        "invalid_key_format",
   180  			key:         "invalid-key-format",
   181  			expected:    veles.ValidationFailed,
   182  			expectError: true,
   183  		},
   184  	}
   185  
   186  	for _, tc := range testCases {
   187  		t.Run(tc.name, func(t *testing.T) {
   188  			key := pypiapitoken.PyPIAPIToken{Token: tc.key}
   189  
   190  			got, err := validator.Validate(context.Background(), key)
   191  
   192  			if tc.expectError {
   193  				if err == nil {
   194  					t.Errorf("Validate() expected error for %s, got nil", tc.name)
   195  				}
   196  			} else {
   197  				if err != nil {
   198  					t.Errorf("Validate() unexpected error for %s: %v", tc.name, err)
   199  				}
   200  			}
   201  			if got != tc.expected {
   202  				t.Errorf("Validate() = %v, want %v for %s", got, tc.expected, tc.name)
   203  			}
   204  		})
   205  	}
   206  }