blob: ccb5e0ba2d50208ad148ae7c615687182471d845 [file] [edit]
// Copyright 2020 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package faketlw provides a fake implementation of the TLW service.
package faketlw
import (
"context"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"go.chromium.org/chromiumos/config/go/api/test/tls"
"go.chromium.org/chromiumos/config/go/api/test/tls/dependencies/longrunning"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/emptypb"
)
// NamePort represents a simple name/port pair.
type NamePort struct {
Name string
Port int32
}
type wiringServerConfig struct {
cacheFileMap map[string][]byte
dutName string
}
// WiringServerOption is an option passed to NewWiringServer to customize WiringServer.
type WiringServerOption func(cfg *wiringServerConfig)
// WithCacheFileMap returns an option that sets the files to be fetched by
// CacheForDUT requests.
func WithCacheFileMap(m map[string][]byte) WiringServerOption {
return func(cfg *wiringServerConfig) {
cfg.cacheFileMap = m
}
}
// WithDUTName returns an option that sets the expected DUT name to be requested by
// CacheForDut requests.
func WithDUTName(n string) WiringServerOption {
return func(cfg *wiringServerConfig) {
cfg.dutName = n
}
}
type operation struct {
srcURL string
}
type operationsMap map[string]operation
// WiringServer is a fake implementation of tls.WiringServer and
// longrunning.UnimplementedOperationsServer for CacheForDUT.
type WiringServer struct {
tls.UnimplementedWiringServer
longrunning.UnimplementedOperationsServer
cfg wiringServerConfig
cacheServer *cacheServer
operationsMu sync.RWMutex
operations operationsMap
}
var _ tls.WiringServer = &WiringServer{}
// NewWiringServer constructs a new WiringServer from given options.
// The caller is responsible for calling Shutdown() of the returned object.
func NewWiringServer(opts ...WiringServerOption) *WiringServer {
var cfg wiringServerConfig
for _, opt := range opts {
opt(&cfg)
}
return &WiringServer{
cfg: cfg,
operations: map[string]operation{},
cacheServer: newCacheServer(),
}
}
// Shutdown shuts down the serer.
func (s *WiringServer) Shutdown() {
s.cacheServer.hs.Close()
}
// OpenDutPort implements tls.WiringServer.OpenDutPort.
func (s *WiringServer) OpenDutPort(ctx context.Context, req *tls.OpenDutPortRequest) (*tls.OpenDutPortResponse, error) {
return nil, status.Error(codes.Unimplemented, "not implemented")
}
// CacheForDut implements tls WiringServer.CacheForDUT
func (s *WiringServer) CacheForDut(ctx context.Context, req *tls.CacheForDutRequest) (*longrunning.Operation, error) {
if req.DutName != s.cfg.dutName {
return nil, status.Errorf(codes.InvalidArgument, "wrong DUT name: got %q, want %q", req.DutName, s.cfg.dutName)
}
_, ok := s.cfg.cacheFileMap[req.Url]
if !ok {
return nil, status.Errorf(codes.NotFound, "not found in cache file map: %s", req.Url)
}
operationName := fmt.Sprintf("CacheForDUTOperation_%s", req.Url)
s.beginOperation(operationName, req.Url)
op := longrunning.Operation{
Name: operationName,
// Pretend operation is not finished yet in order to test the code path for
// waiting the operation to finish.
Done: false,
}
return &op, nil
}
func (s *WiringServer) beginOperation(name, srcURL string) {
s.operationsMu.Lock()
defer s.operationsMu.Unlock()
s.operations[name] = operation{
srcURL: srcURL,
}
}
func (s *WiringServer) operation(name string) (oper *operation, exists bool) {
s.operationsMu.RLock()
defer s.operationsMu.RUnlock()
o, ok := s.operations[name]
return &o, ok
}
func (s *WiringServer) fillCache(srcURL string) (string, error) {
cacheURL := fmt.Sprintf("%s/?s=%s",
s.cacheServer.hs.URL, url.QueryEscape(srcURL))
k, err := cacheKey(cacheURL)
if err != nil {
return "", status.Errorf(codes.InvalidArgument, "failed to generate cache key for %s: %s", cacheURL, err)
}
content, ok := s.cfg.cacheFileMap[srcURL]
if !ok {
// CacheForDUT examines existence of the resource first.
// If the file seen missing here, it indicates the server resource was lost after it.
// (This should not happen with current implementation of the this fake TLW.)
return "", status.Errorf(codes.DataLoss, "requrested URL does not exist: %s", srcURL)
}
s.cacheServer.fillCache(k, content)
return cacheURL, nil
}
// GetOperation implements longrunning.GetOperation.
func (s *WiringServer) GetOperation(ctx context.Context, req *longrunning.GetOperationRequest) (*longrunning.Operation, error) {
return s.finishOperation(req.Name)
}
// WaitOperation implements longrunning.WaitOperation.
func (s *WiringServer) WaitOperation(ctx context.Context, req *longrunning.WaitOperationRequest) (*longrunning.Operation, error) {
return s.finishOperation(req.Name)
}
func (s *WiringServer) finishOperation(name string) (*longrunning.Operation, error) {
o, ok := s.operation(name)
if !ok {
return nil, status.Errorf(codes.InvalidArgument, "invalid argument: %s", name)
}
cacheURL, err := s.fillCache(o.srcURL)
if err != nil {
return nil, err
}
m, err := anypb.New(&tls.CacheForDutResponse{Url: cacheURL})
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to marshal data: %s", err)
}
return &longrunning.Operation{
Done: true,
Name: name,
Result: &longrunning.Operation_Response{
Response: m,
},
}, nil
}
// CancelOperation implements longrunning.CancelOperation.
func (s *WiringServer) CancelOperation(ctx context.Context, req *longrunning.CancelOperationRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "not implemented")
}
// DeleteOperation implements longrunning.CancelOperation.
func (s *WiringServer) DeleteOperation(ctx context.Context, req *longrunning.DeleteOperationRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "not implemented")
}
// ListOperations implements longrunning.ListOperations.
func (s *WiringServer) ListOperations(ctx context.Context, req *longrunning.ListOperationsRequest) (*longrunning.ListOperationsResponse, error) {
return nil, status.Error(codes.Unimplemented, "not implemented")
}
// cacheKey generates the internal key used for matching a URL generated by CacheForDUT,
// and a one that is passed to the HTTP handler.
func cacheKey(cacheURL string) (string, error) {
u, err := url.Parse(cacheURL)
if err != nil {
return "", err
}
// The query parameter name should be kept consistent with WiringServer.fillCache().
q := u.Query()["s"]
if len(q) != 1 {
return "", fmt.Errorf("failed to find query in cache URL %s", cacheURL)
}
return q[0], nil
}
// StartWiringServer is a convenient method for unit tests which starts a gRPC
// server serving WiringServer in the background. It also starts an HTTP server
// for serving cached files by CacheForDUT.
// Callers are responsible for stopping the server by stopFunc().
func StartWiringServer(t *testing.T, opts ...WiringServerOption) (stopFunc func(), addr string) {
ws := NewWiringServer(opts...)
srv := grpc.NewServer()
tls.RegisterWiringServer(srv, ws)
longrunning.RegisterOperationsServer(srv, ws)
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal("Failed to listen: ", err)
}
go srv.Serve(lis)
return func() {
ws.Shutdown()
srv.Stop()
}, lis.Addr().String()
}
type cacheServer struct {
hs *httptest.Server
cachedFilesMu sync.RWMutex
cachedFiles map[string][]byte
}
func newCacheServer() *cacheServer {
c := cacheServer{
cachedFiles: map[string][]byte{},
}
c.hs = httptest.NewServer(&c)
return &c
}
func (c *cacheServer) fillCache(key string, content []byte) {
c.cachedFilesMu.Lock()
defer c.cachedFilesMu.Unlock()
c.cachedFiles[key] = content
}
func (c *cacheServer) cachedFile(key string) (content []byte, exists bool) {
c.cachedFilesMu.RLock()
defer c.cachedFilesMu.RUnlock()
content, ok := c.cachedFiles[key]
return content, ok
}
func (c *cacheServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
k, err := cacheKey(r.URL.String())
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
content, ok := c.cachedFile(k)
if !ok {
http.NotFound(w, r)
return
}
w.Write(content)
}