github.com/google/osv-scalibr@v0.4.1/enricher/huggingfacemeta/huggingfacemeta_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 huggingfacemeta_test
    16  
    17  import (
    18  	"encoding/json"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	"github.com/google/osv-scalibr/enricher/huggingfacemeta"
    27  	"github.com/google/osv-scalibr/inventory"
    28  	"github.com/google/osv-scalibr/veles/secrets/huggingfaceapikey"
    29  )
    30  
    31  func TestEnricher(t *testing.T) {
    32  	type testEnricherSubCase struct {
    33  		name       string
    34  		respBody   any
    35  		statusCode int
    36  		input      inventory.Inventory
    37  		want       inventory.Inventory
    38  		wantErr    error
    39  	}
    40  	validRespBody := func(role string, fineGrainedScope []string) map[string]any {
    41  		return map[string]any{
    42  			"auth": map[string]any{
    43  				"accessToken": map[string]any{
    44  					"role": role,
    45  					"fineGrained": map[string]any{
    46  						"scoped": []map[string]any{
    47  							{"permissions": fineGrainedScope},
    48  						},
    49  					},
    50  				},
    51  			},
    52  		}
    53  	}
    54  	path := "/foo/bar/key.json"
    55  	cases := []struct {
    56  		name string
    57  		subs []testEnricherSubCase
    58  	}{
    59  		{
    60  			name: "Append_role_and_Fine_Grained_Scopes",
    61  			subs: []testEnricherSubCase{
    62  				{
    63  					name:       "supported",
    64  					statusCode: http.StatusOK,
    65  					input: inventory.Inventory{
    66  						Secrets: []*inventory.Secret{
    67  							{
    68  								Secret:   huggingfaceapikey.HuggingfaceAPIKey{Key: "foo"},
    69  								Location: path,
    70  							},
    71  						},
    72  					},
    73  					respBody: validRespBody("read",
    74  						[]string{"inference.endpoints.infer.write", "repo.content.read"}),
    75  					want: inventory.Inventory{
    76  						Secrets: []*inventory.Secret{
    77  							{
    78  								Secret: huggingfaceapikey.HuggingfaceAPIKey{
    79  									Key:              "foo",
    80  									Role:             "read",
    81  									FineGrainedScope: []string{"inference.endpoints.infer.write", "repo.content.read"},
    82  								},
    83  								Location: path,
    84  							},
    85  						},
    86  					},
    87  				},
    88  				{
    89  					name:       "no json response",
    90  					statusCode: http.StatusOK,
    91  					wantErr:    cmpopts.AnyError,
    92  					input: inventory.Inventory{
    93  						Secrets: []*inventory.Secret{
    94  							{
    95  								Secret:   huggingfaceapikey.HuggingfaceAPIKey{Key: "foo2"},
    96  								Location: path,
    97  							},
    98  						},
    99  					},
   100  					respBody: "response body is not json",
   101  					want: inventory.Inventory{
   102  						Secrets: []*inventory.Secret{
   103  							{
   104  								Secret: huggingfaceapikey.HuggingfaceAPIKey{
   105  									Key: "foo2",
   106  								},
   107  								Location: path,
   108  							},
   109  						},
   110  					},
   111  				},
   112  				{
   113  					name:       "non-200 status code",
   114  					statusCode: http.StatusUnauthorized, // 401 Unauthorized
   115  					input: inventory.Inventory{
   116  						Secrets: []*inventory.Secret{
   117  							{
   118  								Secret:   huggingfaceapikey.HuggingfaceAPIKey{Key: "foo3"},
   119  								Location: path,
   120  							},
   121  						},
   122  					},
   123  					want: inventory.Inventory{
   124  						Secrets: []*inventory.Secret{
   125  							{
   126  								Secret: huggingfaceapikey.HuggingfaceAPIKey{
   127  									Key: "foo3",
   128  								},
   129  								Location: path,
   130  							},
   131  						},
   132  					},
   133  				},
   134  			},
   135  		},
   136  	}
   137  	for _, tc := range cases {
   138  		t.Run(tc.name, func(t *testing.T) {
   139  			for _, sc := range tc.subs {
   140  				t.Run(sc.name, func(t *testing.T) {
   141  					// Mock Hugging Face API server responding with the desired role and fineGrainedScope
   142  					ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   143  						if r.URL.Path != "/api/whoami-v2" {
   144  							http.NotFound(w, r)
   145  							return
   146  						}
   147  						// Return the status code defined in the test case
   148  						if sc.statusCode != 0 && sc.statusCode != http.StatusOK {
   149  							w.WriteHeader(sc.statusCode)
   150  							return
   151  						}
   152  
   153  						w.Header().Set("Content-Type", "application/json")
   154  						if _, ok := sc.respBody.(string); ok {
   155  							_, err := w.Write([]byte(sc.respBody.(string)))
   156  							if err != nil {
   157  								return
   158  							}
   159  							return
   160  						}
   161  						_ = json.NewEncoder(w).Encode(sc.respBody)
   162  					}))
   163  					defer ts.Close()
   164  
   165  					// Use enricher configured against the mock server
   166  					enricher := huggingfacemeta.NewWithBaseURL(ts.URL)
   167  
   168  					err := enricher.Enrich(t.Context(), nil, &sc.input)
   169  					if !cmp.Equal(err, sc.wantErr, cmpopts.EquateErrors()) {
   170  						t.Fatalf("Enrich() error: got %v, want %v\n", err, sc.wantErr)
   171  					}
   172  					got := &sc.input
   173  					want := &sc.want
   174  					// We can rely on the order of Secrets in the inventory here, since the enricher is not supposed to change it.
   175  					if diff := cmp.Diff(want, got, cmpopts.EquateErrors(), cmpopts.IgnoreTypes(time.Time{})); diff != "" {
   176  						t.Errorf("Enrich() got diff (-want +got):\n%s", diff)
   177  					}
   178  				})
   179  			}
   180  		})
   181  	}
   182  }