go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/client/coordinator/query_test.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package coordinator
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"testing"
    22  
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	"go.chromium.org/luci/common/clock/testclock"
    29  	"go.chromium.org/luci/common/testing/prpctest"
    30  	logdog "go.chromium.org/luci/logdog/api/endpoints/coordinator/logs/v1"
    31  	"go.chromium.org/luci/logdog/api/logpb"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  )
    35  
    36  type testQueryLogsService struct {
    37  	testLogsServiceBase
    38  
    39  	LR *logdog.QueryRequest
    40  	H  func(*logdog.QueryRequest) (*logdog.QueryResponse, error)
    41  }
    42  
    43  func (s *testQueryLogsService) Query(c context.Context, req *logdog.QueryRequest) (*logdog.QueryResponse, error) {
    44  	s.LR = proto.Clone(req).(*logdog.QueryRequest)
    45  	if h := s.H; h != nil {
    46  		return s.H(req)
    47  	}
    48  	return nil, errors.New("not implemented")
    49  }
    50  
    51  func gen(name string, state *logdog.LogStreamState) *logdog.QueryResponse_Stream {
    52  	return &logdog.QueryResponse_Stream{
    53  		Path:  fmt.Sprintf("test/+/%s", name),
    54  		State: state,
    55  		Desc: &logpb.LogStreamDescriptor{
    56  			Prefix: "test",
    57  			Name:   name,
    58  		},
    59  	}
    60  }
    61  
    62  func shouldHaveLogStreams(actual any, expected ...any) string {
    63  	a := actual.([]*LogStream)
    64  
    65  	aList := make([]string, len(a))
    66  	for i, ls := range a {
    67  		aList[i] = string(ls.Path)
    68  	}
    69  
    70  	eList := make([]string, len(expected))
    71  	for i, exp := range expected {
    72  		eList[i] = exp.(string)
    73  	}
    74  
    75  	return ShouldResemble(aList, eList)
    76  }
    77  
    78  func TestClientQuery(t *testing.T) {
    79  	t.Parallel()
    80  
    81  	Convey(`A testing Client`, t, func() {
    82  		now := testclock.TestTimeLocal
    83  		c := context.Background()
    84  
    85  		ts := prpctest.Server{}
    86  		svc := testQueryLogsService{}
    87  		logdog.RegisterLogsServer(&ts, &svc)
    88  
    89  		// Create a testing server and client.
    90  		ts.Start(c)
    91  		defer ts.Close()
    92  
    93  		prpcClient, err := ts.NewClient()
    94  		if err != nil {
    95  			panic(err)
    96  		}
    97  		client := Client{
    98  			C: logdog.NewLogsPRPCClient(prpcClient),
    99  		}
   100  
   101  		Convey(`When making a query request`, func() {
   102  			const project = "myproj"
   103  			const path = "**/+/**"
   104  			q := QueryOptions{
   105  				Tags: map[string]string{
   106  					"foo": "bar",
   107  					"baz": "qux",
   108  				},
   109  				ContentType: "application/text",
   110  				Purged:      Both,
   111  				State:       true,
   112  			}
   113  
   114  			st := logdog.LogStreamState{
   115  				Created: timestamppb.New(now),
   116  			}
   117  
   118  			var results []*LogStream
   119  			accumulate := func(s *LogStream) bool {
   120  				results = append(results, s)
   121  				return true
   122  			}
   123  
   124  			Convey(`Can accumulate results across queries.`, func() {
   125  				// This handler will return a single query per request, as well as a
   126  				// non-empty Next pointer for the next query element. It progresses
   127  				// "a" => "b" => "final" => "".
   128  				svc.H = func(req *logdog.QueryRequest) (*logdog.QueryResponse, error) {
   129  					r := logdog.QueryResponse{
   130  						Project: string(project),
   131  					}
   132  
   133  					switch req.Next {
   134  					case "":
   135  						r.Streams = append(r.Streams, gen("a", &st))
   136  						r.Next = "b"
   137  					case "b":
   138  						r.Streams = append(r.Streams, gen("b", &st))
   139  						r.Next = "final"
   140  					case "final":
   141  						r.Streams = append(r.Streams, gen("final", &st))
   142  					default:
   143  						return nil, errors.New("invalid cursor")
   144  					}
   145  					return &r, nil
   146  				}
   147  
   148  				So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   149  				So(results, shouldHaveLogStreams, "test/+/a", "test/+/b", "test/+/final")
   150  			})
   151  
   152  			Convey(`Will stop invoking the callback if it returns false.`, func() {
   153  				// This handler will return three query results, "a", "b", and "c".
   154  				svc.H = func(*logdog.QueryRequest) (*logdog.QueryResponse, error) {
   155  					return &logdog.QueryResponse{
   156  						Streams: []*logdog.QueryResponse_Stream{
   157  							gen("a", &st),
   158  							gen("b", &st),
   159  							gen("c", &st),
   160  						},
   161  						Next: "infiniteloop",
   162  					}, nil
   163  				}
   164  
   165  				accumulate = func(s *LogStream) bool {
   166  					results = append(results, s)
   167  					return len(results) < 3
   168  				}
   169  				So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   170  				So(results, shouldHaveLogStreams, "test/+/a", "test/+/b", "test/+/c")
   171  			})
   172  
   173  			Convey(`Will properly handle state and protobuf deserialization.`, func() {
   174  				svc.H = func(*logdog.QueryRequest) (*logdog.QueryResponse, error) {
   175  					return &logdog.QueryResponse{
   176  						Streams: []*logdog.QueryResponse_Stream{
   177  							gen("a", &logdog.LogStreamState{
   178  								Created: timestamppb.New(now),
   179  							}),
   180  						},
   181  					}, nil
   182  				}
   183  
   184  				So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   185  				So(results, shouldHaveLogStreams, "test/+/a")
   186  				So(results[0], ShouldResemble, &LogStream{
   187  					Path: "test/+/a",
   188  					Desc: logpb.LogStreamDescriptor{Prefix: "test", Name: "a"},
   189  					State: StreamState{
   190  						Created: now.UTC(),
   191  					},
   192  				})
   193  			})
   194  
   195  			Convey(`Can query for stream types`, func() {
   196  				svc.H = func(*logdog.QueryRequest) (*logdog.QueryResponse, error) {
   197  					return &logdog.QueryResponse{}, nil
   198  				}
   199  
   200  				Convey(`Text`, func() {
   201  					q.StreamType = Text
   202  					So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   203  					So(svc.LR.StreamType, ShouldResemble, &logdog.QueryRequest_StreamTypeFilter{Value: logpb.StreamType_TEXT})
   204  				})
   205  
   206  				Convey(`Binary`, func() {
   207  					q.StreamType = Binary
   208  					So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   209  					So(svc.LR.StreamType, ShouldResemble, &logdog.QueryRequest_StreamTypeFilter{Value: logpb.StreamType_BINARY})
   210  				})
   211  
   212  				Convey(`Datagram`, func() {
   213  					q.StreamType = Datagram
   214  					So(client.Query(c, project, path, q, accumulate), ShouldBeNil)
   215  					So(svc.LR.StreamType, ShouldResemble, &logdog.QueryRequest_StreamTypeFilter{Value: logpb.StreamType_DATAGRAM})
   216  				})
   217  			})
   218  
   219  			Convey(`Will return ErrNoAccess if unauthenticated.`, func() {
   220  				svc.H = func(*logdog.QueryRequest) (*logdog.QueryResponse, error) {
   221  					return nil, status.Error(codes.Unauthenticated, "unauthenticated")
   222  				}
   223  
   224  				So(client.Query(c, project, path, q, accumulate), ShouldEqual, ErrNoAccess)
   225  			})
   226  
   227  			Convey(`Will return ErrNoAccess if permission denied.`, func() {
   228  				svc.H = func(*logdog.QueryRequest) (*logdog.QueryResponse, error) {
   229  					return nil, status.Error(codes.Unauthenticated, "unauthenticated")
   230  				}
   231  
   232  				So(client.Query(c, project, path, q, accumulate), ShouldEqual, ErrNoAccess)
   233  			})
   234  		})
   235  	})
   236  }