Skip to content

Commit

Permalink
bridge: test device with mocked coap-gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielius1922 authored and Daniel Adam committed Feb 1, 2024
1 parent 315fbe2 commit f00547a
Show file tree
Hide file tree
Showing 32 changed files with 1,518 additions and 107 deletions.
2 changes: 1 addition & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ignore:
- "client/app/app.go"
- "cmd/ocfclient/ocfclient.go"
- "cmd/ocfclient/*.go"
- "cmd/ocfbridge/*.go"
- "**/main.go"
- "**/test/**/*.go"
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"MFG_KEY": "${workspaceFolder}/.tmp/pki_certs/mfgkey.pem",
"IDENTITY_CRT": "${workspaceFolder}/.tmp/pki_certs/identitycrt.pem",
"IDENTITY_KEY": "${workspaceFolder}/.tmp/pki_certs/identitykey.pem",
"COAP_CRT": "${workspaceFolder}/.tmp/pki_certs/coapcrt.pem",
"COAP_KEY": "${workspaceFolder}/.tmp/pki_certs/coapkey.pem",
"CLOUD_ID": "adebc667-1f2b-41e3-bf5c-6d6eabc68cc6",
},
"files.watcherExclude": {
"**/plgd-dev/device/v2/**": true
Expand Down
65 changes: 52 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ SHELL = /bin/bash
SERVICE_NAME = cloud-server-test
VERSION_TAG = vnext-$(shell git rev-parse --short=7 --verify HEAD)
SIMULATOR_NAME_SUFFIX ?= $(shell hostname)
USER_ID := $(shell id -u)
GROUP_ID := $(shell id -g)
TMP_PATH = $(shell pwd)/.tmp
CERT_PATH = $(TMP_PATH)/pki_certs
CLOUD_SID ?= adebc667-1f2b-41e3-bf5c-6d6eabc68cc6
DEVSIM_NET_HOST_PATH = $(shell pwd)/.tmp/devsim-net-host
CERT_TOOL_IMAGE ?= ghcr.io/plgd-dev/hub/cert-tool:vnext
# supported values: ECDSA-SHA256, ECDSA-SHA384, ECDSA-SHA512
Expand Down Expand Up @@ -35,22 +38,52 @@ 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
COAP_CRT = $(CERT_PATH)/coapcrt.pem
COAP_KEY = $(CERT_PATH)/coapkey.pem

certificates:
mkdir -p $(CERT_PATH)
chmod 0777 $(CERT_PATH)
docker pull $(CERT_TOOL_IMAGE)
docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --outCert=/out/cloudca.pem --outKey=/out/cloudcakey.pem \
--cert.subject.cn="ca" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \
--cmd.generateRootCA
docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \
--outCert=/out/intermediatecacrt.pem --outKey=/out/intermediatecakey.pem --cert.basicConstraints.maxPathLen=0 \
--cert.subject.cn="intermediateCA" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \
--cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cmd.generateIntermediateCA
docker run --rm -v $(CERT_PATH):/out $(CERT_TOOL_IMAGE) --signerCert=/out/intermediatecacrt.pem \
--signerKey=/out/intermediatecakey.pem --outCert=/out/mfgcrt.pem --outKey=/out/mfgkey.pem --cert.san.domain=localhost \
--cert.san.ip=127.0.0.1 --cert.subject.cn="mfg" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \
--cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cmd.generateCertificate

docker run \
--rm -v $(CERT_PATH):/out \
--user $(USER_ID):$(GROUP_ID) \
$(CERT_TOOL_IMAGE) \
--outCert=/out/cloudca.pem --outKey=/out/cloudcakey.pem \
--cert.subject.cn="ca" --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \
--cmd.generateRootCA

docker run \
--rm -v $(CERT_PATH):/out \
--user $(USER_ID):$(GROUP_ID) \
$(CERT_TOOL_IMAGE) \
--signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \
--outCert=/out/intermediatecacrt.pem --outKey=/out/intermediatecakey.pem \
--cert.basicConstraints.maxPathLen=0 --cert.subject.cn="intermediateCA" \
--cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) --cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) \
--cmd.generateIntermediateCA

