Compare commits
3 Commits
f5583b3cd5
...
40ffee4d56
| Author | SHA1 | Date | |
|---|---|---|---|
| 40ffee4d56 | |||
| 8774b55d3d | |||
| 8e33f95cb4 |
3
Makefile
3
Makefile
@ -28,7 +28,7 @@ migrateup:
|
||||
.PHONY: mock
|
||||
# Mock database
|
||||
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
|
||||
# Run tests
|
||||
@ -48,6 +48,7 @@ run-prod:
|
||||
.PHONY: lazy-start
|
||||
lazy-start:
|
||||
# Install dependencies, tools, dockerize containers, run tests and run app.
|
||||
go mod download
|
||||
make dockerize-db
|
||||
make dockerize-nats
|
||||
make run-prod
|
||||
18
README.md
18
README.md
@ -151,13 +151,23 @@ 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.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
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,
|
||||
hay un artículo interesante de Rob Pike [hablando sobre ello](https://go.dev/blog/generate).
|
||||
@ -182,4 +192,8 @@ específico (DSL).
|
||||
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
|
||||
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.
|
||||
@ -106,6 +106,8 @@ func (r *SensorDataRequest) Validate() error {
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRegisteringSensor = errors.New("error registering sensor")
|
||||
ErrUpdatingSensor = errors.New("error updating sensor")
|
||||
ErrInvalidSensorIdentifier = errors.New("sensor identifier is required")
|
||||
ErrInvalidSensorType = errors.New("sensor type is required")
|
||||
ErrSensorNotFound = errors.New("sensor not found")
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"nats-app/internal/iot"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
@ -93,11 +92,6 @@ func (h *Handlers) SetupEndpoints() *Handlers {
|
||||
func (h *Handlers) register() {
|
||||
h.NATS.Subscribe(subjectSensorsRegister, func(msg *nats.Msg) {
|
||||
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 {
|
||||
return Sensor{}, err
|
||||
}
|
||||
@ -112,18 +106,7 @@ func (h *Handlers) register() {
|
||||
func (h *Handlers) registerData() {
|
||||
h.NATS.Subscribe(subjectSensorsData+"*", func(msg *nats.Msg) {
|
||||
handlePublish(msg, func(data SensorData) error {
|
||||
if err := data.Validate(); err != nil {
|
||||
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
|
||||
return h.service.RegisterSensorData(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -131,12 +114,6 @@ func (h *Handlers) registerData() {
|
||||
func (h *Handlers) update() {
|
||||
h.NATS.Subscribe(subjectSensorsUpdate, func(msg *nats.Msg) {
|
||||
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 {
|
||||
return Sensor{}, err
|
||||
}
|
||||
@ -151,13 +128,7 @@ func (h *Handlers) update() {
|
||||
func (h *Handlers) get() {
|
||||
h.NATS.Subscribe(subjectSensorsGet, func(msg *nats.Msg) {
|
||||
handleRequest(msg, func(req SensorRequest) (Sensor, error) {
|
||||
slog.Debug("calling sensor.get", "payload", req)
|
||||
|
||||
if err := req.Validate(); err != nil {
|
||||
return Sensor{}, err
|
||||
}
|
||||
|
||||
return h.service.GetSensor(req.SensorID)
|
||||
return h.service.GetSensor(req)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -165,21 +136,7 @@ func (h *Handlers) get() {
|
||||
func (h *Handlers) getValues() {
|
||||
h.NATS.Subscribe(subjectSensorsValuesGet, func(msg *nats.Msg) {
|
||||
handleRequest(msg, func(req SensorDataRequest) ([]SensorData, error) {
|
||||
if err := req.Validate(); err != nil {
|
||||
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)
|
||||
return h.service.GetValues(req)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -3,14 +3,13 @@
|
||||
//
|
||||
// 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 mock
|
||||
// Package sensors is a generated GoMock package.
|
||||
package sensors
|
||||
|
||||
import (
|
||||
sensors "nats-app/internal/domains/sensors"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
@ -42,7 +41,7 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
|
||||
}
|
||||
|
||||
// CreateSensor mocks base method.
|
||||
func (m *MockRepository) CreateSensor(s sensors.Sensor) error {
|
||||
func (m *MockRepository) CreateSensor(s Sensor) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CreateSensor", s)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (m *MockRepository) ReadAllSensors() ([]sensors.Sensor, error) {
|
||||
func (m *MockRepository) ReadAllSensors() ([]Sensor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadAllSensors")
|
||||
ret0, _ := ret[0].([]sensors.Sensor)
|
||||
ret0, _ := ret[0].([]Sensor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@ -71,10 +84,10 @@ func (mr *MockRepositoryMockRecorder) ReadAllSensors() *gomock.Call {
|
||||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "ReadSensor", sensorID)
|
||||
ret0, _ := ret[0].(sensors.Sensor)
|
||||
ret0, _ := ret[0].(Sensor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@ -86,10 +99,10 @@ func (mr *MockRepositoryMockRecorder) ReadSensor(sensorID any) *gomock.Call {
|
||||
}
|
||||
|
||||
// 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()
|
||||
ret := m.ctrl.Call(m, "ReadSensorValues", sensorID, from, to)
|
||||
ret0, _ := ret[0].([]sensors.SensorData)
|
||||
ret0, _ := ret[0].([]SensorData)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@ -101,7 +114,7 @@ func (mr *MockRepositoryMockRecorder) ReadSensorValues(sensorID, from, to any) *
|
||||
}
|
||||
|
||||
// UpdateSensor mocks base method.
|
||||
func (m *MockRepository) UpdateSensor(s sensors.Sensor) error {
|
||||
func (m *MockRepository) UpdateSensor(s Sensor) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateSensor", s)
|
||||
ret0, _ := ret[0].(error)
|
||||
@ -2,6 +2,7 @@ package sensors
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -16,20 +17,32 @@ func NewService(repo Repository) *Service {
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Error("error registering sensor", "error", err)
|
||||
return err
|
||||
if strings.Contains(err.Error(), "duplicate key value") {
|
||||
return ErrSensorAlreadyExists
|
||||
}
|
||||
return ErrRegisteringSensor
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
slog.Error("error registering sensor data")
|
||||
slog.Error("error registering sensor data", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -37,15 +50,51 @@ func (s *Service) RegisterSensorData(data SensorData) 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) {
|
||||
return s.repo.ReadSensor(sensorID)
|
||||
func (s *Service) GetSensor(sensor SensorRequest) (Sensor, error) {
|
||||
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) {
|
||||
return s.repo.ReadSensorValues(sensorID, from, to)
|
||||
func (s *Service) GetValues(sensor SensorDataRequest) ([]SensorData, error) {
|
||||
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) {
|
||||
|
||||
79
internal/domains/sensors/service_test.go
Normal file
79
internal/domains/sensors/service_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
s.mu.Lock()
|
||||
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) {
|
||||
s.mu.Lock()
|
||||
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)
|
||||
}
|
||||
|
||||
// generateData genera datos aleatorios por cada tipo de sensor.
|
||||
func (s *Simulator) generateData(sensor Sensor) SensorData {
|
||||
now := time.Now()
|
||||
data := SensorData{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user