github.com/grafana/pyroscope@v1.18.0/pkg/frontend/vcs/source/find_test.go (about)

     1  package source
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"net/http"
     9  	"path/filepath"
    10  	"sync"
    11  	"testing"
    12  
    13  	"connectrpc.com/connect"
    14  	"github.com/go-kit/log"
    15  	giturl "github.com/kubescape/go-git-url"
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  
    19  	"github.com/grafana/pyroscope/pkg/frontend/vcs/client"
    20  	"github.com/grafana/pyroscope/pkg/frontend/vcs/config"
    21  )
    22  
    23  func newMockVCSClient() *mockVCSClient {
    24  	return &mockVCSClient{
    25  		files: make(map[client.FileRequest]client.File),
    26  	}
    27  }
    28  
    29  type mockFileResponse struct {
    30  	request client.FileRequest
    31  	content string
    32  }
    33  
    34  func newFile(path string) mockFileResponse {
    35  	return mockFileResponse{
    36  		request: client.FileRequest{
    37  			Path: path,
    38  		},
    39  		content: "# Content of " + path,
    40  	}
    41  }
    42  
    43  func (f *mockFileResponse) url() string {
    44  	return fmt.Sprintf(
    45  		"https://github.com/%s/%s/blob/%s/%s",
    46  		f.request.Owner,
    47  		f.request.Repo,
    48  		f.request.Ref,
    49  		f.request.Path,
    50  	)
    51  }
    52  
    53  type mockVCSClient struct {
    54  	mtx              sync.Mutex
    55  	files            map[client.FileRequest]client.File
    56  	searchedSequence []string
    57  }
    58  
    59  func (c *mockVCSClient) GetFile(ctx context.Context, req client.FileRequest) (client.File, error) {
    60  	c.mtx.Lock()
    61  	c.searchedSequence = append(c.searchedSequence, req.Path)
    62  	file, ok := c.files[req]
    63  	c.mtx.Unlock()
    64  	if ok {
    65  		return file, nil
    66  	}
    67  	return client.File{}, client.ErrNotFound
    68  }
    69  
    70  func (c *mockVCSClient) addFiles(files ...mockFileResponse) *mockVCSClient {
    71  	c.mtx.Lock()
    72  	defer c.mtx.Unlock()
    73  	for _, file := range files {
    74  		file.request.Owner = defaultOwner(file.request.Owner)
    75  		file.request.Repo = defaultRepo(file.request.Repo)
    76  		file.request.Ref = defaultRef(file.request.Ref)
    77  		c.files[file.request] = client.File{
    78  			Content: file.content,
    79  			URL:     file.url(),
    80  		}
    81  	}
    82  	return c
    83  }
    84  
    85  func defaultOwner(s string) string {
    86  	if s == "" {
    87  		return "grafana"
    88  	}
    89  	return s
    90  }
    91  
    92  func defaultRepo(s string) string {
    93  	if s == "" {
    94  		return "pyroscope"
    95  	}
    96  	return s
    97  }
    98  func defaultRef(s string) string {
    99  	if s == "" {
   100  		return "main"
   101  	}
   102  	return s
   103  }
   104  
   105  const javaPyroscopeYAML = `---
   106  source_code:
   107    mappings:
   108      - function_name:
   109          - prefix: org/example/rideshare
   110        language: java
   111        source:
   112          local:
   113            path: src/main/java
   114      - function_name:
   115          - prefix: java
   116        language: java
   117        source:
   118          github:
   119            owner: openjdk
   120            repo: jdk
   121            ref: jdk-17+0
   122            path: src/java.base/share/classes
   123      - function_name:
   124          - prefix: org/springframework/http
   125          - prefix: org/springframework/web
   126        language: java
   127        source:
   128          github:
   129            owner: spring-projects
   130            repo: spring-framework
   131            ref: v5.3.20
   132            path: spring-web/src/main/java
   133      - function_name:
   134          - prefix: org/springframework/web/servlet
   135        language: java
   136        source:
   137          github:
   138            owner: spring-projects
   139            repo: spring-framework
   140            ref: v5.3.20
   141            path: spring-webmvc/src/main/java
   142  `
   143  
   144  const goPyroscopeYAML = `---
   145  source_code:
   146    mappings:
   147      - path:
   148          - prefix: $GOROOT/src
   149        language: go
   150        source:
   151         github:
   152          owner: golang
   153          repo: go
   154          ref: go1.24.8
   155          path: src
   156  `
   157  
   158  const goPyroscopeYAMLBazel = `---
   159  source_code:
   160    mappings:
   161      - path:
   162          - prefix: external/gazelle++go_deps+com_github_stretchr_testify
   163        language: go
   164        source:
   165          github:
   166            owner: stretchr
   167            repo: testify
   168            ref: v1.10.0
   169  `
   170  
   171  const pythonPyroscopeYAML = `---
   172  source_code:
   173    mappings:
   174      - path:
   175          - prefix: /app/myproject
   176        language: python
   177        source:
   178          local:
   179            path: src
   180  `
   181  
   182  // TestFileFinder_Find tests the complete happy path integration for find.go using table-driven tests
   183  func TestFileFinder_Find(t *testing.T) {
   184  	tests := []struct {
   185  		name            string
   186  		fileSpec        config.FileSpec
   187  		owner           string
   188  		repo            string
   189  		ref             string
   190  		rootPath        string
   191  		pyroscopeYAML   string
   192  		mockFiles       []mockFileResponse
   193  		expectedContent string
   194  		expectedURL     string
   195  		expectedError   bool
   196  	}{
   197  		// Java tests
   198  		{
   199  			name: "java/mapped-local-path",
   200  			fileSpec: config.FileSpec{
   201  				FunctionName: "org/example/rideshare/RideShareController.orderCar",
   202  			},
   203  			rootPath:      "examples/language-sdk-instrumentation/java/rideshare",
   204  			ref:           "main",
   205  			pyroscopeYAML: javaPyroscopeYAML,
   206  			mockFiles: []mockFileResponse{
   207  				{
   208  					request: client.FileRequest{
   209  						Repo: "pyroscope",
   210  						Path: "examples/language-sdk-instrumentation/java/rideshare/src/main/java/org/example/rideshare/RideShareController.java",
   211  						Ref:  "main",
   212  					},
   213  					content: "# CONTENT RideShareController.java",
   214  				},
   215  			},
   216  			expectedContent: "# CONTENT RideShareController.java",
   217  			expectedURL:     "https://github.com/grafana/pyroscope/blob/main/examples/language-sdk-instrumentation/java/rideshare/src/main/java/org/example/rideshare/RideShareController.java",
   218  			expectedError:   false,
   219  		},
   220  		{
   221  			name: "java/mapped-dependency",
   222  			fileSpec: config.FileSpec{
   223  				FunctionName: "java/lang/Math.floorMod",
   224  			},
   225  			rootPath:      "examples/language-sdk-instrumentation/java/rideshare",
   226  			ref:           "main",
   227  			pyroscopeYAML: javaPyroscopeYAML,
   228  			mockFiles: []mockFileResponse{
   229  				{
   230  					request: client.FileRequest{
   231  						Owner: "openjdk",
   232  						Repo:  "jdk",
   233  						Ref:   "jdk-17+0",
   234  						Path:  "src/java.base/share/classes/java/lang/Math.java",
   235  					},
   236  					content: "# CONTENT Math.java",
   237  				},
   238  			},
   239  			expectedContent: "# CONTENT Math.java",
   240  			expectedURL:     "https://github.com/openjdk/jdk/blob/jdk-17+0/src/java.base/share/classes/java/lang/Math.java",
   241  			expectedError:   false,
   242  		},
   243  		// Go tests
   244  		{
   245  			name: "go/not-mapped-local-path",
   246  			fileSpec: config.FileSpec{
   247  				FunctionName: "github.com/grafana/pyroscope/pkg/compactionworker.(*Worker).runCompaction",
   248  				Path:         "/Users/christian/git/github.com/grafana/pyroscope/pkg/compactionworker/worker.go",
   249  			},
   250  			ref: "main",
   251  			mockFiles: []mockFileResponse{
   252  				{
   253  					request: client.FileRequest{
   254  						Owner: "grafana",
   255  						Repo:  "pyroscope",
   256  						Ref:   "main",
   257  						Path:  "pkg/compactionworker/worker.go",
   258  					},
   259  					content: "# CONTENT worker.go",
   260  				},
   261  			},
   262  			expectedContent: "# CONTENT worker.go",
   263  			expectedURL:     "https://github.com/grafana/pyroscope/blob/main/pkg/compactionworker/worker.go",
   264  			expectedError:   false,
   265  		},
   266  		{
   267  			name: "go/not-mapped-dependency-gomod",
   268  			fileSpec: config.FileSpec{
   269  				FunctionName: "github.com/parquet-go/parquet-go.(*bufferPool).newBuffer",
   270  				Path:         "/Users/christian/.golang/packages/pkg/mod/github.com/parquet-go/parquet-go@v0.23.0/buffer.go",
   271  			},
   272  			ref: "main",
   273  			mockFiles: []mockFileResponse{
   274  				{
   275  					request: client.FileRequest{
   276  						Path: "go.mod",
   277  					},
   278  					content: `
   279  module github.com/grafana/pyroscope
   280  
   281  go 1.24.6
   282  
   283  toolchain go1.24.9
   284  
   285  require (
   286  	github.com/parquet-go/parquet-go v0.25.0
   287  )
   288  `,
   289  				},
   290  				{
   291  					request: client.FileRequest{
   292  						Owner: "parquet-go",
   293  						Repo:  "parquet-go",
   294  						Ref:   "v0.25.0",
   295  						Path:  "buffer.go",
   296  					},
   297  					content: "# CONTENT buffer.go",
   298  				},
   299  			},
   300  			expectedContent: "# CONTENT buffer.go",
   301  			expectedURL:     "https://github.com/parquet-go/parquet-go/blob/v0.25.0/buffer.go",
   302  			expectedError:   false,
   303  		},
   304  		{
   305  			name: "go/not-mapped-dependency-no-gomod-file",
   306  			fileSpec: config.FileSpec{
   307  				FunctionName: "github.com/parquet-go/parquet-go.(*bufferPool).newBuffer",
   308  				// without go.mod file in the version of the dependency comes from the file path
   309  				Path: "/Users/christian/.golang/packages/pkg/mod/github.com/parquet-go/parquet-go@v0.23.0/buffer.go",
   310  			},
   311  			ref: "main",
   312  			mockFiles: []mockFileResponse{
   313  				{
   314  					request: client.FileRequest{
   315  						Owner: "parquet-go",
   316  						Repo:  "parquet-go",
   317  						Ref:   "v0.23.0",
   318  						Path:  "buffer.go",
   319  					},
   320  					content: "# CONTENT buffer.go",
   321  				},
   322  			},
   323  			expectedContent: "# CONTENT buffer.go",
   324  			expectedURL:     "https://github.com/parquet-go/parquet-go/blob/v0.23.0/buffer.go",
   325  			expectedError:   false,
   326  		},
   327  		{
   328  			name: "go/not-mapped-dependency-vendor",
   329  			fileSpec: config.FileSpec{
   330  				FunctionName: "github.com/grafana/loki/v3/pkg/iter/v2.(*PeekIter).cacheNext",
   331  				Path:         "/src/enterprise-logs/vendor/github.com/grafana/loki/v3/pkg/iter/v2/iter.go",
   332  			},
   333  			ref:  "HEAD",
   334  			repo: "enterprise-logs",
   335  			mockFiles: []mockFileResponse{
   336  				{
   337  					request: client.FileRequest{
   338  						Owner: "grafana",
   339  						Repo:  "enterprise-logs",
   340  						Ref:   "HEAD",
   341  						Path:  "vendor/github.com/grafana/loki/v3/pkg/iter/v2/iter.go",
   342  					},
   343  					content: "# CONTENT iter.go",
   344  				},
   345  			},
   346  			expectedContent: "# CONTENT iter.go",
   347  			expectedURL:     "https://github.com/grafana/enterprise-logs/blob/HEAD/vendor/github.com/grafana/loki/v3/pkg/iter/v2/iter.go",
   348  			expectedError:   false,
   349  		},
   350  		{
   351  			name: "go/not-mapped-stdlib",
   352  			fileSpec: config.FileSpec{
   353  				FunctionName: "bufio.(*Reader).ReadSlice",
   354  				Path:         "/usr/local/go/src/bufio/bufio.go",
   355  			},
   356  			mockFiles: []mockFileResponse{
   357  				{
   358  					request: client.FileRequest{
   359  						Owner: "golang",
   360  						Repo:  "go",
   361  						Ref:   "master",
   362  						Path:  "src/bufio/bufio.go",
   363  					},
   364  					content: "# CONTENT bufio.go",
   365  				},
   366  			},
   367  			expectedContent: "# CONTENT bufio.go",
   368  			expectedURL:     "https://github.com/golang/go/blob/master/src/bufio/bufio.go",
   369  			expectedError:   false,
   370  		},
   371  		{
   372  			name: "go/mapped-stdlib",
   373  			fileSpec: config.FileSpec{
   374  				FunctionName: "bufio.(*Reader).ReadSlice",
   375  				Path:         "/usr/local/go/src/bufio/bufio.go",
   376  			},
   377  			pyroscopeYAML: goPyroscopeYAML,
   378  			mockFiles: []mockFileResponse{
   379  				{
   380  					request: client.FileRequest{
   381  						Owner: "golang",
   382  						Repo:  "go",
   383  						Ref:   "go1.24.8",
   384  						Path:  "src/bufio/bufio.go",
   385  					},
   386  					content: "# CONTENT bufio.go",
   387  				},
   388  			},
   389  			expectedContent: "# CONTENT bufio.go",
   390  			expectedURL:     "https://github.com/golang/go/blob/go1.24.8/src/bufio/bufio.go",
   391  			expectedError:   false,
   392  		},
   393  		{
   394  			name: "go/mapped-dependency-bazel",
   395  			fileSpec: config.FileSpec{
   396  				FunctionName: "github.com/stretchr/testify/require.NoError",
   397  				Path:         "external/gazelle++go_deps+com_github_stretchr_testify/require/require.go",
   398  			},
   399  			pyroscopeYAML: goPyroscopeYAMLBazel,
   400  			owner:         "bazel-contrib",
   401  			repo:          "rules_go",
   402  			rootPath:      "examples/basic_gazelle",
   403  			ref:           "v0.59.0",
   404  
   405  			mockFiles: []mockFileResponse{
   406  				{
   407  					request: client.FileRequest{
   408  						Owner: "stretchr",
   409  						Repo:  "testify",
   410  						Ref:   "v1.10.0",
   411  						Path:  "require/require.go",
   412  					},
   413  					content: "# CONTENT require.go",
   414  				},
   415  			},
   416  			expectedContent: "# CONTENT require.go",
   417  			expectedURL:     "https://github.com/stretchr/testify/blob/v1.10.0/require/require.go",
   418  			expectedError:   false,
   419  		},
   420  		{
   421  			name: "python/stdlib",
   422  			fileSpec: config.FileSpec{
   423  				FunctionName: "difflib.SequenceMatcher.find_longest_match",
   424  				Path:         "/usr/lib/python3.12/difflib.py",
   425  			},
   426  			ref: "main",
   427  			mockFiles: []mockFileResponse{
   428  				{
   429  					request: client.FileRequest{
   430  						Owner: "python",
   431  						Repo:  "cpython",
   432  						Ref:   "3.12",
   433  						Path:  "Lib/difflib.py",
   434  					},
   435  					content: "# CONTENT difflib.py",
   436  				},
   437  			},
   438  			expectedContent: "# CONTENT difflib.py",
   439  			expectedURL:     "https://github.com/python/cpython/blob/3.12/Lib/difflib.py",
   440  			expectedError:   false,
   441  		},
   442  		{
   443  			name: "python/mapped-local-path",
   444  			fileSpec: config.FileSpec{
   445  				FunctionName: "myproject.main.run",
   446  				Path:         "/app/myproject/module/main.py",
   447  			},
   448  			rootPath:      "examples/python-app",
   449  			ref:           "main",
   450  			pyroscopeYAML: pythonPyroscopeYAML,
   451  			mockFiles: []mockFileResponse{
   452  				{
   453  					request: client.FileRequest{
   454  						Owner: "grafana",
   455  						Repo:  "pyroscope",
   456  						Ref:   "main",
   457  						Path:  "examples/python-app/src/module/main.py",
   458  					},
   459  					content: "# CONTENT main.py",
   460  				},
   461  			},
   462  			expectedContent: "# CONTENT main.py",
   463  			expectedURL:     "https://github.com/grafana/pyroscope/blob/main/examples/python-app/src/module/main.py",
   464  			expectedError:   false,
   465  		},
   466  		{
   467  			name: "python/relative-path",
   468  			fileSpec: config.FileSpec{
   469  				FunctionName: "ListRecommendations",
   470  				Path:         "recommendation_server.py",
   471  			},
   472  			rootPath: "examples/python-app",
   473  			ref:      "main",
   474  			mockFiles: []mockFileResponse{
   475  				{
   476  					request: client.FileRequest{
   477  						Owner: "grafana",
   478  						Repo:  "pyroscope",
   479  						Ref:   "main",
   480  						Path:  "examples/python-app/recommendation_server.py",
   481  					},
   482  					content: "# CONTENT recommendation_server.py",
   483  				},
   484  			},
   485  			expectedContent: "# CONTENT recommendation_server.py",
   486  			expectedURL:     "https://github.com/grafana/pyroscope/blob/main/examples/python-app/recommendation_server.py",
   487  			expectedError:   false,
   488  		},
   489  		{
   490  			name: "fallback/unknown-file-extension",
   491  			fileSpec: config.FileSpec{
   492  				FunctionName: "some.function",
   493  				Path:         "scripts/example.unknown_extension",
   494  			},
   495  			ref: "main",
   496  			mockFiles: []mockFileResponse{
   497  				{
   498  					request: client.FileRequest{
   499  						Owner: "grafana",
   500  						Repo:  "pyroscope",
   501  						Ref:   "main",
   502  						Path:  "scripts/example.unknown_extension",
   503  					},
   504  					content: "# Python script content\nprint('hello')",
   505  				},
   506  			},
   507  			expectedContent: "# Python script content\nprint('hello')",
   508  			expectedURL:     "https://github.com/grafana/pyroscope/blob/main/scripts/example.unknown_extension",
   509  			expectedError:   false,
   510  		},
   511  	}
   512  
   513  	for _, tt := range tests {
   514  		t.Run(tt.name, func(t *testing.T) {
   515  			ctx := context.Background()
   516  
   517  			// Setup mock VCS client
   518  			mockClient := newMockVCSClient()
   519  
   520  			// Populate pyroscopeYAML content into first mock file (if present)
   521  			mockFiles := tt.mockFiles
   522  			if tt.pyroscopeYAML != "" {
   523  				mockFiles = append(mockFiles, mockFileResponse{
   524  					request: client.FileRequest{
   525  						Owner: tt.owner,
   526  						Repo:  tt.repo,
   527  						Ref:   tt.ref,
   528  						Path:  filepath.Join(tt.rootPath, ".pyroscope.yaml"),
   529  					},
   530  					content: tt.pyroscopeYAML,
   531  				})
   532  			}
   533  			mockClient.addFiles(mockFiles...)
   534  
   535  			// Setup repository URL
   536  			repoURL, err := giturl.NewGitURL(fmt.Sprintf("https://github.com/%s/%s", defaultOwner(tt.owner), defaultRepo(tt.repo)))
   537  			require.NoError(t, err)
   538  
   539  			// Create HTTP client
   540  			httpClient := &http.Client{}
   541  
   542  			// Create FileFinder
   543  			finder := NewFileFinder(
   544  				mockClient,
   545  				repoURL,
   546  				tt.fileSpec,
   547  				tt.rootPath,
   548  				defaultRef(tt.ref),
   549  				httpClient,
   550  				log.NewNopLogger(),
   551  			)
   552  
   553  			// Execute the Find method
   554  			response, err := finder.Find(ctx)
   555  
   556  			// Assertions
   557  			if tt.expectedError {
   558  				require.Error(t, err)
   559  			} else {
   560  				require.NoError(t, err, "Find should succeed")
   561  				require.NotNil(t, response, "Response should not be nil")
   562  
   563  				// Decode and verify content
   564  				decodedContent, err := base64.StdEncoding.DecodeString(response.Content)
   565  				require.NoError(t, err, "Content should be valid base64")
   566  				assert.Equal(t, tt.expectedContent, string(decodedContent), "Content should match expected file")
   567  
   568  				// Verify URL
   569  				assert.Equal(t, tt.expectedURL, response.URL, "URL should point to correct location")
   570  			}
   571  		})
   572  	}
   573  }
   574  
   575  // TestFileFinder_Find_FileNotFound tests that Find returns client.ErrNotFound when files are not found
   576  func TestFileFinder_Find_FileNotFound(t *testing.T) {
   577  	tests := []struct {
   578  		name          string
   579  		fileSpec      config.FileSpec
   580  		rootPath      string
   581  		ref           string
   582  		pyroscopeYAML string
   583  	}{
   584  		{
   585  			name: "fallback/file-not-found",
   586  			fileSpec: config.FileSpec{
   587  				FunctionName: "some.function",
   588  				Path:         "nonexistent/file.txt",
   589  			},
   590  			ref: "main",
   591  		},
   592  		{
   593  			name: "go/local-file-not-found",
   594  			fileSpec: config.FileSpec{
   595  				FunctionName: "github.com/grafana/pyroscope/pkg/foo.Bar",
   596  				Path:         "/Users/christian/git/github.com/grafana/pyroscope/pkg/foo/bar.go",
   597  			},
   598  			ref: "main",
   599  		},
   600  		{
   601  			name: "go/stdlib-not-found",
   602  			fileSpec: config.FileSpec{
   603  				FunctionName: "bufio.(*Reader).ReadSlice",
   604  				Path:         "/usr/local/go/src/bufio/bufio.go",
   605  			},
   606  			ref: "main",
   607  		},
   608  		{
   609  			name: "python/stdlib-not-found",
   610  			fileSpec: config.FileSpec{
   611  				FunctionName: "difflib.SequenceMatcher.find_longest_match",
   612  				Path:         "/usr/lib/python3.12/difflib.py",
   613  			},
   614  			ref: "main",
   615  		},
   616  		{
   617  			name: "python/no-stdlib-no-mappings",
   618  			fileSpec: config.FileSpec{
   619  				FunctionName: "myapp.module.function",
   620  				Path:         "/app/myapp/module.py",
   621  			},
   622  			ref: "main",
   623  		},
   624  		{
   625  			name: "python/mappings-file-not-found",
   626  			fileSpec: config.FileSpec{
   627  				FunctionName: "myproject.main.run",
   628  				Path:         "/app/myproject/module/main.py",
   629  			},
   630  			rootPath:      "examples/python-app",
   631  			ref:           "main",
   632  			pyroscopeYAML: pythonPyroscopeYAML,
   633  		},
   634  		{
   635  			name: "java/no-mappings",
   636  			fileSpec: config.FileSpec{
   637  				FunctionName: "org/example/MyClass.myMethod",
   638  				Path:         "",
   639  			},
   640  			ref: "main",
   641  			pyroscopeYAML: `---
   642  source_code:
   643    mappings:
   644      - function_name:
   645          - prefix: org/example
   646        language: java
   647        source:
   648          local:
   649            path: src/main/java
   650  `,
   651  		},
   652  		{
   653  			name: "go/dependency-not-found",
   654  			fileSpec: config.FileSpec{
   655  				FunctionName: "github.com/parquet-go/parquet-go.(*bufferPool).newBuffer",
   656  				Path:         "/Users/christian/.golang/packages/pkg/mod/github.com/parquet-go/parquet-go@v0.23.0/buffer.go",
   657  			},
   658  			ref: "main",
   659  		},
   660  	}
   661  
   662  	for _, tt := range tests {
   663  		t.Run(tt.name, func(t *testing.T) {
   664  			ctx := context.Background()
   665  
   666  			// Setup mock VCS client with no files (everything returns ErrNotFound)
   667  			mockClient := newMockVCSClient()
   668  
   669  			// Add pyroscope.yaml if provided (but no actual source files)
   670  			if tt.pyroscopeYAML != "" {
   671  				mockClient.addFiles(mockFileResponse{
   672  					request: client.FileRequest{
   673  						Ref:  tt.ref,
   674  						Path: filepath.Join(tt.rootPath, ".pyroscope.yaml"),
   675  					},
   676  					content: tt.pyroscopeYAML,
   677  				})
   678  			}
   679  
   680  			// Setup repository URL
   681  			repoURL, err := giturl.NewGitURL("https://github.com/grafana/pyroscope")
   682  			require.NoError(t, err)
   683  
   684  			// Create FileFinder
   685  			finder := NewFileFinder(
   686  				mockClient,
   687  				repoURL,
   688  				tt.fileSpec,
   689  				tt.rootPath,
   690  				defaultRef(tt.ref),
   691  				&http.Client{},
   692  				log.NewNopLogger(),
   693  			)
   694  
   695  			// Execute the Find method
   696  			response, err := finder.Find(ctx)
   697  
   698  			// Assertions
   699  			require.Error(t, err, "Find should return an error when file is not found")
   700  
   701  			// Check if error is a connect error with CodeNotFound
   702  			var connectErr *connect.Error
   703  			if errors.As(err, &connectErr) {
   704  				require.Equal(t, connect.CodeNotFound, connectErr.Code(), "Connect error should have CodeNotFound")
   705  			} else {
   706  				// Fallback check for client.ErrNotFound
   707  				require.ErrorIs(t, err, client.ErrNotFound, "Error should be client.ErrNotFound")
   708  			}
   709  			require.Nil(t, response, "Response should be nil when file is not found")
   710  		})
   711  	}
   712  }