Compare commits

...

9 Commits

Author SHA1 Message Date
pedro 6e89d5a8f5 update readme 2025-10-10 04:53:27 +02:00
pedro 7bc9f1c987 fix test error 2025-10-10 03:09:34 +02:00
pedro 40ffee4d56 moved all validation and parsing from handler to service layer 2025-10-10 02:42:29 +02:00
pedro 8774b55d3d update readme 2025-10-10 02:23:53 +02:00
pedro 8e33f95cb4 update mock and add register sensor test 2025-10-10 02:23:49 +02:00
pedro f5583b3cd5 add test for sensor data validate 2025-10-10 02:03:16 +02:00
pedro d2f0e8ccf5 add tests for nil values 2025-10-10 02:00:48 +02:00
pedro 772ce9a7b7 add tdlr 2025-10-10 01:49:39 +02:00
pedro 57254fcbc9 add sensors to migration 2025-10-10 01:46:08 +02:00
11 changed files with 856 additions and 78 deletions
+6 -1
View File
@@ -28,8 +28,12 @@ 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
tests:
go test ./... -cover
.PHONY: run
# Start app in development environment
@@ -44,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
+94 -9
View File
@@ -3,6 +3,11 @@
Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar
por el puesto de programador Go.
## TL:DR
- make lazy-start
- abre terminal y escribe: `nats sub sensors.data.*`
## Requisitos previos
- Docker
@@ -47,7 +52,8 @@ por el puesto de programador Go.
### Obtener valores de un sensor
- Campo obligatorio: `sensor_id`.
- Campos opcionales: `from` y `to` en formato RFC3339. Si no se especifican, se toman los últimos 7 días.
- Campos opcionales: `from` y `to` en formato RFC3339. Si no se especifican,
se toman los últimos 7 días.
`nats req sensors.values.get '{
"sensor_id": "sensor-001",
@@ -117,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
@@ -145,13 +151,31 @@ 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.
## LLMS
### El simulador
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.
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.
## Generadores de código
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
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.
## 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).
@@ -177,3 +201,64 @@ 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.
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.
## Diagrama
![diagrama app](./assets/app-architecture.jpg)
El diagrama explica básicamente la estrucutra del proyecto en términos generales,
se demuestra que el dominio sensors solo se comunica al exterior mediante el
NATS y el _logger_.
Amarillo: exterior.
Morado: infraestructura.
Verde: dominio
Blanco: simulador, los _handlers_ está conectado sólo para llamar a la
gorutina, pero en la realidad debería ir independiente.
## Conclusión y cierre
Interesante reto donde realmente lo que más me ha costado son la realización
de pruebas unitarias, hay veces que me cuesta coger el concepto. Tengo la teoría
muy clara, pero a la hora de la verdad se me complica un poco las cosas. Además
que _mockear_ el sistema de mensajería debe tener su especial complejidad.
También ha sido algo complejo entender la concurrencia y los canales, no es algo
que haya trabajado en profundidad pero sí que se le ha puesto bastante empeño,
cariño y nivel de detalle.
He puesto mucho en valor la arquitectura limpia con un toque personal, no es DDD
puro ya que hay elementos que no deberían estar en dominio, pero en el proyecto
que estoy trabajando ahora mismo se está diseñando de la misma manera y está
funcionando muy bien.
También se ha evitado todo lo posible el uso de LLMs para la generación de
código, y su uso ha sido para la toma de decisiones arquitectónicas, discusión y
lectura rápida sobre los distintos funcionamientos de algunas librerías. En más
de una ocasión he cuestionado las respuestas que da, teniendo que verificar con
la documentación oficial. Pongo en valor mi capacidad para aprovechar la IA de
la mejor forma posible, verificando la información, además recalco que justo el
proyecto actual es una migración de un código en PHP completamente hecho con IA,
y se puede ver patrones y errores comunes que comete.
Soy consciente de que hay margen de mejora, por ejemplo con los tests o con la
documentación, se ha puesto especial esfuerzo y atención a que los nombres de
las funciones, variables, métodos, estructuras y paquetes sean lo más
autodescriptivos posibles. Se han puesto algunos comentarios. También hay
esfuerzo por permitir ejecutar el proyecto por primera vez con la mínima
intervención.
Un problema interesante que tuve que resolver, que como el sensor puede mandar
un valor ausente, el tipo `float64` al hacer el `unmarshal` se establece a 0.0,
con lo que se puede considerar válido, con lo cual su solución fue el uso de
puntero, si se descubre que es `nil` se considera no válido.
Digamos que este proyecto resuelve el problema que se propone, un sistema que
permite registrar y actualziar un sensor. Se puede ver su estado y los datos que
se recogen (simulados) se guardan en una base de datos.
Espero que el proyecto sea de vuestro agrado y podamos tener una siguiente
reunión.
+9
View File
@@ -35,3 +35,12 @@ create table registry
timescaledb.partition_column = 'created_at',
timescaledb.segmentby = 'sensor_id'
);
insert into sensors (sensor_id, sensor_type, sampling_interval, threshold_above, threshold_below)
values
('temp-001', 'temperature', 10, 100, 0),
('hum-001', 'humidity', 15, 80, 20),
('co2-001', 'carbon_dioxide', 20, 1000, 400),
('pres-001', 'pressure', 30, 1050, 950),
('prox-001', 'proximity', 5, 200, 0),
('light-001', 'light', 10, 10000, 0);
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

+2
View File
@@ -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")
+150
View File
@@ -195,6 +195,136 @@ func Test_SensorValidate(t *testing.T) {
}
}
func Test_SensorData_Validate(t *testing.T) {
type testCase struct {
name string
given SensorData
expected SensorData
expecErr bool
}
timestamp := time.Now()
tests := []testCase{
{
name: "success with all fields",
given: SensorData{
SensorID: "temp-001",
Value: ptr(25.5),
Timestamp: ptr(timestamp),
},
expected: SensorData{
SensorID: "temp-001",
Value: ptr(25.5),
Timestamp: ptr(timestamp),
},
expecErr: false,
},
{
name: "error when sensor_id is empty",
given: SensorData{
SensorID: "",
Value: ptr(25.5),
},
expecErr: true,
},
{
name: "error when value is nil",
given: SensorData{
SensorID: "temp-001",
Value: nil,
},
expecErr: true,
},
{
name: "default timestamp when nil",
given: SensorData{
SensorID: "temp-001",
Value: ptr(30.0),
Timestamp: nil,
},
expected: SensorData{
SensorID: "temp-001",
Value: ptr(30.0),
Timestamp: nil, // Will be set by Validate
},
expecErr: false,
},
{
name: "zero value is preserved",
given: SensorData{
SensorID: "temp-001",
Value: ptr(0.0),
Timestamp: ptr(timestamp),
},
expected: SensorData{
SensorID: "temp-001",
Value: ptr(0.0),
Timestamp: ptr(timestamp),
},
expecErr: false,
},
{
name: "negative value is valid",
given: SensorData{
SensorID: "temp-001",
Value: ptr(-15.5),
Timestamp: ptr(timestamp),
},
expected: SensorData{
SensorID: "temp-001",
Value: ptr(-15.5),
Timestamp: ptr(timestamp),
},
expecErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.given.Validate()
if tt.expecErr && err == nil {
t.Errorf("expected error, got nil")
return
}
if !tt.expecErr && err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if tt.expecErr {
return
}
if tt.given.SensorID != tt.expected.SensorID {
t.Errorf("SensorID: expected %q, got %q", tt.expected.SensorID, tt.given.SensorID)
}
if tt.given.Value == nil || tt.expected.Value == nil {
if tt.given.Value != tt.expected.Value {
t.Errorf("Value: expected %v, got %v", tt.expected.Value, tt.given.Value)
}
} else if *tt.given.Value != *tt.expected.Value {
t.Errorf("Value: expected %v, got %v", *tt.expected.Value, *tt.given.Value)
}
if tt.expected.Timestamp == nil && tt.given.Timestamp != nil {
if time.Since(*tt.given.Timestamp) > time.Minute {
t.Errorf("Timestamp: expected default to be approximately now, got %v", *tt.given.Timestamp)
}
} else if tt.given.Timestamp == nil || tt.expected.Timestamp == nil {
if tt.given.Timestamp != tt.expected.Timestamp {
t.Errorf("Timestamp: expected %v, got %v", tt.expected.Timestamp, tt.given.Timestamp)
}
} else if !tt.given.Timestamp.Equal(*tt.expected.Timestamp) {
t.Errorf("Timestamp: expected %v, got %v", *tt.expected.Timestamp, *tt.given.Timestamp)
}
})
}
}
func Test_SensorData_IsOutOfRangeAbove(t *testing.T) {
type testCase struct {
name string
@@ -256,6 +386,16 @@ func Test_SensorData_IsOutOfRangeAbove(t *testing.T) {
},
expected: true,
},
{
name: "nil values",
data: SensorData{
SensorID: "temp-001",
},
sensor: Sensor{
SensorID: "temp-001",
},
expected: false,
},
}
for _, tt := range tests {
@@ -343,6 +483,16 @@ func Test_SensorData_IsOutOfRangeBelow(t *testing.T) {
},
expected: true,
},
{
name: "nil values",
data: SensorData{
SensorID: "temp-001",
},
sensor: Sensor{
SensorID: "temp-001",
},
expected: false,
},
}
for _, tt := range tests {
+3 -46
View File
@@ -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)
+56 -7
View File
@@ -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
}
func (s *Service) GetSensor(sensorID string) (Sensor, error) {
return s.repo.ReadSensor(sensorID)
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
}
func (s *Service) GetValues(sensorID string, from, to time.Time) ([]SensorData, error) {
return s.repo.ReadSensorValues(sensorID, from, to)
return nil
}
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(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) {
+502
View File
@@ -0,0 +1,502 @@
package sensors
import (
"errors"
"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
expectErr error
}
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(errors.New("duplicate key value"))
},
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,
},
}
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 && 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: &timestamp,
},
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: &timestamp,
},
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))
}
}
})
}
}
+6
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) {
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{