diff --git a/integration/util/common.go b/integration/util/common.go index 1b635f27..2d93fb9d 100644 --- a/integration/util/common.go +++ b/integration/util/common.go @@ -12,7 +12,13 @@ package util -import "encoding/json" +import ( + "encoding/json" + "fmt" + "os" +) + +const configDefaultMode = 0666 // Convert marshals an object(e.g. map) to a JSON payload and unmarshals it to the given structure func Convert(from interface{}, to interface{}) error { @@ -22,3 +28,56 @@ func Convert(from interface{}, to interface{}) error { } return json.Unmarshal(jsonValue, to) } + +// CombineErrors combine multiple errors in one error. +func CombineErrors(errors []error) error { + if errors != nil { + return fmt.Errorf("%s", errors) + } + return nil +} + +// WriteConfigFile writes interface data to the path file, creating it if necessary. +func WriteConfigFile(path string, cfg interface{}) error { + jsonContents, err := json.MarshalIndent(cfg, "", "\t") + if err != nil { + return fmt.Errorf("unable to marshal to json: %v", err) + } + + // Preserve the file mode if the file already exists + mode, err := getFileModeOrDefault(path, configDefaultMode) + if err != nil { + return fmt.Errorf("unable to get file mode %s: %v", path, err) + } + if err = os.WriteFile(path, jsonContents, mode); err != nil { + return fmt.Errorf("unable to save file %s: %v", path, err) + } + return nil +} + +func getFileModeOrDefault(path string, defaultMode os.FileMode) (os.FileMode, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return defaultMode, err + } + return fileInfo.Mode(), nil +} + +// CopyFile copies source file to the destination. +func CopyFile(src, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err + } + // If the destination file exists, preserve its file mode. + // If the destination file doesn't exist, use the file mode of the source file. + srcMode, err := getFileModeOrDefault(src, configDefaultMode) + if err != nil { + return err + } + dstMode, err := getFileModeOrDefault(dst, srcMode) + if err != nil { + return err + } + return os.WriteFile(dst, data, dstMode) +} diff --git a/integration/util/registry.go b/integration/util/registry.go new file mode 100644 index 00000000..2f43bef1 --- /dev/null +++ b/integration/util/registry.go @@ -0,0 +1,193 @@ +// Copyright (c) 2023 Contributors to the Eclipse Foundation +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + +package util + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// Resource holds all needed properties to create resources for the device. +type Resource struct { + url string + + method string + body string + + user string + pass string + + delete bool +} + +// BootstrapConfiguration holds the required configuration to suite bootstrapping to connect and +// where to receive post bootstrapping files and script. +type BootstrapConfiguration struct { + LogFile string `json:"logFile"` + PostBootstrapFile string `json:"postBootstrapFile"` + PostBootstrapScript []string `json:"postBootstrapScript"` + CaCert string `json:"caCert"` + Address string `json:"address"` + TenantID string `json:"tenantId"` + DeviceID string `json:"deviceId"` + AuthID string `json:"authId"` + Password string `json:"password"` +} + +// ConnectorConfiguration holds the minimum required configuration to suite connector to connect. +type ConnectorConfiguration struct { + CaCert string `json:"caCert"` + LogFile string `json:"logFile"` + Address string `json:"address"` + TenantID string `json:"tenantId"` + DeviceID string `json:"deviceId"` + AuthID string `json:"authId"` + Password string `json:"password"` +} + +// CreateDeviceResources creates device resources. +func CreateDeviceResources(newDeviceID, tenantID, policyID, password, registryAPI, + registryAPIUsername, registryAPIPassword string, cfg *TestConfiguration) []*Resource { + + devicePath := tenantID + "/" + newDeviceID + return []*Resource{ + &Resource{ + url: registryAPI + "/devices/" + devicePath, + method: http.MethodPost, + body: `{"authorities":["auto-provisioning-enabled"]}`, + user: registryAPIUsername, + pass: registryAPIPassword, + delete: true}, + &Resource{ + url: registryAPI + "/credentials/" + devicePath, + method: http.MethodPut, + body: getCredentialsBody(strings.ReplaceAll(newDeviceID, ":", "_"), password), + user: registryAPIUsername, + pass: registryAPIPassword}, + &Resource{ + url: GetThingURL(cfg.DigitalTwinAPIAddress, newDeviceID), + method: http.MethodPut, + body: fmt.Sprintf(`{"policyId": "%s"}`, policyID), + user: cfg.DigitalTwinAPIUsername, + pass: cfg.DigitalTwinAPIPassword, + delete: true}, + } +} + +func getCredentialsBody(authID, pass string) string { + type pwdPlain struct { + PwdPlain string `json:"pwd-plain"` + } + + type authStruct struct { + TypeStr string `json:"type"` + AuthID string `json:"auth-id"` + Secrets []pwdPlain `json:"secrets"` + } + auth := authStruct{"hashed-password", authID, []pwdPlain{pwdPlain{pass}}} + + data, _ := json.MarshalIndent([]authStruct{auth}, "", "\t") + return string(data) +} + +// DeleteResources deletes all given resources and all related devices. +func DeleteResources(cfg *TestConfiguration, resources []*Resource, deviceID, url, user, pass string) error { + var errors []error + if err := deleteRelatedDevices(cfg, deviceID, url, user, pass); err != nil { + errors = append(errors, err) + } + + // Delete in reverse order of creation + for i := len(resources) - 1; i >= 0; i-- { + r := resources[i] + + if r.delete { + if _, err := SendDeviceRegistryRequest(nil, http.MethodDelete, r.url, r.user, r.pass); err != nil { + errors = append(errors, err) + } + } + + } + return CombineErrors(errors) +} + +func deleteRelatedDevices(cfg *TestConfiguration, viaDeviceID, url, user, pass string) error { + devicesVia, err := findDeviceRegistryDevicesVia(viaDeviceID, url, user, pass) + if err != nil { + return err + } + + var errors []error + // Digital Twin API things are created after Device Registry devices, so delete them first + if err = deleteDigitalTwinThings(cfg, devicesVia); err != nil { + errors = append(errors, err) + } + // Then delete Device Registry devices + if err = deleteRegistryDevices(devicesVia, url, user, pass); err != nil { + errors = append(errors, err) + } + return CombineErrors(errors) +} + +func findDeviceRegistryDevicesVia(viaDeviceID, url, user, pass string) ([]string, error) { + type registryDevice struct { + ID string `json:"id"` + Via []string `json:"via"` + } + type registryDevices struct { + Devices []*registryDevice `json:"result"` + } + devicesJSON, err := SendDeviceRegistryRequest(nil, http.MethodGet, url, user, pass) + if err != nil { + return nil, err + } + devices := ®istryDevices{} + err = json.Unmarshal(devicesJSON, devices) + if err != nil { + return nil, err + } + var devicesVia []string + for _, device := range devices.Devices { + for _, via := range device.Via { + if via == viaDeviceID { + devicesVia = append(devicesVia, device.ID) + break + } + } + } + + return devicesVia, nil +} + +func deleteDigitalTwinThings(cfg *TestConfiguration, things []string) error { + var errors []error + for _, thingID := range things { + if _, err := SendDigitalTwinRequest( + cfg, http.MethodDelete, GetThingURL(cfg.DigitalTwinAPIAddress, thingID), nil); err != nil { + errors = append(errors, err) + } + } + return CombineErrors(errors) +} + +func deleteRegistryDevices(devices []string, tenantURL, user, pass string) error { + var errors []error + for _, device := range devices { + if _, err := SendDeviceRegistryRequest(nil, http.MethodDelete, tenantURL+device, user, pass); err != nil { + errors = append(errors, err) + } + } + return CombineErrors(errors) +} diff --git a/integration/util/web.go b/integration/util/web.go index ab761262..010130ed 100644 --- a/integration/util/web.go +++ b/integration/util/web.go @@ -60,16 +60,40 @@ const ( StopSendMessages UnsubscribeEventType = "STOP-SEND-MESSAGES" ) -// SendDigitalTwinRequest sends а new HTTP request to the Ditto REST API +// SendDigitalTwinRequest sends a new HTTP request to the Ditto REST API func SendDigitalTwinRequest(cfg *TestConfiguration, method string, url string, body interface{}) ([]byte, error) { - var reqBody io.Reader - + var ( + payload []byte + err error + ) if body != nil { - jsonValue, err := json.Marshal(body) + payload, err = json.Marshal(body) if err != nil { return nil, err } - reqBody = bytes.NewBuffer(jsonValue) + } + + req, err := createRequest(payload, true, method, url, cfg.DigitalTwinAPIUsername, cfg.DigitalTwinAPIPassword) + if err != nil { + return nil, err + } + return sendRequest(req, method, url) +} + +// SendDeviceRegistryRequest sends a new HTTP request to the Ditto API +func SendDeviceRegistryRequest(payload []byte, method string, url string, username string, password string) ([]byte, error) { + req, err := createRequest(payload, false, method, url, username, password) + if err != nil { + return nil, err + } + return sendRequest(req, method, url) +} + +func createRequest(payload []byte, rspRequired bool, method, url, username, password string) (*http.Request, error) { + var reqBody io.Reader + + if payload != nil { + reqBody = bytes.NewBuffer(payload) } req, err := http.NewRequest(method, url, reqBody) @@ -77,14 +101,19 @@ func SendDigitalTwinRequest(cfg *TestConfiguration, method string, url string, b return nil, err } - if body != nil { - correlationID := uuid.New().String() + if payload != nil { req.Header.Add("Content-Type", "application/json") - req.Header.Add("correlation-id", correlationID) - req.Header.Add("response-required", "true") + if rspRequired { + req.Header.Add("correlation-id", uuid.New().String()) + req.Header.Add("response-required", "true") + } } - req.SetBasicAuth(cfg.DigitalTwinAPIUsername, cfg.DigitalTwinAPIPassword) + req.SetBasicAuth(username, password) + return req, nil +} + +func sendRequest(req *http.Request, method string, url string) ([]byte, error) { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err