docker run \
--rm -v $(CERT_PATH):/out \
--user $(USER_ID):$(GROUP_ID) \
$(CERT_TOOL_IMAGE) \
--signerCert=/out/intermediatecacrt.pem --signerKey=/out/intermediatecakey.pem \
--outCert=/out/mfgcrt.pem --outKey=/out/mfgkey.pem --cert.san.domain=localhost \
--cert.san.ip=127.0.0.1 --cert.subject.cn="mfg" \
--cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \
--cmd.generateCertificate

docker run \
--rm -v $(CERT_PATH):/out \
--user $(USER_ID):$(GROUP_ID) \
${CERT_TOOL_IMAGE} \
--signerCert=/out/cloudca.pem --signerKey=/out/cloudcakey.pem \
--outCert=/out/coapcrt.pem --outKey=/out/coapkey.pem \
--cert.san.ip=127.0.0.1 --cert.san.domain=localhost \
--cert.signatureAlgorithm=$(CERT_TOOL_SIGN_ALG) --cert.ellipticCurve=$(CERT_TOOL_ELLIPTIC_CURVE) \
--cmd.generateCertificate --cert.subject.cn=uuid:$(CLOUD_SID)

sudo chown -R $(shell whoami) $(CERT_PATH)
chmod -R 0777 $(CERT_PATH)

Expand All @@ -72,9 +105,15 @@ 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.unit.coverage.txt
ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(ROOT_CA_KEY)" \
MFG_CRT="$(MFG_CRT)" MFG_KEY="$(MFG_KEY)" \
INTERMEDIATE_CA_CRT="$(INTERMEDIATE_CA_CRT)" INTERMEDIATE_CA_KEY=$(INTERMEDIATE_CA_KEY) \
COAP_CRT="$(COAP_CRT)" COAP_KEY="$(COAP_KEY)" \
CLOUD_SID=$(CLOUD_SID) \
go test -race -parallel 1 -v ./bridge/... -coverpkg=./... -covermode=atomic -coverprofile=$(TMP_PATH)/bridge.unit.coverage.txt
go test -race -v ./schema/... -covermode=atomic -coverprofile=$(TMP_PATH)/schema.unit.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.unit.coverage.txt
ROOT_CA_CRT="$(ROOT_CA_CRT)" ROOT_CA_KEY="$(ROOT_CA_KEY)" \
go test -race -v ./pkg/... -covermode=atomic -coverprofile=$(TMP_PATH)/pkg.unit.coverage.txt

test: env build-testcontainer
docker run \
Expand Down
11 changes: 6 additions & 5 deletions bridge/device/cloud/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/plgd-dev/device/v2/bridge/resources"
"github.com/plgd-dev/device/v2/bridge/resources/discovery"
"github.com/plgd-dev/device/v2/pkg/codec/cbor"
ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud"
"github.com/plgd-dev/device/v2/schema"
"github.com/plgd-dev/device/v2/schema/cloud"
"github.com/plgd-dev/device/v2/schema/device"
Expand Down Expand Up @@ -68,7 +69,7 @@ type Manager struct {
cfg Configuration
}

