865 lines
25 KiB
Go
865 lines
25 KiB
Go
package mxgateway
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/grpc/test/bufconn"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
)
|
|
|
|
func TestGalaxyTestConnectionAttachesAuthAndReturnsOk(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
testReply: &pb.TestConnectionReply{Ok: true},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
ok, err := client.TestConnection(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TestConnection() error = %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatalf("TestConnection() ok = false, want true")
|
|
}
|
|
if got := fake.testAuth; got != "Bearer test-api-key" {
|
|
t.Fatalf("authorization metadata = %q, want %q", got, "Bearer test-api-key")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyGetLastDeployTimeReturnsAbsentForPresentFalse(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
deployReply: &pb.GetLastDeployTimeReply{Present: false},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
got, present, err := client.GetLastDeployTime(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetLastDeployTime() error = %v", err)
|
|
}
|
|
if present {
|
|
t.Fatalf("present = true, want false")
|
|
}
|
|
if !got.IsZero() {
|
|
t.Fatalf("time = %v, want zero", got)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyGetLastDeployTimeReturnsTimestampWhenPresent(t *testing.T) {
|
|
want := time.Date(2026, 4, 28, 12, 34, 56, 0, time.UTC)
|
|
fake := &fakeGalaxyServer{
|
|
deployReply: &pb.GetLastDeployTimeReply{
|
|
Present: true,
|
|
TimeOfLastDeploy: timestamppb.New(want),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
got, present, err := client.GetLastDeployTime(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetLastDeployTime() error = %v", err)
|
|
}
|
|
if !present {
|
|
t.Fatalf("present = false, want true")
|
|
}
|
|
if !got.Equal(want) {
|
|
t.Fatalf("time = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyGetLastDeployTimeReturnsAbsentWhenTimestampNil(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
deployReply: &pb.GetLastDeployTimeReply{Present: true, TimeOfLastDeploy: nil},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
got, present, err := client.GetLastDeployTime(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("GetLastDeployTime() error = %v", err)
|
|
}
|
|
if present {
|
|
t.Fatalf("present = true, want false (nil timestamp)")
|
|
}
|
|
if !got.IsZero() {
|
|
t.Fatalf("time = %v, want zero", got)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyDiscoverHierarchyReturnsObjects(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
discoverReply: &pb.DiscoverHierarchyReply{
|
|
Objects: []*pb.GalaxyObject{
|
|
{
|
|
GobjectId: 1,
|
|
TagName: "TestMachine_001",
|
|
ContainedName: "TestMachine_001",
|
|
BrowseName: "TestMachine_001",
|
|
IsArea: false,
|
|
CategoryId: 7,
|
|
TemplateChain: []string{"$Object", "$AppObject"},
|
|
Attributes: []*pb.GalaxyAttribute{
|
|
{
|
|
AttributeName: "DownloadPath",
|
|
FullTagReference: "TestMachine_001.DownloadPath",
|
|
MxDataType: 8,
|
|
DataTypeName: "String",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
GobjectId: 2,
|
|
TagName: "TestMachine_002",
|
|
ContainedName: "TestMachine_002",
|
|
ParentGobjectId: 1,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
objects, err := client.DiscoverHierarchy(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("DiscoverHierarchy() error = %v", err)
|
|
}
|
|
if len(objects) != 2 {
|
|
t.Fatalf("len(objects) = %d, want 2", len(objects))
|
|
}
|
|
if objects[0].GetTagName() != "TestMachine_001" {
|
|
t.Fatalf("objects[0].TagName = %q", objects[0].GetTagName())
|
|
}
|
|
if len(objects[0].GetAttributes()) != 1 {
|
|
t.Fatalf("len(attributes) = %d, want 1", len(objects[0].GetAttributes()))
|
|
}
|
|
if objects[0].GetAttributes()[0].GetFullTagReference() != "TestMachine_001.DownloadPath" {
|
|
t.Fatalf("FullTagReference = %q", objects[0].GetAttributes()[0].GetFullTagReference())
|
|
}
|
|
}
|
|
|
|
func TestGalaxyDiscoverHierarchyPaginatesAcrossMultiplePages(t *testing.T) {
|
|
page1 := &pb.DiscoverHierarchyReply{
|
|
Objects: []*pb.GalaxyObject{
|
|
{GobjectId: 1, TagName: "A"},
|
|
{GobjectId: 2, TagName: "B"},
|
|
},
|
|
NextPageToken: "page-2",
|
|
TotalObjectCount: 3,
|
|
}
|
|
page2 := &pb.DiscoverHierarchyReply{
|
|
Objects: []*pb.GalaxyObject{
|
|
{GobjectId: 3, TagName: "C"},
|
|
},
|
|
TotalObjectCount: 3,
|
|
}
|
|
fake := &fakeGalaxyServer{
|
|
discoverHierarchyReplies: []*pb.DiscoverHierarchyReply{page1, page2},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
objs, err := client.DiscoverHierarchy(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("DiscoverHierarchy: %v", err)
|
|
}
|
|
if got, want := len(objs), 3; got != want {
|
|
t.Fatalf("len(objs) = %d, want %d", got, want)
|
|
}
|
|
if len(fake.discoverHierarchyCalls) != 2 {
|
|
t.Fatalf("expected 2 RPC calls, got %d", len(fake.discoverHierarchyCalls))
|
|
}
|
|
if fake.discoverHierarchyCalls[0].GetPageSize() != discoverHierarchyPageSize {
|
|
t.Fatalf("first call PageSize = %d, want %d",
|
|
fake.discoverHierarchyCalls[0].GetPageSize(), discoverHierarchyPageSize)
|
|
}
|
|
if fake.discoverHierarchyCalls[1].GetPageToken() != "page-2" {
|
|
t.Fatalf("second call page token = %q, want %q",
|
|
fake.discoverHierarchyCalls[1].GetPageToken(), "page-2")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyDialReturnsGatewayErrorOnRpcFailure(t *testing.T) {
|
|
fake := &fakeGalaxyServer{failTest: true}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
_, err := client.TestConnection(context.Background())
|
|
if err == nil {
|
|
t.Fatal("TestConnection() error = nil, want error")
|
|
}
|
|
var gwErr *GatewayError
|
|
if !errors.As(err, &gwErr) {
|
|
t.Fatalf("error %T does not support errors.As(*GatewayError)", err)
|
|
}
|
|
if gwErr.Op != "galaxy test connection" {
|
|
t.Fatalf("Op = %q, want %q", gwErr.Op, "galaxy test connection")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyWatchDeployEventsReceivesEventsInOrder(t *testing.T) {
|
|
bootstrap := time.Date(2026, 4, 28, 10, 0, 0, 0, time.UTC)
|
|
deploy1 := time.Date(2026, 4, 28, 10, 5, 0, 0, time.UTC)
|
|
deploy2 := time.Date(2026, 4, 28, 10, 6, 0, 0, time.UTC)
|
|
fake := &fakeGalaxyServer{
|
|
watchEvents: []*pb.DeployEvent{
|
|
{
|
|
Sequence: 1,
|
|
ObservedAt: timestamppb.New(bootstrap),
|
|
TimeOfLastDeploy: timestamppb.New(deploy1),
|
|
TimeOfLastDeployPresent: true,
|
|
ObjectCount: 10,
|
|
AttributeCount: 42,
|
|
},
|
|
{
|
|
Sequence: 2,
|
|
ObservedAt: timestamppb.New(deploy2),
|
|
TimeOfLastDeploy: timestamppb.New(deploy2),
|
|
TimeOfLastDeployPresent: true,
|
|
ObjectCount: 11,
|
|
AttributeCount: 44,
|
|
},
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
events, errs, err := client.WatchDeployEvents(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("WatchDeployEvents() error = %v", err)
|
|
}
|
|
|
|
got := make([]*DeployEvent, 0, 2)
|
|
loop:
|
|
for {
|
|
select {
|
|
case ev, ok := <-events:
|
|
if !ok {
|
|
break loop
|
|
}
|
|
got = append(got, ev)
|
|
case errVal := <-errs:
|
|
if errVal != nil {
|
|
t.Fatalf("error channel: %v", errVal)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("timeout waiting for events; got %d", len(got))
|
|
}
|
|
}
|
|
|
|
if len(got) != 2 {
|
|
t.Fatalf("len(events) = %d, want 2", len(got))
|
|
}
|
|
if got[0].GetSequence() != 1 || got[1].GetSequence() != 2 {
|
|
t.Fatalf("sequences = [%d,%d], want [1,2]", got[0].GetSequence(), got[1].GetSequence())
|
|
}
|
|
if !got[0].GetTimeOfLastDeployPresent() {
|
|
t.Fatalf("event[0] TimeOfLastDeployPresent = false, want true")
|
|
}
|
|
if got[0].GetObjectCount() != 10 || got[0].GetAttributeCount() != 42 {
|
|
t.Fatalf("event[0] counts = (%d,%d), want (10,42)", got[0].GetObjectCount(), got[0].GetAttributeCount())
|
|
}
|
|
if !got[0].GetTimeOfLastDeploy().AsTime().Equal(deploy1) {
|
|
t.Fatalf("event[0] TimeOfLastDeploy = %v, want %v", got[0].GetTimeOfLastDeploy().AsTime(), deploy1)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyWatchDeployEventsForwardsLastSeenDeployTime(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
watchEvents: []*pb.DeployEvent{
|
|
{Sequence: 7},
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
lastSeen := time.Date(2026, 4, 28, 9, 0, 0, 0, time.UTC)
|
|
events, errs, err := client.WatchDeployEvents(ctx, &lastSeen)
|
|
if err != nil {
|
|
t.Fatalf("WatchDeployEvents() error = %v", err)
|
|
}
|
|
|
|
// Drain everything.
|
|
loop:
|
|
for {
|
|
select {
|
|
case _, ok := <-events:
|
|
if !ok {
|
|
break loop
|
|
}
|
|
case errVal := <-errs:
|
|
if errVal != nil {
|
|
t.Fatalf("error channel: %v", errVal)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("timeout draining events")
|
|
}
|
|
}
|
|
|
|
if fake.watchRequest == nil {
|
|
t.Fatalf("server did not receive a request")
|
|
}
|
|
gotTs := fake.watchRequest.GetLastSeenDeployTime()
|
|
if gotTs == nil {
|
|
t.Fatalf("LastSeenDeployTime = nil, want %v", lastSeen)
|
|
}
|
|
if !gotTs.AsTime().Equal(lastSeen) {
|
|
t.Fatalf("LastSeenDeployTime = %v, want %v", gotTs.AsTime(), lastSeen)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyWatchDeployEventsCancelTearsDownStream(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
watchEvents: []*pb.DeployEvent{
|
|
{Sequence: 1},
|
|
},
|
|
watchHoldOpen: true,
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
streamCtx, cancelStream := context.WithCancel(context.Background())
|
|
|
|
events, errs, err := client.WatchDeployEvents(streamCtx, nil)
|
|
if err != nil {
|
|
t.Fatalf("WatchDeployEvents() error = %v", err)
|
|
}
|
|
|
|
// Wait for the bootstrap event to arrive.
|
|
select {
|
|
case ev, ok := <-events:
|
|
if !ok {
|
|
t.Fatalf("events channel closed before delivering bootstrap")
|
|
}
|
|
if ev.GetSequence() != 1 {
|
|
t.Fatalf("got seq=%d, want 1", ev.GetSequence())
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatalf("timeout waiting for bootstrap event")
|
|
}
|
|
|
|
// Cancel the stream; both channels must close cleanly without delivering an error.
|
|
cancelStream()
|
|
|
|
deadline := time.After(2 * time.Second)
|
|
for events != nil || errs != nil {
|
|
select {
|
|
case _, ok := <-events:
|
|
if !ok {
|
|
events = nil
|
|
}
|
|
case errVal, ok := <-errs:
|
|
if !ok {
|
|
errs = nil
|
|
continue
|
|
}
|
|
if errVal != nil {
|
|
t.Fatalf("error after cancel: %v", errVal)
|
|
}
|
|
case <-deadline:
|
|
t.Fatalf("channels did not close after cancel; events nil=%v errs nil=%v", events == nil, errs == nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newGalaxyBufconnClient(t *testing.T, fake *fakeGalaxyServer) (*GalaxyClient, func()) {
|
|
t.Helper()
|
|
|
|
listener := bufconn.Listen(bufSize)
|
|
server := grpc.NewServer()
|
|
pb.RegisterGalaxyRepositoryServer(server, fake)
|
|
go func() {
|
|
if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
|
t.Errorf("bufconn server failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
|
return listener.DialContext(ctx)
|
|
}
|
|
client, err := DialGalaxy(context.Background(), Options{
|
|
Endpoint: "bufnet",
|
|
APIKey: "test-api-key",
|
|
Plaintext: true,
|
|
DialOptions: []grpc.DialOption{
|
|
grpc.WithContextDialer(dialer),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("DialGalaxy() error = %v", err)
|
|
}
|
|
|
|
return client, func() {
|
|
client.Close()
|
|
server.Stop()
|
|
listener.Close()
|
|
}
|
|
}
|
|
|
|
type fakeGalaxyServer struct {
|
|
pb.UnimplementedGalaxyRepositoryServer
|
|
|
|
testReply *pb.TestConnectionReply
|
|
testAuth string
|
|
failTest bool
|
|
deployReply *pb.GetLastDeployTimeReply
|
|
discoverReply *pb.DiscoverHierarchyReply
|
|
discoverHierarchyCalls []*pb.DiscoverHierarchyRequest
|
|
discoverHierarchyReplies []*pb.DiscoverHierarchyReply
|
|
watchEvents []*pb.DeployEvent
|
|
watchRequest *pb.WatchDeployEventsRequest
|
|
watchSendInterval time.Duration
|
|
watchHoldOpen bool
|
|
browseChildrenCalls []*pb.BrowseChildrenRequest
|
|
browseChildrenReplies []*pb.BrowseChildrenReply
|
|
browseChildrenError error
|
|
}
|
|
|
|
func (s *fakeGalaxyServer) TestConnection(ctx context.Context, req *pb.TestConnectionRequest) (*pb.TestConnectionReply, error) {
|
|
s.testAuth = authorizationFromContext(ctx)
|
|
if s.failTest {
|
|
return nil, errors.New("simulated failure")
|
|
}
|
|
if s.testReply != nil {
|
|
return s.testReply, nil
|
|
}
|
|
return &pb.TestConnectionReply{Ok: true}, nil
|
|
}
|
|
|
|
func (s *fakeGalaxyServer) GetLastDeployTime(ctx context.Context, req *pb.GetLastDeployTimeRequest) (*pb.GetLastDeployTimeReply, error) {
|
|
if s.deployReply != nil {
|
|
return s.deployReply, nil
|
|
}
|
|
return &pb.GetLastDeployTimeReply{Present: false}, nil
|
|
}
|
|
|
|
func (s *fakeGalaxyServer) DiscoverHierarchy(ctx context.Context, req *pb.DiscoverHierarchyRequest) (*pb.DiscoverHierarchyReply, error) {
|
|
s.discoverHierarchyCalls = append(s.discoverHierarchyCalls, req)
|
|
if len(s.discoverHierarchyReplies) > 0 {
|
|
reply := s.discoverHierarchyReplies[0]
|
|
s.discoverHierarchyReplies = s.discoverHierarchyReplies[1:]
|
|
return reply, nil
|
|
}
|
|
if s.discoverReply != nil {
|
|
return s.discoverReply, nil
|
|
}
|
|
return &pb.DiscoverHierarchyReply{}, nil
|
|
}
|
|
|
|
func (s *fakeGalaxyServer) WatchDeployEvents(req *pb.WatchDeployEventsRequest, stream grpc.ServerStreamingServer[pb.DeployEvent]) error {
|
|
s.watchRequest = req
|
|
for _, event := range s.watchEvents {
|
|
if err := stream.Send(event); err != nil {
|
|
return err
|
|
}
|
|
if s.watchSendInterval > 0 {
|
|
select {
|
|
case <-time.After(s.watchSendInterval):
|
|
case <-stream.Context().Done():
|
|
return stream.Context().Err()
|
|
}
|
|
}
|
|
}
|
|
if s.watchHoldOpen {
|
|
<-stream.Context().Done()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *fakeGalaxyServer) BrowseChildren(ctx context.Context, req *pb.BrowseChildrenRequest) (*pb.BrowseChildrenReply, error) {
|
|
s.browseChildrenCalls = append(s.browseChildrenCalls, req)
|
|
if s.browseChildrenError != nil {
|
|
err := s.browseChildrenError
|
|
s.browseChildrenError = nil
|
|
return nil, err
|
|
}
|
|
if len(s.browseChildrenReplies) == 0 {
|
|
return &pb.BrowseChildrenReply{}, nil
|
|
}
|
|
reply := s.browseChildrenReplies[0]
|
|
s.browseChildrenReplies = s.browseChildrenReplies[1:]
|
|
return reply, nil
|
|
}
|
|
|
|
func obj(id int32, tag string, isArea bool) *pb.GalaxyObject {
|
|
return &pb.GalaxyObject{
|
|
GobjectId: id,
|
|
TagName: tag,
|
|
BrowseName: tag,
|
|
IsArea: isArea,
|
|
}
|
|
}
|
|
|
|
func buildBrowseReply(children []*pb.GalaxyObject, hasChildren []bool, seq uint64) *pb.BrowseChildrenReply {
|
|
return &pb.BrowseChildrenReply{
|
|
TotalChildCount: int32(len(children)),
|
|
CacheSequence: seq,
|
|
Children: children,
|
|
ChildHasChildren: hasChildren,
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseNoParentReturnsRoots(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true), obj(99, "Other", false)},
|
|
[]bool{true, false},
|
|
7,
|
|
),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
roots, err := client.Browse(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
if got, want := len(roots), 2; got != want {
|
|
t.Fatalf("len(roots) = %d, want %d", got, want)
|
|
}
|
|
if roots[0].Object().GetTagName() != "Plant" {
|
|
t.Fatalf("roots[0].TagName = %q", roots[0].Object().GetTagName())
|
|
}
|
|
if !roots[0].HasChildrenHint() {
|
|
t.Fatal("roots[0].HasChildrenHint = false, want true")
|
|
}
|
|
if roots[0].IsExpanded() {
|
|
t.Fatal("roots[0].IsExpanded = true, want false")
|
|
}
|
|
if roots[1].HasChildrenHint() {
|
|
t.Fatal("roots[1].HasChildrenHint = true, want false")
|
|
}
|
|
if len(fake.browseChildrenCalls) != 1 {
|
|
t.Fatalf("BrowseChildren calls = %d, want 1", len(fake.browseChildrenCalls))
|
|
}
|
|
if fake.browseChildrenCalls[0].GetParent() != nil {
|
|
t.Fatalf("root browse should not set Parent oneof, got %T", fake.browseChildrenCalls[0].GetParent())
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseExpandPopulatesChildrenAndMarksExpanded(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
[]bool{true},
|
|
1,
|
|
),
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(10, "Area1", true), obj(11, "Tank1", false)},
|
|
[]bool{true, false},
|
|
1,
|
|
),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
roots, err := client.Browse(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
if len(roots) != 1 {
|
|
t.Fatalf("len(roots) = %d, want 1", len(roots))
|
|
}
|
|
plant := roots[0]
|
|
if plant.IsExpanded() {
|
|
t.Fatal("plant.IsExpanded = true before Expand, want false")
|
|
}
|
|
if err := plant.Expand(context.Background()); err != nil {
|
|
t.Fatalf("Expand: %v", err)
|
|
}
|
|
if !plant.IsExpanded() {
|
|
t.Fatal("plant.IsExpanded = false after Expand, want true")
|
|
}
|
|
children := plant.Children()
|
|
if len(children) != 2 {
|
|
t.Fatalf("len(children) = %d, want 2", len(children))
|
|
}
|
|
if children[0].Object().GetTagName() != "Area1" {
|
|
t.Fatalf("children[0].TagName = %q, want Area1", children[0].Object().GetTagName())
|
|
}
|
|
if !children[0].HasChildrenHint() {
|
|
t.Fatal("children[0].HasChildrenHint = false, want true")
|
|
}
|
|
if children[1].HasChildrenHint() {
|
|
t.Fatal("children[1].HasChildrenHint = true, want false")
|
|
}
|
|
if len(fake.browseChildrenCalls) != 2 {
|
|
t.Fatalf("BrowseChildren calls = %d, want 2", len(fake.browseChildrenCalls))
|
|
}
|
|
parent := fake.browseChildrenCalls[1].GetParent()
|
|
parentGobj, ok := parent.(*pb.BrowseChildrenRequest_ParentGobjectId)
|
|
if !ok {
|
|
t.Fatalf("Parent oneof = %T, want *BrowseChildrenRequest_ParentGobjectId", parent)
|
|
}
|
|
if parentGobj.ParentGobjectId != 1 {
|
|
t.Fatalf("ParentGobjectId = %d, want 1", parentGobj.ParentGobjectId)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseExpandIdempotentNoSecondRpc(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
[]bool{true},
|
|
1,
|
|
),
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(10, "Area1", true)},
|
|
[]bool{false},
|
|
1,
|
|
),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
roots, err := client.Browse(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
plant := roots[0]
|
|
if err := plant.Expand(context.Background()); err != nil {
|
|
t.Fatalf("Expand #1: %v", err)
|
|
}
|
|
callsAfterFirst := len(fake.browseChildrenCalls)
|
|
if callsAfterFirst != 2 {
|
|
t.Fatalf("BrowseChildren calls after first Expand = %d, want 2", callsAfterFirst)
|
|
}
|
|
if err := plant.Expand(context.Background()); err != nil {
|
|
t.Fatalf("Expand #2: %v", err)
|
|
}
|
|
if got := len(fake.browseChildrenCalls); got != callsAfterFirst {
|
|
t.Fatalf("BrowseChildren calls after second Expand = %d, want %d (no extra RPC)", got, callsAfterFirst)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseExpandUnknownParentReturnsNotFoundError(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
[]bool{true},
|
|
1,
|
|
),
|
|
},
|
|
browseChildrenError: status.Error(codes.NotFound, "parent not found"),
|
|
}
|
|
// The first Browse() consumes the first reply; the next call (Expand) will
|
|
// then hit browseChildrenError. We need the error to fire only on the second
|
|
// call, so seed the reply first and let the call sequence consume them in
|
|
// order. Because BrowseChildren in the fake consumes browseChildrenError
|
|
// before falling through to replies, swap the strategy: keep the root reply
|
|
// but have BrowseChildren return the error on the second call. We do this by
|
|
// emptying the reply list after the first Browse.
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
// First call returns the error (because browseChildrenError takes precedence).
|
|
// To avoid that, clear it for the root call by performing a manual setup: we
|
|
// pre-stage replies first, then set the error after the first call. Easiest:
|
|
// pre-Browse() with error=nil, then set error before Expand.
|
|
fake.browseChildrenError = nil
|
|
roots, err := client.Browse(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
if len(roots) != 1 {
|
|
t.Fatalf("len(roots) = %d, want 1", len(roots))
|
|
}
|
|
fake.browseChildrenError = status.Error(codes.NotFound, "parent not found")
|
|
|
|
err = roots[0].Expand(context.Background())
|
|
if err == nil {
|
|
t.Fatal("Expand: error = nil, want NotFound")
|
|
}
|
|
if status.Code(err) != codes.NotFound {
|
|
t.Fatalf("status.Code = %s, want NotFound", status.Code(err))
|
|
}
|
|
if roots[0].IsExpanded() {
|
|
t.Fatal("roots[0].IsExpanded = true after failed Expand, want false")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseExpandMultiPageGathersAllPages(t *testing.T) {
|
|
firstPage := buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
[]bool{true},
|
|
7,
|
|
)
|
|
|
|
pageA := buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(10, "Child1", false), obj(11, "Child2", false)},
|
|
[]bool{false, false},
|
|
7,
|
|
)
|
|
pageA.NextPageToken = "7:abc:2"
|
|
pageB := buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(12, "Child3", false)},
|
|
[]bool{false},
|
|
7,
|
|
)
|
|
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{firstPage, pageA, pageB},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
roots, err := client.Browse(context.Background(), nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
if err := roots[0].Expand(context.Background()); err != nil {
|
|
t.Fatalf("Expand: %v", err)
|
|
}
|
|
children := roots[0].Children()
|
|
if len(children) != 3 {
|
|
t.Fatalf("len(children) = %d, want 3", len(children))
|
|
}
|
|
if len(fake.browseChildrenCalls) != 3 {
|
|
t.Fatalf("BrowseChildren calls = %d, want 3", len(fake.browseChildrenCalls))
|
|
}
|
|
if got := fake.browseChildrenCalls[2].GetPageToken(); got != "7:abc:2" {
|
|
t.Fatalf("third call PageToken = %q, want %q", got, "7:abc:2")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseWithFilterForwardsToRequest(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
buildBrowseReply(nil, nil, 1),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
include := true
|
|
opts := &BrowseChildrenOptions{
|
|
CategoryIds: []int32{7, 9},
|
|
TemplateChainContains: []string{"$AppObject"},
|
|
TagNameGlob: "Tank*",
|
|
IncludeAttributes: &include,
|
|
AlarmBearingOnly: true,
|
|
HistorizedOnly: true,
|
|
}
|
|
if _, err := client.Browse(context.Background(), opts); err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
if len(fake.browseChildrenCalls) != 1 {
|
|
t.Fatalf("BrowseChildren calls = %d, want 1", len(fake.browseChildrenCalls))
|
|
}
|
|
got := fake.browseChildrenCalls[0]
|
|
if want := []int32{7, 9}; len(got.GetCategoryIds()) != 2 || got.GetCategoryIds()[0] != want[0] || got.GetCategoryIds()[1] != want[1] {
|
|
t.Fatalf("CategoryIds = %v, want %v", got.GetCategoryIds(), want)
|
|
}
|
|
if want := []string{"$AppObject"}; len(got.GetTemplateChainContains()) != 1 || got.GetTemplateChainContains()[0] != want[0] {
|
|
t.Fatalf("TemplateChainContains = %v, want %v", got.GetTemplateChainContains(), want)
|
|
}
|
|
if got.GetTagNameGlob() != "Tank*" {
|
|
t.Fatalf("TagNameGlob = %q, want %q", got.GetTagNameGlob(), "Tank*")
|
|
}
|
|
if !got.GetIncludeAttributes() {
|
|
t.Fatal("IncludeAttributes = false, want true")
|
|
}
|
|
if !got.GetAlarmBearingOnly() {
|
|
t.Fatal("AlarmBearingOnly = false, want true")
|
|
}
|
|
if !got.GetHistorizedOnly() {
|
|
t.Fatal("HistorizedOnly = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseExpandConcurrentCallersOnlyFireOneRpc(t *testing.T) {
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{
|
|
// roots
|
|
buildBrowseReply([]*pb.GalaxyObject{obj(1, "Plant", true)}, []bool{true}, 7),
|
|
// one expand: one child
|
|
buildBrowseReply([]*pb.GalaxyObject{obj(2, "Mixer", false)}, []bool{false}, 7),
|
|
},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
roots, err := client.Browse(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("Browse: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, 10)
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs <- roots[0].Expand(ctx)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
for err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("concurrent Expand: %v", err)
|
|
}
|
|
}
|
|
|
|
if !roots[0].IsExpanded() {
|
|
t.Fatal("IsExpanded() = false after 10 concurrent expands")
|
|
}
|
|
if got, want := len(roots[0].Children()), 1; got != want {
|
|
t.Fatalf("len(children) = %d, want %d", got, want)
|
|
}
|
|
// 1 roots fetch + exactly 1 expand fetch.
|
|
if got, want := len(fake.browseChildrenCalls), 2; got != want {
|
|
t.Fatalf("RPC count = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func TestGalaxyBrowseChildrenRejectsRepeatedPageToken(t *testing.T) {
|
|
// Build a reply that carries a non-empty NextPageToken so browseChildrenInner
|
|
// will request a second page. Queue the same reply twice so the second response
|
|
// returns the same page token, triggering the duplicate-token guard.
|
|
page := buildBrowseReply(
|
|
[]*pb.GalaxyObject{obj(1, "Plant", true)},
|
|
[]bool{true},
|
|
1,
|
|
)
|
|
page.NextPageToken = "1:abc:1"
|
|
|
|
fake := &fakeGalaxyServer{
|
|
browseChildrenReplies: []*pb.BrowseChildrenReply{page, page},
|
|
}
|
|
client, cleanup := newGalaxyBufconnClient(t, fake)
|
|
defer cleanup()
|
|
|
|
_, err := client.Browse(context.Background(), nil)
|
|
if err == nil {
|
|
t.Fatal("Browse: error = nil, want repeated-page-token error")
|
|
}
|
|
var gwErr *GatewayError
|
|
if !errors.As(err, &gwErr) {
|
|
t.Fatalf("error type = %T, want *GatewayError; err = %v", err, err)
|
|
}
|
|
}
|