diff --git a/.vscode/settings.json b/.vscode/settings.json index dfa25688..5f77fbf6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "-race", "-v", "-cover", + "-coverpkg=./...", ], "go.testEnvVars": { "ROOT_CA_CRT": "${workspaceFolder}/.tmp/pki_certs/cloudca.pem", @@ -15,7 +16,7 @@ "IDENTITY_KEY": "${workspaceFolder}/.tmp/pki_certs/identitykey.pem", }, "files.watcherExclude": { - "**/plgd-dev/device/v2/**": true - }, + "**/plgd-dev/device/v2/**": true + }, "go.testTimeout": "600s", } \ No newline at end of file diff --git a/Makefile b/Makefile index e6af0958..81057176 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,13 @@ build-testcontainer: build: build-testcontainer +ROOT_CA_CRT = $(CERT_PATH)/cloudca.pem +ROOT_CA_KEY = $(CERT_PATH)/cloudcakey.pem +INTERMEDIATE_CA_CRT = $(CERT_PATH)/intermediatecacrt.pem +INTERMEDIATE_CA_KEY = $(CERT_PATH)/intermediatecakey.pem +MFG_CRT = $(CERT_PATH)/mfgcrt.pem +MFG_KEY = $(CERT_PATH)/mfgkey.pem + certificates: mkdir -p $(CERT_PATH) chmod 0777 $(CERT_PATH) @@ -64,8 +71,9 @@ env: clean certificates unit-test: certificates mkdir -p $(TMP_PATH) + ROOT_CA_CRT="$(ROOT_CA_CRT)" MFG_CRT="$(MFG_CRT)" MFG_KEY="$(MFG_KEY)" INTERMEDIATE_CA_CRT="$(INTERMEDIATE_CA_CRT)" INTERMEDIATE_CA_KEY=$(INTERMEDIATE_CA_KEY) go test -race -v ./bridge/... -coverpkg=./... -covermode=atomic -coverprofile=$(TMP_PATH)/bridge.coverage.txt go test -race -v ./schema/... -covermode=atomic -coverprofile=$(TMP_PATH)/schema.coverage.txt - ROOT_CA_CRT="$(CERT_PATH)/cloudca.pem" ROOT_CA_KEY="$(CERT_PATH)/cloudcakey.pem" go test -race -v ./pkg/... -covermode=atomic -coverprofile=$(TMP_PATH)/pkg.coverage.txt + ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(CERT_PATH)/cloudcakey.pem" go test -race -v ./pkg/... -covermode=atomic -coverprofile=$(TMP_PATH)/pkg.coverage.txt test: env build-testcontainer docker run \ diff --git a/bridge/device/cloud/manager.go b/bridge/device/cloud/manager.go index 6816f995..22e00a2f 100644 --- a/bridge/device/cloud/manager.go +++ b/bridge/device/cloud/manager.go @@ -142,7 +142,9 @@ func (c *Manager) resetCredentials(ctx context.Context, signOff bool) { c.creds = CoapSignUpResponse{} c.signedIn = false c.resourcesPublished = false - c.close() + if err := c.close(); err != nil { + log.Printf("cannot close connection: %v\n", err) + } c.save() } @@ -299,19 +301,18 @@ func (c *Manager) close() error { } client := c.client c.client = nil - err := client.Close() - return err + return client.Close() } func (c *Manager) dial(ctx context.Context) error { if c.client != nil && c.client.Context().Err() == nil { return nil } - c.close() + _ = c.close() cfg := c.getCloudConfiguration() tlsConfig := &tls.Config{ // TODO: set RootCAs from configuration - InsecureSkipVerify: true, + InsecureSkipVerify: true, //nolint:gosec } ep := schema.Endpoint{ URI: cfg.URL, @@ -334,7 +335,9 @@ func (c *Manager) dial(ctx context.Context) error { }), options.WithKeepAlive(2, time.Second*10, func(c *client.Conn) { log.Printf("keepalive timeout\n") - c.Close() + if errC := c.Close(); errC != nil { + log.Printf("cannot close connection: %v\n", errC) + } })) if err != nil { return fmt.Errorf("cannot dial to %v: %w", addr.String(), err) @@ -430,7 +433,7 @@ func (c *Manager) signOff(ctx context.Context) error { } // signIn / refresh token fails if ctx.Err() != nil { - return nil + return ctx.Err() } req, err := c.newSignOffReq(ctx) if err != nil { @@ -678,7 +681,7 @@ func (c *Manager) connect(ctx context.Context) error { } err := r(ctx) if err != nil { - c.close() + _ = c.close() return err } } diff --git a/bridge/device/device_test.go b/bridge/device/device_test.go new file mode 100644 index 00000000..35510bbc --- /dev/null +++ b/bridge/device/device_test.go @@ -0,0 +1,68 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package device_test + +import ( + "testing" + + "github.com/google/uuid" + bridgeDevice "github.com/plgd-dev/device/v2/bridge/device" + "github.com/stretchr/testify/require" +) + +func TestConfigValidate(t *testing.T) { + tests := []struct { + name string + cfg bridgeDevice.Config + wantError bool + }{ + { + name: "Valid Configuration", + cfg: bridgeDevice.Config{ + ProtocolIndependentID: uuid.New(), + ID: uuid.New(), + Name: "ValidName", + }, + }, + { + name: "Valid Configuration with empty ID and Name", + cfg: bridgeDevice.Config{ + ProtocolIndependentID: uuid.New(), + }, + }, + { + name: "Invalid ProtocolIndependentID", + cfg: bridgeDevice.Config{ + ID: uuid.New(), + }, + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if tt.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/bridge/getResource_test.go b/bridge/getResource_test.go new file mode 100644 index 00000000..5f7d2429 --- /dev/null +++ b/bridge/getResource_test.go @@ -0,0 +1,175 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package bridge_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/resources" + bridgeTest "github.com/plgd-dev/device/v2/bridge/test" + "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/client/core" + "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/schema/device" + "github.com/plgd-dev/device/v2/schema/interfaces" + testClient "github.com/plgd-dev/device/v2/test/client" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/stretchr/testify/require" +) + +func withDeviceID(deviceID string) client.ResourceQueryOption { + return client.WithQuery(fmt.Sprintf("di=%v", deviceID)) +} + +func TestGetResource(t *testing.T) { + s := bridgeTest.NewBridgeService(t) + deviceID1 := uuid.New().String() + d1 := bridgeTest.NewBridgedDevice(t, s, false, deviceID1) + deviceID2 := uuid.New().String() + d2 := bridgeTest.NewBridgedDevice(t, s, false, deviceID2) + defer func() { + s.DeleteAndCloseDevice(d2.GetID()) + s.DeleteAndCloseDevice(d1.GetID()) + }() + + failRes := resources.NewResource("/fail", + nil, + nil, + []string{"oic.d.virtual", "oic.d.test"}, + []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}, + ) + d1.AddResource(failRes) + + cleanup := bridgeTest.RunBridgeService(s) + defer cleanup() + + type args struct { + deviceID string + href string + opts []client.GetOption + } + tests := []struct { + name string + args args + want coap.DetailedResponse[interface{}] + wantErr bool + }{ + { + name: "valid", + args: args{ + deviceID: d1.GetID().String(), + href: device.ResourceURI, + opts: []client.GetOption{ + client.WithDiscoveryConfiguration(core.DefaultDiscoveryConfiguration()), + withDeviceID(d1.GetID().String()), + }, + }, + want: coap.DetailedResponse[interface{}]{ + Code: codes.Content, + Body: map[interface{}]interface{}{ + "di": d1.GetID().String(), + "piid": d1.GetProtocolIndependentID().String(), + "n": d1.GetName(), + }, + }, + }, + { + name: "valid with interface", + args: args{ + deviceID: d2.GetID().String(), + href: device.ResourceURI, + opts: []client.GetOption{ + client.WithInterface(interfaces.OC_IF_BASELINE), + withDeviceID(d2.GetID().String()), + }, + }, + + want: coap.DetailedResponse[interface{}]{ + Code: codes.Content, + Body: map[interface{}]interface{}{ + "di": d2.GetID().String(), + "piid": d2.GetProtocolIndependentID().String(), + "n": d2.GetName(), + "if": []interface{}{interfaces.OC_IF_BASELINE, interfaces.OC_IF_R}, + "rt": []interface{}{"oic.d.virtual", "oic.wk.d"}, + }, + }, + }, + { + name: "invalid href", + args: args{ + deviceID: d1.GetID().String(), + href: "/invalid/href", + opts: []client.GetOption{ + withDeviceID(d1.GetID().String()), + }, + }, + wantErr: true, + }, + { + name: "invalid deviceID", + args: args{ + deviceID: "notfound", + href: device.ResourceURI, + opts: []client.GetOption{ + withDeviceID(d1.GetID().String()), + }, + }, + wantErr: true, + }, + { + name: "invalid get handler", + args: args{ + deviceID: d1.GetID().String(), + href: failRes.GetHref(), + opts: []client.GetOption{ + withDeviceID(d1.GetID().String()), + }, + }, + wantErr: true, + }, + } + + c, err := testClient.NewTestSecureClient() + require.NoError(t, err) + defer func() { + errC := c.Close(context.Background()) + require.NoError(t, errC) + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*4) + defer cancel() + var got coap.DetailedResponse[interface{}] + err := c.GetResource(ctx, tt.args.deviceID, tt.args.href, &got, tt.args.opts...) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + got.ETag = nil + require.Equal(t, tt.want, got) + }) + } +} diff --git a/bridge/net/network.go b/bridge/net/network.go index f3443de6..6278d570 100644 --- a/bridge/net/network.go +++ b/bridge/net/network.go @@ -24,7 +24,6 @@ import ( "fmt" "io" "log" - "math" gonet "net" "strconv" "strings" @@ -63,6 +62,8 @@ type Net struct { mux *mux.Router } +const DefaultMaxMessageSize = 2 * 1024 * 1024 + func (cfg *Config) Validate() error { if cfg.ExternalAddress == "" { return fmt.Errorf("externalAddress is required") @@ -81,11 +82,8 @@ func (cfg *Config) Validate() error { if port == 0 { return fmt.Errorf("invalid externalAddress: port cannot be 0") } - if port > math.MaxUint16 { - return fmt.Errorf("invalid externalAddress: port cannot be greater than %v", math.MaxUint16) - } if cfg.MaxMessageSize == 0 { - cfg.MaxMessageSize = 2 * 1024 * 1024 + cfg.MaxMessageSize = DefaultMaxMessageSize } cfg.externalAddressPort = portStr @@ -126,7 +124,7 @@ func initConnectivity(listenAddress string) (*net.UDPConn, *net.UDPConn, error) } if !anySet { _ = mcastListener.Close() - return nil, nil, fmt.Errorf("cannot JoinGroup(%v): %v", a, err) + return nil, nil, fmt.Errorf("cannot JoinGroup(%v): %w", a, err) } err = mcastListener.SetMulticastLoopback(true) diff --git a/bridge/resources/device/resource.go b/bridge/resources/device/resource.go index d47c4225..83862f94 100644 --- a/bridge/resources/device/resource.go +++ b/bridge/resources/device/resource.go @@ -91,8 +91,8 @@ func (d *Resource) Get(request *net.Request) (*pool.Message, error) { ID: d.device.GetID().String(), Name: d.device.GetName(), ProtocolIndependentID: d.device.GetProtocolIndependentID().String(), - //DataModelVersion: "ocf.res.1.3.0", - //SpecificationVersion: "ocf.2.0.5", + // DataModelVersion: "ocf.res.1.3.0", + // SpecificationVersion: "ocf.2.0.5", } if request.Interface() == interfaces.OC_IF_BASELINE { deviceProperties.ResourceTypes = d.Resource.ResourceTypes diff --git a/bridge/resources/device/resource_internal_test.go b/bridge/resources/device/resource_internal_test.go new file mode 100644 index 00000000..9a390646 --- /dev/null +++ b/bridge/resources/device/resource_internal_test.go @@ -0,0 +1,94 @@ +package device + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMergeCBORStructs(t *testing.T) { + type args struct { + structs []interface{} + } + tests := []struct { + name string + args args + want interface{} + }{ + { + name: "valid", + args: args{ + structs: []interface{}{ + map[interface{}]interface{}{"key1": "value1"}, + map[interface{}]interface{}{"key2": "value2"}, + }, + }, + want: map[interface{}]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "merging nil structs", + args: args{ + structs: []interface{}{ + nil, + nil, + }, + }, + want: nil, + }, + { + name: "invalid CBOR encoding", + args: args{ + structs: []interface{}{ + map[interface{}]interface{}{"key1": "value1"}, + "invalid CBOR", + }, + }, + want: map[interface{}]interface{}{ + "key1": "value1", + }, + }, + { + name: "merging struct with empty CBOR encoding", + args: args{ + structs: []interface{}{ + map[interface{}]interface{}{"key1": "value1"}, + map[interface{}]interface{}{}, + }, + }, + want: map[interface{}]interface{}{ + "key1": "value1", + }, + }, + { + name: "merging multiple structs", + args: args{ + structs: []interface{}{ + map[interface{}]interface{}{"key1": "value1"}, + map[interface{}]interface{}{"key2": "value2"}, + map[interface{}]interface{}{"key3": "value3"}, + map[interface{}]interface{}{"key4": "value4"}, + }, + }, + want: map[interface{}]interface{}{ + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mergeCBORStructs(tt.args.structs...) + if tt.want != nil { + require.Equal(t, tt.want, got) + return + } + require.Nil(t, got) + }) + } +} diff --git a/bridge/resources/resource.go b/bridge/resources/resource.go index dd695e6b..3451adda 100644 --- a/bridge/resources/resource.go +++ b/bridge/resources/resource.go @@ -47,11 +47,11 @@ type CreateSubscriptionFunc func(req *net.Request, handler func(msg *pool.Messag const PublishToCloud schema.BitMask = 1 << 7 func ToUUID(id string) uuid.UUID { - if v, err := uuid.Parse(id); err != nil { + v, err := uuid.Parse(id) + if err != nil { return uuid.NewSHA1(uuid.NameSpaceURL, []byte(id)) - } else { - return v } + return v } type subscription struct { @@ -262,7 +262,7 @@ func (r *Resource) observerHandler(req *net.Request, createSubscription bool) (* resp.SetObserve(sequence.Inc()) } else { defer r.removeSubscription(req.Conn.RemoteAddr().String()) - resp, err = CreateResponseBadRequest(req.Conn.Context(), fmt.Errorf("error while observing %s: %v", r.Href, err)) + resp, err = CreateResponseBadRequest(req.Conn.Context(), fmt.Errorf("error while observing %s: %w", r.Href, err)) if err != nil { return } diff --git a/bridge/service/config.go b/bridge/service/config.go index 79681576..6fd312c9 100644 --- a/bridge/service/config.go +++ b/bridge/service/config.go @@ -1,3 +1,21 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + package service import ( diff --git a/bridge/service/config_test.go b/bridge/service/config_test.go new file mode 100644 index 00000000..059f93c0 --- /dev/null +++ b/bridge/service/config_test.go @@ -0,0 +1,119 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package service_test + +import ( + "testing" + + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/bridge/service" + "github.com/stretchr/testify/require" +) + +func TestCoAPConfigValidate(t *testing.T) { + tests := []struct { + name string + coapConfig service.CoAPConfig + wantErr bool + }{ + { + name: "ValidCoAPConfig", + coapConfig: service.CoAPConfig{ID: "test", Config: net.Config{ExternalAddress: "localhost:12345"}}, + }, + { + name: "MissingID", + coapConfig: service.CoAPConfig{Config: net.Config{ExternalAddress: "localhost:12345"}}, + wantErr: true, + }, + { + name: "InvalidConfig", + coapConfig: service.CoAPConfig{ID: "test", Config: net.Config{}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.coapConfig.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestAPIConfigValidate(t *testing.T) { + tests := []struct { + name string + apiConfig service.APIConfig + wantErr bool + }{ + { + name: "ValidAPIConfig", + apiConfig: service.APIConfig{CoAP: service.CoAPConfig{ID: "test", Config: net.Config{ExternalAddress: "localhost:12345"}}}, + }, + { + name: "InvalidCoAPConfig", + apiConfig: service.APIConfig{CoAP: service.CoAPConfig{Config: net.Config{ExternalAddress: "localhost:12345"}}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.apiConfig.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestConfigValidate(t *testing.T) { + tests := []struct { + name string + config service.Config + wantErr bool + }{ + { + name: "ValidConfig", + config: service.Config{API: service.APIConfig{CoAP: service.CoAPConfig{ID: "test", Config: net.Config{ExternalAddress: "localhost:12345"}}}}, + }, + { + name: "InvalidAPIConfig", + config: service.Config{API: service.APIConfig{CoAP: service.CoAPConfig{Config: net.Config{ExternalAddress: "localhost:12345"}}}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/bridge/service/service.go b/bridge/service/service.go index e4bbbdaa..b8cfe94d 100644 --- a/bridge/service/service.go +++ b/bridge/service/service.go @@ -1,3 +1,21 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + package service import ( @@ -91,11 +109,11 @@ func (c *Service) handleDiscoverAllLinks(req *net.Request) (*pool.Message, error func (c *Service) DefaultRequestHandler(req *net.Request) (*pool.Message, error) { uriPath := req.URIPath() if uriPath == "" { - return nil, nil + return nil, nil //nolint:nilnil } if req.Code() == codes.GET && uriPath == "/.well-known/core" { // ignore well-known/core - return nil, nil + return nil, nil //nolint:nilnil } if req.Code() == codes.GET && uriPath == plgdResources.ResourceURI { return c.handleDiscoverAllLinks(req) diff --git a/bridge/test/test.go b/bridge/test/test.go new file mode 100644 index 00000000..8e22d5f5 --- /dev/null +++ b/bridge/test/test.go @@ -0,0 +1,80 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/device" + "github.com/plgd-dev/device/v2/bridge/service" + "github.com/stretchr/testify/require" +) + +const ( + BRIDGE_SERVICE_PIID = "f47ac10b-58cc-4372-a567-0e02b2c3d479" + BRIDGE_DEVICE_HOST = "127.0.0.1:15000" +) + +func MakeConfig(t *testing.T) service.Config { + var cfg service.Config + cfg.API.CoAP.ID = BRIDGE_SERVICE_PIID + cfg.API.CoAP.Config.ExternalAddress = BRIDGE_DEVICE_HOST + require.NoError(t, cfg.API.Validate()) + return cfg +} + +func NewBridgeService(t *testing.T) *service.Service { + s, err := service.New(MakeConfig(t)) + require.NoError(t, err) + return s +} + +func RunBridgeService(s *service.Service) func() { + cleanup := func() { + _ = s.Shutdown() + } + go func() { + _ = s.Serve() + }() + return cleanup +} + +func NewBridgedDevice(t *testing.T, s *service.Service, cloudEnabled bool, id string) service.Device { + newDevice := func(id uuid.UUID, piid uuid.UUID) service.Device { + cfg := device.Config{ + Name: "bridged-device", + ResourceTypes: []string{"oic.d.virtual"}, + ID: id, + ProtocolIndependentID: piid, + MaxMessageSize: 1024 * 256, + } + if cloudEnabled { + cfg.Cloud.Enabled = true + } + require.NoError(t, cfg.Validate()) + return device.New(cfg, nil, nil) + } + u, err := uuid.Parse(id) + require.NoError(t, err) + d, ok := s.CreateDevice(u, newDevice) + require.True(t, ok) + d.Init() + return d +} diff --git a/bridge/updateResource_test.go b/bridge/updateResource_test.go new file mode 100644 index 00000000..657e31f5 --- /dev/null +++ b/bridge/updateResource_test.go @@ -0,0 +1,116 @@ +package bridge_test + +import ( + "bytes" + "context" + "sync" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/bridge/resources" + bridgeTest "github.com/plgd-dev/device/v2/bridge/test" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" + codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" + "github.com/plgd-dev/device/v2/pkg/net/coap" + "github.com/plgd-dev/device/v2/schema/interfaces" + testClient "github.com/plgd-dev/device/v2/test/client" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + "github.com/stretchr/testify/require" +) + +type resourceData struct { + Name string `json:"name,omitempty"` +} + +type resourceDataSync struct { + resourceData + lock sync.Mutex +} + +func (r *resourceDataSync) setName(name string) { + r.lock.Lock() + defer r.lock.Unlock() + r.Name = name +} + +func (r *resourceDataSync) getName() string { + r.lock.Lock() + defer r.lock.Unlock() + return r.Name +} + +func (r *resourceDataSync) copy() resourceData { + r.lock.Lock() + defer r.lock.Unlock() + return resourceData{ + Name: r.Name, + } +} + +func TestUpdateResource(t *testing.T) { + s := bridgeTest.NewBridgeService(t) + d := bridgeTest.NewBridgedDevice(t, s, false, uuid.New().String()) + defer func() { + s.DeleteAndCloseDevice(d.GetID()) + }() + + rds := resourceDataSync{ + resourceData: resourceData{ + Name: "test", + }, + } + + resHandler := func(req *net.Request) (*pool.Message, error) { + resp := pool.NewMessage(req.Context()) + data, err := cbor.Encode(rds.copy()) + if err != nil { + return nil, err + } + resp.SetCode(codes.Changed) + resp.SetContentFormat(message.AppOcfCbor) + resp.SetBody(bytes.NewReader(data)) + return resp, nil + } + + res := resources.NewResource("/test", resHandler, func(req *net.Request) (*pool.Message, error) { + codec := codecOcf.VNDOCFCBORCodec{} + var newData resourceData + err := codec.Decode(req.Message, &newData) + if err != nil { + return nil, err + } + rds.setName(newData.Name) + return resHandler(req) + }, []string{"oic.d.virtual", "oic.d.test"}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}) + d.AddResource(res) + + cleanup := bridgeTest.RunBridgeService(s) + defer cleanup() + + c, err := testClient.NewTestSecureClient() + require.NoError(t, err) + defer func() { + errC := c.Close(context.Background()) + require.NoError(t, errC) + }() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*8) + defer cancel() + var got coap.DetailedResponse[interface{}] + err = c.UpdateResource(ctx, d.GetID().String(), "/test", map[string]interface{}{ + "name": "updated", + }, &got, withDeviceID(d.GetID().String())) + require.NoError(t, err) + require.Equal(t, codes.Changed, got.Code) + require.Equal(t, "updated", rds.getName()) + + // fail - invalid data + err = c.UpdateResource(ctx, d.GetID().String(), "/test", map[string]interface{}{ + "name": 1, + }, &got, withDeviceID(d.GetID().String())) + require.Error(t, err) +} diff --git a/client/client_test.go b/client/client_test.go index 515e4d0b..d097c003 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -18,9 +18,6 @@ package client_test import ( "context" - "crypto/tls" - "crypto/x509" - "encoding/pem" "fmt" "log" "testing" @@ -30,11 +27,11 @@ import ( "github.com/plgd-dev/device/v2/client/core" codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" "github.com/plgd-dev/device/v2/pkg/net/coap" - "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/stretchr/testify/require" ) @@ -87,7 +84,7 @@ func init() { } deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() panicIfErr(err) defer func() { errC := c.Close(context.Background()) @@ -133,101 +130,9 @@ func init() { } } -type testSetupSecureClient struct { - ca []*x509.Certificate - mfgCA []*x509.Certificate - mfgCert tls.Certificate -} - -func (c *testSetupSecureClient) GetManufacturerCertificate() (tls.Certificate, error) { - if c.mfgCert.PrivateKey == nil { - return c.mfgCert, fmt.Errorf("private key not set") - } - return c.mfgCert, nil -} - -func (c *testSetupSecureClient) GetManufacturerCertificateAuthorities() ([]*x509.Certificate, error) { - if len(c.mfgCA) == 0 { - return nil, fmt.Errorf("certificate authority not set") - } - return c.mfgCA, nil -} - -func (c *testSetupSecureClient) GetRootCertificateAuthorities() ([]*x509.Certificate, error) { - if len(c.ca) == 0 { - return nil, fmt.Errorf("certificate authorities not set") - } - return c.ca, nil -} - -func NewTestSecureClient() (*client.Client, error) { - return newTestSecureClient(test.IdentityIntermediateCA, test.IdentityIntermediateCAKey) -} - -func NewTestSecureClientWithGeneratedCertificate() (*client.Client, error) { - var cfgCA generateCertificate.Configuration - cfgCA.Subject.CommonName = "anotherClient" - cfgCA.ValidFrom = "now" - cfgCA.ValidFor = time.Hour - - priv, err := cfgCA.GenerateKey() - if err != nil { - return nil, fmt.Errorf("cannot generate private key: %w", err) - } - cert, err := generateCertificate.GenerateRootCA(cfgCA, priv) - if err != nil { - return nil, fmt.Errorf("cannot generate root ca: %w", err) - } - derKey, err := x509.MarshalECPrivateKey(priv) - if err != nil { - return nil, fmt.Errorf("cannot marhsal private key: %w", err) - } - key := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: derKey}) - return newTestSecureClient(cert, key) -} - -func newTestSecureClient(signerCert, signerKey []byte) (*client.Client, error) { - cfg := client.Config{ - DeviceOwnershipSDK: &client.DeviceOwnershipSDKConfig{ - ID: CertIdentity, - Cert: string(signerCert), - CertKey: string(signerKey), - CreateSignerFunc: test.NewIdentityCertificateSigner, - }, - } - mfgTrustedCABlock, _ := pem.Decode(test.RootCACrt) - if mfgTrustedCABlock == nil { - return nil, fmt.Errorf("mfgTrustedCABlock is empty") - } - mfgCA, err := x509.ParseCertificates(mfgTrustedCABlock.Bytes) - if err != nil { - return nil, err - } - mfgCert, err := tls.X509KeyPair(test.MfgCert, test.MfgKey) - if err != nil { - return nil, fmt.Errorf("cannot X509KeyPair: %w", err) - } - - client, err := client.NewClientFromConfig(&cfg, &testSetupSecureClient{ - mfgCA: mfgCA, - mfgCert: mfgCert, - }, core.NewNilLogger(), - ) - if err != nil { - return nil, err - } - err = client.Initialization(context.Background()) - if err != nil { - return nil, err - } - return client, nil -} - func disown(t *testing.T, c *client.Client, deviceID string) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) defer cancel() err := c.DisownDevice(ctx, deviceID) require.NoError(t, err) } - -var CertIdentity = "00000000-0000-0000-0000-000000000001" diff --git a/client/createResource_test.go b/client/createResource_test.go index 8f11ae73..2af4f7c8 100644 --- a/client/createResource_test.go +++ b/client/createResource_test.go @@ -25,6 +25,7 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/device/v2/test/resource/types" "github.com/stretchr/testify/require" ) @@ -78,7 +79,7 @@ func TestClientCreateResource(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/deleteDevices_test.go b/client/deleteDevices_internal_test.go similarity index 100% rename from client/deleteDevices_test.go rename to client/deleteDevices_internal_test.go diff --git a/client/deleteResource_test.go b/client/deleteResource_test.go index ffdabc04..094ff4ae 100644 --- a/client/deleteResource_test.go +++ b/client/deleteResource_test.go @@ -28,6 +28,7 @@ import ( "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/device/v2/test/resource/types" "github.com/stretchr/testify/require" ) @@ -88,7 +89,7 @@ func TestClientDeleteResource(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -117,7 +118,7 @@ func TestClientDeleteResource(t *testing.T) { func TestClientBatchDeleteResources(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/deviceOwnershipBackend_test.go b/client/deviceOwnershipBackend_test.go index 8bea464e..650da318 100644 --- a/client/deviceOwnershipBackend_test.go +++ b/client/deviceOwnershipBackend_test.go @@ -12,6 +12,7 @@ import ( "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/client/core" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -40,11 +41,7 @@ func TestBackendOwnershipClient(t *testing.T) { mfgCert, err := tls.X509KeyPair(test.MfgCert, test.MfgKey) require.NoError(t, err) - client, err := client.NewClientFromConfig(&cfg, &testSetupSecureClient{ - mfgCA: mfgCA, - mfgCert: mfgCert, - }, core.NewNilLogger(), - ) + client, err := client.NewClientFromConfig(&cfg, testClient.NewTestSetupSecureClient(nil, mfgCA, mfgCert), core.NewNilLogger()) require.NoError(t, err) ctxWithToken := test.CtxWithToken(context.Background(), jwtWithSubUserID) diff --git a/client/getDevice_test.go b/client/getDevice_test.go index 873fdbd9..24ff7f90 100644 --- a/client/getDevice_test.go +++ b/client/getDevice_test.go @@ -28,6 +28,7 @@ import ( "github.com/plgd-dev/device/v2/schema/doxm" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" testTypes "github.com/plgd-dev/device/v2/test/resource/types" "github.com/stretchr/testify/require" ) @@ -121,7 +122,7 @@ func TestClientGetDevice(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -189,7 +190,7 @@ func TestClientGetDeviceByIP(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -232,7 +233,7 @@ func TestClientCheckForDuplicityDeviceInCache(t *testing.T) { ip := test.MustFindDeviceIP(test.DevsimName, test.IP4) ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(ctx) @@ -255,7 +256,7 @@ func TestClientCheckForDuplicityDeviceInCache(t *testing.T) { require.NoError(t, err) // change deviceID by another client - c1, err := NewTestSecureClient() + c1, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c1.Close(ctx) @@ -297,14 +298,14 @@ func TestClientGetDeviceByIPOwnedByOther(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errClose := c.Close(context.Background()) require.NoError(t, errClose) }() - c1, err := NewTestSecureClientWithGeneratedCertificate() + c1, err := testClient.NewTestSecureClientWithGeneratedCertificate() require.NoError(t, err) defer func() { errClose := c1.Close(context.Background()) diff --git a/client/getDevices_test.go b/client/getDevices_test.go index eadb7642..6790c246 100644 --- a/client/getDevices_test.go +++ b/client/getDevices_test.go @@ -26,6 +26,7 @@ import ( "github.com/plgd-dev/device/v2/client/core" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -33,7 +34,7 @@ import ( func TestDeviceDiscovery(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) secureDeviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -83,7 +84,7 @@ func TestDeviceDiscovery(t *testing.T) { func TestDeviceDiscoveryWithFilter(t *testing.T) { secureDeviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -105,7 +106,7 @@ func TestDeviceDiscoveryWithFilter(t *testing.T) { func TestDevicesWithFoundByIP(t *testing.T) { ip4 := test.MustFindDeviceIP(test.DevsimName, test.IP4) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/getResource_test.go b/client/getResource_test.go index d4bf4c48..46c032f9 100644 --- a/client/getResource_test.go +++ b/client/getResource_test.go @@ -36,6 +36,7 @@ import ( "github.com/plgd-dev/device/v2/schema/plgdtime" "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/stretchr/testify/require" ) @@ -102,7 +103,7 @@ func TestClientGetResource(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -130,7 +131,7 @@ func TestClientGetResource(t *testing.T) { func TestClientGetDiscoveryResourceWithResourceTypeFilter(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -168,7 +169,7 @@ func updateConfigurationResource(ctx context.Context, c *client.Client, deviceID func TestClientGetDiscoveryResourceWithBatchInterface(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -245,7 +246,7 @@ func TestClientGetDiscoveryResourceWithBatchInterfaceCreateAndDeleteResource(t * } deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -296,7 +297,7 @@ func TestClientGetDiscoveryResourceWithBatchInterfaceCreateAndDeleteResource(t * func TestClientGetConResourceByETag(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/maitenance_test.go b/client/maitenance_test.go index d5c72eef..cf3e3624 100644 --- a/client/maitenance_test.go +++ b/client/maitenance_test.go @@ -23,6 +23,7 @@ import ( "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/client/core" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -54,7 +55,7 @@ func TestClientFactoryReset(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/observeDeviceResources_test.go b/client/observeDeviceResources_test.go index cfd29aa8..2ded68ff 100644 --- a/client/observeDeviceResources_test.go +++ b/client/observeDeviceResources_test.go @@ -18,13 +18,14 @@ package client_test import ( "context" - "fmt" + "errors" "testing" "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/schema" "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -41,7 +42,7 @@ func isDeviceResourcesObservable(ctx context.Context, t *testing.T, c *client.Cl } func runObserveDeviceResourcesTest(ctx context.Context, t *testing.T, c *client.Client, deviceID string) { - h := makeMockDeviceResourcesObservationHandler() + h := testClient.MakeMockDeviceResourcesObservationHandler() if !isDeviceResourcesObservable(ctx, t, c, deviceID) { t.Skip("resource is not observable") return @@ -50,7 +51,7 @@ func runObserveDeviceResourcesTest(ctx context.Context, t *testing.T, c *client. ID, err := c.ObserveDeviceResources(ctx, deviceID, h) require.NoError(t, err) - e, err := h.waitForNotification(ctx) + e, err := h.WaitForNotification(ctx) require.NoError(t, err) test.CheckResourceLinks(t, test.DefaultDevsimResourceLinks(), e) @@ -58,58 +59,21 @@ func runObserveDeviceResourcesTest(ctx context.Context, t *testing.T, c *client. err = c.CreateResource(ctx, deviceID, test.TestResourceSwitchesHref, test.MakeSwitchResourceDefaultData(), nil) require.NoError(t, err) - e, err = h.waitForNotification(ctx) + e, err = h.WaitForNotification(ctx) require.NoError(t, err) test.CheckResourceLinks(t, append(test.DefaultDevsimResourceLinks(), test.DefaultSwitchResourceLink("1")), e) err = c.DeleteResource(ctx, deviceID, test.TestResourceSwitchesInstanceHref("1"), nil) require.NoError(t, err) - e, err = h.waitForNotification(ctx) + e, err = h.WaitForNotification(ctx) require.NoError(t, err) test.CheckResourceLinks(t, test.DefaultDevsimResourceLinks(), e) ok, err := c.StopObservingDeviceResources(ctx, ID) require.NoError(t, err) require.True(t, ok) - select { - case <-h.res: - require.NoError(t, fmt.Errorf("unexpected event")) - default: - } -} - -func makeMockDeviceResourcesObservationHandler() *mockDeviceResourcesObservationHandler { - return &mockDeviceResourcesObservationHandler{ - res: make(chan schema.ResourceLinks, 100), - close: make(chan struct{}), - } -} - -type mockDeviceResourcesObservationHandler struct { - res chan schema.ResourceLinks - close chan struct{} -} -func (h *mockDeviceResourcesObservationHandler) Handle(_ context.Context, body schema.ResourceLinks) { - h.res <- body -} - -func (h *mockDeviceResourcesObservationHandler) Error(err error) { - fmt.Println(err) -} - -func (h *mockDeviceResourcesObservationHandler) OnClose() { - close(h.close) -} - -func (h *mockDeviceResourcesObservationHandler) waitForNotification(ctx context.Context) (schema.ResourceLinks, error) { - select { - case e := <-h.res: - return e, nil - case <-ctx.Done(): - return nil, ctx.Err() - case <-h.close: - return nil, fmt.Errorf("unexpected close") - } + err = h.WaitForClose(ctx) + require.True(t, err == nil || errors.Is(err, context.DeadlineExceeded)) } diff --git a/client/observeDevices_test.go b/client/observeDevices_test.go index 9562ae6f..a574f825 100644 --- a/client/observeDevices_test.go +++ b/client/observeDevices_test.go @@ -25,6 +25,7 @@ import ( "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/client/core" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -47,7 +48,7 @@ LOOP: func TestObserveDevicesAddedByIP(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) ip := test.MustFindDeviceIP(test.DevsimName, test.IP4) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -102,7 +103,7 @@ LOOP: func TestObserveDevices(t *testing.T) { deviceID := test.MustFindDeviceByName(test.DevsimName) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/observeResource_test.go b/client/observeResource_test.go index 5c28db05..fa86b08b 100644 --- a/client/observeResource_test.go +++ b/client/observeResource_test.go @@ -36,6 +36,7 @@ import ( "github.com/plgd-dev/device/v2/schema/plgdtime" "github.com/plgd-dev/device/v2/schema/resources" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -86,7 +87,7 @@ func (h *observationHandler) waitForClose(ctx context.Context) error { func testDevice(t *testing.T, name string, runTest func(ctx context.Context, t *testing.T, c *client.Client, deviceID string)) { deviceID := test.MustFindDeviceByName(name) - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/onboardDevice_test.go b/client/onboardDevice_test.go index bf32ed87..74b5c2e8 100644 --- a/client/onboardDevice_test.go +++ b/client/onboardDevice_test.go @@ -22,6 +22,7 @@ import ( "time" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -62,7 +63,7 @@ func TestClientOnboardDevice(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/options.go b/client/options.go index 8647bd97..426372d2 100644 --- a/client/options.go +++ b/client/options.go @@ -161,6 +161,13 @@ func (r ResourceQueryOption) applyOnObserve(opts observeOptions) observeOptions return opts } +func (r ResourceQueryOption) applyOnUpdate(opts updateOptions) updateOptions { + if r.resourceQuery != "" { + opts.opts = append(opts.opts, coap.WithQuery(r.resourceQuery)) + } + return opts +} + type DiscoveryConfigurationOption struct { cfg core.DiscoveryConfiguration } diff --git a/client/ownDevice_test.go b/client/ownDevice_test.go index c7fde194..ec4fcc70 100644 --- a/client/ownDevice_test.go +++ b/client/ownDevice_test.go @@ -24,6 +24,7 @@ import ( "github.com/plgd-dev/device/v2/client" "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/stretchr/testify/require" ) @@ -45,7 +46,7 @@ func TestClientOwnDevice(t *testing.T) { }, } - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/client/updateResource_test.go b/client/updateResource_test.go index 5d0e03f9..888eb14a 100644 --- a/client/updateResource_test.go +++ b/client/updateResource_test.go @@ -27,6 +27,7 @@ import ( "github.com/plgd-dev/device/v2/schema/device" "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/device/v2/test" + testClient "github.com/plgd-dev/device/v2/test/client" "github.com/plgd-dev/go-coap/v3/message/codes" "github.com/stretchr/testify/require" ) @@ -118,7 +119,7 @@ func TestClientUpdateResource(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) @@ -230,7 +231,7 @@ func TestClientUpdateResourceInRFOTM(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() - c, err := NewTestSecureClient() + c, err := testClient.NewTestSecureClient() require.NoError(t, err) defer func() { errC := c.Close(context.Background()) diff --git a/sonar-project.properties b/sonar-project.properties index 71edc9de..c1fc71e1 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -18,4 +18,4 @@ sonar.test.inclusions=**/*_test.go sonar.test.exclusions= sonar.go.coverage.reportPaths=coverage/**/*coverage*.txt -sonar.coverage.exclusions=test/*.go,client/app/app.go,cmd/ocfclient/ocfclient.go,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py +sonar.coverage.exclusions=bridge/test/**/*.go,test/**/*.go,client/app/app.go,cmd/ocfclient/ocfclient.go,**/main.go,**/*.pb.go,**/*.pb.gw.go,**/*.js,**/*.py diff --git a/test/client/client.go b/test/client/client.go new file mode 100644 index 00000000..1da4f56d --- /dev/null +++ b/test/client/client.go @@ -0,0 +1,180 @@ +/**************************************************************************** + * + * Copyright (c) 2024 plgd.dev s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"), + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + ****************************************************************************/ + +package client + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + "github.com/plgd-dev/device/v2/client" + "github.com/plgd-dev/device/v2/client/core" + "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" + "github.com/plgd-dev/device/v2/schema" + "github.com/plgd-dev/device/v2/test" +) + +var CertIdentity = "00000000-0000-0000-0000-000000000001" + +type testSetupSecureClient struct { + ca []*x509.Certificate + mfgCA []*x509.Certificate + mfgCert tls.Certificate +} + +func (c *testSetupSecureClient) GetManufacturerCertificate() (tls.Certificate, error) { + if c.mfgCert.PrivateKey == nil { + return c.mfgCert, fmt.Errorf("private key not set") + } + return c.mfgCert, nil +} + +func (c *testSetupSecureClient) GetManufacturerCertificateAuthorities() ([]*x509.Certificate, error) { + if len(c.mfgCA) == 0 { + return nil, fmt.Errorf("certificate authority not set") + } + return c.mfgCA, nil +} + +func (c *testSetupSecureClient) GetRootCertificateAuthorities() ([]*x509.Certificate, error) { + if len(c.ca) == 0 { + return nil, fmt.Errorf("certificate authorities not set") + } + return c.ca, nil +} + +func NewTestSetupSecureClient(ca, mfgCA []*x509.Certificate, mfgCert tls.Certificate) client.ApplicationCallback { + return &testSetupSecureClient{ + ca: ca, + mfgCA: mfgCA, + mfgCert: mfgCert, + } +} + +func NewTestSecureClient() (*client.Client, error) { + return newTestSecureClient(test.IdentityIntermediateCA, test.IdentityIntermediateCAKey) +} + +func NewTestSecureClientWithGeneratedCertificate() (*client.Client, error) { + var cfgCA generateCertificate.Configuration + cfgCA.Subject.CommonName = "anotherClient" + cfgCA.ValidFrom = "now" + cfgCA.ValidFor = time.Hour + + priv, err := cfgCA.GenerateKey() + if err != nil { + return nil, fmt.Errorf("cannot generate private key: %w", err) + } + cert, err := generateCertificate.GenerateRootCA(cfgCA, priv) + if err != nil { + return nil, fmt.Errorf("cannot generate root ca: %w", err) + } + derKey, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, fmt.Errorf("cannot marhsal private key: %w", err) + } + key := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: derKey}) + return newTestSecureClient(cert, key) +} + +func newTestSecureClient(signerCert, signerKey []byte) (*client.Client, error) { + cfg := client.Config{ + DeviceOwnershipSDK: &client.DeviceOwnershipSDKConfig{ + ID: CertIdentity, + Cert: string(signerCert), + CertKey: string(signerKey), + CreateSignerFunc: test.NewIdentityCertificateSigner, + }, + } + mfgTrustedCABlock, _ := pem.Decode(test.RootCACrt) + if mfgTrustedCABlock == nil { + return nil, fmt.Errorf("mfgTrustedCABlock is empty") + } + mfgCA, err := x509.ParseCertificates(mfgTrustedCABlock.Bytes) + if err != nil { + return nil, err + } + mfgCert, err := tls.X509KeyPair(test.MfgCert, test.MfgKey) + if err != nil { + return nil, fmt.Errorf("X509KeyPair failed: %w", err) + } + + client, err := client.NewClientFromConfig(&cfg, &testSetupSecureClient{ + mfgCA: mfgCA, + mfgCert: mfgCert, + }, core.NewNilLogger(), + ) + if err != nil { + return nil, err + } + err = client.Initialization(context.Background()) + if err != nil { + return nil, err + } + return client, nil +} + +func MakeMockDeviceResourcesObservationHandler() *MockDeviceResourcesObservationHandler { + return &MockDeviceResourcesObservationHandler{ + res: make(chan schema.ResourceLinks, 100), + close: make(chan struct{}), + } +} + +type MockDeviceResourcesObservationHandler struct { + res chan schema.ResourceLinks + close chan struct{} +} + +func (h *MockDeviceResourcesObservationHandler) Handle(_ context.Context, body schema.ResourceLinks) { + h.res <- body +} + +func (h *MockDeviceResourcesObservationHandler) Error(err error) { + fmt.Println(err) +} + +func (h *MockDeviceResourcesObservationHandler) OnClose() { + close(h.close) +} + +func (h *MockDeviceResourcesObservationHandler) WaitForNotification(ctx context.Context) (schema.ResourceLinks, error) { + select { + case e := <-h.res: + return e, nil + case <-ctx.Done(): + return nil, ctx.Err() + case <-h.close: + return nil, fmt.Errorf("unexpected close") + } +} + +func (h *MockDeviceResourcesObservationHandler) WaitForClose(ctx context.Context) error { + select { + case e := <-h.res: + return fmt.Errorf("unexpected notification %v", e) + case <-ctx.Done(): + return ctx.Err() + case <-h.close: + return nil + } +}