creds CoapSignUpResponse
creds ocfCloud.CoapSignUpResponse
client *client.Conn
signedIn bool
resourcesPublished bool
Expand Down Expand Up @@ -115,7 +116,7 @@ func (c *Manager) ImportConfig(cfg Config) {
URL: cfg.CloudURL,
CloudID: cfg.CloudID,
})
c.setCreds(CoapSignUpResponse{
c.setCreds(ocfCloud.CoapSignUpResponse{
AccessToken: cfg.AccessToken,
UserID: cfg.UserID,
RefreshToken: cfg.RefreshToken,
Expand All @@ -138,7 +139,7 @@ func (c *Manager) resetCredentials(ctx context.Context, signOff bool) {
log.Printf("%v\n", err)
}
}
c.creds = CoapSignUpResponse{}
c.creds = ocfCloud.CoapSignUpResponse{}
c.signedIn = false
c.resourcesPublished = false
if err := c.close(); err != nil {
Expand Down Expand Up @@ -226,12 +227,12 @@ func validUntil(expiresIn int64) time.Time {
return time.Now().Add(time.Duration(expiresIn) * time.Second)
}

func (c *Manager) setCreds(creds CoapSignUpResponse) {
func (c *Manager) setCreds(creds ocfCloud.CoapSignUpResponse) {
c.creds = creds
c.signedIn = false
}

func (c *Manager) getCreds() CoapSignUpResponse {
func (c *Manager) getCreds() ocfCloud.CoapSignUpResponse {
return c.creds
}

Expand Down
110 changes: 110 additions & 0 deletions bridge/device/cloud/manager_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/****************************************************************************
*
* 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 cloud_test

import (
"context"
"fmt"
"testing"
"time"

"github.com/google/uuid"
"github.com/plgd-dev/device/v2/bridge/device/cloud"
bridgeTest "github.com/plgd-dev/device/v2/bridge/test"
cloudSchema "github.com/plgd-dev/device/v2/schema/cloud"
testClient "github.com/plgd-dev/device/v2/test/client"
mockCoapGW "github.com/plgd-dev/device/v2/test/coap-gateway"
mockCoapGWService "github.com/plgd-dev/device/v2/test/coap-gateway/service"
"github.com/stretchr/testify/require"
)

// device is restarted with an imported configuration with valid cloud credentials
func TestProvisioningOnDeviceRestart(t *testing.T) {
ch := mockCoapGW.NewCoapHandlerWithCounter(-1)
makeHandler := func(s *mockCoapGWService.Service, opts ...mockCoapGWService.Option) mockCoapGWService.ServiceHandler {
return ch
}
coapShutdown := mockCoapGW.New(t, makeHandler, func(handler mockCoapGWService.ServiceHandler) {
h := handler.(*mockCoapGW.DefaultHandlerWithCounter)
fmt.Printf("%+v\n", h.CallCounter.Data)
// d1 -> signup + signin + publish
// d2 -> should use the stored credentials to skip signup and only do sign in + publish
require.Equal(t, 1, h.CallCounter.Data[mockCoapGW.SignUpKey])
require.Equal(t, 2, h.CallCounter.Data[mockCoapGW.SignInKey])
require.Equal(t, 2, h.CallCounter.Data[mockCoapGW.PublishKey])
require.Equal(t, 0, h.CallCounter.Data[mockCoapGW.RefreshTokenKey])
})
defer coapShutdown()

s1 := bridgeTest.NewBridgeService(t)
t.Cleanup(func() {
_ = s1.Shutdown()
})
deviceID := uuid.New().String()
d1 := bridgeTest.NewBridgedDevice(t, s1, true, deviceID)
s1Shutdown := bridgeTest.RunBridgeService(s1)
t.Cleanup(func() {
_ = s1Shutdown()
})

c, err := testClient.NewTestSecureClientWithBridgeSupport()
require.NoError(t, err)
defer func() {
errC := c.Close(context.Background())
require.NoError(t, errC)
}()

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err = c.OnboardDevice(ctx, deviceID, "authorizationProvider", "coaps+tcp://"+mockCoapGW.COAP_GW_HOST, "authorizationCode", "cloudID")
require.NoError(t, err)

// wait for sign in
require.Equal(t, 1, ch.WaitForSignIn(time.Second*20))

// stop service
err = s1Shutdown()
require.NoError(t, err)

// save the device configuration
cfg := d1.ExportConfig()

// recreate device using the saved configuration from a signed in device
s2 := bridgeTest.NewBridgeService(t)
t.Cleanup(func() {
_ = s2.Shutdown()
})
d2 := bridgeTest.NewBridgedDeviceWithConfig(t, s2, cfg)
s2Shutdown := bridgeTest.RunBridgeService(s2)
defer func() {
errS := s2Shutdown()
require.NoError(t, errS)
}()
require.Equal(t, 2, ch.WaitForSignIn(time.Second*20))

// check provisioning status
var cloudCfg cloud.Configuration
err = c.GetResource(ctx, deviceID, cloudSchema.ResourceURI, &cloudCfg)
require.NoError(t, err)
require.Equal(t, cloudCfg.ProvisioningStatus, cloudSchema.ProvisioningStatus_REGISTERED)

// sign off
d2.UnregisterFromCloud()
require.Equal(t, 1, ch.WaitForSignOff(time.Second*20))
}
13 changes: 4 additions & 9 deletions bridge/device/cloud/publishResources.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,11 @@ import (
"log"

"github.com/plgd-dev/device/v2/bridge/resources"
ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud"
"github.com/plgd-dev/device/v2/schema"
"github.com/plgd-dev/go-coap/v3/message/codes"
)

type PublishResourcesRequest struct {
DeviceID string `json:"di"`
Links schema.ResourceLinks `json:"links"`
TimeToLive int `json:"ttl"`
}

var ErrCannotPublishResources = fmt.Errorf("cannot publish resources")

func errCannotPublishResources(err error) error {
Expand All @@ -47,12 +42,12 @@ func (c *Manager) publishResources(ctx context.Context) error {

links := c.getLinks(schema.Endpoints{}, c.deviceID, nil, resources.PublishToCloud)
links = patchDeviceLink(links)
wkRd := PublishResourcesRequest{
wkRd := ocfCloud.PublishResourcesRequest{
DeviceID: c.deviceID.String(),
Links: links,
TimeToLive: 0,
}
req, err := newPostRequest(ctx, c.client, ResourceDirectory, wkRd)
req, err := newPostRequest(ctx, c.client, ocfCloud.ResourceDirectory, wkRd)
if err != nil {
return errCannotPublishResources(err)
}
Expand All @@ -64,6 +59,6 @@ func (c *Manager) publishResources(ctx context.Context) error {
return errCannotPublishResources(fmt.Errorf("unexpected status code %v", resp.Code()))
}
c.resourcesPublished = true
log.Printf("resourcesPublished\n")
log.Printf("resources published\n")
return nil
}
21 changes: 5 additions & 16 deletions bridge/device/cloud/refreshToken.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,11 @@ import (
"log"

"github.com/plgd-dev/device/v2/pkg/codec/cbor"
ocfCloud "github.com/plgd-dev/device/v2/pkg/ocf/cloud"
"github.com/plgd-dev/device/v2/schema/cloud"
"github.com/plgd-dev/go-coap/v3/message/codes"
)

type CoapRefreshTokenRequest struct {
DeviceID string `json:"di"`
UserID string `json:"uid"`
RefreshToken string `json:"refreshtoken"`
}

type CoapRefreshTokenResponse struct {
AccessToken string `json:"accesstoken"`
RefreshToken string `json:"refreshtoken"`
ExpiresIn int64 `json:"expiresin"`
}

var ErrCannotRefreshToken = fmt.Errorf("cannot refresh token")

func errCannotRefreshToken(err error) error {
Expand All @@ -51,7 +40,7 @@ func (c *Manager) refreshToken(ctx context.Context) error {
if creds.RefreshToken == "" {
return nil
}
req, err := newPostRequest(ctx, c.client, RefreshToken, CoapRefreshTokenRequest{
req, err := newPostRequest(ctx, c.client, ocfCloud.RefreshToken, ocfCloud.CoapRefreshTokenRequest{
DeviceID: c.deviceID.String(),
UserID: creds.UserID,
RefreshToken: creds.RefreshToken,
Expand All @@ -70,17 +59,17 @@ func (c *Manager) refreshToken(ctx context.Context) error {
}
return errCannotRefreshToken(fmt.Errorf("unexpected status code %v", resp.Code()))
}
var refreshResp CoapRefreshTokenResponse
var refreshResp ocfCloud.CoapRefreshTokenResponse
if err = cbor.ReadFrom(resp.Body(), &refreshResp); err != nil {
return errCannotRefreshToken(err)
}
c.updateCredsByRefreshTokenResponse(refreshResp)
log.Printf("refresh token\n")
log.Printf("refreshed token\n")
c.save()
return nil
}

func (c *Manager) updateCredsByRefreshTokenResponse(resp CoapRefreshTokenResponse) {
func (c *Manager) updateCredsByRefreshTokenResponse(resp ocfCloud.CoapRefreshTokenResponse) {
c.creds.AccessToken = resp.AccessToken
c.creds.RefreshToken = resp.RefreshToken
c.creds.ValidUntil = validUntil(resp.ExpiresIn)
Expand Down
Loading

0 comments on commit f00547a

Please sign in to comment.