Files
cocos/pkg/sdk/agent_test.go
T
Sammy Kerata Oina 8eb1fac9ad NOISSUE - Refactor and update dependencies in the project (#491)
* Refactor and update dependencies in the project

- Updated go.sum to replace `github.com/absmach/magistrala` with `github.com/absmach/supermq` across various modules.
- Removed VSock configuration from environment variables and QEMU arguments.
- Updated QEMU configuration and related tests to remove references to guest CID and VSock.
- Added new HTTP transport layer for API endpoints in the manager.
- Introduced Prometheus monitoring configuration with alert rules and Alertmanager setup.
- Updated service and VM interfaces to remove unused methods and references.
- Refactored tests to align with the new structure and dependencies.

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add MaxVMs configuration and enforce limit on VM creation

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add comprehensive tests for HTTP transport handlers and endpoints

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Add test case for exceeding maximum number of VMs in TestRun

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Improve error handling in TestHandlerWithCustomRouter to ensure response writing is checked

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

* Update dependencies to latest versions

- Upgrade cel.dev/expr from v0.23.0 to v0.24.0
- Upgrade github.com/absmach/supermq from v0.16.0 to v0.17.0
- Upgrade github.com/cenkalti/backoff from v4.3.0 to v5.0.2
- Upgrade github.com/cncf/xds/go to v0.0.0-20250501225837-2ac532fd4443
- Upgrade github.com/go-chi/chi/v5 from v5.2.1 to v5.2.2
- Upgrade github.com/go-jose/go-jose/v3 from v3.0.3 to v3.0.4
- Upgrade github.com/gofrs/uuid/v5 from v5.3.0 to v5.3.2
- Upgrade github.com/prometheus/client_golang from v1.22.0 to v1.23.0
- Upgrade github.com/prometheus/client_model from v0.6.1 to v0.6.2
- Upgrade github.com/prometheus/common from v0.62.0 to v0.65.0
- Upgrade github.com/prometheus/procfs from v0.15.1 to v0.16.1
- Upgrade go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp from v0.60.0 to v0.62.0
- Upgrade go.opentelemetry.io/otel/exporters/otlp/otlptrace from v1.36.0 to v1.37.0
- Upgrade golang.org/x/crypto from v0.39.0 to v0.40.0
- Upgrade golang.org/x/sys from v0.33.0 to v0.34.0
- Upgrade golang.org/x/text from v0.26.0 to v0.27.0
- Upgrade golang.org/x/time from v0.11.0 to v0.12.0
- Upgrade google.golang.org/grpc from v1.73.0 to v1.74.2

Signed-off-by: Sammy Oina <sammyoina@gmail.com>

---------

Signed-off-by: Sammy Oina <sammyoina@gmail.com>
2025-08-05 11:22:02 +02:00

664 lines
16 KiB
Go

// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package sdk_test
import (
"context"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"os"
"testing"
"github.com/absmach/supermq/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider"
"github.com/ultravioletrs/cocos/pkg/attestation/vtpm"
"github.com/ultravioletrs/cocos/pkg/sdk"
"golang.org/x/crypto/sha3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
)
var (
algoPath = "../../test/manual/algo/lin_reg.py"
dataPath = "../../test/manual/data/iris.csv"
errInappropriateIoctl = errors.New("failed to get terminal width: inappropriate ioctl for device")
)
func TestAlgo(t *testing.T) {
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
algo, err := os.ReadFile(algoPath)
require.NoError(t, err)
algoHash := sha3.Sum256(algo)
algorithmProviderKey, algorithmProviderPubKey := generateKeys(t, "ed25519")
algoProvider1Key, algoProvider1PubKey := generateKeys(t, "ed25519")
algorithm := agent.Algorithm{
Algorithm: algo,
Hash: algoHash,
UserKey: algorithmProviderPubKey,
}
cases := []struct {
name string
err error
algo agent.Algorithm
userKey any
}{
{
name: "Test Algo successfully",
algo: algorithm,
userKey: algorithmProviderKey,
err: nil,
},
{
name: "hash mismatch",
algo: agent.Algorithm{
Algorithm: algo,
Hash: sha3.Sum256([]byte("algo")),
UserKey: algoProvider1PubKey,
},
userKey: algoProvider1Key,
err: errInappropriateIoctl,
},
{
name: "no manifest expected",
algo: agent.Algorithm{
Algorithm: algo,
UserKey: algorithmProviderPubKey,
Hash: algoHash,
},
userKey: algorithmProviderKey,
err: errInappropriateIoctl,
},
{
name: "state not ready",
algo: agent.Algorithm{
Algorithm: algo,
UserKey: algorithmProviderPubKey,
Hash: algoHash,
},
userKey: algorithmProviderKey,
err: errInappropriateIoctl,
},
{
name: "gRPC client error",
algo: agent.Algorithm{
Algorithm: algo,
Hash: algoHash,
UserKey: algoProvider1PubKey,
},
userKey: algoProvider1Key,
err: errors.New("gRPC client error"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("Algo", mock.Anything, mock.Anything).Return(tc.err)
algo, err := os.CreateTemp("", "algo")
require.NoError(t, err)
defer os.Remove(algo.Name())
_, err = algo.Write(algorithm.Algorithm)
require.NoError(t, err)
err = algo.Close()
require.NoError(t, err)
algo, err = os.Open(algo.Name())
require.NoError(t, err)
err = sdk.Algo(context.Background(), algo, nil, tc.userKey)
st, _ := status.FromError(err)
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s : Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
svcCall.Unset()
})
}
}
func TestData(t *testing.T) {
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
data, err := os.ReadFile(dataPath)
require.NoError(t, err)
dataHash := sha3.Sum256(data)
dataProviderKey, dataProviderPubKey := generateKeys(t, "ecdsa")
dataProvider1Key, dataProvider1PubKey := generateKeys(t, "ed25519")
dataset := agent.Dataset{
Hash: dataHash,
Dataset: data,
UserKey: dataProviderPubKey,
}
cases := []struct {
name string
data agent.Dataset
userKey any
svcErr error
}{
{
name: "Test data successfully",
data: dataset,
userKey: dataProviderKey,
},
{
name: "undeclared dataset",
data: agent.Dataset{
Dataset: data,
UserKey: dataProvider1PubKey,
Hash: dataHash,
},
userKey: dataProvider1Key,
svcErr: errInappropriateIoctl,
},
{
name: "hash mismatch",
data: agent.Dataset{
Dataset: data,
UserKey: dataProvider1PubKey,
Hash: dataHash,
},
userKey: dataProvider1Key,
svcErr: errInappropriateIoctl,
},
{
name: "all manifest items received",
data: agent.Dataset{
Dataset: data,
UserKey: dataProvider1PubKey,
Hash: dataHash,
},
userKey: dataProvider1Key,
svcErr: errInappropriateIoctl,
},
{
name: "missing dataset file",
data: agent.Dataset{
UserKey: dataProvider1PubKey,
Hash: dataHash,
},
userKey: dataProvider1Key,
svcErr: errors.New("dataset CSV file is required"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dataCall := svc.On("Data", mock.Anything, mock.Anything).Return(tc.svcErr)
data, err := os.CreateTemp("", "data")
require.NoError(t, err)
_, err = data.Write(dataset.Dataset)
require.NoError(t, err)
err = data.Close()
require.NoError(t, err)
data, err = os.Open(data.Name())
require.NoError(t, err)
err = sdk.Data(context.Background(), data, tc.data.Filename, tc.userKey)
st, _ := status.FromError(err)
if tc.svcErr != nil {
if st.Message() != tc.svcErr.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.svcErr.Error(), st.Message())
}
}
dataCall.Unset()
})
}
}
func TestResult(t *testing.T) {
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
resultConsumerKey, _ := generateKeys(t, "ecdsa")
resultConsumer1Key, _ := generateKeys(t, "ed25519")
response := &agent.ResultResponse{
File: []byte{
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
},
}
cases := []struct {
name string
userKey any
response *agent.ResultResponse
svcRes []byte
err error
}{
{
name: "Test result successfully",
userKey: resultConsumerKey,
response: response,
svcRes: response.File,
err: nil,
},
{
name: "Test result successfully with ed25519 key type",
userKey: resultConsumer1Key,
response: response,
svcRes: response.File,
err: nil,
},
{
name: "Results not ready",
userKey: resultConsumer1Key,
response: &agent.ResultResponse{
File: []byte{},
},
svcRes: nil,
err: agent.ErrResultsNotReady,
},
{
name: "All manifest items received",
userKey: resultConsumer1Key,
response: &agent.ResultResponse{
File: []byte{},
},
svcRes: nil,
err: agent.ErrAllManifestItemsReceived,
},
{
name: "Undeclared consumer",
userKey: resultConsumer1Key,
response: &agent.ResultResponse{
File: []byte{},
},
svcRes: nil,
err: agent.ErrUndeclaredConsumer,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("Result", mock.Anything, mock.Anything).Return(tc.svcRes, tc.err)
resultFile, err := os.CreateTemp("", "result")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(resultFile.Name())
})
err = sdk.Result(context.Background(), tc.userKey, resultFile)
require.NoError(t, resultFile.Close())
st, ok := status.FromError(err)
if !ok {
t.Fatalf("Expected gRPC status error, but got: %v", err)
}
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
res, err := os.ReadFile(resultFile.Name())
require.NoError(t, err)
assert.Equal(t, tc.response.File, res, tc.name)
svcCall.Unset()
})
}
}
func TestAttestation(t *testing.T) {
resultConsumerKey, _ := generateKeys(t, "rsa")
resultConsumer1Key, _ := generateKeys(t, "ed25519")
reportData := make([]byte, 64)
nonce := make([]byte, 64)
report := []byte{
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
}
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
_, err = rand.Read(reportData)
require.NoError(t, err)
cases := []struct {
name string
userKey any
reportData [quoteprovider.Nonce]byte
nonce [vtpm.Nonce]byte
response *agent.AttestationResponse
svcRes []byte
err error
}{
{
name: "fetch attestation report successfully",
userKey: resultConsumerKey,
reportData: [quoteprovider.Nonce]byte(reportData),
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResponse{
File: report,
},
svcRes: report,
err: nil,
},
{
name: "fetch attestation report with different key type",
userKey: resultConsumer1Key,
reportData: [quoteprovider.Nonce]byte(reportData),
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResponse{
File: report,
},
svcRes: report,
err: nil,
},
{
name: "failed to fetch attestation report",
userKey: resultConsumerKey,
reportData: [quoteprovider.Nonce]byte(reportData),
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResponse{
File: []byte{},
},
err: nil,
},
{
name: "invalid report data",
userKey: resultConsumerKey,
reportData: [quoteprovider.Nonce]byte{},
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResponse{
File: []byte{},
},
svcRes: nil,
err: errors.New("invalid report data"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("Attestation", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.svcRes, tc.err)
file, err := os.CreateTemp("", "attestation")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(file.Name())
})
err = sdk.Attestation(context.Background(), tc.reportData, tc.nonce, 0, file)
require.NoError(t, file.Close())
st, ok := status.FromError(err)
if !ok {
t.Fatalf("Expected gRPC status error, but got: %v", err)
}
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
res, err := os.ReadFile(file.Name())
require.NoError(t, err)
assert.Equal(t, tc.response.File, res, tc.name)
svcCall.Unset()
})
}
}
func TestAttestationResult(t *testing.T) {
reportData := make([]byte, 64)
nonce := make([]byte, 64)
report := []byte{
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
}
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
_, err = rand.Read(reportData)
require.NoError(t, err)
cases := []struct {
name string
nonce [vtpm.Nonce]byte
response *agent.AttestationResultResponse
svcRes []byte
err error
}{
{
name: "fetch attestation report successfully",
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResultResponse{
File: report,
},
svcRes: report,
err: nil,
},
{
name: "failed to fetch attestation report",
nonce: [vtpm.Nonce]byte(nonce),
response: &agent.AttestationResultResponse{
File: []byte{},
},
err: nil,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("AttestationResult", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.svcRes, tc.err)
file, err := os.CreateTemp("", "attestation")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(file.Name())
})
err = sdk.AttestationResult(context.Background(), tc.nonce, 0, file)
require.NoError(t, file.Close())
st, ok := status.FromError(err)
if !ok {
t.Fatalf("Expected gRPC status error, but got: %v", err)
}
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
res, err := os.ReadFile(file.Name())
require.NoError(t, err)
assert.Equal(t, tc.response.File, res, tc.name)
svcCall.Unset()
})
}
}
func TestIMAMeasurements(t *testing.T) {
conn, err := grpc.NewClient("passthrough://bufnet", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(bufDialer))
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := agent.NewAgentServiceClient(conn)
sdk := sdk.NewAgentSDK(client)
response := &agent.IMAMeasurementsResponse{
File: []byte{
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
},
}
cases := []struct {
name string
response *agent.IMAMeasurementsResponse
svcRes []byte
err error
}{
{
name: "fetch IMA measurements successfully",
response: response,
svcRes: response.File,
err: nil,
},
{
name: "failed to fetch IMA measurements",
response: &agent.IMAMeasurementsResponse{File: []byte{}},
svcRes: nil,
err: errors.New("failed to fetch IMA measurements"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
svcCall := svc.On("IMAMeasurements", mock.Anything).Return(tc.svcRes, tc.svcRes, tc.err)
file, err := os.CreateTemp("", "ima_measurements")
require.NoError(t, err)
t.Cleanup(func() {
os.Remove(file.Name())
})
_, err = sdk.IMAMeasurements(context.Background(), file)
require.NoError(t, file.Close())
st, ok := status.FromError(err)
if !ok {
t.Fatalf("Expected gRPC status error, but got: %v", err)
}
if tc.err != nil {
if st.Message() != tc.err.Error() {
t.Errorf("%s: Expected error message %q, but got %q", tc.name, tc.err.Error(), st.Message())
}
}
res, err := os.ReadFile(file.Name())
require.NoError(t, err)
assert.Equal(t, tc.response.File, res, tc.name)
svcCall.Unset()
})
}
}
func generateKeys(t *testing.T, keyType string) (priv any, pub []byte) {
switch keyType {
case "ecdsa":
privEcdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privEcdsaKey.PublicKey)
require.NoError(t, err)
return privEcdsaKey, pubKeyBytes
case "ed25519":
pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pubKey, err := x509.MarshalPKIXPublicKey(pubEd25519Key)
require.NoError(t, err)
return privEd25519Key, pubKey
default:
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey)
require.NoError(t, err)
return privKey, pubKeyBytes
}
}