From b6be68f751fee22f4069de4e377e5962a37c5c36 Mon Sep 17 00:00:00 2001 From: Daniel Adam Date: Mon, 27 May 2024 10:59:29 +0200 Subject: [PATCH] fixup! Introduce snippet-service --- .golangci.yml | 4 - .../service/subscribeToDevices_test.go | 12 +- cloud2cloud-gateway/test/events.go | 6 +- snippet-service/pb/service.proto | 1 - snippet-service/service/grpc/server.go | 69 +++++- .../service/http/createConfiguration_test.go | 162 +++++++++++--- snippet-service/service/http/service.go | 24 ++- .../service/http/updateConfiguration_test.go | 199 ++++++++++++++++++ snippet-service/service/http/uri.go | 6 + snippet-service/store/cqldb/configuration.go | 10 +- .../store/mongodb/configuration.go | 69 +++--- .../store/mongodb/configuration_test.go | 12 +- snippet-service/store/store.go | 13 +- snippet-service/test/service.go | 41 +--- test/http/request.go | 8 + test/http/uri.go | 8 +- test/iotivity-lite/service/offboard_test.go | 6 +- test/iotivity-lite/service/republish_test.go | 2 +- 18 files changed, 509 insertions(+), 143 deletions(-) create mode 100644 snippet-service/service/http/updateConfiguration_test.go diff --git a/.golangci.yml b/.golangci.yml index 9a83c0b0a..133a5c9a7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -35,7 +35,6 @@ linters: - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - - execinquery # Execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds. - exportloopref # checks for pointers to enclosing loop variables # - forcetypeassert # finds forced type assertions - gci # Gci control golang package import order and make it always deterministic. @@ -97,7 +96,6 @@ linters: - cyclop # checks function and package cyclomatic complexity - depguard # Go linter that checks if package imports are in a list of acceptable packages - exhaustive # Check exhaustiveness of enum switch statements - - exhaustivestruct # Checks if all struct's fields are initialized - exhaustruct # Checks if all structure fields are initialized. - forbidigo # Forbids identifiers - funlen # Tool for detection of long functions @@ -105,14 +103,12 @@ linters: - gochecknoinits # Checks that no init functions are present in Go code - godot # Check if comments end in a period - gomnd # An analyzer to detect magic numbers. - - ifshort # Checks that your code uses short syntax for if-statements whenever possible - inamedparam # Reports interfaces with unnamed method parameters. - interfacebloat # A linter that checks the number of methods inside an interface - ireturn # Accept Interfaces, Return Concrete Types - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - - maligned # Tool to detect Go structs that would take less memory if their fields were sorted - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - nonamedreturns # Reports all named returns - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test diff --git a/cloud2cloud-gateway/service/subscribeToDevices_test.go b/cloud2cloud-gateway/service/subscribeToDevices_test.go index 6306e519c..b400e4506 100644 --- a/cloud2cloud-gateway/service/subscribeToDevices_test.go +++ b/cloud2cloud-gateway/service/subscribeToDevices_test.go @@ -78,16 +78,16 @@ func TestRequestHandlerSubscribeToDevices(t *testing.T) { r.StrictSlash(true) r.HandleFunc(eventsURI, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h, err2 := events.ParseEventHeader(r) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) defer func() { _ = r.Body.Close() }() assert.Equal(t, wantEventType, h.EventType) buf, err2 := io.ReadAll(r.Body) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) var v interface{} err2 = json.Decode(buf, &v) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) assert.Equal(t, wantEventContent, v) w.WriteHeader(http.StatusOK) err2 = eventsServer.Close() @@ -171,16 +171,16 @@ func TestRequestHandlerSubscribeToDevicesOffline(t *testing.T) { r.StrictSlash(true) r.HandleFunc(eventsURI, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h, err2 := events.ParseEventHeader(r) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) defer func() { _ = r.Body.Close() }() assert.Equal(t, wantEventType, h.EventType) buf, err2 := io.ReadAll(r.Body) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) var v interface{} err2 = json.Decode(buf, &v) - assert.NoError(t, err2) //nolint:testifylint + assert.NoError(t, err2) assert.Equal(t, wantEventContent, v) w.WriteHeader(http.StatusOK) err2 = eventsServer.Close() diff --git a/cloud2cloud-gateway/test/events.go b/cloud2cloud-gateway/test/events.go index aad72ac83..674260f55 100644 --- a/cloud2cloud-gateway/test/events.go +++ b/cloud2cloud-gateway/test/events.go @@ -140,15 +140,15 @@ func (s *EventsServer) Run(t *testing.T) EventChan { r.StrictSlash(true) r.HandleFunc(s.uri, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { h, err := events.ParseEventHeader(r) - assert.NoError(t, err) //nolint:testifylint + assert.NoError(t, err) defer func() { _ = r.Body.Close() }() buf, err := io.ReadAll(r.Body) - assert.NoError(t, err) //nolint:testifylint + assert.NoError(t, err) data, err := decodeEvent(h.EventType, buf) - assert.NoError(t, err) //nolint:testifylint + assert.NoError(t, err) dataChan <- Event{ header: h, data: data, diff --git a/snippet-service/pb/service.proto b/snippet-service/pb/service.proto index 463cd251a..240a73e4a 100644 --- a/snippet-service/pb/service.proto +++ b/snippet-service/pb/service.proto @@ -236,7 +236,6 @@ service SnippetService { }; } - rpc GetAppliedConfigurations(GetAppliedDeviceConfigurationsRequest) returns (stream AppliedDeviceConfiguration) { option (google.api.http) = { get: "/api/v1/configurations/applied"; diff --git a/snippet-service/service/grpc/server.go b/snippet-service/service/grpc/server.go index bb76c1d30..5b1c15fc8 100644 --- a/snippet-service/service/grpc/server.go +++ b/snippet-service/service/grpc/server.go @@ -2,10 +2,14 @@ package grpc import ( "context" + "errors" "github.com/plgd-dev/hub/v2/pkg/log" + "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/snippet-service/pb" "github.com/plgd-dev/hub/v2/snippet-service/store" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) // SnippetServiceServer handles incoming requests. @@ -29,12 +33,73 @@ func NewSnippetServiceServer(ownerClaim string, hubID string, store store.Store, return s, nil } +func (s *SnippetServiceServer) checkOwner(ctx context.Context, owner string) (string, error) { + ownerFromToken, err := grpc.OwnerFromTokenMD(ctx, s.ownerClaim) + if err != nil { + return "", err + } + if owner != "" && ownerFromToken != owner { + return "", errors.New("owner mismatch") + } + return ownerFromToken, nil +} + func (s *SnippetServiceServer) CreateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { - return s.store.CreateConfiguration(ctx, conf) + owner, err := s.checkOwner(ctx, conf.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "cannot create configuration: %v", err)) + } + + conf.Owner = owner + c, err := s.store.CreateConfiguration(ctx, conf) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "cannot create configuration: %v", err)) + } + return c, nil } func (s *SnippetServiceServer) UpdateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) { - return s.store.UpdateConfiguration(ctx, conf) + owner, err := s.checkOwner(ctx, conf.GetOwner()) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "cannot update configuration: %v", err)) + } + + conf.Owner = owner + c, err := s.store.UpdateConfiguration(ctx, conf) + if err != nil { + return nil, s.logger.LogAndReturnError(status.Errorf(codes.Internal, "cannot update configuration: %v", err)) + } + return c, nil +} + +func (s *SnippetServiceServer) GetConfigurations(req *pb.GetConfigurationsRequest, srv pb.SnippetService_GetConfigurationsServer) error { + owner, err := s.checkOwner(srv.Context(), "") + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.PermissionDenied, "cannot update configuration: %v", err)) + } + + err = s.store.GetConfigurations(srv.Context(), owner, req, func(ctx context.Context, iter store.Iterator[store.Configuration]) error { + storedCfg := store.Configuration{} + for iter.Next(ctx, &storedCfg) { + for _, version := range storedCfg.Versions { + errS := srv.Send(&pb.Configuration{ + Id: storedCfg.Id, + Owner: storedCfg.Owner, + Name: storedCfg.Name, + Version: version.Version, + Resources: version.Resources, + }) + if errS != nil { + return errS + } + } + } + return nil + }) + if err != nil { + return s.logger.LogAndReturnError(status.Errorf(codes.Internal, "cannot get configurations: %v", err)) + } + return nil } func (s *SnippetServiceServer) Close(ctx context.Context) error { diff --git a/snippet-service/service/http/createConfiguration_test.go b/snippet-service/service/http/createConfiguration_test.go index 95a5a33e2..aae3e55b1 100644 --- a/snippet-service/service/http/createConfiguration_test.go +++ b/snippet-service/service/http/createConfiguration_test.go @@ -9,7 +9,6 @@ import ( "github.com/google/uuid" "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/hub/v2/grpc-gateway/pb" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" "github.com/plgd-dev/hub/v2/resource-aggregate/commands" snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" @@ -18,65 +17,162 @@ import ( "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/require" ) -func HTTPURI(uri string) string { - return httpTest.HTTPS_SCHEME + config.SNIPPET_SERVICE_HTTP_HOST + uri +func makeTestResource(t *testing.T, href string, power int) *snippetPb.Configuration_Resource { + return &snippetPb.Configuration_Resource{ + Href: href, + Content: &commands.Content{ + Data: test.EncodeToCbor(t, map[string]interface{}{"power": power}), + ContentType: message.AppOcfCbor.String(), + CoapContentFormat: int32(message.AppOcfCbor), + }, + TimeToLive: 60, + } } func TestRequestHandlerCreateConfiguration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(ctx, t, service.SetUpServicesOAuth) + defer shutDown() + + snippetCfg := snippetTest.MakeConfig(t) + shutdownHttp := snippetTest.New(t, snippetCfg) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + confID1 := uuid.NewString() + type args struct { accept string conf *snippetPb.Configuration + token string } tests := []struct { name string args args wantData map[string]interface{} wantHTTPCode int + wantErr bool }{ { name: "create", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Id: confID1, + Name: "first", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 41), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "create (with owner)", args: args{ accept: pkgHttp.ApplicationProtoJsonContentType, conf: &snippetPb.Configuration{ Id: uuid.New().String(), - Owner: "owner", - Name: "first", + Owner: oauthService.DeviceUserID, + Name: "second", Resources: []*snippetPb.Configuration_Resource{ - { - Href: "/test/1", - Content: &commands.Content{ - Data: test.EncodeToCbor(t, map[string]interface{}{ - "power": 42, - }), - ContentType: message.AppOcfCbor.String(), - CoapContentFormat: int32(message.AppOcfCbor), - }, - TimeToLive: 60, - }, + makeTestResource(t, "/test/2", 42), }, }, + token: token, }, wantHTTPCode: http.StatusOK, }, + { + name: "non-matching owner", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Id: uuid.New().String(), + Owner: "non-matching-owner", + Name: "third", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/3", 43), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, + { + name: "missing id", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Name: "fourth", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/4", 44), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "duplicit ID", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Id: confID1, + Name: "fifth", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/5", 45), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Id: uuid.New().String(), + Name: "fifth", + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "missing owner in token", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + conf: &snippetPb.Configuration{ + Id: uuid.New().String(), + Name: "sixth", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/6", 46), + }, + }, + token: oauthTest.GetAccessToken(t, config.OAUTH_SERVER_HOST, oauthTest.ClientTest, map[string]interface{}{ + snippetCfg.APIs.GRPC.Authorization.OwnerClaim: nil, + }), + }, + wantHTTPCode: http.StatusForbidden, + wantErr: true, + }, } - ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) - defer cancel() - - shutDown := service.SetUpServices(context.Background(), t, service.SetUpServicesOAuth) - defer shutDown() - - shutdownHttp := snippetTest.SetUp(t) - defer shutdownHttp() - - token := oauthTest.GetDefaultAccessToken(t) - ctx = kitNetGrpc.CtxWithToken(ctx, token) - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { data, err := httpTest.GetContentData(&pb.Content{ @@ -85,7 +181,7 @@ func TestRequestHandlerCreateConfiguration(t *testing.T) { }, message.AppJSON.String()) require.NoError(t, err) - rb := httpTest.NewRequest(http.MethodPost, HTTPURI(snippetHttp.Configurations), bytes.NewReader(data)).AuthToken(token) + rb := httpTest.NewRequest(http.MethodPost, snippetTest.HTTPURI(snippetHttp.Configurations), bytes.NewReader(data)).AuthToken(tt.args.token) rb.Accept(tt.args.accept).ContentType(message.AppJSON.String()) resp := httpTest.Do(t, rb.Build(ctx, t)) defer func() { @@ -95,9 +191,15 @@ func TestRequestHandlerCreateConfiguration(t *testing.T) { var got snippetPb.Configuration err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } require.NoError(t, err) - snippetTest.CmpConfiguration(t, tt.args.conf, &got) + want := tt.args.conf + want.Owner = oauthService.DeviceUserID + snippetTest.CmpConfiguration(t, want, &got) }) } } diff --git a/snippet-service/service/http/service.go b/snippet-service/service/http/service.go index 8b29af82c..e772cdea1 100644 --- a/snippet-service/service/http/service.go +++ b/snippet-service/service/http/service.go @@ -2,6 +2,7 @@ package http import ( "fmt" + "strings" "github.com/plgd-dev/hub/v2/http-gateway/uri" "github.com/plgd-dev/hub/v2/pkg/fsnotify" @@ -22,16 +23,15 @@ type Service struct { // New parses configuration and creates new Server with provided store and bus func New(serviceName string, config Config, snippetServiceServer *grpcService.SnippetServiceServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { service, err := httpService.New(httpService.Config{ - HTTPConnection: config.Connection, - HTTPServer: config.Server, - ServiceName: serviceName, - AuthRules: kitNetHttp.NewDefaultAuthorizationRules(uri.API), - // WhiteEndpointList: whiteList, - FileWatcher: fileWatcher, - Logger: logger, - TraceProvider: tracerProvider, - Validator: validator, - // QueryCaseInsensitive: map[string]string{}, + HTTPConnection: config.Connection, + HTTPServer: config.Server, + ServiceName: serviceName, + AuthRules: kitNetHttp.NewDefaultAuthorizationRules(uri.API), + FileWatcher: fileWatcher, + Logger: logger, + TraceProvider: tracerProvider, + Validator: validator, + QueryCaseInsensitive: queryCaseInsensitive, }) if err != nil { return nil, fmt.Errorf("cannot create http service: %w", err) @@ -48,3 +48,7 @@ func New(serviceName string, config Config, snippetServiceServer *grpcService.Sn requestHandler: requestHandler, }, nil } + +var queryCaseInsensitive = map[string]string{ + strings.ToLower(ConfigurationIDKey): "id", +} diff --git a/snippet-service/service/http/updateConfiguration_test.go b/snippet-service/service/http/updateConfiguration_test.go new file mode 100644 index 000000000..550e9e8ae --- /dev/null +++ b/snippet-service/service/http/updateConfiguration_test.go @@ -0,0 +1,199 @@ +package http_test + +import ( + "bytes" + "context" + "crypto/tls" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/hub/v2/grpc-gateway/pb" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + snippetPb "github.com/plgd-dev/hub/v2/snippet-service/pb" + snippetHttp "github.com/plgd-dev/hub/v2/snippet-service/service/http" + snippetTest "github.com/plgd-dev/hub/v2/snippet-service/test" + "github.com/plgd-dev/hub/v2/test" + "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" + oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func TestRequestHandlerUpdateConfiguration(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), config.TEST_TIMEOUT) + defer cancel() + + shutDown := service.SetUpServices(context.Background(), t, service.SetUpServicesOAuth) + defer shutDown() + + shutdownHttp := snippetTest.SetUp(t) + defer shutdownHttp() + + token := oauthTest.GetDefaultAccessToken(t) + + conn, err := grpc.NewClient(config.SNIPPET_SERVICE_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + defer func() { + _ = conn.Close() + }() + c := snippetPb.NewSnippetServiceClient(conn) + conf := &snippetPb.Configuration{ + Id: uuid.NewString(), + Version: 0, + Name: "configurationToUpdate", + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 1), + }, + } + _, err = c.CreateConfiguration(pkgGrpc.CtxWithToken(ctx, token), conf) + require.NoError(t, err) + + type args struct { + accept string + id string + conf *snippetPb.Configuration + token string + } + tests := []struct { + name string + args args + wantData map[string]interface{} + wantHTTPCode int + wantErr bool + }{ + { + name: "update", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 1, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/1", 42), + makeTestResource(t, "/test/2", 52), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "update (with owner)", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 2, + Owner: oauthService.DeviceUserID, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/3", 62), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "update (with overwritten ID)", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Id: uuid.NewString(), // this ID will get overwritten by the ID in the query + Version: 3, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/4", 72), + makeTestResource(t, "/test/5", 82), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusOK, + }, + { + name: "invalid ID", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: "invalid", + conf: &snippetPb.Configuration{ + Version: 42, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/6", 92), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "duplicit version", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 1, + Resources: []*snippetPb.Configuration_Resource{ + makeTestResource(t, "/test/7", 102), + }, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + { + name: "missing resources", + args: args{ + accept: pkgHttp.ApplicationProtoJsonContentType, + id: conf.GetId(), + conf: &snippetPb.Configuration{ + Version: 42, + }, + token: token, + }, + wantHTTPCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := httpTest.GetContentData(&pb.Content{ + ContentType: message.AppOcfCbor.String(), + Data: test.EncodeToCbor(t, tt.args.conf), + }, message.AppJSON.String()) + require.NoError(t, err) + + rb := httpTest.NewRequest(http.MethodPut, snippetTest.HTTPURI(snippetHttp.AliasConfiguration), bytes.NewReader(data)).AuthToken(tt.args.token) + rb.Accept(tt.args.accept).ContentType(message.AppJSON.String()).ID(tt.args.id) + resp := httpTest.Do(t, rb.Build(ctx, t)) + defer func() { + _ = resp.Body.Close() + }() + require.Equal(t, tt.wantHTTPCode, resp.StatusCode) + + var got snippetPb.Configuration + err = httpTest.Unmarshal(resp.StatusCode, resp.Body, &got) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + want := tt.args.conf + want.Id = tt.args.id + want.Owner = oauthService.DeviceUserID + snippetTest.CmpConfiguration(t, want, &got) + }) + } +} diff --git a/snippet-service/service/http/uri.go b/snippet-service/service/http/uri.go index c26b34c99..da47719a5 100644 --- a/snippet-service/service/http/uri.go +++ b/snippet-service/service/http/uri.go @@ -1,6 +1,9 @@ package http const ( + IDKey = "id" + ConfigurationIDKey = "configurationId" + API string = "/api/v1" // (GRPC + HTTP) GET /api/v1/conditions -> rpc GetConditions @@ -12,4 +15,7 @@ const ( // (GRPC + HTTP) DELETE /api/v1/configurations -> rpc DeleteConfigurations // (GRPC + HTTP) POST /api/v1/configurations -> rpc CreateConfiguration Configurations = API + "/configurations" + + // (GRPC + HTTP) PUT /api/v1/configurations/{id} -> rpc Update Configuration + AliasConfiguration = Configurations + "/{" + IDKey + "}" ) diff --git a/snippet-service/store/cqldb/configuration.go b/snippet-service/store/cqldb/configuration.go index 9cd79622a..ecb08bf68 100644 --- a/snippet-service/store/cqldb/configuration.go +++ b/snippet-service/store/cqldb/configuration.go @@ -11,10 +11,14 @@ func (s *Store) CreateConfiguration(context.Context, *pb.Configuration) (*pb.Con return nil, store.ErrNotSupported } -func (s *Store) UpdateConfiguration(context.Context, *pb.Configuration) (*pb.Configuration, error) { - return nil, store.ErrNotSupported +func (s *Store) DeleteConfigurations(context.Context, string, *pb.DeleteConfigurationsRequest) (int64, error) { + return 0, store.ErrNotSupported } -func (s *Store) LoadConfigurations(context.Context, string, *pb.GetConfigurationsRequest, store.LoadConfigurationsFunc) error { +func (s *Store) GetConfigurations(context.Context, string, *pb.GetConfigurationsRequest, store.GetConfigurationsFunc) error { return store.ErrNotSupported } + +func (s *Store) UpdateConfiguration(context.Context, *pb.Configuration) (*pb.Configuration, error) { + return nil, store.ErrNotSupported +} diff --git a/snippet-service/store/mongodb/configuration.go b/snippet-service/store/mongodb/configuration.go index b29aa4b42..216da33dc 100644 --- a/snippet-service/store/mongodb/configuration.go +++ b/snippet-service/store/mongodb/configuration.go @@ -95,37 +95,48 @@ func normalizeIdFilter(idfilter []*pb.IDFilter) []*pb.IDFilter { var idLatest bool var idValue bool var idValueVersion uint64 + setNextInitial := func(idf *pb.IDFilter) { + idAll = idf.GetAll() + idLatest = idf.GetLatest() + idValue = !idAll && !idLatest + idValueVersion = idf.GetValue() + } + setNextLatest := func(idf *pb.IDFilter) { + // we already have the latest filter + if idLatest { + // skip + return + } + idLatest = true + updatedFilter = append(updatedFilter, idf) + } + setNextValue := func(idf *pb.IDFilter) { + value := idf.GetValue() + if idValue && value == idValueVersion { + // skip + return + } + idValue = true + idValueVersion = value + updatedFilter = append(updatedFilter, idf) + } prevID := "" for _, idf := range idfilter { if idf.GetId() != prevID { - idAll = idf.GetAll() - idLatest = idf.GetLatest() - idValue = !idAll && !idLatest - idValueVersion = idf.GetValue() + setNextInitial(idf) updatedFilter = append(updatedFilter, idf) } - var value uint64 if idAll { goto next } if idf.GetLatest() { - // we already have the latest filter - if idLatest { - goto next - } - idLatest = true - updatedFilter = append(updatedFilter, idf) - } - - value = idf.GetValue() - if idValue && value == idValueVersion { + setNextLatest(idf) goto next } - idValue = true - idValueVersion = value - updatedFilter = append(updatedFilter, idf) + + setNextValue(idf) next: prevID = idf.GetId() @@ -138,8 +149,8 @@ type versionFilter struct { versions []uint64 } -func partitionQuery(query *pb.GetConfigurationsRequest) ([]string, map[string]versionFilter) { - idFilter := normalizeIdFilter(query.GetIdFilter()) +func partitionQuery(idfilter []*pb.IDFilter) ([]string, map[string]versionFilter) { + idFilter := normalizeIdFilter(idfilter) if len(idFilter) == 0 { return nil, nil } @@ -198,7 +209,7 @@ func toIdFilterQuery(owner string, idfAlls []string) interface{} { return bson.M{"$and": filters} } -func processCursor(ctx context.Context, cr *mongo.Cursor, h store.LoadConfigurationsFunc) error { +func processCursor(ctx context.Context, cr *mongo.Cursor, h store.GetConfigurationsFunc) error { if h == nil { return nil } @@ -213,7 +224,7 @@ func processCursor(ctx context.Context, cr *mongo.Cursor, h store.LoadConfigurat return errors.ErrorOrNil() } -func (s *Store) loadConfigurationsByFind(ctx context.Context, owner string, idfAlls []string, h store.LoadConfigurationsFunc) error { +func (s *Store) getConfigurationsByFind(ctx context.Context, owner string, idfAlls []string, h store.GetConfigurationsFunc) error { cur, err := s.Collection(configurationsCol).Find(ctx, toIdFilterQuery(owner, idfAlls)) if err != nil { return err @@ -221,7 +232,7 @@ func (s *Store) loadConfigurationsByFind(ctx context.Context, owner string, idfA return processCursor(ctx, cur, h) } -func (s *Store) loadConfigurationsByAggregation(ctx context.Context, id, owner string, vf versionFilter, h store.LoadConfigurationsFunc) error { +func (s *Store) getConfigurationsByAggregation(ctx context.Context, id, owner string, vf versionFilter, h store.GetConfigurationsFunc) error { match := bson.D{ {Key: store.VersionsKey + ".0", Value: bson.M{"$exists": true}}, } @@ -281,18 +292,22 @@ func (s *Store) loadConfigurationsByAggregation(ctx context.Context, id, owner s return processCursor(ctx, cur, h) } -func (s *Store) LoadConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, h store.LoadConfigurationsFunc) error { - idVersionAll, idVersions := partitionQuery(query) +func (s *Store) GetConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, h store.GetConfigurationsFunc) error { + idVersionAll, idVersions := partitionQuery(query.GetIdFilter()) var errors *multierror.Error if len(idVersionAll) > 0 || len(idVersions) == 0 { - err := s.loadConfigurationsByFind(ctx, owner, idVersionAll, h) + err := s.getConfigurationsByFind(ctx, owner, idVersionAll, h) errors = multierror.Append(errors, err) } if len(idVersions) > 0 { for id, vf := range idVersions { - err := s.loadConfigurationsByAggregation(ctx, id, owner, vf, h) + err := s.getConfigurationsByAggregation(ctx, id, owner, vf, h) errors = multierror.Append(errors, err) } } return errors.ErrorOrNil() } + +func (s *Store) DeleteConfigurations(context.Context, string, *pb.DeleteConfigurationsRequest) (int64, error) { + return 0, store.ErrNotSupported +} diff --git a/snippet-service/store/mongodb/configuration_test.go b/snippet-service/store/mongodb/configuration_test.go index 241c00581..9a72929ec 100644 --- a/snippet-service/store/mongodb/configuration_test.go +++ b/snippet-service/store/mongodb/configuration_test.go @@ -587,22 +587,12 @@ func TestStoreGetConfigurations(t *testing.T) { } }, }, - // { - // name: "owner0/version/{1, 49, latest}", args: args{ - // owner: testConfigurationOwner(0), - // query: &pb.GetConfigurationsRequest{ - // IdFilter: []*pb.IDFilter{ - - // }, - // }, - // }, - // }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var configurations []*store.Configuration - err := s.LoadConfigurations(ctx, tt.args.owner, tt.args.query, func(iterCtx context.Context, iter store.Iterator[store.Configuration]) error { + err := s.GetConfigurations(ctx, tt.args.owner, tt.args.query, func(iterCtx context.Context, iter store.Iterator[store.Configuration]) error { var conf store.Configuration for iter.Next(iterCtx, &conf) { configurations = append(configurations, conf.Clone()) diff --git a/snippet-service/store/store.go b/snippet-service/store/store.go index 31ca5beb9..a2fbc0040 100644 --- a/snippet-service/store/store.go +++ b/snippet-service/store/store.go @@ -33,12 +33,13 @@ type Iterator[T any] interface { type ( // LoadConditionsFunc = func(ctx context.Context, iter Iterator[Condition]) (err error) - LoadConfigurationsFunc = func(ctx context.Context, iter Iterator[Configuration]) (err error) + GetConfigurationsFunc = func(ctx context.Context, iter Iterator[Configuration]) (err error) ) var ( - ErrNotSupported = errors.New("not supported") - ErrNotFound = errors.New("not found") + ErrNotSupported = errors.New("not supported") + ErrNotFound = errors.New("not found") + ErrInvalidArgument = errors.New("invalid argument") ) type MongoIterator[T any] struct { @@ -69,10 +70,10 @@ type Store interface { CreateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) // UpdateConfiguration updates an existing configuration in the database. UpdateConfiguration(ctx context.Context, conf *pb.Configuration) (*pb.Configuration, error) - // LoadConfigurations loads a configuration from the database. - LoadConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, h LoadConfigurationsFunc) error + // GetConfigurations loads a configuration from the database. + GetConfigurations(ctx context.Context, owner string, query *pb.GetConfigurationsRequest, h GetConfigurationsFunc) error // DeleteConfigurations deletes configurations from the database. - // DeleteConfigurations(ctx context.Context, owner string, query *DeleteConfigurationsQuery) (int64, error) + DeleteConfigurations(ctx context.Context, owner string, query *pb.DeleteConfigurationsRequest) (int64, error) Close(ctx context.Context) error } diff --git a/snippet-service/test/service.go b/snippet-service/test/service.go index f23a7c6ed..8cea9d0f2 100644 --- a/snippet-service/test/service.go +++ b/snippet-service/test/service.go @@ -10,15 +10,19 @@ import ( "github.com/plgd-dev/hub/v2/pkg/log" "github.com/plgd-dev/hub/v2/pkg/mongodb" "github.com/plgd-dev/hub/v2/snippet-service/service" - "github.com/plgd-dev/hub/v2/snippet-service/store" storeConfig "github.com/plgd-dev/hub/v2/snippet-service/store/config" storeCqlDB "github.com/plgd-dev/hub/v2/snippet-service/store/cqldb" storeMongo "github.com/plgd-dev/hub/v2/snippet-service/store/mongodb" "github.com/plgd-dev/hub/v2/test/config" + httpTest "github.com/plgd-dev/hub/v2/test/http" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace/noop" ) +func HTTPURI(uri string) string { + return httpTest.HTTPS_SCHEME + config.SNIPPET_SERVICE_HTTP_HOST + uri +} + func MakeConfig(t require.TestingT) service.Config { var cfg service.Config @@ -72,7 +76,9 @@ func MakeStorageConfig() service.StorageConfig { return service.StorageConfig{ CleanUpRecords: "0 1 * * *", Embedded: storeConfig.Config{ - Use: config.ACTIVE_DATABASE(), + // TODO: add cqldb support + // Use: config.ACTIVE_DATABASE(), + Use: database.MongoDB, MongoDB: &storeMongo.Config{ Mongo: mongodb.Config{ MaxPoolSize: 16, @@ -90,37 +96,6 @@ func MakeStorageConfig() service.StorageConfig { } } -func NewCQLStore(t require.TestingT) (*storeCqlDB.Store, func()) { - cfg := MakeConfig(t) - logger := log.NewLogger(cfg.Log) - - fileWatcher, err := fsnotify.NewWatcher(logger) - require.NoError(t, err) - - ctx := context.Background() - store, err := storeCqlDB.New(ctx, cfg.Clients.Storage.Embedded.CqlDB, fileWatcher, logger, noop.NewTracerProvider()) - require.NoError(t, err) - - cleanUp := func() { - err := store.Clear(ctx) - require.NoError(t, err) - _ = store.Close(ctx) - - err = fileWatcher.Close() - require.NoError(t, err) - } - - return store, cleanUp -} - -func NewStore(t require.TestingT) (store.Store, func()) { - cfg := MakeConfig(t) - if cfg.Clients.Storage.Embedded.Use == database.CqlDB { - return NewCQLStore(t) - } - return NewMongoStore(t) -} - func NewMongoStore(t require.TestingT) (*storeMongo.Store, func()) { cfg := MakeConfig(t) logger := log.NewLogger(cfg.Log) diff --git a/test/http/request.go b/test/http/request.go index a86a9faf5..1afdb8dfa 100644 --- a/test/http/request.go +++ b/test/http/request.go @@ -102,6 +102,14 @@ func (c *RequestBuilder) DeviceId(deviceID string) *RequestBuilder { return c } +func (c *RequestBuilder) ID(id string) *RequestBuilder { + if id == "" { + return c + } + c.uriParams[IDKey] = id + return c +} + func (c *RequestBuilder) ResourceHref(resourceHref string) *RequestBuilder { if resourceHref == "" { return c diff --git a/test/http/uri.go b/test/http/uri.go index f3ba99c3f..67fe7ddd6 100644 --- a/test/http/uri.go +++ b/test/http/uri.go @@ -3,9 +3,11 @@ package http const ( HTTPS_SCHEME = "https://" - DeviceIDKey = "deviceID" - ResourceHrefKey = "resourceHref" - SubscriptionIDKey = "subscriptionID" + IDKey = "id" + DeviceIDKey = "deviceID" + ResourceHrefKey = "resourceHref" + SubscriptionIDKey = "subscriptionID" + ConfigurationIDKey = "configurationId" ContentQueryKey = "content" ) diff --git a/test/iotivity-lite/service/offboard_test.go b/test/iotivity-lite/service/offboard_test.go index 7a44b2571..f8c09c2df 100644 --- a/test/iotivity-lite/service/offboard_test.go +++ b/test/iotivity-lite/service/offboard_test.go @@ -49,13 +49,13 @@ func TestOffboard(t *testing.T) { log.Debugf("%+v", h.CallCounter.Data) signInCount, ok := h.CallCounter.Data[iotService.SignInKey] require.True(t, ok) - require.Greater(t, signInCount, 0) + require.Positive(t, signInCount) publishCount, ok := h.CallCounter.Data[iotService.PublishKey] require.True(t, ok) require.Equal(t, 1, publishCount) singOffCount, ok := h.CallCounter.Data[iotService.SignOffKey] require.True(t, ok) - require.Greater(t, singOffCount, 0) + require.Positive(t, singOffCount) } coapShutdown := coapgwTest.SetUp(t, makeHandler, validateHandler) @@ -337,7 +337,7 @@ func TestOffboardWithSignInByRefreshToken(t *testing.T) { require.Greater(t, signInCount, 1) refreshCount, ok := h.CallCounter.Data[iotService.RefreshTokenKey] require.True(t, ok) - require.Greater(t, refreshCount, 0) + require.Positive(t, refreshCount) signOffCount, ok := h.CallCounter.Data[iotService.SignOffKey] require.True(t, ok) require.Equal(t, 1, signOffCount) diff --git a/test/iotivity-lite/service/republish_test.go b/test/iotivity-lite/service/republish_test.go index 9e43def74..a6367aae6 100644 --- a/test/iotivity-lite/service/republish_test.go +++ b/test/iotivity-lite/service/republish_test.go @@ -45,7 +45,7 @@ func TestRepublishAfterRefresh(t *testing.T) { require.Greater(t, signInCount, 1) refreshCount, ok := h.CallCounter.Data[iotService.RefreshTokenKey] require.True(t, ok) - require.Greater(t, refreshCount, 0) + require.Positive(t, refreshCount) publishCount, ok := h.CallCounter.Data[iotService.PublishKey] require.True(t, ok) require.Equal(t, 1, publishCount)