diff --git a/Makefile b/Makefile index 2cccf86..5dacde4 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ mock: .PHONY: test # Run tests tests: - go test ./... + go test ./... -cover .PHONY: run # Start app in development environment diff --git a/README.md b/README.md index 30de5ea..9c2a9b0 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ Por otro lado también hay un sistema de caché muy rudimentario, en memoria que es un mapa de valores. Para el registro de valores y mantener ambos se ha usado el patrón decorador que -bajo un mismo _struct_ se incluye las dos implementaciones y se llama a ambas -funciones. Desde la capa servicios sólo tiene que llamar al decorador sin saber -los detalles de la implementación. +bajo un mismo _struct_ se incluye las dos implementaciones y registra cambios en +ambas partes. Desde la capa servicios sólo tiene que llamar al decorador sin +saber los detalles de la implementación. ### Continuamos con los servicios @@ -151,6 +151,20 @@ documentación me quedé con los conceptos clave: Esto es todo, entonces los controladores de la entidad _sensors_ están constituidos por una serie de _endpoints_ haciendo las acciones que se solicita. +### El simulador + +Basada en _gorutinas_ y canales, cuando se inicia el simulador, se crea un canal +para detener simuladores que están en ejecución para su actualización o +detención. + +Cuando se registra un nuevo sensor, está la función SimulateSensor, que se +inicia como una _gorutina_ y usa el `SamplingInterval` para el canal `ticker`, +así llamar a `generateData` cada vez que toque. + +Una vez que el dato está generado se hace una publicación al asunto _sensor.data_, +que al mismo tiempo, el _handler_ registerData lo captura al estar registrado +al mismo asunto _sensor.data_. + ## Pruebas La realización de pruebas unitarias de lo que son los controladores de NATS me diff --git a/internal/domains/sensors/service_test.go b/internal/domains/sensors/service_test.go index 244f211..cefdec7 100644 --- a/internal/domains/sensors/service_test.go +++ b/internal/domains/sensors/service_test.go @@ -1,6 +1,7 @@ package sensors import ( + "errors" "testing" "time" @@ -20,6 +21,7 @@ func Test_RegisterSensor(t *testing.T) { given Sensor setupMock func(q *MockRepository, params Sensor) expecErr bool + expectErr error } tests := []testCase{ @@ -47,9 +49,25 @@ func Test_RegisterSensor(t *testing.T) { ThresholdBelow: ptr(0.0), }, setupMock: func(q *MockRepository, params Sensor) { - q.EXPECT().CreateSensor(params).Return(ErrSensorAlreadyExists) + q.EXPECT().CreateSensor(params).Return(errors.New("duplicate key value")) }, - expecErr: true, + expecErr: true, + expectErr: ErrSensorAlreadyExists, + }, + { + name: "error - some db error", + given: Sensor{ + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + setupMock: func(q *MockRepository, params Sensor) { + q.EXPECT().CreateSensor(params).Return(errors.New("some db error")) + }, + expecErr: true, + expectErr: ErrRegisteringSensor, }, } @@ -71,8 +89,413 @@ func Test_RegisterSensor(t *testing.T) { return } - if tt.expecErr && err != ErrSensorAlreadyExists { - t.Errorf("expected ErrSensorAlreadyExists, got %v", err) + if tt.expecErr && tt.expectErr != nil && err != tt.expectErr { + t.Errorf("expected error %v, got %v", tt.expectErr, err) + } + }) + } +} + +func Test_RegisterSensorData(t *testing.T) { + type testCase struct { + name string + given SensorData + setupMock func(q *MockRepository, params SensorData) + expecErr bool + } + + timestamp := time.Now() + value := 25.5 + + tests := []testCase{ + { + name: "success - registers sensor data", + given: SensorData{ + SensorID: "temp-001", + Value: &value, + Timestamp: ×tamp, + }, + setupMock: func(q *MockRepository, params SensorData) { + q.EXPECT().CreateSensorData(params).Return(nil) + }, + expecErr: false, + }, + { + name: "error - database error", + given: SensorData{ + SensorID: "temp-001", + Value: &value, + Timestamp: ×tamp, + }, + setupMock: func(q *MockRepository, params SensorData) { + q.EXPECT().CreateSensorData(params).Return(errors.New("database error")) + }, + expecErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, q := setup(t) + + tt.setupMock(q, tt.given) + + err := s.RegisterSensorData(tt.given) + + if tt.expecErr && err == nil { + t.Error("expected error, got nil") + return + } + + if !tt.expecErr && err != nil { + t.Errorf("expected no error, got %v", err) + } + }) + } +} + +func Test_UpdateSensor(t *testing.T) { + type testCase struct { + name string + given Sensor + setupMock func(q *MockRepository, params Sensor) + expecErr bool + expectErr error + } + + tests := []testCase{ + { + name: "success - updates sensor", + given: Sensor{ + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute * 2), + ThresholdAbove: ptr(120.0), + ThresholdBelow: ptr(10.0), + }, + setupMock: func(q *MockRepository, params Sensor) { + q.EXPECT().UpdateSensor(params).Return(nil) + }, + expecErr: false, + }, + { + name: "error - sensor already exists (duplicate)", + given: Sensor{ + SensorID: "temp-002", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + setupMock: func(q *MockRepository, params Sensor) { + q.EXPECT().UpdateSensor(params).Return(errors.New("duplicate key value")) + }, + expecErr: true, + expectErr: ErrSensorAlreadyExists, + }, + { + name: "error - general database error", + given: Sensor{ + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + setupMock: func(q *MockRepository, params Sensor) { + q.EXPECT().UpdateSensor(params).Return(errors.New("connection failed")) + }, + expecErr: true, + expectErr: ErrUpdatingSensor, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, q := setup(t) + + tt.setupMock(q, tt.given) + + err := s.UpdateSensor(tt.given) + + if tt.expecErr && err == nil { + t.Error("expected error, got nil") + return + } + + if !tt.expecErr && err != nil { + t.Errorf("expected no error, got %v", err) + return + } + + if tt.expecErr && tt.expectErr != nil && err != tt.expectErr { + t.Errorf("expected error %v, got %v", tt.expectErr, err) + } + }) + } +} + +func Test_GetSensor(t *testing.T) { + type testCase struct { + name string + given SensorRequest + setupMock func(q *MockRepository, sensorID string) + expected Sensor + expecErr bool + } + + tests := []testCase{ + { + name: "success - retrieves sensor", + given: SensorRequest{ + SensorID: "temp-001", + }, + setupMock: func(q *MockRepository, sensorID string) { + q.EXPECT().ReadSensor(sensorID).Return(Sensor{ + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, nil) + }, + expected: Sensor{ + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + expecErr: false, + }, + { + name: "error - sensor not found", + given: SensorRequest{ + SensorID: "temp-999", + }, + setupMock: func(q *MockRepository, sensorID string) { + q.EXPECT().ReadSensor(sensorID).Return(Sensor{}, ErrSensorNotFound) + }, + expected: Sensor{}, + expecErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, q := setup(t) + + tt.setupMock(q, tt.given.SensorID) + + result, err := s.GetSensor(tt.given) + + if tt.expecErr && err == nil { + t.Error("expected error, got nil") + return + } + + if !tt.expecErr && err != nil { + t.Errorf("expected no error, got %v", err) + return + } + + if !tt.expecErr { + if result.SensorID != tt.expected.SensorID { + t.Errorf("expected sensor_id %q, got %q", tt.expected.SensorID, result.SensorID) + } + if result.SensorType != tt.expected.SensorType { + t.Errorf("expected sensor_type %q, got %q", tt.expected.SensorType, result.SensorType) + } + } + }) + } +} + +func Test_GetValues(t *testing.T) { + type testCase struct { + name string + given SensorDataRequest + setupMock func(q *MockRepository, sensorID string, from, to time.Time) + expected []SensorData + expecErr bool + } + + now := time.Now() + weekAgo := now.AddDate(0, 0, -7) + fromStr := weekAgo.Format(time.RFC3339) + toStr := now.Format(time.RFC3339) + + value1 := 25.5 + value2 := 26.0 + + tests := []testCase{ + { + name: "success - retrieves sensor data", + given: SensorDataRequest{ + SensorID: "temp-001", + From: &fromStr, + To: &toStr, + }, + setupMock: func(q *MockRepository, sensorID string, from, to time.Time) { + q.EXPECT().ReadSensorValues(sensorID, from, to).Return([]SensorData{ + { + SensorID: "temp-001", + Value: &value1, + Timestamp: &weekAgo, + }, + { + SensorID: "temp-001", + Value: &value2, + Timestamp: &now, + }, + }, nil) + }, + expected: []SensorData{ + { + SensorID: "temp-001", + Value: &value1, + Timestamp: &weekAgo, + }, + { + SensorID: "temp-001", + Value: &value2, + Timestamp: &now, + }, + }, + expecErr: false, + }, + { + name: "error - database error", + given: SensorDataRequest{ + SensorID: "temp-001", + From: &fromStr, + To: &toStr, + }, + setupMock: func(q *MockRepository, sensorID string, from, to time.Time) { + q.EXPECT().ReadSensorValues(sensorID, from, to).Return([]SensorData{}, errors.New("database error")) + }, + expected: []SensorData{}, + expecErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, q := setup(t) + + from, _ := time.Parse(time.RFC3339, *tt.given.From) + to, _ := time.Parse(time.RFC3339, *tt.given.To) + + tt.setupMock(q, tt.given.SensorID, from, to) + + result, err := s.GetValues(tt.given) + + if tt.expecErr && err == nil { + t.Error("expected error, got nil") + return + } + + if !tt.expecErr && err != nil { + t.Errorf("expected no error, got %v", err) + return + } + + if !tt.expecErr { + if len(result) != len(tt.expected) { + t.Errorf("expected %d values, got %d", len(tt.expected), len(result)) + } + } + }) + } +} + +func Test_ListSensors(t *testing.T) { + type testCase struct { + name string + setupMock func(q *MockRepository) + expected []Sensor + expecErr bool + } + + tests := []testCase{ + { + name: "success - retrieves all sensors", + setupMock: func(q *MockRepository) { + q.EXPECT().ReadAllSensors().Return([]Sensor{ + { + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + { + SensorID: "hum-001", + SensorType: Humidity, + SamplingInterval: ptr(time.Minute * 2), + ThresholdAbove: ptr(80.0), + ThresholdBelow: ptr(20.0), + }, + }, nil) + }, + expected: []Sensor{ + { + SensorID: "temp-001", + SensorType: Temperature, + SamplingInterval: ptr(time.Minute), + ThresholdAbove: ptr(100.0), + ThresholdBelow: ptr(0.0), + }, + { + SensorID: "hum-001", + SensorType: Humidity, + SamplingInterval: ptr(time.Minute * 2), + ThresholdAbove: ptr(80.0), + ThresholdBelow: ptr(20.0), + }, + }, + expecErr: false, + }, + { + name: "success - empty list when no sensors", + setupMock: func(q *MockRepository) { + q.EXPECT().ReadAllSensors().Return([]Sensor{}, nil) + }, + expected: []Sensor{}, + expecErr: false, + }, + { + name: "error - database error", + setupMock: func(q *MockRepository) { + q.EXPECT().ReadAllSensors().Return([]Sensor{}, errors.New("database error")) + }, + expected: []Sensor{}, + expecErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, q := setup(t) + + tt.setupMock(q) + + result, err := s.ListSensors() + + if tt.expecErr && err == nil { + t.Error("expected error, got nil") + return + } + + if !tt.expecErr && err != nil { + t.Errorf("expected no error, got %v", err) + return + } + + if !tt.expecErr { + if len(result) != len(tt.expected) { + t.Errorf("expected %d sensors, got %d", len(tt.expected), len(result)) + } } }) }