Compare commits

..

3 Commits

8 changed files with 189 additions and 68 deletions

View File

@ -28,7 +28,7 @@ migrateup:
.PHONY: mock .PHONY: mock
# Mock database # Mock database
mock: mock:
go run go.uber.org/mock/mockgen@latest -package mock -destination internal/domains/sensors/mock/querier.go $(MOD_NAME)/internal/domains/sensors Repository go run go.uber.org/mock/mockgen@latest -package sensors -destination internal/domains/sensors/repository_mock.go $(MOD_NAME)/internal/domains/sensors Repository
.PHONY: test .PHONY: test
# Run tests # Run tests
@ -48,6 +48,7 @@ run-prod:
.PHONY: lazy-start .PHONY: lazy-start
lazy-start: lazy-start:
# Install dependencies, tools, dockerize containers, run tests and run app. # Install dependencies, tools, dockerize containers, run tests and run app.
go mod download
make dockerize-db make dockerize-db
make dockerize-nats make dockerize-nats
make run-prod make run-prod

View File

@ -151,13 +151,23 @@ documentación me quedé con los conceptos clave:
Esto es todo, entonces los controladores de la entidad _sensors_ están Esto es todo, entonces los controladores de la entidad _sensors_ están
constituidos por una serie de _endpoints_ haciendo las acciones que se solicita. constituidos por una serie de _endpoints_ haciendo las acciones que se solicita.
## Pruebas
La realización de pruebas unitarias de lo que son los controladores de NATS me
han sido imposible hacerlas en condiciones, podría haber usado Claude pero es
que no daba pie con bola y no entendía nada, así que por la máxima transparencia
he optado por no incorporarlas.
Las pruebas más interesantes son las de reglas de negocio y validación, lo que
viene a ser los servicios y dominio.
## LLMS ## LLMS
He usado Claude para la toma de decisiones y ayuda con el _boilerplate_, que no He usado Claude para la toma de decisiones y ayuda con el _boilerplate_, que no
es poca cosa, además también se ha usado para la generación de las pruebas es poca cosa, además también se ha usado para la generación de las pruebas
unitarias, además de resolución de algunos problemas complejos. unitarias, además de resolución de algunos problemas complejos.
## Generadores de código ## Generadores y otras librerías
Existen generadores de código para Golang, de hecho, se fomenta su desarrollo, Existen generadores de código para Golang, de hecho, se fomenta su desarrollo,
hay un artículo interesante de Rob Pike [hablando sobre ello](https://go.dev/blog/generate). hay un artículo interesante de Rob Pike [hablando sobre ello](https://go.dev/blog/generate).
@ -183,3 +193,7 @@ No se ha incorporado porque hay que instalar la herramienta que ejecutan las
pruebas, y no quería correr el riesgo de que no funcionase en otro equipo o no pruebas, y no quería correr el riesgo de que no funcionase en otro equipo o no
diesen los resultados esperados. Que se podría haber usado un contenedor Docker, diesen los resultados esperados. Que se podría haber usado un contenedor Docker,
sí, pero la prueba no consiste en eso. sí, pero la prueba no consiste en eso.
También se ha planteado incorporar la librería _testify_, descartado porque para
comprobar si existe el error y algunas comparaciones no era necesario meter una
dependencia más.

View File

@ -106,6 +106,8 @@ func (r *SensorDataRequest) Validate() error {
} }
var ( var (
ErrRegisteringSensor = errors.New("error registering sensor")
ErrUpdatingSensor = errors.New("error updating sensor")
ErrInvalidSensorIdentifier = errors.New("sensor identifier is required") ErrInvalidSensorIdentifier = errors.New("sensor identifier is required")
ErrInvalidSensorType = errors.New("sensor type is required") ErrInvalidSensorType = errors.New("sensor type is required")
ErrSensorNotFound = errors.New("sensor not found") ErrSensorNotFound = errors.New("sensor not found")

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"log/slog" "log/slog"
"nats-app/internal/iot" "nats-app/internal/iot"
"time"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
@ -93,11 +92,6 @@ func (h *Handlers) SetupEndpoints() *Handlers {
func (h *Handlers) register() { func (h *Handlers) register() {
h.NATS.Subscribe(subjectSensorsRegister, func(msg *nats.Msg) { h.NATS.Subscribe(subjectSensorsRegister, func(msg *nats.Msg) {
handleRequest(msg, func(req Sensor) (Sensor, error) { handleRequest(msg, func(req Sensor) (Sensor, error) {
if err := req.Validate(); err != nil {
slog.Error("error validating sensor", "error", err)
return Sensor{}, err
}
if err := h.service.RegisterSensor(req); err != nil { if err := h.service.RegisterSensor(req); err != nil {
return Sensor{}, err return Sensor{}, err
} }
@ -112,18 +106,7 @@ func (h *Handlers) register() {
func (h *Handlers) registerData() { func (h *Handlers) registerData() {
h.NATS.Subscribe(subjectSensorsData+"*", func(msg *nats.Msg) { h.NATS.Subscribe(subjectSensorsData+"*", func(msg *nats.Msg) {
handlePublish(msg, func(data SensorData) error { handlePublish(msg, func(data SensorData) error {
if err := data.Validate(); err != nil { return h.service.RegisterSensorData(data)
slog.Error("error validating sensor data", "error", err)
return err
}
if err := h.service.RegisterSensorData(data); err != nil {
slog.Error("failed to save sensor data", "error", err, "sensor_id", data.SensorID)
return err
}
slog.Debug("sensor data saved", "sensor_id", data.SensorID, "value", data.Value)
return nil
}) })
}) })
} }
@ -131,12 +114,6 @@ func (h *Handlers) registerData() {
func (h *Handlers) update() { func (h *Handlers) update() {
h.NATS.Subscribe(subjectSensorsUpdate, func(msg *nats.Msg) { h.NATS.Subscribe(subjectSensorsUpdate, func(msg *nats.Msg) {
handleRequest(msg, func(req Sensor) (Sensor, error) { handleRequest(msg, func(req Sensor) (Sensor, error) {
slog.Debug("calling sensor.update", "payload", req)
if err := req.Validate(); err != nil {
return Sensor{}, err
}
if err := h.service.UpdateSensor(req); err != nil { if err := h.service.UpdateSensor(req); err != nil {
return Sensor{}, err return Sensor{}, err
} }
@ -151,13 +128,7 @@ func (h *Handlers) update() {
func (h *Handlers) get() { func (h *Handlers) get() {
h.NATS.Subscribe(subjectSensorsGet, func(msg *nats.Msg) { h.NATS.Subscribe(subjectSensorsGet, func(msg *nats.Msg) {
handleRequest(msg, func(req SensorRequest) (Sensor, error) { handleRequest(msg, func(req SensorRequest) (Sensor, error) {
slog.Debug("calling sensor.get", "payload", req) return h.service.GetSensor(req)
if err := req.Validate(); err != nil {
return Sensor{}, err
}
return h.service.GetSensor(req.SensorID)
}) })
}) })
} }
@ -165,21 +136,7 @@ func (h *Handlers) get() {
func (h *Handlers) getValues() { func (h *Handlers) getValues() {
h.NATS.Subscribe(subjectSensorsValuesGet, func(msg *nats.Msg) { h.NATS.Subscribe(subjectSensorsValuesGet, func(msg *nats.Msg) {
handleRequest(msg, func(req SensorDataRequest) ([]SensorData, error) { handleRequest(msg, func(req SensorDataRequest) ([]SensorData, error) {
if err := req.Validate(); err != nil { return h.service.GetValues(req)
return []SensorData{}, err
}
from, err := time.Parse(time.RFC3339, *req.From)
if err != nil {
return []SensorData{}, err
}
to, err := time.Parse(time.RFC3339, *req.To)
if err != nil {
return []SensorData{}, err
}
return h.service.GetValues(req.SensorID, from, to)
}) })
}) })
} }

View File

@ -3,14 +3,13 @@
// //
// Generated by this command: // Generated by this command:
// //
// mockgen -package mock -destination internal/domains/sensors/mock/querier.go nats-app/internal/domains/sensors Repository // mockgen -package sensors -destination internal/domains/sensors/repository_mock.go nats-app/internal/domains/sensors Repository
// //
// Package mock is a generated GoMock package. // Package sensors is a generated GoMock package.
package mock package sensors
import ( import (
sensors "nats-app/internal/domains/sensors"
reflect "reflect" reflect "reflect"
time "time" time "time"
@ -42,7 +41,7 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
} }
// CreateSensor mocks base method. // CreateSensor mocks base method.
func (m *MockRepository) CreateSensor(s sensors.Sensor) error { func (m *MockRepository) CreateSensor(s Sensor) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSensor", s) ret := m.ctrl.Call(m, "CreateSensor", s)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -55,11 +54,25 @@ func (mr *MockRepositoryMockRecorder) CreateSensor(s any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSensor", reflect.TypeOf((*MockRepository)(nil).CreateSensor), s) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSensor", reflect.TypeOf((*MockRepository)(nil).CreateSensor), s)
} }
// CreateSensorData mocks base method.
func (m *MockRepository) CreateSensorData(data SensorData) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSensorData", data)
ret0, _ := ret[0].(error)
return ret0
}
// CreateSensorData indicates an expected call of CreateSensorData.
func (mr *MockRepositoryMockRecorder) CreateSensorData(data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSensorData", reflect.TypeOf((*MockRepository)(nil).CreateSensorData), data)
}
// ReadAllSensors mocks base method. // ReadAllSensors mocks base method.
func (m *MockRepository) ReadAllSensors() ([]sensors.Sensor, error) { func (m *MockRepository) ReadAllSensors() ([]Sensor, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadAllSensors") ret := m.ctrl.Call(m, "ReadAllSensors")
ret0, _ := ret[0].([]sensors.Sensor) ret0, _ := ret[0].([]Sensor)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@ -71,10 +84,10 @@ func (mr *MockRepositoryMockRecorder) ReadAllSensors() *gomock.Call {
} }
// ReadSensor mocks base method. // ReadSensor mocks base method.
func (m *MockRepository) ReadSensor(sensorID string) (sensors.Sensor, error) { func (m *MockRepository) ReadSensor(sensorID string) (Sensor, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadSensor", sensorID) ret := m.ctrl.Call(m, "ReadSensor", sensorID)
ret0, _ := ret[0].(sensors.Sensor) ret0, _ := ret[0].(Sensor)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@ -86,10 +99,10 @@ func (mr *MockRepositoryMockRecorder) ReadSensor(sensorID any) *gomock.Call {
} }
// ReadSensorValues mocks base method. // ReadSensorValues mocks base method.
func (m *MockRepository) ReadSensorValues(sensorID string, from, to time.Time) ([]sensors.SensorData, error) { func (m *MockRepository) ReadSensorValues(sensorID string, from, to time.Time) ([]SensorData, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadSensorValues", sensorID, from, to) ret := m.ctrl.Call(m, "ReadSensorValues", sensorID, from, to)
ret0, _ := ret[0].([]sensors.SensorData) ret0, _ := ret[0].([]SensorData)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@ -101,7 +114,7 @@ func (mr *MockRepositoryMockRecorder) ReadSensorValues(sensorID, from, to any) *
} }
// UpdateSensor mocks base method. // UpdateSensor mocks base method.
func (m *MockRepository) UpdateSensor(s sensors.Sensor) error { func (m *MockRepository) UpdateSensor(s Sensor) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateSensor", s) ret := m.ctrl.Call(m, "UpdateSensor", s)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)

View File

@ -2,6 +2,7 @@ package sensors
import ( import (
"log/slog" "log/slog"
"strings"
"time" "time"
) )
@ -16,20 +17,32 @@ func NewService(repo Repository) *Service {
} }
func (s *Service) RegisterSensor(sensor Sensor) error { func (s *Service) RegisterSensor(sensor Sensor) error {
if err := sensor.Validate(); err != nil {
slog.Error("error validating sensor", "error", err)
return err
}
err := s.repo.CreateSensor(sensor) err := s.repo.CreateSensor(sensor)
if err != nil { if err != nil {
slog.Error("error registering sensor", "error", err) slog.Error("error registering sensor", "error", err)
return err if strings.Contains(err.Error(), "duplicate key value") {
return ErrSensorAlreadyExists
}
return ErrRegisteringSensor
} }
return nil return nil
} }
func (s *Service) RegisterSensorData(data SensorData) error { func (s *Service) RegisterSensorData(data SensorData) error {
if err := data.Validate(); err != nil {
slog.Error("error validating sensor data", "error", err)
return err
}
err := s.repo.CreateSensorData(data) err := s.repo.CreateSensorData(data)
if err != nil { if err != nil {
slog.Error("error registering sensor data") slog.Error("error registering sensor data", "error", err)
return err return err
} }
@ -37,15 +50,51 @@ func (s *Service) RegisterSensorData(data SensorData) error {
} }
func (s *Service) UpdateSensor(sensor Sensor) error { func (s *Service) UpdateSensor(sensor Sensor) error {
return s.repo.UpdateSensor(sensor) if err := sensor.Validate(); err != nil {
slog.Error("error validating sensor data", "error", err)
return err
}
err := s.repo.UpdateSensor(sensor)
if err != nil {
slog.Error("error updating sensor", "error", err)
if strings.Contains(err.Error(), "duplicate key value") {
return ErrSensorAlreadyExists
}
return ErrUpdatingSensor
}
return nil
} }
func (s *Service) GetSensor(sensorID string) (Sensor, error) { func (s *Service) GetSensor(sensor SensorRequest) (Sensor, error) {
return s.repo.ReadSensor(sensorID) if err := sensor.Validate(); err != nil {
slog.Error("error getting sensor", "error", err)
return Sensor{}, err
}
return s.repo.ReadSensor(sensor.SensorID)
} }
func (s *Service) GetValues(sensorID string, from, to time.Time) ([]SensorData, error) { func (s *Service) GetValues(sensor SensorDataRequest) ([]SensorData, error) {
return s.repo.ReadSensorValues(sensorID, from, to) if err := sensor.Validate(); err != nil {
slog.Error("error validating sensor data request", "error", err)
return []SensorData{}, err
}
from, err := time.Parse(time.RFC3339, *sensor.From)
if err != nil {
slog.Error("error parsing from date", "error", err)
return []SensorData{}, err
}
to, err := time.Parse(time.RFC3339, *sensor.To)
if err != nil {
slog.Error("error parsing to date", "error", err)
return []SensorData{}, err
}
return s.repo.ReadSensorValues(sensor.SensorID, from, to)
} }
func (s *Service) ListSensors() ([]Sensor, error) { func (s *Service) ListSensors() ([]Sensor, error) {

View File

@ -0,0 +1,79 @@
package sensors
import (
"testing"
"time"
"go.uber.org/mock/gomock"
)
func setup(t *testing.T) (*Service, *MockRepository) {
ctrl := gomock.NewController(t)
q := NewMockRepository(ctrl)
s := NewService(q)
return s, q
}
func Test_RegisterSensor(t *testing.T) {
type testCase struct {
name string
given Sensor
setupMock func(q *MockRepository, params Sensor)
expecErr bool
}
tests := []testCase{
{
name: "success - registers new sensor",
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(nil)
},
expecErr: false,
},
{
name: "error - sensor already exists",
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(ErrSensorAlreadyExists)
},
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.RegisterSensor(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 && err != ErrSensorAlreadyExists {
t.Errorf("expected ErrSensorAlreadyExists, got %v", err)
}
})
}
}

View File

@ -22,6 +22,9 @@ func Start(nats *broker.NATS) *Simulator {
} }
} }
// SimulateSensor simula lo que es un sensor, se llama a ese método como una
// go-rutina separada. Hace uso del SamplingInterval como temporizador para
// el canal ticker.
func (s *Simulator) SimulateSensor(sensor Sensor) { func (s *Simulator) SimulateSensor(sensor Sensor) {
s.mu.Lock() s.mu.Lock()
stopChan := make(chan bool) stopChan := make(chan bool)
@ -60,6 +63,8 @@ func (s *Simulator) SimulateSensor(sensor Sensor) {
} }
} }
// UpdateSensor para la gorutina que haya activa de dicho sensor, y comienza una
// nueva con el intervalo actualizado.
func (s *Simulator) UpdateSensor(sensor Sensor) { func (s *Simulator) UpdateSensor(sensor Sensor) {
s.mu.Lock() s.mu.Lock()
stopChan, exists := s.stopChannels[sensor.SensorID] stopChan, exists := s.stopChannels[sensor.SensorID]
@ -77,6 +82,7 @@ func (s *Simulator) UpdateSensor(sensor Sensor) {
slog.Info("simulator updated for sensor", "sensor_id", sensor.SensorID, "new_interval", sensor.SamplingInterval) slog.Info("simulator updated for sensor", "sensor_id", sensor.SensorID, "new_interval", sensor.SamplingInterval)
} }
// generateData genera datos aleatorios por cada tipo de sensor.
func (s *Simulator) generateData(sensor Sensor) SensorData { func (s *Simulator) generateData(sensor Sensor) SensorData {
now := time.Now() now := time.Now()
data := SensorData{ data := SensorData{