diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index 4e08b2c2..7e53bcd4 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -186,25 +186,24 @@ func (s *Service) BrickCreate( ) error { brick, present := s.bricksIndex.FindBrickByID(req.ID) if !present { - return fmt.Errorf("brick not found with id %s", req.ID) + return fmt.Errorf("brick %q not found", req.ID) } for name, reqValue := range req.Variables { value, exist := brick.GetVariable(name) if !exist { - return errors.New("variable does not exist") + return fmt.Errorf("variable %q does not exist on brick %q", name, brick.ID) } if value.DefaultValue == "" && reqValue == "" { - return errors.New("variable default value cannot be empty") + return fmt.Errorf("variable %q cannot be empty", name) } } for _, brickVar := range brick.Variables { if brickVar.DefaultValue == "" { if _, exist := req.Variables[brickVar.Name]; !exist { - return errors.New("variable does not exist") + return fmt.Errorf("required variable %q is mandatory", brickVar.Name) } - return errors.New("variable default value cannot be empty") } } @@ -227,25 +226,20 @@ func (s *Service) BrickCreate( if idx == -1 { return fmt.Errorf("model %s does not exsist", *req.Model) } - brickInstance.Model = models[idx].ID } brickInstance.Variables = req.Variables if brickIndex == -1 { - appCurrent.Descriptor.Bricks = append(appCurrent.Descriptor.Bricks, brickInstance) - } else { appCurrent.Descriptor.Bricks[brickIndex] = brickInstance - } err := appCurrent.Save() if err != nil { return fmt.Errorf("cannot save brick instance with id %s", req.ID) } - return nil } diff --git a/internal/orchestrator/bricks/bricks_test.go b/internal/orchestrator/bricks/bricks_test.go new file mode 100644 index 00000000..b5a93173 --- /dev/null +++ b/internal/orchestrator/bricks/bricks_test.go @@ -0,0 +1,112 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-app-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package bricks + +import ( + "testing" + + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/require" + "go.bug.st/f" + + "github.com/arduino/arduino-app-cli/internal/orchestrator/app" + "github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex" +) + +func TestBrickCreate(t *testing.T) { + bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(paths.New("testdata")) + require.Nil(t, err) + brickService := NewService(nil, bricksIndex, nil) + + t.Run("fails if brick id does not exist", func(t *testing.T) { + err = brickService.BrickCreate(BrickCreateUpdateRequest{ID: "not-existing-id"}, f.Must(app.Load("testdata/dummy-app"))) + require.Error(t, err) + require.Equal(t, "brick \"not-existing-id\" not found", err.Error()) + }) + + t.Run("fails if the requestes variable is not present in the brick definition", func(t *testing.T) { + req := BrickCreateUpdateRequest{ID: "arduino:arduino_cloud", Variables: map[string]string{ + "NON_EXISTING_VARIABLE": "some-value", + }} + err = brickService.BrickCreate(req, f.Must(app.Load("testdata/dummy-app"))) + require.Error(t, err) + require.Equal(t, "variable \"NON_EXISTING_VARIABLE\" does not exist on brick \"arduino:arduino_cloud\"", err.Error()) + }) + + t.Run("fails if a required variable is set empty", func(t *testing.T) { + req := BrickCreateUpdateRequest{ID: "arduino:arduino_cloud", Variables: map[string]string{ + "ARDUINO_DEVICE_ID": "", + "ARDUINO_SECRET": "a-secret-a", + }} + err = brickService.BrickCreate(req, f.Must(app.Load("testdata/dummy-app"))) + require.Error(t, err) + require.Equal(t, "variable \"ARDUINO_DEVICE_ID\" cannot be empty", err.Error()) + }) + + t.Run("fails if a mandatory variable is not present in the request", func(t *testing.T) { + req := BrickCreateUpdateRequest{ID: "arduino:arduino_cloud", Variables: map[string]string{ + "ARDUINO_SECRET": "a-secret-a", + }} + err = brickService.BrickCreate(req, f.Must(app.Load("testdata/dummy-app"))) + require.Error(t, err) + require.Equal(t, "required variable \"ARDUINO_DEVICE_ID\" is mandatory", err.Error()) + }) + + t.Run("the brick is added if it does not exist in the app", func(t *testing.T) { + tempDummyApp := paths.New("testdata/dummy-app.temp") + err := tempDummyApp.RemoveAll() + require.Nil(t, err) + require.Nil(t, paths.New("testdata/dummy-app").CopyDirTo(tempDummyApp)) + + req := BrickCreateUpdateRequest{ID: "arduino:dbstorage_sqlstore"} + err = brickService.BrickCreate(req, f.Must(app.Load(tempDummyApp.String()))) + require.Nil(t, err) + after, err := app.Load(tempDummyApp.String()) + require.Nil(t, err) + require.Len(t, after.Descriptor.Bricks, 2) + require.Equal(t, "arduino:dbstorage_sqlstore", after.Descriptor.Bricks[1].ID) + }) + t.Run("the variables of a brick are updated", func(t *testing.T) { + tempDummyApp := paths.New("testdata/dummy-app.brick-override.temp") + err := tempDummyApp.RemoveAll() + require.Nil(t, err) + err = paths.New("testdata/dummy-app").CopyDirTo(tempDummyApp) + require.Nil(t, err) + bricksIndex, err := bricksindex.GenerateBricksIndexFromFile(paths.New("testdata")) + require.Nil(t, err) + brickService := NewService(nil, bricksIndex, nil) + + deviceID := "this-is-a-device-id" + secret := "this-is-a-secret" + req := BrickCreateUpdateRequest{ + ID: "arduino:arduino_cloud", + Variables: map[string]string{ + "ARDUINO_DEVICE_ID": deviceID, + "ARDUINO_SECRET": secret, + }, + } + + err = brickService.BrickCreate(req, f.Must(app.Load(tempDummyApp.String()))) + require.Nil(t, err) + + after, err := app.Load(tempDummyApp.String()) + require.Nil(t, err) + require.Len(t, after.Descriptor.Bricks, 1) + require.Equal(t, "arduino:arduino_cloud", after.Descriptor.Bricks[0].ID) + require.Equal(t, deviceID, after.Descriptor.Bricks[0].Variables["ARDUINO_DEVICE_ID"]) + require.Equal(t, secret, after.Descriptor.Bricks[0].Variables["ARDUINO_SECRET"]) + }) +} diff --git a/internal/orchestrator/bricks/testdata/.gitignore b/internal/orchestrator/bricks/testdata/.gitignore new file mode 100644 index 00000000..d4685d62 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/.gitignore @@ -0,0 +1 @@ +*.temp \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/bricks-list.yaml b/internal/orchestrator/bricks/testdata/bricks-list.yaml new file mode 100644 index 00000000..8e3114d6 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/bricks-list.yaml @@ -0,0 +1,25 @@ +bricks: +- id: arduino:arduino_cloud + name: Arduino Cloud + description: Connects to Arduino Cloud + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: null + variables: + - name: ARDUINO_DEVICE_ID + description: Arduino Cloud Device ID + - name: ARDUINO_SECRET + description: Arduino Cloud Secret +- id: arduino:dbstorage_sqlstore + name: Database - SQL + description: Simplified database storage layer for Arduino sensor data using SQLite + local database. + require_container: false + require_model: false + require_devices: false + mount_devices_into_container: false + ports: [] + category: storage diff --git a/internal/orchestrator/bricks/testdata/dummy-app/app.yaml b/internal/orchestrator/bricks/testdata/dummy-app/app.yaml new file mode 100644 index 00000000..281821c6 --- /dev/null +++ b/internal/orchestrator/bricks/testdata/dummy-app/app.yaml @@ -0,0 +1,6 @@ +name: Copy of Blinking LED from Arduino Cloud +description: Control the LED from the Arduino IoT Cloud using RPC calls +icon: ☁️ +ports: [] +bricks: +- arduino:arduino_cloud: \ No newline at end of file diff --git a/internal/orchestrator/bricks/testdata/dummy-app/python/main.py b/internal/orchestrator/bricks/testdata/dummy-app/python/main.py new file mode 100644 index 00000000..336e825c --- /dev/null +++ b/internal/orchestrator/bricks/testdata/dummy-app/python/main.py @@ -0,0 +1,2 @@ +def main(): + pass