blob: eb1b766b481199318f4d914942b9edde95221424 [file] [log] [blame]
// Copyright 2022 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gerrit
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/data/caching/lru"
"go.chromium.org/luci/common/errors"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/gae/impl/memory"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/auth/authtest"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestPSSAMigrationClient(t *testing.T) {
t.Parallel()
Convey("PSSAMigrationClient", t, func() {
ctx := memory.Use(context.Background())
ctx, tclock := testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
ctx = authtest.MockAuthConfig(ctx)
const migratedProject = "lProject"
So(projectsToMigrate.Has(migratedProject), ShouldBeFalse)
notMigratedProject, ok := projectsToMigrate.Peek()
So(ok, ShouldBeTrue)
So(notMigratedProject, ShouldNotBeEmpty)
var httpRequests []*http.Request
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This test calls ListChanges RPC because it is the easiest to mock empty
// response for.
httpRequests = append(httpRequests, r)
_, _ = w.Write([]byte(")]}'\n[]")) // no changes.
}))
defer srv.Close()
u, err := url.Parse(srv.URL)
So(err, ShouldBeNil)
gHost := u.Host
So(datastore.Put(ctx, &netrcToken{gHost, "legacy-tok"}), ShouldBeNil)
factory := pssaMigrationFactory{
passClientfactory: fakeFactory{},
baseTransport: srv.Client().Transport,
legacyTokenCache: lru.New(10),
}
Convey("For migrated project", func() {
Convey("Work", func() {
client, err := factory.MakeClient(ctx, gHost, migratedProject)
So(err, ShouldBeNil)
So(client.(pssaMigrationClient).legacyClient, ShouldBeNil)
res, err := client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ListChangesResponse{})
So(httpRequests, ShouldBeEmpty)
})
Convey("Error is passed through", func() {
factory.passClientfactory = fakeFactory{returnErr: errors.New("bad request")}
client, err := factory.MakeClient(ctx, gHost, migratedProject)
So(err, ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldErrLike, "bad request")
})
})
Convey("For not migrated project", func() {
Convey("Request goes through using pssa", func() {
client, err := factory.MakeClient(ctx, gHost, notMigratedProject)
So(err, ShouldBeNil)
res, err := client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ListChangesResponse{})
So(httpRequests, ShouldBeEmpty)
})
Convey("Fallback to legacy netrc token", func() {
Convey("When pssa client returns empty project token error", func() {
factory.passClientfactory = fakeFactory{
// Mimic production error.
returnErr: status.Errorf(codes.Internal, "failed to execute GET HTTP request: %s", errors.Annotate(errEmptyProjectToken, "project %q", notMigratedProject).Err()),
}
})
Convey("When pssa client returns permission denied", func() {
factory.passClientfactory = fakeFactory{
returnErr: status.Errorf(codes.PermissionDenied, "no access"),
}
})
client, err := factory.MakeClient(ctx, gHost, notMigratedProject)
So(err, ShouldBeNil)
res, err := client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldBeNil)
So(res, ShouldResembleProto, &gerritpb.ListChangesResponse{})
So(httpRequests, ShouldHaveLength, 1)
tokenB64 := base64.StdEncoding.EncodeToString([]byte("legacy-tok"))
So(httpRequests[0].Header["Authorization"], ShouldResemble, []string{"Basic " + tokenB64})
})
Convey("Error when missing netrc token", func() {
So(datastore.Delete(ctx, &netrcToken{gHost, "legacy-tok"}), ShouldBeNil)
factory.passClientfactory = fakeFactory{
// Mimic production error.
returnErr: status.Errorf(codes.Internal, "failed to execute GET HTTP request: %s", errors.Annotate(errEmptyProjectToken, "project %q", notMigratedProject).Err()),
}
client, err := factory.MakeClient(ctx, gHost, notMigratedProject)
So(err, ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldErrLike, "No legacy credentials for host", gHost)
})
Convey("Pass through other errors", func() {
factory.passClientfactory = fakeFactory{
returnErr: status.Errorf(codes.InvalidArgument, "invalid query"),
}
client, err := factory.MakeClient(ctx, gHost, notMigratedProject)
So(err, ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldErrLike, "invalid query")
So(httpRequests, ShouldBeEmpty)
})
Convey("Cache token for 10 minutes", func() {
factory.passClientfactory = fakeFactory{
// Mimic production error.
returnErr: status.Errorf(codes.Internal, "failed to execute GET HTTP request: %s", errors.Annotate(errEmptyProjectToken, "project %q", notMigratedProject).Err()),
}
client, err := factory.MakeClient(ctx, gHost, notMigratedProject)
So(err, ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldBeNil)
tclock.Add(10*time.Minute + 1*time.Second)
So(datastore.Put(ctx, &netrcToken{gHost, "legacy-tok-2"}), ShouldBeNil)
_, err = client.ListChanges(ctx, &gerritpb.ListChangesRequest{})
So(err, ShouldBeNil)
So(httpRequests, ShouldHaveLength, 2)
So(httpRequests[0].Header["Authorization"], ShouldResemble, []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("legacy-tok"))})
So(httpRequests[1].Header["Authorization"], ShouldResemble, []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("legacy-tok-2"))})
})
})
})
}
type fakeFactory struct {
returnErr error
}
func (f fakeFactory) MakeClient(ctx context.Context, gerritHost, luciProject string) (Client, error) {
return fakePSSAClient{err: f.returnErr}, nil
}
func (f fakeFactory) MakeMirrorIterator(ctx context.Context) *MirrorIterator {
return nil
}
type fakePSSAClient struct {
err error
}
func (f fakePSSAClient) ListChanges(ctx context.Context, in *gerritpb.ListChangesRequest, opts ...grpc.CallOption) (*gerritpb.ListChangesResponse, error) {
// ListChanges is the only rpc called.
if f.err != nil {
return nil, f.err
}
return &gerritpb.ListChangesResponse{}, nil
}
func (f fakePSSAClient) GetChange(ctx context.Context, in *gerritpb.GetChangeRequest, opts ...grpc.CallOption) (*gerritpb.ChangeInfo, error) {
panic(fmt.Errorf("not implemented"))
}
func (f fakePSSAClient) GetRelatedChanges(ctx context.Context, in *gerritpb.GetRelatedChangesRequest, opts ...grpc.CallOption) (*gerritpb.GetRelatedChangesResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (f fakePSSAClient) ListFiles(ctx context.Context, in *gerritpb.ListFilesRequest, opts ...grpc.CallOption) (*gerritpb.ListFilesResponse, error) {
panic(fmt.Errorf("not implemented"))
}
func (f fakePSSAClient) SetReview(ctx context.Context, in *gerritpb.SetReviewRequest, opts ...grpc.CallOption) (*gerritpb.ReviewResult, error) {
panic(fmt.Errorf("not implemented"))
}
func (f fakePSSAClient) SubmitRevision(ctx context.Context, in *gerritpb.SubmitRevisionRequest, opts ...grpc.CallOption) (*gerritpb.SubmitInfo, error) {
panic(fmt.Errorf("not implemented"))
}