blob: 7654ef31b0afc1fb045a235a1db6374ddf7aaded [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || darwin
// +build linux darwin
package gomote
import (
"context"
"fmt"
"io"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/build/internal/access"
"golang.org/x/build/internal/coordinator/remote"
"golang.org/x/build/internal/coordinator/schedule"
"golang.org/x/build/internal/gomote/protos"
"golang.org/x/net/nettest"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/testing/protocmp"
)
func fakeGomoteServer(ctx context.Context) protos.GomoteServiceServer {
return &Server{
buildlets: remote.NewSessionPool(ctx),
scheduler: schedule.NewFake(),
}
}
func setupGomoteTest(t *testing.T, ctx context.Context) protos.GomoteServiceClient {
lis, err := nettest.NewLocalListener("tcp")
if err != nil {
t.Fatalf("unable to create net listener: %s", err)
}
sopts := access.FakeIAPAuthInterceptorOptions()
s := grpc.NewServer(sopts...)
protos.RegisterGomoteServiceServer(s, fakeGomoteServer(ctx))
go s.Serve(lis)
// create GRPC client
copts := []grpc.DialOption{
grpc.WithInsecure(),
grpc.WithBlock(),
grpc.WithTimeout(time.Second),
}
conn, err := grpc.Dial(lis.Addr().String(), copts...)
if err != nil {
lis.Close()
t.Fatalf("unable to create GRPC client: %s", err)
}
gc := protos.NewGomoteServiceClient(conn)
t.Cleanup(func() {
conn.Close()
s.Stop()
lis.Close()
})
return gc
}
func TestAuthenticate(t *testing.T) {
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
client := setupGomoteTest(t, context.Background())
got, err := client.Authenticate(ctx, &protos.AuthenticateRequest{})
if err != nil {
t.Fatalf("client.Authenticate(ctx, request) = %v, %s; want no error", got, err)
}
}
func TestAuthenticateError(t *testing.T) {
wantCode := codes.Unauthenticated
client := setupGomoteTest(t, context.Background())
_, err := client.Authenticate(context.Background(), &protos.AuthenticateRequest{})
if status.Code(err) != wantCode {
t.Fatalf("client.Authenticate(ctx, request) = _, %s; want %s", status.Code(err), wantCode)
}
}
func TestCreateInstance(t *testing.T) {
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
req := &protos.CreateInstanceRequest{BuilderType: "linux-amd64"}
client := setupGomoteTest(t, context.Background())
stream, err := client.CreateInstance(ctx, req)
if err != nil {
t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", req, stream, err)
}
var updateComplete bool
for {
update, err := stream.Recv()
if err == io.EOF && !updateComplete {
t.Fatal("stream.Recv = stream, io.EOF; want no EOF")
}
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("stream.Recv() = nil, %s; want no error", err)
}
if update.GetStatus() == protos.CreateInstanceResponse_COMPLETE {
updateComplete = true
}
}
}
func TestCreateInstanceError(t *testing.T) {
testCases := []struct {
desc string
ctx context.Context
request *protos.CreateInstanceRequest
wantCode codes.Code
}{
{
desc: "unauthenticated request",
ctx: context.Background(),
request: &protos.CreateInstanceRequest{},
wantCode: codes.Unauthenticated,
},
{
desc: "missing builder type",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
request: &protos.CreateInstanceRequest{},
wantCode: codes.InvalidArgument,
},
{
desc: "invalid builder type",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
request: &protos.CreateInstanceRequest{
BuilderType: "funky-time-builder",
},
wantCode: codes.InvalidArgument,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
client := setupGomoteTest(t, context.Background())
stream, err := client.CreateInstance(tc.ctx, tc.request)
if err != nil {
t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", tc.request, stream, err)
}
for {
_, got := stream.Recv()
if got == io.EOF {
t.Fatal("stream.Recv = stream, io.EOF; want no EOF")
}
if got != nil && status.Code(got) != tc.wantCode {
t.Fatalf("unexpected error: %s", err)
}
return
}
})
}
}
func TestInstanceAlive(t *testing.T) {
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
req := &protos.InstanceAliveRequest{
GomoteId: gomoteID,
}
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
got, err := client.InstanceAlive(ctx, req)
if err != nil {
t.Fatalf("client.InstanceAlive(ctx, %v) = %v, %s; want no error", req, got, err)
}
}
func TestInstanceAliveError(t *testing.T) {
// This test will create a gomote instance and attempt to call InstanceAlive.
// If overrideID is set to true, the test will use a diffrent gomoteID than the
// the one created for the test.
testCases := []struct {
desc string
ctx context.Context
overrideID bool
gomoteID string // Used iff overrideID is true.
wantCode codes.Code
}{
{
desc: "unauthenticated request",
ctx: context.Background(),
wantCode: codes.Unauthenticated,
},
{
desc: "missing gomote id",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
overrideID: true,
wantCode: codes.InvalidArgument,
},
{
desc: "gomote does not exist",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
overrideID: true,
gomoteID: "xyz",
wantCode: codes.NotFound,
},
{
desc: "gomote is not owned by caller",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("user-x", "email-y")),
wantCode: codes.PermissionDenied,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
if tc.overrideID {
gomoteID = tc.gomoteID
}
req := &protos.InstanceAliveRequest{
GomoteId: gomoteID,
}
got, err := client.InstanceAlive(tc.ctx, req)
if err != nil && status.Code(err) != tc.wantCode {
t.Fatalf("unexpected error: %s", err)
}
if err == nil {
t.Fatalf("client.InstanceAlive(ctx, %v) = %v, nil; want error", req, got)
}
})
}
}
func TestListInstance(t *testing.T) {
client := setupGomoteTest(t, context.Background())
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
var want []*protos.Instance
for i := 0; i < 3; i++ {
want = append(want, &protos.Instance{
GomoteId: mustCreateInstance(t, client, fakeIAP()),
BuilderType: "linux-amd64",
})
}
mustCreateInstance(t, client, fakeIAPWithUser("user-x", "uuid-user-x"))
response, err := client.ListInstances(ctx, &protos.ListInstancesRequest{})
if err != nil {
t.Fatalf("client.ListInstances = nil, %s; want no error", err)
}
got := response.GetInstances()
if diff := cmp.Diff(want, got, protocmp.Transform(), protocmp.IgnoreFields(&protos.Instance{}, "expires", "host_type")); diff != "" {
t.Errorf("ListInstances() mismatch (-want, +got):\n%s", diff)
}
}
func TestDestroyInstance(t *testing.T) {
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
if _, err := client.DestroyInstance(ctx, &protos.DestroyInstanceRequest{
GomoteId: gomoteID,
}); err != nil {
t.Fatalf("client.DestroyInstance(ctx, req) = response, %s; want no error", err)
}
}
func TestDestroyInstanceError(t *testing.T) {
// This test will create a gomote instance and attempt to call DestroyInstance.
// If overrideID is set to true, the test will use a diffrent gomoteID than the
// the one created for the test.
testCases := []struct {
desc string
ctx context.Context
overrideID bool
gomoteID string // Used iff overrideID is true.
wantCode codes.Code
}{
{
desc: "unauthenticated request",
ctx: context.Background(),
wantCode: codes.Unauthenticated,
},
{
desc: "missing gomote id",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
overrideID: true,
gomoteID: "",
wantCode: codes.InvalidArgument,
},
{
desc: "gomote does not exist",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")),
overrideID: true,
gomoteID: "chucky",
wantCode: codes.NotFound,
},
{
desc: "wrong gomote id",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")),
overrideID: false,
wantCode: codes.PermissionDenied,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
if tc.overrideID {
gomoteID = tc.gomoteID
}
req := &protos.DestroyInstanceRequest{
GomoteId: gomoteID,
}
got, err := client.DestroyInstance(tc.ctx, req)
if err != nil && status.Code(err) != tc.wantCode {
t.Fatalf("unexpected error: %s", err)
}
if err == nil {
t.Fatalf("client.DestroyInstance(ctx, %v) = %v, nil; want error", req, got)
}
})
}
}
func TestWriteTGZFromURL(t *testing.T) {
ctx := access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP())
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
if _, err := client.WriteTGZFromURL(ctx, &protos.WriteTGZFromURLRequest{
GomoteId: gomoteID,
Directory: "foo",
Url: `https://go.dev/dl/go1.17.6.linux-amd64.tar.gz`,
}); err != nil {
t.Fatalf("client.WriteTGZFromURL(ctx, req) = response, %s; want no error", err)
}
}
func TestWriteTGZFromURLError(t *testing.T) {
// This test will create a gomote instance and attempt to call TestWriteTGZFromURL.
// If overrideID is set to true, the test will use a diffrent gomoteID than the
// the one created for the test.
testCases := []struct {
desc string
ctx context.Context
overrideID bool
gomoteID string // Used iff overrideID is true.
url string
directory string
wantCode codes.Code
}{
{
desc: "unauthenticated request",
ctx: context.Background(),
wantCode: codes.Unauthenticated,
},
{
desc: "missing gomote id",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
overrideID: true,
gomoteID: "",
wantCode: codes.InvalidArgument,
},
{
desc: "missing URL",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAP()),
wantCode: codes.InvalidArgument,
},
{
desc: "gomote does not exist",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")),
overrideID: true,
gomoteID: "chucky",
url: "go.dev/dl/1_14.tar.gz",
wantCode: codes.NotFound,
},
{
desc: "wrong gomote id",
ctx: access.FakeContextWithOutgoingIAPAuth(context.Background(), fakeIAPWithUser("foo", "bar")),
overrideID: false,
url: "go.dev/dl/1_14.tar.gz",
wantCode: codes.PermissionDenied,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
client := setupGomoteTest(t, context.Background())
gomoteID := mustCreateInstance(t, client, fakeIAP())
if tc.overrideID {
gomoteID = tc.gomoteID
}
req := &protos.WriteTGZFromURLRequest{
GomoteId: gomoteID,
Url: tc.url,
Directory: tc.directory,
}
got, err := client.WriteTGZFromURL(tc.ctx, req)
if err != nil && status.Code(err) != tc.wantCode {
t.Fatalf("unexpected error: %s", err)
}
if err == nil {
t.Fatalf("client.WriteTGZFromURL(ctx, %v) = %v, nil; want error", req, got)
}
})
}
}
func TestIsPrivilegedUser(t *testing.T) {
in := "accounts.google.com:example@google.com"
if !isPrivilegedUser(in) {
t.Errorf("isPrivilagedUser(%q) = false; want true", in)
}
}
func TestEmailToUser(t *testing.T) {
testCases := []struct {
desc string
email string
want string
}{
{"valid email", "accounts.google.com:example@gmail.com", "example"},
{"valid email", "accounts.google.com:mary@google.com", "mary"},
{"valid email", "accounts.google.com:george@funky.com", "george"},
{"single digit local", "accounts.google.com:g@funky.com", "g"},
{"single digit domain", "accounts.google.com:g@funky.com", "g"},
{"multiple colon", "accounts.google.com:example@gmail.com:more-info", "example"}, // while not desired, wont lead to a panic
{"multiple at", "accounts.google.com:example@gmail.com:example@gmail.com", "example@gmail.com:example"}, // while not desired, wont lead to a panic
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
if got, err := emailToUser(tc.email); got != tc.want || err != nil {
t.Errorf("emailToUser(%q) = %q, %s; want %q, no error", tc.email, got, err, tc.want)
}
})
}
}
func TestEmailToUserError(t *testing.T) {
testCases := []struct {
desc string
email string
}{
{"no local", "accounts.google.com:@funky.com"},
{"incorrect authority", "accountsxgoogleycom:george@funky.com"},
{"hyphens authority", "accounts-google-com:george@funky.com"},
{"no domain", "accounts.google.com:george@"},
{"missing colon", "accounts.google.comxgeorge@a.b"},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
if got, err := emailToUser(tc.email); err == nil {
t.Errorf("emailToUser(%q) = %q, no error; want error", tc.email, got)
}
})
}
}
func fakeAuthContext(ctx context.Context, privileged bool) context.Context {
iap := access.IAPFields{
Email: "accounts.google.com:example@gmail.com",
ID: "accounts.google.com:randomuuidstuff",
}
if privileged {
iap.Email = "accounts.google.com:test@google.com"
}
return access.ContextWithIAP(ctx, iap)
}
func fakeIAP() access.IAPFields {
return fakeIAPWithUser("example", "randomuuidstuff")
}
func fakeIAPWithUser(user string, id string) access.IAPFields {
return access.IAPFields{
Email: fmt.Sprintf("accounts.google.com:%s@gmail.com", user),
ID: fmt.Sprintf("accounts.google.com:%s", id),
}
}
func mustCreateInstance(t *testing.T, client protos.GomoteServiceClient, iap access.IAPFields) string {
req := &protos.CreateInstanceRequest{
BuilderType: "linux-amd64",
}
stream, err := client.CreateInstance(access.FakeContextWithOutgoingIAPAuth(context.Background(), iap), req)
if err != nil {
t.Fatalf("client.CreateInstance(ctx, %v) = %v, %s; want no error", req, stream, err)
}
var updateComplete bool
var gomoteID string
for {
update, err := stream.Recv()
if err == io.EOF && !updateComplete {
t.Fatal("stream.Recv = stream, io.EOF; want no EOF")
}
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("stream.Recv() = nil, %s; want no error", err)
}
if update.GetStatus() == protos.CreateInstanceResponse_COMPLETE {
gomoteID = update.Instance.GetGomoteId()
updateComplete = true
}
}
return gomoteID
}