diff --git a/go/internal/gitter/client.go b/go/internal/gitter/client.go index 399beb86c76..44fb2399750 100644 --- a/go/internal/gitter/client.go +++ b/go/internal/gitter/client.go @@ -36,4 +36,7 @@ type Client interface { // GetFileContent retrieves the raw, uncompressed content of a single file path. GetFileContent(ctx context.Context, req *pb.FileContentRequest) (*pb.FileContentResponse, error) + + // GetCommitDiffs retrieves structured commit data and diff patches since a given last synced commit or timestamp. + GetCommitDiffs(ctx context.Context, req *pb.CommitDiffsRequest) (*pb.CommitDiffsResponse, error) } diff --git a/go/internal/gitter/http.go b/go/internal/gitter/http.go index 25dc001cc58..abbfac10487 100644 --- a/go/internal/gitter/http.go +++ b/go/internal/gitter/http.go @@ -193,5 +193,15 @@ func (c *httpClient) GetFileContent(ctx context.Context, req *pb.FileContentRequ return &fileContentResp, nil } +// GetCommitDiffs handles POST /commit-diffs +func (c *httpClient) GetCommitDiffs(ctx context.Context, req *pb.CommitDiffsRequest) (*pb.CommitDiffsResponse, error) { + var commitDiffsResp pb.CommitDiffsResponse + if err := c.doAndUnmarshal(ctx, http.MethodPost, "/commit-diffs", nil, req, &commitDiffsResp); err != nil { + return nil, err + } + + return &commitDiffsResp, nil +} + // Verify interface compliance at compile time. var _ Client = (*httpClient)(nil) diff --git a/go/internal/gitter/http_test.go b/go/internal/gitter/http_test.go index 3b71fad61aa..e1b09876ca0 100644 --- a/go/internal/gitter/http_test.go +++ b/go/internal/gitter/http_test.go @@ -516,3 +516,120 @@ func TestGetFileContent(t *testing.T) { }) } } + +func TestGetCommitDiffs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + req *pb.CommitDiffsRequest + statusCode int + respProto *pb.CommitDiffsResponse + wantResp *pb.CommitDiffsResponse + wantErr error + }{ + { + name: "success", + req: &pb.CommitDiffsRequest{ + Url: "https://github.com/oliverchang/osv-test.git", + LastSyncedCommit: "b9b3fd4732695b83c3068b7b6a14bb372ec31f98", + }, + statusCode: http.StatusOK, + respProto: &pb.CommitDiffsResponse{ + Url: "https://github.com/oliverchang/osv-test.git", + Branch: "master", + LatestCommit: "c0ffee", + NumCommits: 1, + Commits: []*pb.CommitDiff{ + { + Commit: "c0ffee", + Message: "fix: something", + Patch: "diff --git a/a b/b\n...", + FilesChanged: []*pb.FileChange{ + {FromPath: "a", ToPath: "b"}, + }, + }, + }, + }, + wantResp: &pb.CommitDiffsResponse{ + Url: "https://github.com/oliverchang/osv-test.git", + Branch: "master", + LatestCommit: "c0ffee", + NumCommits: 1, + Commits: []*pb.CommitDiff{ + { + Commit: "c0ffee", + Message: "fix: something", + Patch: "diff --git a/a b/b\n...", + FilesChanged: []*pb.FileChange{ + {FromPath: "a", ToPath: "b"}, + }, + }, + }, + }, + }, + { + name: "not found error mapping", + req: &pb.CommitDiffsRequest{ + Url: "https://github.com/oliverchang/osv-test.git", + LastSyncedCommit: "nonexistent", + }, + statusCode: http.StatusNotFound, + wantErr: gitter.ErrRepoNotFound, + }, + { + name: "forbidden error mapping", + req: &pb.CommitDiffsRequest{ + Url: "https://github.com/oliverchang/osv-test.git", + LastSyncedCommit: "b9b3fd4732695b83c3068b7b6a14bb372ec31f98", + }, + statusCode: http.StatusForbidden, + wantErr: gitter.ErrRepoInaccessible, + }, + { + name: "bad request error mapping", + req: &pb.CommitDiffsRequest{ + Url: "https://github.com/oliverchang/osv-test.git", + }, + statusCode: http.StatusBadRequest, + wantErr: gitter.ErrInvalidInput, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(tt.statusCode) + if tt.statusCode == http.StatusOK && tt.respProto != nil { + w.Header().Set("Content-Type", "application/x-protobuf") + payload, _ := proto.Marshal(tt.respProto) + _, _ = w.Write(payload) + } + })) + t.Cleanup(ts.Close) + + client, err := gitter.NewClient(ts.URL, nil) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + + resp, err := client.GetCommitDiffs(context.Background(), tt.req) + if tt.wantErr != nil { + if !errors.Is(err, tt.wantErr) { + t.Fatalf("GetCommitDiffs() error = %v, wantErr = %v", err, tt.wantErr) + } + + return + } + if err != nil { + t.Fatalf("unexpected GetCommitDiffs() error: %v", err) + } + + if diff := cmp.Diff(tt.wantResp, resp, protocmp.Transform()); diff != "" { + t.Errorf("GetCommitDiffs() response mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/go/internal/importer/mock_gitter_client_test.go b/go/internal/importer/mock_gitter_client_test.go index 170f8dbfd68..5c7593ad550 100644 --- a/go/internal/importer/mock_gitter_client_test.go +++ b/go/internal/importer/mock_gitter_client_test.go @@ -2,8 +2,6 @@ package importer import ( "context" - "errors" - "io" "github.com/google/osv.dev/go/internal/gitter" pb "github.com/google/osv.dev/go/internal/gitter/pb/repository" @@ -12,26 +10,12 @@ import ( // mockGitterClient is a mock implementation of gitter.Client for unit testing importer Git operations. // Tests can set file-diffs and file-content funcs to mock specific responses or errors. type mockGitterClient struct { + gitter.Client + fileDiffsFunc func(ctx context.Context, req *pb.FileDiffsRequest) (*pb.FileDiffsResponse, error) fileContentFunc func(ctx context.Context, req *pb.FileContentRequest) (*pb.FileContentResponse, error) } -func (m *mockGitterClient) GetGit(_ context.Context, _ string, _ bool) (io.ReadCloser, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) Cache(_ context.Context, _ string) error { - return nil -} - -func (m *mockGitterClient) GetTags(_ context.Context, _ string) (*pb.TagsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) GetAffectedCommits(_ context.Context, _ *pb.AffectedCommitsRequest) (*pb.AffectedCommitsResponse, error) { - return nil, errors.New("not implemented") -} - func (m *mockGitterClient) GetFileDiffs(ctx context.Context, req *pb.FileDiffsRequest) (*pb.FileDiffsResponse, error) { if m.fileDiffsFunc != nil { return m.fileDiffsFunc(ctx, req) diff --git a/go/internal/recoverer/recoverer_test.go b/go/internal/recoverer/recoverer_test.go index dde482d5179..0f282d9716f 100644 --- a/go/internal/recoverer/recoverer_test.go +++ b/go/internal/recoverer/recoverer_test.go @@ -16,9 +16,7 @@ package recoverer_test import ( "context" - "errors" "fmt" - "io" "net/http" "net/http/httptest" "testing" @@ -41,27 +39,9 @@ import ( ) type mockGitterClient struct { - fileContentFunc func(ctx context.Context, req *gitterpb.FileContentRequest) (*gitterpb.FileContentResponse, error) -} - -func (m *mockGitterClient) GetGit(_ context.Context, _ string, _ bool) (io.ReadCloser, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) Cache(_ context.Context, _ string) error { - return nil -} + gitter.Client -func (m *mockGitterClient) GetTags(_ context.Context, _ string) (*gitterpb.TagsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) GetAffectedCommits(_ context.Context, _ *gitterpb.AffectedCommitsRequest) (*gitterpb.AffectedCommitsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) GetFileDiffs(_ context.Context, _ *gitterpb.FileDiffsRequest) (*gitterpb.FileDiffsResponse, error) { - return nil, errors.New("not implemented") + fileContentFunc func(ctx context.Context, req *gitterpb.FileContentRequest) (*gitterpb.FileContentResponse, error) } func (m *mockGitterClient) GetFileContent(ctx context.Context, req *gitterpb.FileContentRequest) (*gitterpb.FileContentResponse, error) { diff --git a/go/internal/worker/pipeline/githubactions/githubactions_test.go b/go/internal/worker/pipeline/githubactions/githubactions_test.go index f65cd44b7a2..b3b13fdd07f 100644 --- a/go/internal/worker/pipeline/githubactions/githubactions_test.go +++ b/go/internal/worker/pipeline/githubactions/githubactions_test.go @@ -3,8 +3,6 @@ package githubactions import ( "context" "encoding/hex" - "errors" - "io" "testing" "github.com/google/osv.dev/go/internal/gitter" @@ -14,6 +12,8 @@ import ( ) type mockGitterClient struct { + gitter.Client + tagsByRepo map[string]*gitterpb.TagsResponse errByRepo map[string]error callCount map[string]int @@ -35,26 +35,6 @@ func (m *mockGitterClient) GetTags(_ context.Context, repoURL string) (*gitterpb return &gitterpb.TagsResponse{}, nil } -func (m *mockGitterClient) GetGit(_ context.Context, _ string, _ bool) (io.ReadCloser, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) Cache(_ context.Context, _ string) error { - return errors.New("not implemented") -} - -func (m *mockGitterClient) GetAffectedCommits(_ context.Context, _ *gitterpb.AffectedCommitsRequest) (*gitterpb.AffectedCommitsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) GetFileDiffs(_ context.Context, _ *gitterpb.FileDiffsRequest) (*gitterpb.FileDiffsResponse, error) { - return nil, errors.New("not implemented") -} - -func (m *mockGitterClient) GetFileContent(_ context.Context, _ *gitterpb.FileContentRequest) (*gitterpb.FileContentResponse, error) { - return nil, errors.New("not implemented") -} - func makeRef(t *testing.T, label string, hashHex string) *gitterpb.Ref { t.Helper() b, err := hex.DecodeString(hashHex)