diff --git a/internal/service/domain/tree/handle_new_sensor_data.go b/internal/service/domain/tree/handle_new_sensor_data.go new file mode 100644 index 00000000..a4cb3eb7 --- /dev/null +++ b/internal/service/domain/tree/handle_new_sensor_data.go @@ -0,0 +1,39 @@ +package tree + +import ( + "context" + "log/slog" + + "github.com/green-ecolution/green-ecolution-backend/internal/entities" + "github.com/green-ecolution/green-ecolution-backend/internal/logger" + "github.com/green-ecolution/green-ecolution-backend/internal/service/domain/utils" + "github.com/green-ecolution/green-ecolution-backend/internal/storage/postgres/tree" +) + +func (s *TreeService) HandleNewSensorData(ctx context.Context, event *entities.EventNewSensorData) error { + log := logger.GetLogger(ctx) + log.Debug("handle event", "event", event.Type(), "service", "TreeService") + t, err := s.treeRepo.GetBySensorID(ctx, event.New.SensorID) + if err != nil { + log.Error("failed to get tree by sensor id", "sensor_id", event.New.SensorID, "err", err) + return nil + } + + status := utils.CalculateWateringStatus(ctx, t.PlantingYear, event.New.Data.Watermarks) + + if status == t.WateringStatus { + log.Debug("sensor status has not changed", "sensor_status", status) + return nil + } + + newTree, err := s.treeRepo.Update(ctx, t.ID, tree.WithWateringStatus(status)) + if err != nil { + log.Error("failed to update tree with new watering status", "tree_id", t.ID, "watering_status", status, "err", err) + return err + } + + slog.Info("updating tree watering status", "prev_status", t.WateringStatus, "new_status", status) + + s.publishUpdateTreeEvent(ctx, t, newTree) + return nil +} diff --git a/internal/service/domain/tree/handle_new_sensor_data_test.go b/internal/service/domain/tree/handle_new_sensor_data_test.go new file mode 100644 index 00000000..6e0fc84a --- /dev/null +++ b/internal/service/domain/tree/handle_new_sensor_data_test.go @@ -0,0 +1,162 @@ +package tree + +import ( + "context" + "testing" + "time" + + "github.com/green-ecolution/green-ecolution-backend/internal/entities" + "github.com/green-ecolution/green-ecolution-backend/internal/storage" + storageMock "github.com/green-ecolution/green-ecolution-backend/internal/storage/_mock" + "github.com/green-ecolution/green-ecolution-backend/internal/worker" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestTreeService_HandleNewSensorData(t *testing.T) { + t.Run("should update watering status on new sensor data event", func(t *testing.T) { + treeRepo := storageMock.NewMockTreeRepository(t) + sensorRepo := storageMock.NewMockSensorRepository(t) + imageRepo := storageMock.NewMockImageRepository(t) + clusterRepo := storageMock.NewMockTreeClusterRepository(t) + eventManager := worker.NewEventManager(entities.EventTypeUpdateTree) + svc := NewTreeService(treeRepo, sensorRepo, imageRepo, clusterRepo, eventManager) + + _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTree) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go eventManager.Run(ctx) + + sensorDataEvent := entities.SensorData{ + SensorID: "sensor-1", + Data: &entities.MqttPayload{ + Watermarks: []entities.Watermark{ + {Centibar: 30, Depth: 30}, + {Centibar: 40, Depth: 60}, + {Centibar: 50, Depth: 90}, + }, + }, + } + + treeNew := entities.Tree{ + ID: 1, + PlantingYear: int32(time.Now().Year() - 2), + WateringStatus: entities.WateringStatusGood, + } + + tree := entities.Tree{ + ID: 1, + PlantingYear: int32(time.Now().Year() - 2), + WateringStatus: entities.WateringStatusUnknown, + } + + event := entities.NewEventSensorData(&sensorDataEvent) + + treeRepo.EXPECT().GetBySensorID(mock.Anything, "sensor-1").Return(&tree, nil) + treeRepo.EXPECT().Update(mock.Anything, mock.Anything, mock.Anything).Return(&treeNew, nil) + + err := svc.HandleNewSensorData(context.Background(), &event) + + assert.NoError(t, err) + select { + case receivedEvent := <-ch: + e, ok := receivedEvent.(entities.EventUpdateTree) + assert.True(t, ok) + assert.Equal(t, *e.Prev, tree) + assert.Equal(t, *e.New, treeNew) + case <-time.After(100 * time.Millisecond): + t.Fatal("event was not received") + } + }) + + t.Run("should not update and not send event if the sensor has no linked tree", func(t *testing.T) { + treeRepo := storageMock.NewMockTreeRepository(t) + sensorRepo := storageMock.NewMockSensorRepository(t) + imageRepo := storageMock.NewMockImageRepository(t) + clusterRepo := storageMock.NewMockTreeClusterRepository(t) + eventManager := worker.NewEventManager(entities.EventTypeUpdateTree) + svc := NewTreeService(treeRepo, sensorRepo, imageRepo, clusterRepo, eventManager) + + // event + _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTree) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go eventManager.Run(ctx) + + sensorDataEvent := entities.SensorData{ + SensorID: "sensor-1", + Data: &entities.MqttPayload{ + Watermarks: []entities.Watermark{ + {Centibar: 61, Depth: 30}, + {Centibar: 24, Depth: 60}, + {Centibar: 24, Depth: 90}, + }, + }, + } + + event := entities.NewEventSensorData(&sensorDataEvent) + + treeRepo.EXPECT().GetBySensorID(mock.Anything, "sensor-1").Return(nil, storage.ErrTreeNotFound) + + // when + err := svc.HandleNewSensorData(context.Background(), &event) + + // then + assert.NoError(t, err) + select { + case <-ch: + t.Fatal("event was received. It should not have been sent") + case <-time.After(100 * time.Millisecond): + assert.True(t, true) + } + }) + + t.Run("should not update and not send event if tree could not be updated", func(t *testing.T) { + treeRepo := storageMock.NewMockTreeRepository(t) + sensorRepo := storageMock.NewMockSensorRepository(t) + imageRepo := storageMock.NewMockImageRepository(t) + clusterRepo := storageMock.NewMockTreeClusterRepository(t) + eventManager := worker.NewEventManager(entities.EventTypeUpdateTree) + svc := NewTreeService(treeRepo, sensorRepo, imageRepo, clusterRepo, eventManager) + + // event + _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTree) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go eventManager.Run(ctx) + + sensorDataEvent := entities.SensorData{ + SensorID: "sensor-1", + Data: &entities.MqttPayload{ + Watermarks: []entities.Watermark{ + {Centibar: 30, Depth: 30}, + {Centibar: 40, Depth: 60}, + {Centibar: 50, Depth: 90}, + }, + }, + } + + tree := entities.Tree{ + ID: 1, + PlantingYear: int32(time.Now().Year() - 2), + WateringStatus: entities.WateringStatusUnknown, + } + + event := entities.NewEventSensorData(&sensorDataEvent) + + treeRepo.EXPECT().GetBySensorID(mock.Anything, "sensor-1").Return(&tree, nil) + treeRepo.EXPECT().Update(mock.Anything, mock.Anything, mock.Anything).Return(nil, storage.ErrTreeNotFound) + + // when + err := svc.HandleNewSensorData(context.Background(), &event) + + // then + assert.ErrorIs(t, err, storage.ErrTreeNotFound) + select { + case <-ch: + t.Fatal("event was received. It should not have been sent") + case <-time.After(100 * time.Millisecond): + assert.True(t, true) + } + }) +} diff --git a/internal/service/domain/tree/import_tree_test.go b/internal/service/domain/tree/import_tree_test.go index dad8f29a..a04e887a 100644 --- a/internal/service/domain/tree/import_tree_test.go +++ b/internal/service/domain/tree/import_tree_test.go @@ -65,6 +65,7 @@ func TestTreeService_ImportTree(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything).Return(updatedTree, nil) // When diff --git a/internal/service/domain/tree/tree.go b/internal/service/domain/tree/tree.go index d3516da6..ce670511 100644 --- a/internal/service/domain/tree/tree.go +++ b/internal/service/domain/tree/tree.go @@ -42,32 +42,6 @@ func NewTreeService( } } -func (s *TreeService) HandleNewSensorData(ctx context.Context, event *entities.EventNewSensorData) error { - log := logger.GetLogger(ctx) - log.Debug("handle event", "event", event.Type(), "service", "TreeService") - t, err := s.treeRepo.GetBySensorID(ctx, event.New.SensorID) - if err != nil { - log.Error("failed to get tree by sensor id", "sensor_id", event.New.SensorID, "err", err) - return nil - } - - status := utils.CalculateWateringStatus(ctx, t.PlantingYear, event.New.Data.Watermarks) - - if status == t.WateringStatus { - return nil - } - - newTree, err := s.treeRepo.Update(ctx, t.ID, tree.WithWateringStatus(status)) - if err != nil { - log.Error("failed to update tree with new watering status", "tree_id", t.ID, "watering_status", status, "err", err) - } - - slog.Info("updating tree watering status", "prev_status", t.WateringStatus, "new_status", status) - - s.publishUpdateTreeEvent(ctx, t, newTree) - return nil -} - func (s *TreeService) GetAll(ctx context.Context) ([]*entities.Tree, error) { log := logger.GetLogger(ctx) trees, err := s.treeRepo.GetAll(ctx) @@ -146,12 +120,17 @@ func (s *TreeService) Create(ctx context.Context, treeCreate *entities.TreeCreat } if treeCreate.SensorID != nil { - sensorID, err := s.sensorRepo.GetByID(ctx, *treeCreate.SensorID) + sensor, err := s.sensorRepo.GetByID(ctx, *treeCreate.SensorID) if err != nil { log.Debug("failed to fetch sensor by id specified in the tree create request", "sensor_id", treeCreate.SensorID) return nil, service.MapError(ctx, err, service.ErrorLogEntityNotFound) } - fn = append(fn, tree.WithSensor(sensorID)) + fn = append(fn, tree.WithSensor(sensor)) + + if sensor.LatestData != nil && sensor.LatestData.Data != nil && len(sensor.LatestData.Data.Watermarks) > 0 { + status := utils.CalculateWateringStatus(ctx, treeCreate.PlantingYear, sensor.LatestData.Data.Watermarks) + fn = append(fn, tree.WithWateringStatus(status)) + } } fn = append(fn, @@ -202,6 +181,7 @@ func (s *TreeService) Update(ctx context.Context, id int32, tu *entities.TreeUpd return nil, service.MapError(ctx, err, service.ErrorLogEntityNotFound) } + // TODO: Why is this still commented out? // Check if the tree is readonly (imported from csv) // if currentTree.Readonly { // return nil, handleError(fmt.Errorf("tree with ID %d is readonly and cannot be updated", id)) @@ -228,8 +208,15 @@ func (s *TreeService) Update(ctx context.Context, id int32, tu *entities.TreeUpd return nil, service.MapError(ctx, fmt.Errorf("failed to find Sensor with ID %v: %w", *tu.SensorID, err), service.ErrorLogEntityNotFound) } fn = append(fn, tree.WithSensor(sensor)) + + if sensor.LatestData != nil && sensor.LatestData.Data != nil && len(sensor.LatestData.Data.Watermarks) > 0 { + status := utils.CalculateWateringStatus(ctx, tu.PlantingYear, sensor.LatestData.Data.Watermarks) + fn = append(fn, tree.WithWateringStatus(status)) + } } else { - fn = append(fn, tree.WithSensor(nil)) + fn = append(fn, + tree.WithSensor(nil), + tree.WithWateringStatus(entities.WateringStatusUnknown)) } fn = append(fn, tree.WithPlantingYear(tu.PlantingYear), diff --git a/internal/service/domain/tree/tree_test.go b/internal/service/domain/tree/tree_test.go index f469ee0d..e500f73d 100644 --- a/internal/service/domain/tree/tree_test.go +++ b/internal/service/domain/tree/tree_test.go @@ -251,6 +251,7 @@ func TestTreeService_Create(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything).Return(expectedTree, nil) // when @@ -359,6 +360,7 @@ func TestTreeService_Create(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything).Return(nil, expectedError) // when @@ -509,6 +511,8 @@ func TestTreeService_Update(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, + mock.Anything, mock.Anything).Return(updatedTree, nil) // when @@ -655,6 +659,8 @@ func TestTreeService_Update(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, + mock.Anything, mock.Anything).Return(nil, expectedError) // when @@ -743,6 +749,7 @@ func TestTreeService_EventSystem(t *testing.T) { mock.Anything, mock.Anything, mock.Anything, + mock.Anything, mock.Anything).Return(&expectedTree, nil) svc := tree.NewTreeService(treeRepo, sensorRepo, imageRepo, treeClusterRepo, eventManager) diff --git a/internal/service/domain/tree/utils_test.go b/internal/service/domain/tree/utils_test.go index 8b2cfa05..fb5693ff 100644 --- a/internal/service/domain/tree/utils_test.go +++ b/internal/service/domain/tree/utils_test.go @@ -41,28 +41,30 @@ var ( TestTreesList = []*entities.Tree{ { - ID: 1, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Species: "Oak", - Number: "T001", - Latitude: testLatitude, - Longitude: testLongitude, - Description: "A mature oak tree", - PlantingYear: 2023, - Readonly: true, + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Species: "Oak", + Number: "T001", + Latitude: testLatitude, + Longitude: testLongitude, + Description: "A mature oak tree", + PlantingYear: 2023, + Readonly: true, + WateringStatus: entities.WateringStatusBad, }, { - ID: 2, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Species: "Pine", - Number: "T002", - Latitude: 9.446700, - Longitude: 54.801510, - Description: "A young pine tree", - PlantingYear: 2023, - Readonly: true, + ID: 2, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Species: "Pine", + Number: "T002", + Latitude: 9.446700, + Longitude: 54.801510, + Description: "A young pine tree", + PlantingYear: 2023, + Readonly: true, + WateringStatus: entities.WateringStatusUnknown, }, } @@ -74,7 +76,7 @@ var ( Status: entities.SensorStatusUnknown, Latitude: 54.82124518093376, Longitude: 9.485702120628517, - LatestData: &entities.SensorData{}, + LatestData: TestSensorDataBad, }, { ID: "sensor-2", @@ -115,4 +117,34 @@ var ( Longitude: testLongitude, Description: "Updated description", } + + TestSensorDataBad = &entities.SensorData{ + ID: 1, + SensorID: "sensor-1", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Data: &entities.MqttPayload{ + Device: "sensor-1", + Temperature: 2.0, + Humidity: 0.5, + Battery: 3.3, + Watermarks: []entities.Watermark{ + { + Resistance: 2000, + Centibar: 80, + Depth: 30, + }, + { + Resistance: 2200, + Centibar: 85, + Depth: 60, + }, + { + Resistance: 2500, + Centibar: 90, + Depth: 90, + }, + }, + }, + } ) diff --git a/internal/service/domain/treecluster/handle_new_sensor_data.go b/internal/service/domain/treecluster/handle_new_sensor_data.go index da8426e8..d6dce360 100644 --- a/internal/service/domain/treecluster/handle_new_sensor_data.go +++ b/internal/service/domain/treecluster/handle_new_sensor_data.go @@ -31,78 +31,102 @@ func (s *TreeClusterService) HandleNewSensorData(ctx context.Context, event *ent return nil } - sensorData, err := s.treeClusterRepo.GetAllLatestSensorDataByClusterID(ctx, tree.TreeCluster.ID) + wateringStatus, err := s.getWateringStatusOfTreeCluster(ctx, tree.TreeCluster.ID) if err != nil { - log.Error("failed to get latest sensor data", "cluster_id", tree.TreeCluster.ID, "error", err) + log.Error("error while calculating watering status of tree cluster", "error", err) return nil } - var wateringStatus entities.WateringStatus - if len(sensorData) == 0 { - // assertion - if there is no sensor data after receiving the event, the world is ending + if wateringStatus == tree.TreeCluster.WateringStatus { + log.Debug("watering status has not changed", "watering_status", wateringStatus) return nil - } else if len(sensorData) == 1 { - wateringStatus = tree.WateringStatus - } else { - sensorIDs := utils.Map(sensorData, func(data *entities.SensorData) string { - return data.SensorID - }) - - trees, err := s.treeRepo.GetBySensorIDs(ctx, sensorIDs...) - if err != nil { - log.Error("failed to get trees by sensor id", "sensor_ids", sensorIDs, "error", err) - return nil - } + } - slices.SortFunc(trees, func(a, b *entities.Tree) int { - return int(b.PlantingYear - a.PlantingYear) - }) + updateFn := func(tc *entities.TreeCluster) (bool, error) { + tc.WateringStatus = wateringStatus + return true, nil + } - youngestTree := trees[0] + if err := s.treeClusterRepo.Update(ctx, tree.TreeCluster.ID, updateFn); err == nil { + return s.publishUpdateEvent(ctx, tree.TreeCluster) + } - var w30CentibarAvg, w60CentibarAvg, w90CentibarAvg int - for _, data := range sensorData { - w30, w60, w90, err := svcUtils.CheckAndSortWatermarks(data.Data.Watermarks) - if err != nil { - log.Error("sensor data watermarks are malformed", "watermarks", data.Data.Watermarks) - return nil - } + return nil +} - w30CentibarAvg += w30.Centibar - w60CentibarAvg += w60.Centibar - w90CentibarAvg += w90.Centibar - } +func (s *TreeClusterService) getWateringStatusOfTreeCluster(ctx context.Context, clusterID int32) (entities.WateringStatus, error) { + log := logger.GetLogger(ctx) + sensorData, err := s.treeClusterRepo.GetAllLatestSensorDataByClusterID(ctx, clusterID) + if err != nil { + log.Error("failed to get latest sensor data", "cluster_id", clusterID, "err", err) + return entities.WateringStatusUnknown, errors.New("failed to get latest sensor data") + } - watermarks := []entities.Watermark{ - { - Centibar: w30CentibarAvg / len(sensorData), - Depth: 30, - }, - { - Centibar: w60CentibarAvg / len(sensorData), - Depth: 60, - }, - { - Centibar: w90CentibarAvg / len(sensorData), - Depth: 90, - }, - } + // assertion - if there is no sensor data after receiving the event, the world is ending + if len(sensorData) == 0 { + log.Error("sensor data is empty") + return entities.WateringStatusUnknown, errors.New("sensor data is empty") + } - wateringStatus = svcUtils.CalculateWateringStatus(ctx, youngestTree.PlantingYear, watermarks) + sensorIDs := utils.Map(sensorData, func(data *entities.SensorData) string { + return data.SensorID + }) + + youngestTree, err := s.getYoungestTree(ctx, sensorIDs) + if err != nil { + return entities.WateringStatusUnknown, errors.New("failed to get youngest tree") } - if wateringStatus == tree.TreeCluster.WateringStatus { - return nil + watermarks, err := s.getWatermarkSensorData(ctx, sensorData) + if err != nil { + return entities.WateringStatusUnknown, errors.New("failed getting watermark sensor data") } - updateFn := func(tc *entities.TreeCluster) (bool, error) { - tc.WateringStatus = wateringStatus - return true, nil + return svcUtils.CalculateWateringStatus(ctx, youngestTree.PlantingYear, watermarks), nil +} + +func (s *TreeClusterService) getYoungestTree(ctx context.Context, sensorIDs []string) (*entities.Tree, error) { + log := logger.GetLogger(ctx) + trees, err := s.treeRepo.GetBySensorIDs(ctx, sensorIDs...) + if err != nil { + log.Error("failed to get trees by sensor id", "sensor_ids", sensorIDs, "err", err) + return nil, errors.New("failed to get trees by sensor id") } - if err := s.treeClusterRepo.Update(ctx, tree.TreeCluster.ID, updateFn); err == nil { - return s.publishUpdateEvent(ctx, tree.TreeCluster) + slices.SortFunc(trees, func(a, b *entities.Tree) int { + return int(b.PlantingYear - a.PlantingYear) + }) + + return trees[0], nil +} + +func (s *TreeClusterService) getWatermarkSensorData(ctx context.Context, sensorData []*entities.SensorData) ([]entities.Watermark, error) { + log := logger.GetLogger(ctx) + var w30CentibarAvg, w60CentibarAvg, w90CentibarAvg int + for _, data := range sensorData { + w30, w60, w90, err := svcUtils.CheckAndSortWatermarks(data.Data.Watermarks) + if err != nil { + log.Error("sensor data watermarks are malformed", "watermarks", data.Data.Watermarks) + return nil, errors.New("sensor data watermarks are malformed") + } + + w30CentibarAvg += w30.Centibar + w60CentibarAvg += w60.Centibar + w90CentibarAvg += w90.Centibar } - return nil + return []entities.Watermark{ + { + Centibar: w30CentibarAvg / len(sensorData), + Depth: 30, + }, + { + Centibar: w60CentibarAvg / len(sensorData), + Depth: 60, + }, + { + Centibar: w90CentibarAvg / len(sensorData), + Depth: 90, + }, + }, nil } diff --git a/internal/service/domain/treecluster/handle_new_sensor_data_test.go b/internal/service/domain/treecluster/handle_new_sensor_data_test.go index 316b631c..0f3f5fde 100644 --- a/internal/service/domain/treecluster/handle_new_sensor_data_test.go +++ b/internal/service/domain/treecluster/handle_new_sensor_data_test.go @@ -162,21 +162,31 @@ func TestTreeClusterService_HandleNewSensorData(t *testing.T) { } tree := entities.Tree{ - ID: 1, + ID: 1, + TreeCluster: tc, + PlantingYear: int32(time.Now().Year() - 2), + } + + treeWithSensorID1 := entities.Tree{ + ID: 2, TreeCluster: tc, - WateringStatus: entities.WateringStatusModerate, - PlantingYear: int32(time.Now().Year() - 2), + WateringStatus: entities.WateringStatusBad, + Sensor: &entities.Sensor{ + ID: "sensor-1", + }, + PlantingYear: int32(time.Now().Year() - 1), } event := entities.NewEventSensorData(&sensorDataEvent) treeRepo.EXPECT().GetBySensorID(mock.Anything, "sensor-1").Return(&tree, nil) clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID(mock.Anything, int32(1)).Return([]*entities.SensorData{&sensorDataEvent}, nil) + treeRepo.EXPECT().GetBySensorIDs(mock.Anything, "sensor-1").Return([]*entities.Tree{&treeWithSensorID1}, nil) clusterRepo.EXPECT().Update(mock.Anything, int32(1), mock.Anything).RunAndReturn(func(ctx context.Context, i int32, f func(*entities.TreeCluster) (bool, error)) error { cluster := entities.TreeCluster{} _, err := f(&cluster) assert.NoError(t, err) - assert.Equal(t, entities.WateringStatusModerate, cluster.WateringStatus) + assert.Equal(t, entities.WateringStatusBad, cluster.WateringStatus) return nil }) clusterRepo.EXPECT().GetByID(mock.Anything, int32(1)).Return(tcNew, nil) @@ -233,10 +243,21 @@ func TestTreeClusterService_HandleNewSensorData(t *testing.T) { PlantingYear: int32(time.Now().Year() - 2), } + treeWithSensorID1 := entities.Tree{ + ID: 2, + TreeCluster: tc, + WateringStatus: entities.WateringStatusBad, + Sensor: &entities.Sensor{ + ID: "sensor-1", + }, + PlantingYear: int32(time.Now().Year() - 1), + } + event := entities.NewEventSensorData(&sensorDataEvent) treeRepo.EXPECT().GetBySensorID(mock.Anything, "sensor-1").Return(&tree, nil) clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID(mock.Anything, int32(1)).Return([]*entities.SensorData{&sensorDataEvent}, nil) + treeRepo.EXPECT().GetBySensorIDs(mock.Anything, "sensor-1").Return([]*entities.Tree{&treeWithSensorID1}, nil) // when err := svc.HandleNewSensorData(context.Background(), &event) diff --git a/internal/service/domain/treecluster/handle_tree_update.go b/internal/service/domain/treecluster/handle_tree_update.go index 68d9beec..4afdc263 100644 --- a/internal/service/domain/treecluster/handle_tree_update.go +++ b/internal/service/domain/treecluster/handle_tree_update.go @@ -15,7 +15,7 @@ func (s *TreeClusterService) HandleCreateTree(ctx context.Context, event *entiti return nil } - return s.handleTreeClusterUpdate(ctx, event.New.TreeCluster) + return s.handleTreeClusterUpdate(ctx, event.New.TreeCluster, event.New) } func (s *TreeClusterService) HandleDeleteTree(ctx context.Context, event *entities.EventDeleteTree) error { @@ -26,7 +26,7 @@ func (s *TreeClusterService) HandleDeleteTree(ctx context.Context, event *entiti return nil } - return s.handleTreeClusterUpdate(ctx, event.Prev.TreeCluster) + return s.handleTreeClusterUpdate(ctx, event.Prev.TreeCluster, event.Prev) } func (s *TreeClusterService) HandleUpdateTree(ctx context.Context, event *entities.EventUpdateTree) error { @@ -41,12 +41,12 @@ func (s *TreeClusterService) HandleUpdateTree(ctx context.Context, event *entiti return nil } - if err := s.handleTreeClusterUpdate(ctx, event.Prev.TreeCluster); err != nil { + if err := s.handleTreeClusterUpdate(ctx, event.Prev.TreeCluster, event.New); err != nil { return err } if event.Prev.TreeCluster != nil && event.New.TreeCluster != nil && event.Prev.TreeCluster.ID != event.New.TreeCluster.ID { - if err := s.handleTreeClusterUpdate(ctx, event.New.TreeCluster); err != nil { + if err := s.handleTreeClusterUpdate(ctx, event.New.TreeCluster, event.New); err != nil { return err } } @@ -57,15 +57,21 @@ func (s *TreeClusterService) HandleUpdateTree(ctx context.Context, event *entiti func (s *TreeClusterService) isNoUpdateNeeded(event *entities.EventUpdateTree) bool { treePosSame := event.Prev.Latitude == event.New.Latitude && event.Prev.Longitude == event.New.Longitude tcSame := event.Prev.TreeCluster != nil && event.New.TreeCluster != nil && event.Prev.TreeCluster.ID == event.New.TreeCluster.ID - return treePosSame && tcSame + sensorSame := event.Prev.Sensor == event.New.Sensor + return treePosSame && tcSame && sensorSame } -func (s *TreeClusterService) handleTreeClusterUpdate(ctx context.Context, tc *entities.TreeCluster) error { +func (s *TreeClusterService) handleTreeClusterUpdate(ctx context.Context, tc *entities.TreeCluster, tree *entities.Tree) error { log := logger.GetLogger(ctx) if tc == nil { return nil } + wateringStatus, err := s.getWateringStatusOfTreeCluster(ctx, tree.TreeCluster.ID) + if err != nil { + log.Error("could not update watering status", "error", err) + } + updateFn := func(tc *entities.TreeCluster) (bool, error) { lat, long, region, err := s.getUpdatedLatLong(ctx, tc) if err != nil { @@ -75,6 +81,7 @@ func (s *TreeClusterService) handleTreeClusterUpdate(ctx context.Context, tc *en tc.Latitude = lat tc.Longitude = long tc.Region = region + tc.WateringStatus = wateringStatus return true, nil } @@ -82,5 +89,6 @@ func (s *TreeClusterService) handleTreeClusterUpdate(ctx context.Context, tc *en log.Info("successfully updated new tree cluster position", "cluster_id", tc.ID) return s.publishUpdateEvent(ctx, tc) } + return nil } diff --git a/internal/service/domain/treecluster/handle_tree_update_test.go b/internal/service/domain/treecluster/handle_tree_update_test.go index 6ce2622f..cd6d0f77 100644 --- a/internal/service/domain/treecluster/handle_tree_update_test.go +++ b/internal/service/domain/treecluster/handle_tree_update_test.go @@ -6,6 +6,8 @@ import ( "time" "github.com/green-ecolution/green-ecolution-backend/internal/entities" + "github.com/green-ecolution/green-ecolution-backend/internal/service" + "github.com/green-ecolution/green-ecolution-backend/internal/storage" storageMock "github.com/green-ecolution/green-ecolution-backend/internal/storage/_mock" "github.com/green-ecolution/green-ecolution-backend/internal/utils" "github.com/green-ecolution/green-ecolution-backend/internal/worker" @@ -13,13 +15,10 @@ import ( mock "github.com/stretchr/testify/mock" ) +//nolint:gocyclo // function handles multiple test cases and complex event logic, which requires higher complexity to cover all scenarios. func TestTreeClusterService_HandleUpdateTree(t *testing.T) { - t.Run("should update tree cluster lat long and region and send treecluster update event", func(t *testing.T) { - clusterRepo := storageMock.NewMockTreeClusterRepository(t) - treeRepo := storageMock.NewMockTreeRepository(t) - regionRepo := storageMock.NewMockRegionRepository(t) - eventManager := worker.NewEventManager(entities.EventTypeUpdateTreeCluster) - svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, eventManager) + t.Run("should update tree cluster lat, long, region, watering status and send treecluster update event", func(t *testing.T) { + clusterRepo, treeRepo, _, eventManager, svc := setupTest(t) // event _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTreeCluster) @@ -27,44 +26,54 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { defer cancel() go eventManager.Run(ctx) - prevTc := entities.TreeCluster{ - ID: 1, - Region: &entities.Region{ - ID: 1, - Name: "Sandberg", - }, - Latitude: utils.P(54.776366336440255), - Longitude: utils.P(9.451084144617182), - } - prevTree := entities.Tree{ - ID: 1, - TreeCluster: &prevTc, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } + event := entities.NewEventUpdateTree(&prevTree, &updatedTree) - updatedTree := entities.Tree{ - ID: 1, - TreeCluster: &prevTc, - Number: "T001", - Latitude: 54.811733806341856, - Longitude: 9.482958846410169, - } + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID(mock.Anything, int32(1)).Return(allLatestSensorData, nil) + treeRepo.EXPECT().GetBySensorIDs(mock.Anything, "sensor-1").Return([]*entities.Tree{&updatedTree}, nil) + clusterRepo.EXPECT().Update(mock.Anything, int32(1), mock.Anything).RunAndReturn(func(ctx context.Context, i int32, f func(*entities.TreeCluster) (bool, error)) error { + cluster := entities.TreeCluster{} + _, err := f(&cluster) + assert.NoError(t, err) + assert.Equal(t, entities.WateringStatusGood, cluster.WateringStatus) + return nil + }) + clusterRepo.EXPECT().GetByID(mock.Anything, int32(1)).Return(&updatedTc, nil) - updatedTc := entities.TreeCluster{ - ID: 1, - Region: &entities.Region{ - ID: 2, - Name: "Mürwik", - }, - Latitude: utils.P(54.811733806341856), - Longitude: utils.P(9.482958846410169), + // when + err := svc.HandleUpdateTree(context.Background(), &event) + + // then + assert.NoError(t, err) + select { + case recievedEvent, ok := <-ch: + assert.True(t, ok) + e := recievedEvent.(entities.EventUpdateTreeCluster) + assert.Equal(t, e.Prev, &prevTc) + assert.Equal(t, e.New, &updatedTc) + case <-time.After(1 * time.Second): + t.Fatal("event was not received") } + }) + + t.Run("should update tree cluster watering status to unkown and send treecluster update event", func(t *testing.T) { + clusterRepo, _, _, eventManager, svc := setupTest(t) + + // event + _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTreeCluster) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go eventManager.Run(ctx) event := entities.NewEventUpdateTree(&prevTree, &updatedTree) - clusterRepo.EXPECT().Update(mock.Anything, int32(1), mock.Anything).Return(nil) + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID(mock.Anything, int32(1)).Return(nil, storage.ErrSensorNotFound) + clusterRepo.EXPECT().Update(mock.Anything, int32(1), mock.Anything).RunAndReturn(func(ctx context.Context, i int32, f func(*entities.TreeCluster) (bool, error)) error { + cluster := entities.TreeCluster{} + _, err := f(&cluster) + assert.NoError(t, err) + assert.Equal(t, entities.WateringStatusUnknown, cluster.WateringStatus) + return nil + }) clusterRepo.EXPECT().GetByID(mock.Anything, int32(1)).Return(&updatedTc, nil) // when @@ -84,11 +93,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { }) t.Run("should not update tree cluster if treeclusters in event are nil", func(t *testing.T) { - clusterRepo := storageMock.NewMockTreeClusterRepository(t) - treeRepo := storageMock.NewMockTreeRepository(t) - regionRepo := storageMock.NewMockRegionRepository(t) - eventManager := worker.NewEventManager(entities.EventTypeUpdateTreeCluster) - svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, eventManager) + clusterRepo, _, regionRepo, eventManager, svc := setupTest(t) // event _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTreeCluster) @@ -96,23 +101,13 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { defer cancel() go eventManager.Run(ctx) - prevTree := entities.Tree{ - ID: 1, - TreeCluster: nil, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } + prevWithoutCluster := prevTree + prevWithoutCluster.TreeCluster = nil - updatedTree := entities.Tree{ - ID: 1, - TreeCluster: nil, - Number: "T002", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } + updatedWithoutCluster := updatedTree + updatedWithoutCluster.TreeCluster = nil - event := entities.NewEventUpdateTree(&prevTree, &updatedTree) + event := entities.NewEventUpdateTree(&prevWithoutCluster, &updatedWithoutCluster) // when err := svc.HandleUpdateTree(context.Background(), &event) @@ -132,11 +127,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { }) t.Run("should not update tree cluster if tree has not changed location", func(t *testing.T) { - clusterRepo := storageMock.NewMockTreeClusterRepository(t) - treeRepo := storageMock.NewMockTreeRepository(t) - regionRepo := storageMock.NewMockRegionRepository(t) - eventManager := worker.NewEventManager(entities.EventTypeUpdateTreeCluster) - svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, eventManager) + clusterRepo, _, regionRepo, eventManager, svc := setupTest(t) // event _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTreeCluster) @@ -144,29 +135,16 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { defer cancel() go eventManager.Run(ctx) - tc := entities.TreeCluster{ - ID: 1, - Region: &entities.Region{ - ID: 1, - Name: "Sandberg", - }, - Latitude: utils.P(54.776366336440255), - Longitude: utils.P(9.451084144617182), - } prevTree := entities.Tree{ - ID: 1, - TreeCluster: &tc, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, + TreeCluster: &prevTc, + Latitude: *prevTc.Latitude, + Longitude: *prevTc.Longitude, } updatedTree := entities.Tree{ - ID: 1, - TreeCluster: &tc, - Number: "T002", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, + TreeCluster: &prevTc, + Latitude: *prevTc.Latitude, + Longitude: *prevTc.Longitude, } event := entities.NewEventUpdateTree(&prevTree, &updatedTree) @@ -189,11 +167,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { }) t.Run("should update if tree location is equal but tree has changed treecluster", func(t *testing.T) { - clusterRepo := storageMock.NewMockTreeClusterRepository(t) - treeRepo := storageMock.NewMockTreeRepository(t) - regionRepo := storageMock.NewMockRegionRepository(t) - eventManager := worker.NewEventManager(entities.EventTypeUpdateTreeCluster) - svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, eventManager) + clusterRepo, _, regionRepo, eventManager, svc := setupTest(t) // event _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTreeCluster) @@ -201,23 +175,6 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { defer cancel() go eventManager.Run(ctx) - prevTc := entities.TreeCluster{ - ID: 1, - Region: &entities.Region{ - ID: 1, - Name: "Sandberg", - }, - Latitude: utils.P(54.776366336440255), - Longitude: utils.P(9.451084144617182), - } - prevTree := entities.Tree{ - ID: 1, - TreeCluster: &prevTc, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } - newTc := entities.TreeCluster{ ID: 2, Region: &entities.Region{ @@ -227,16 +184,11 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { Latitude: utils.P(54.776366336440255), Longitude: utils.P(9.451084144617182), } - updatedTree := entities.Tree{ - ID: 1, - TreeCluster: &newTc, - Number: "T002", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } + updatedTree.TreeCluster = &newTc event := entities.NewEventUpdateTree(&prevTree, &updatedTree) + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID(mock.Anything, int32(2)).Return(nil, storage.ErrSensorNotFound) clusterRepo.EXPECT().Update(mock.Anything, int32(1), mock.Anything).Return(nil) clusterRepo.EXPECT().Update(mock.Anything, int32(2), mock.Anything).Return(nil) clusterRepo.EXPECT().GetByID(mock.Anything, int32(1)).Return(&prevTc, nil) @@ -262,13 +214,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { t.Run("should listen on create new tree event", func(t *testing.T) { // given eventManager := worker.NewEventManager(entities.EventTypeCreateTree) - newTree := entities.Tree{ - ID: 1, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } - event := entities.NewEventCreateTree(&newTree) + event := entities.NewEventCreateTree(&updatedTree) _, ch, _ := eventManager.Subscribe(entities.EventTypeCreateTree) ctx, cancel := context.WithCancel(context.Background()) @@ -291,19 +237,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { t.Run("should listen on update tree event", func(t *testing.T) { // given eventManager := worker.NewEventManager(entities.EventTypeUpdateTree) - prevTree := entities.Tree{ - ID: 1, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.4510841446171324, - } - newTree := entities.Tree{ - ID: 1, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } - event := entities.NewEventUpdateTree(&prevTree, &newTree) + event := entities.NewEventUpdateTree(&prevTree, &updatedTree) _, ch, _ := eventManager.Subscribe(entities.EventTypeUpdateTree) ctx, cancel := context.WithCancel(context.Background()) @@ -326,13 +260,7 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { t.Run("should listen on delete tree event", func(t *testing.T) { // given eventManager := worker.NewEventManager(entities.EventTypeDeleteTree) - newTree := entities.Tree{ - ID: 1, - Number: "T001", - Latitude: 54.776366336440255, - Longitude: 9.451084144617182, - } - event := entities.NewEventDeleteTree(&newTree) + event := entities.NewEventDeleteTree(&updatedTree) _, ch, _ := eventManager.Subscribe(entities.EventTypeDeleteTree) ctx, cancel := context.WithCancel(context.Background()) @@ -352,3 +280,66 @@ func TestTreeClusterService_HandleUpdateTree(t *testing.T) { } }) } + +func setupTest(t *testing.T) (*storageMock.MockTreeClusterRepository, *storageMock.MockTreeRepository, *storageMock.MockRegionRepository, *worker.EventManager, service.TreeClusterService) { + clusterRepo := storageMock.NewMockTreeClusterRepository(t) + treeRepo := storageMock.NewMockTreeRepository(t) + regionRepo := storageMock.NewMockRegionRepository(t) + eventManager := worker.NewEventManager(entities.EventTypeUpdateTreeCluster) + svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, eventManager) + return clusterRepo, treeRepo, regionRepo, eventManager, svc +} + +var prevTc = entities.TreeCluster{ + ID: 1, + Region: &entities.Region{ + ID: 1, + Name: "Sandberg", + }, + Latitude: utils.P(54.776366336440255), + Longitude: utils.P(9.451084144617182), +} + +var prevTree = entities.Tree{ + ID: 1, + TreeCluster: &prevTc, + Number: "T001", + Latitude: 54.776366336440255, + Longitude: 9.451084144617182, + PlantingYear: int32(time.Now().Year() - 2), +} + +var updatedTree = entities.Tree{ + ID: 1, + TreeCluster: &prevTc, + Number: "T001", + Latitude: 54.811733806341856, + Longitude: 9.482958846410169, + PlantingYear: int32(time.Now().Year() - 2), + Sensor: &entities.Sensor{ + ID: "sensor-1", + }, +} + +var updatedTc = entities.TreeCluster{ + ID: 1, + Region: &entities.Region{ + ID: 2, + Name: "Mürwik", + }, + Latitude: utils.P(54.811733806341856), + Longitude: utils.P(9.482958846410169), +} + +var allLatestSensorData = []*entities.SensorData{ + { + SensorID: "sensor-1", + Data: &entities.MqttPayload{ + Watermarks: []entities.Watermark{ + {Centibar: 61, Depth: 30}, + {Centibar: 24, Depth: 60}, + {Centibar: 23, Depth: 90}, + }, + }, + }, +} diff --git a/internal/service/domain/treecluster/treecluster.go b/internal/service/domain/treecluster/treecluster.go index 3994ae95..2b7e54e2 100644 --- a/internal/service/domain/treecluster/treecluster.go +++ b/internal/service/domain/treecluster/treecluster.go @@ -232,7 +232,12 @@ func (s *TreeClusterService) Ready() bool { // otherwise the center point of the tree cluster cannot be set func (s *TreeClusterService) updateTreeClusterPosition(ctx context.Context, id int32) error { log := logger.GetLogger(ctx) - err := s.treeClusterRepo.Update(ctx, id, func(tc *domain.TreeCluster) (bool, error) { + wateringStatus, err := s.getWateringStatusOfTreeCluster(ctx, id) + if err != nil { + log.Error("could not update watering status", "error", err) + } + + err = s.treeClusterRepo.Update(ctx, id, func(tc *domain.TreeCluster) (bool, error) { lat, long, region, err := s.getUpdatedLatLong(ctx, tc) if err != nil { log.Debug("cancel transaction on updateting tree cluster position due to error", "error", err, "cluster_id", id) @@ -243,6 +248,7 @@ func (s *TreeClusterService) updateTreeClusterPosition(ctx context.Context, id i tc.Latitude = lat tc.Longitude = long tc.Region = region + tc.WateringStatus = wateringStatus log.Info("update tree cluster position due to changed trees inside the tree cluster", "cluster_id", id) log.Debug("detailed updated tree cluster position informations", "cluster_id", id, diff --git a/internal/service/domain/treecluster/treecluster_test.go b/internal/service/domain/treecluster/treecluster_test.go index 19651a65..82853787 100644 --- a/internal/service/domain/treecluster/treecluster_test.go +++ b/internal/service/domain/treecluster/treecluster_test.go @@ -26,7 +26,7 @@ func TestTreeClusterService_GetAll(t *testing.T) { regionRepo := storageMock.NewMockRegionRepository(t) svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) - expectedClusters := getTestTreeClusters() + expectedClusters := testClusters clusterRepo.EXPECT().GetAll(ctx).Return(expectedClusters, nil) // when @@ -83,7 +83,7 @@ func TestTreeClusterService_GetByID(t *testing.T) { t.Run("should return tree cluster when found", func(t *testing.T) { id := int32(1) - expectedCluster := getTestTreeClusters()[0] + expectedCluster := testClusters[0] clusterRepo.EXPECT().GetByID(ctx, id).Return(expectedCluster, nil) // when @@ -125,19 +125,28 @@ func TestTreeClusterService_Create(t *testing.T) { regionRepo := storageMock.NewMockRegionRepository(t) svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) - expectedCluster := getTestTreeClusters()[0] - expectedTrees := getTestTrees() + expectedCluster := testClusters[0] treeRepo.EXPECT().GetTreesByIDs( ctx, []int32{1, 2}, - ).Return(expectedTrees, nil) + ).Return(testTrees, nil) clusterRepo.EXPECT().Create( ctx, mock.Anything, ).Return(expectedCluster, nil) + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(1), + ).Return(allLatestSensorData, nil) + + treeRepo.EXPECT().GetBySensorIDs( + ctx, + "sensor-1", + ).Return(testTrees, nil) + clusterRepo.EXPECT().Update( ctx, expectedCluster.ID, @@ -166,7 +175,7 @@ func TestTreeClusterService_Create(t *testing.T) { TreeIDs: []*int32{}, } - expectedCluster := getTestTreeClusters()[1] + expectedCluster := testClusters[1] treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -178,6 +187,11 @@ func TestTreeClusterService_Create(t *testing.T) { mock.Anything, ).Return(expectedCluster, nil) + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(2), + ).Return(nil, storage.ErrSensorNotFound) + clusterRepo.EXPECT().Update( ctx, expectedCluster.ID, @@ -221,7 +235,7 @@ func TestTreeClusterService_Create(t *testing.T) { svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) expectedErr := errors.New("Failed to create cluster") - expectedTrees := getTestTrees() + expectedTrees := testTrees treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -248,9 +262,9 @@ func TestTreeClusterService_Create(t *testing.T) { regionRepo := storageMock.NewMockRegionRepository(t) svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) - expectedCluster := getTestTreeClusters()[0] + expectedCluster := testClusters[0] expectedErr := errors.New("Failed to create cluster") - expectedTrees := getTestTrees() + expectedTrees := testTrees treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -262,6 +276,16 @@ func TestTreeClusterService_Create(t *testing.T) { mock.Anything, ).Return(expectedCluster, nil) + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(1), + ).Return(allLatestSensorData, nil) + + treeRepo.EXPECT().GetBySensorIDs( + ctx, + "sensor-1", + ).Return(testTrees, nil) + clusterRepo.EXPECT().Update( ctx, expectedCluster.ID, @@ -319,8 +343,8 @@ func TestTreeClusterService_Update(t *testing.T) { regionRepo := storageMock.NewMockRegionRepository(t) svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) - expectedCluster := getTestTreeClusters()[0] - expectedTrees := getTestTrees() + expectedCluster := testClusters[0] + expectedTrees := testTrees treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -328,6 +352,17 @@ func TestTreeClusterService_Update(t *testing.T) { ).Return(expectedTrees, nil) clusterRepo.EXPECT().GetByID(ctx, clusterID).Return(expectedCluster, nil) + + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(1), + ).Return(allLatestSensorData, nil) + + treeRepo.EXPECT().GetBySensorIDs( + ctx, + "sensor-1", + ).Return(testTrees, nil) + clusterRepo.EXPECT().Update( ctx, clusterID, @@ -356,7 +391,7 @@ func TestTreeClusterService_Update(t *testing.T) { TreeIDs: []*int32{}, } - expectedCluster := getTestTreeClusters()[1] + expectedCluster := testClusters[1] treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -364,6 +399,12 @@ func TestTreeClusterService_Update(t *testing.T) { ).Return(nil, nil) clusterRepo.EXPECT().GetByID(ctx, expectedCluster.ID).Return(expectedCluster, nil) + + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(2), + ).Return(nil, storage.ErrSensorNotFound) + clusterRepo.EXPECT().Update( ctx, expectedCluster.ID, @@ -405,7 +446,7 @@ func TestTreeClusterService_Update(t *testing.T) { svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) expectedErr := errors.New("failed to update cluster") - expectedTrees := getTestTrees() + expectedTrees := testTrees treeRepo.EXPECT().GetTreesByIDs( ctx, @@ -434,12 +475,10 @@ func TestTreeClusterService_Update(t *testing.T) { regionRepo := storageMock.NewMockRegionRepository(t) svc := NewTreeClusterService(clusterRepo, treeRepo, regionRepo, globalEventManager) - expectedTrees := getTestTrees() - treeRepo.EXPECT().GetTreesByIDs( ctx, []int32{1, 2}, - ).Return(expectedTrees, nil) + ).Return(testTrees, nil) clusterRepo.EXPECT().GetByID(ctx, clusterID).Return(nil, nil) clusterRepo.EXPECT().Update( @@ -488,7 +527,7 @@ func TestTreeClusterService_EventSystem(t *testing.T) { treeRepo := storageMock.NewMockTreeRepository(t) regionRepo := storageMock.NewMockRegionRepository(t) - clusters := getTestTreeClusters() + clusters := testClusters prevCluster := *clusters[1] updatedClusterEmptyTrees := &entities.TreeClusterUpdate{ Name: "Cluster 1", @@ -513,6 +552,17 @@ func TestTreeClusterService_EventSystem(t *testing.T) { ).Return(nil, nil) clusterRepo.EXPECT().GetByID(ctx, expectedCluster.ID).Return(&expectedCluster, nil) + + clusterRepo.EXPECT().GetAllLatestSensorDataByClusterID( + ctx, + int32(2), + ).Return(allLatestSensorData, nil) + + treeRepo.EXPECT().GetBySensorIDs( + ctx, + "sensor-1", + ).Return(testTrees, nil) + clusterRepo.EXPECT().Update( ctx, expectedCluster.ID, @@ -552,7 +602,7 @@ func TestTreeClusterService_Delete(t *testing.T) { t.Run("should successfully delete a tree cluster", func(t *testing.T) { id := int32(1) - clusterRepo.EXPECT().GetByID(ctx, id).Return(getTestTreeClusters()[0], nil) + clusterRepo.EXPECT().GetByID(ctx, id).Return(testClusters[0], nil) treeRepo.EXPECT().UnlinkTreeClusterID(ctx, id).Return(nil) clusterRepo.EXPECT().Delete(ctx, id).Return(nil) @@ -582,7 +632,7 @@ func TestTreeClusterService_Delete(t *testing.T) { id := int32(3) expectedErr := errors.New("failed to unlink treecluster ID") - clusterRepo.EXPECT().GetByID(ctx, id).Return(getTestTreeClusters()[0], nil) + clusterRepo.EXPECT().GetByID(ctx, id).Return(testClusters[0], nil) treeRepo.EXPECT().UnlinkTreeClusterID(ctx, id).Return(expectedErr) // when @@ -597,7 +647,7 @@ func TestTreeClusterService_Delete(t *testing.T) { id := int32(4) expectedErr := errors.New("failed to delete") - clusterRepo.EXPECT().GetByID(ctx, id).Return(getTestTreeClusters()[0], nil) + clusterRepo.EXPECT().GetByID(ctx, id).Return(testClusters[0], nil) treeRepo.EXPECT().UnlinkTreeClusterID(ctx, id).Return(nil) clusterRepo.EXPECT().Delete(ctx, id).Return(expectedErr) @@ -635,66 +685,64 @@ func TestReady(t *testing.T) { }) } -func getTestTreeClusters() []*entities.TreeCluster { - now := time.Now() - - return []*entities.TreeCluster{ - { - ID: 1, - CreatedAt: now, - UpdatedAt: now, - Name: "Cluster 1", - Address: "123 Main St", - Description: "Test description", - SoilCondition: entities.TreeSoilConditionLehmig, - Archived: false, - Latitude: utils.P(9.446741), - Longitude: utils.P(54.801539), - Trees: getTestTrees(), - }, - { - ID: 2, - CreatedAt: now, - UpdatedAt: now, - Name: "Cluster 2", - Address: "456 Second St", - Description: "Test description", - SoilCondition: entities.TreeSoilConditionSandig, - Archived: false, - Latitude: nil, - Longitude: nil, - Trees: []*entities.Tree{}, - }, - } +var testClusters = []*entities.TreeCluster{ + { + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "Cluster 1", + Address: "123 Main St", + Description: "Test description", + SoilCondition: entities.TreeSoilConditionLehmig, + Archived: false, + Latitude: utils.P(9.446741), + Longitude: utils.P(54.801539), + Trees: testTrees, + }, + { + ID: 2, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "Cluster 2", + Address: "456 Second St", + Description: "Test description", + SoilCondition: entities.TreeSoilConditionSandig, + Archived: false, + Latitude: nil, + Longitude: nil, + Trees: testTrees, + }, } -func getTestTrees() []*entities.Tree { - now := time.Now() - - return []*entities.Tree{ - { - ID: 1, - CreatedAt: now, - UpdatedAt: now, - Species: "Oak", - Number: "T001", - Latitude: 9.446741, - Longitude: 54.801539, - Description: "A mature oak tree", - PlantingYear: 2023, - Readonly: true, +var testTrees = []*entities.Tree{ + { + ID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Species: "Oak", + Number: "T001", + Latitude: 9.446741, + Longitude: 54.801539, + Description: "A mature oak tree", + PlantingYear: 2023, + Readonly: true, + Sensor: &entities.Sensor{ + ID: "sensor-1", }, - { - ID: 2, - CreatedAt: now, - UpdatedAt: now, - Species: "Pine", - Number: "T002", - Latitude: 9.446700, - Longitude: 54.801510, - Description: "A young pine tree", - PlantingYear: 2023, - Readonly: true, + }, + { + ID: 2, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Species: "Pine", + Number: "T002", + Latitude: 9.446700, + Longitude: 54.801510, + Description: "A young pine tree", + PlantingYear: 2023, + Readonly: true, + Sensor: &entities.Sensor{ + ID: "sensor-2", }, - } + }, } diff --git a/internal/storage/postgres/queries/trees.sql b/internal/storage/postgres/queries/trees.sql index b028a1cc..f6f7388d 100644 --- a/internal/storage/postgres/queries/trees.sql +++ b/internal/storage/postgres/queries/trees.sql @@ -82,7 +82,9 @@ DELETE FROM trees WHERE id = $1 RETURNING id; UPDATE trees SET tree_cluster_id = NULL WHERE tree_cluster_id = $1 RETURNING id; -- name: UnlinkSensorIDFromTrees :exec -UPDATE trees SET sensor_id = NULL WHERE sensor_id = $1; +UPDATE trees +SET sensor_id = NULL, watering_status = 'unknown' +WHERE sensor_id = $1; -- name: CalculateGroupedCentroids :one SELECT ST_AsText(ST_Centroid(ST_Collect(geometry)))::text AS centroid FROM trees WHERE id = ANY($1::int[]);