Compare commits
31 Commits
b51f00e499
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e89d5a8f5 | |||
| 7bc9f1c987 | |||
| 40ffee4d56 | |||
| 8774b55d3d | |||
| 8e33f95cb4 | |||
| f5583b3cd5 | |||
| d2f0e8ccf5 | |||
| 772ce9a7b7 | |||
| 57254fcbc9 | |||
| 1f6d0a58e7 | |||
| f5013a88a2 | |||
| 65be69d164 | |||
| f6d94bff1a | |||
| 23ec2d5789 | |||
| 78723d3fcd | |||
| eee829a665 | |||
| fb58ce55fb | |||
| fcc0d06f96 | |||
| b012db856c | |||
| 9d86fd394c | |||
| 6d3408f34c | |||
| 92640d1ad0 | |||
| 8cf9219534 | |||
| 92e94df61a | |||
| 15b1e9c82f | |||
| 5b281bb295 | |||
| cfea406424 | |||
| d342efadbc | |||
| 37e65ddd76 | |||
| 9e72b58917 | |||
| 8fe4ba1483 |
@@ -10,12 +10,30 @@ NATS_VERSION := 2.12.0-alpine3.22
|
|||||||
dockerize-db:
|
dockerize-db:
|
||||||
docker rm -f $(DB_NAME)
|
docker rm -f $(DB_NAME)
|
||||||
docker run --name $(DB_NAME) -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=$(DB_NAME) -p 5432:5432 -d timescale/timescaledb-ha:$(TIMESCALE_VERSION)
|
docker run --name $(DB_NAME) -e POSTGRES_PASSWORD=secret -e POSTGRES_USER=developer -e POSTGRES_DB=$(DB_NAME) -p 5432:5432 -d timescale/timescaledb-ha:$(TIMESCALE_VERSION)
|
||||||
|
sleep 10
|
||||||
|
make migrateup
|
||||||
|
|
||||||
.PHONY: dockerize-nats
|
.PHONY: dockerize-nats
|
||||||
# Remove and create a NATS server.
|
# Remove and create a NATS server.
|
||||||
dockerize-nats:
|
dockerize-nats:
|
||||||
docker rm -f $(NATS_NAME)
|
docker rm -f $(NATS_NAME)
|
||||||
docker run --name $(NATS_NAME) -p 4222:4222 -d nats:$(NATS_VERSION)
|
docker run --name $(NATS_NAME) -p 4222:4222 -d nats:$(NATS_VERSION)
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
.PHONY: migrateup
|
||||||
|
# Migrate all schemas, triggers and data located in database/migrations.
|
||||||
|
migrateup:
|
||||||
|
go run -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest -path app/database -database "postgresql://developer:secret@localhost:5432/$(DB_NAME)?sslmode=disable" -verbose up
|
||||||
|
|
||||||
|
.PHONY: mock
|
||||||
|
# Mock database
|
||||||
|
mock:
|
||||||
|
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
|
.PHONY: run
|
||||||
# Start app in development environment
|
# Start app in development environment
|
||||||
@@ -25,4 +43,12 @@ run:
|
|||||||
.PHONY: run-prod
|
.PHONY: run-prod
|
||||||
run-prod:
|
run-prod:
|
||||||
# Start app in production environment
|
# Start app in production environment
|
||||||
go run ./app/. -env=prod
|
go run ./app/. -env=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
|
||||||
@@ -3,12 +3,79 @@
|
|||||||
Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar
|
Lectura de datos de sensores en un dispositivo IoT. Prueba técnica para optar
|
||||||
por el puesto de programador Go.
|
por el puesto de programador Go.
|
||||||
|
|
||||||
|
## TL:DR
|
||||||
|
|
||||||
|
- make lazy-start
|
||||||
|
- abre terminal y escribe: `nats sub sensors.data.*`
|
||||||
|
|
||||||
## Requisitos previos
|
## Requisitos previos
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- NATS CLI
|
- NATS CLI
|
||||||
- Make, si prefieres la comodidad de usar Makefile
|
- Make, si prefieres la comodidad de usar Makefile
|
||||||
|
|
||||||
|
## Comandos
|
||||||
|
|
||||||
|
### Registrar un sensor
|
||||||
|
|
||||||
|
- Campos obligatorios: `sensor_id` y `sensor_type`.
|
||||||
|
- Campos opcionales: `sampling`, `thresholdabove` y `thresholdbelow`.
|
||||||
|
|
||||||
|
`nats req sensors.register '{
|
||||||
|
"sensor_id": "sensor-001",
|
||||||
|
"sensor_type": "temperature",
|
||||||
|
"sampling": 3600,
|
||||||
|
"thresoldabove": 50.0,
|
||||||
|
"thresoldbelow": -10.0
|
||||||
|
}'`
|
||||||
|
|
||||||
|
### Actualizar configuración sensor
|
||||||
|
|
||||||
|
- Campo obligatorio: `sensor_id` y la presencia de al menos un parámetro.
|
||||||
|
|
||||||
|
`nats req sensors.update '{
|
||||||
|
"sensor_id": "sensor-001",
|
||||||
|
"sensor_type": "temperature",
|
||||||
|
"sampling": 10,
|
||||||
|
"thresoldabove": 100.0,
|
||||||
|
"thresoldbelow": -15.0
|
||||||
|
}'`
|
||||||
|
|
||||||
|
### Obtener información de un sensor
|
||||||
|
|
||||||
|
- Campo obligatorio: `sensor_id`.
|
||||||
|
|
||||||
|
`nats req sensors.get '{
|
||||||
|
"sensor_id": "sensor-001"
|
||||||
|
}'`
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
`nats req sensors.values.get '{
|
||||||
|
"sensor_id": "sensor-001",
|
||||||
|
"from": "2025-10-03T00:00:00Z",
|
||||||
|
"to": "2025-10-10T23:59:59Z"
|
||||||
|
}'`
|
||||||
|
|
||||||
|
### Obtener listado de sensores
|
||||||
|
|
||||||
|
No hay _payload_, pero hay que poner comillas dobles o si no se queda esperando
|
||||||
|
una entrada de datos.
|
||||||
|
|
||||||
|
`nats req sensors.list ""`
|
||||||
|
|
||||||
|
### Suscribirse a un sensor
|
||||||
|
|
||||||
|
`nats sub sensors.data.sensor-001`
|
||||||
|
|
||||||
|
### Suscribirse a todos los sensores
|
||||||
|
|
||||||
|
`nats sub sensors.data.*`
|
||||||
|
|
||||||
## Consideraciones
|
## Consideraciones
|
||||||
|
|
||||||
Hay partes de códigos que son _snippets_ extraídos de una librería de autoría
|
Hay partes de códigos que son _snippets_ extraídos de una librería de autoría
|
||||||
@@ -16,6 +83,8 @@ propia. [Repositorio GitHub](https://github.com/zepyrshut/gopher-toolbox). De
|
|||||||
las cuales son:
|
las cuales son:
|
||||||
|
|
||||||
- El _logger_ usando la _stdlib log/slog_.
|
- El _logger_ usando la _stdlib log/slog_.
|
||||||
|
- La conexión con la base de datos, usando el controlador [pgx](https://github.com/jackc/pgx).
|
||||||
|
|
||||||
|
|
||||||
## Bitácora
|
## Bitácora
|
||||||
|
|
||||||
@@ -54,6 +123,142 @@ Por otro lado también hay un sistema de caché muy rudimentario, en memoria que
|
|||||||
es un mapa de valores.
|
es un mapa de valores.
|
||||||
|
|
||||||
Para el registro de valores y mantener ambos se ha usado el patrón decorador que
|
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
|
bajo un mismo _struct_ se incluye las dos implementaciones y registra cambios en
|
||||||
funciones. Desde la capa servicios sólo tiene que llamar al decorador sin saber
|
ambas partes. Desde la capa servicios sólo tiene que llamar al decorador sin
|
||||||
los detalles de la implementación.
|
saber los detalles de la implementación.
|
||||||
|
|
||||||
|
### Continuamos con los servicios
|
||||||
|
|
||||||
|
Con el repositorio sin implementar, se puede realizar los servicios. En ese
|
||||||
|
proyecto ha quedado muy básico, sirviendo solamente de enlace entre los
|
||||||
|
controladores y el repositorio.
|
||||||
|
|
||||||
|
Cuando se creó el _broker_ de NATS, inicialmente fue para crear una interfaz de
|
||||||
|
mensajería donde se pudiese manejar _websockets_, _SSE_ y otras mensajerías pero
|
||||||
|
se consideró que había otras prioridades. Así se quedó.
|
||||||
|
|
||||||
|
### Y finalmente los controladores
|
||||||
|
|
||||||
|
Ahí tuve muchas dudas con el entendimiento de NATS, estaba muy arraigado en el
|
||||||
|
patrón REST, y cambiar la mentalidad costó un poco, al final mirando un poco la
|
||||||
|
documentación me quedé con los conceptos clave:
|
||||||
|
|
||||||
|
1. Está basado en asuntos, los canales se crean de forma jerárquica.
|
||||||
|
2. Cuando se hace un _subscribe_, se suma a un canal del asunto dado.
|
||||||
|
3. Para escribir en el canal, hay que hacer el _publish_.
|
||||||
|
4. Finalmente para solicitar un recurso, está el _request_.
|
||||||
|
|
||||||
|
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
|
||||||
|
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).
|
||||||
|
Muchas de las herramientas son muy interesantes usarlas ya que acelera mucho la
|
||||||
|
generación de código repetitivo. Da mas confianza usar esas herramientas que la
|
||||||
|
IA.
|
||||||
|
|
||||||
|
Para las consultas SQL con seguridad de tipos, existe la herramienta [sqlc](https://sqlc.dev/).
|
||||||
|
|
||||||
|
Para ese proyecto sólo se ha usado [GoMock](https://github.com/uber-go/mock),
|
||||||
|
mantenida por Uber y sirve para usar la interfaz `Repository` sin usar una
|
||||||
|
base de datos real.
|
||||||
|
|
||||||
|
Por otro lado, hubiese sido interesante incorporar [ginkgo](https://onsi.github.io/ginkgo/),
|
||||||
|
es un marco de trabajo de pruebas unitarias usando un lenguaje de dominio
|
||||||
|
específico (DSL).
|
||||||
|
|
||||||
|
> En lugar de escribir una prueba así: `Test_Validate(t *testing.T) { ... }`
|
||||||
|
> se puede hacer de la siguiente manera: `var _ Describe("Models", func () { ... })`,
|
||||||
|
> y dentro del cuerpo se describen las pruebas a realizar.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
@@ -25,7 +25,7 @@ create index idx_sensors_sensor_id on sensors (sensor_id);
|
|||||||
|
|
||||||
create table registry
|
create table registry
|
||||||
(
|
(
|
||||||
sensor_id int not null references sensors (id),
|
sensor_id varchar(255) not null references sensors (sensor_id),
|
||||||
|
|
||||||
value float not null,
|
value float not null,
|
||||||
created_at timestamp not null default now()
|
created_at timestamp not null default now()
|
||||||
@@ -34,4 +34,13 @@ create table registry
|
|||||||
timescaledb.hypertable,
|
timescaledb.hypertable,
|
||||||
timescaledb.partition_column = 'created_at',
|
timescaledb.partition_column = 'created_at',
|
||||||
timescaledb.segmentby = 'sensor_id'
|
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 |
@@ -5,6 +5,7 @@ go 1.25.1
|
|||||||
require (
|
require (
|
||||||
github.com/jackc/pgx/v5 v5.7.6
|
github.com/jackc/pgx/v5 v5.7.6
|
||||||
github.com/nats-io/nats.go v1.46.1
|
github.com/nats-io/nats.go v1.46.1
|
||||||
|
go.uber.org/mock v0.6.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -15,7 +16,7 @@ require (
|
|||||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||||
github.com/nats-io/nuid v1.0.1 // indirect
|
github.com/nats-io/nuid v1.0.1 // indirect
|
||||||
golang.org/x/crypto v0.37.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/sync v0.13.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,12 +22,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
|||||||
@@ -1 +1,116 @@
|
|||||||
package sensors
|
package sensors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Sensor) Validate() error {
|
||||||
|
if s.SensorID == "" {
|
||||||
|
return ErrInvalidSensorIdentifier
|
||||||
|
}
|
||||||
|
if s.SensorType == "" {
|
||||||
|
return ErrInvalidSensorType
|
||||||
|
}
|
||||||
|
|
||||||
|
validTypes := []SType{Temperature, Humidity, CarbonDioxide, Pressure, Proximity, Light}
|
||||||
|
isValid := slices.Contains(validTypes, s.SensorType)
|
||||||
|
if !isValid {
|
||||||
|
return ErrInvalidSensorType
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.SamplingInterval == nil {
|
||||||
|
defaultInterval := time.Second * 3600
|
||||||
|
s.SamplingInterval = &defaultInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ThresholdAbove == nil {
|
||||||
|
defaultAbove := 100.0
|
||||||
|
s.ThresholdAbove = &defaultAbove
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ThresholdBelow == nil {
|
||||||
|
defaultBelow := 0.0
|
||||||
|
s.ThresholdBelow = &defaultBelow
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SensorData) Validate() error {
|
||||||
|
if d.SensorID == "" {
|
||||||
|
return ErrInvalidSensorIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Value == nil {
|
||||||
|
return ErrMissingValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Timestamp == nil {
|
||||||
|
now := time.Now()
|
||||||
|
d.Timestamp = &now
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement this in service layer for alerts
|
||||||
|
func (d *SensorData) IsOutOfRangeAbove(sensor Sensor) bool {
|
||||||
|
if d.Value == nil || sensor.ThresholdAbove == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *d.Value > *sensor.ThresholdAbove
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *SensorData) IsOutOfRangeBelow(sensor Sensor) bool {
|
||||||
|
if d.Value == nil || sensor.ThresholdBelow == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return *d.Value < *sensor.ThresholdBelow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SensorRequest) Validate() error {
|
||||||
|
if r.SensorID == "" {
|
||||||
|
return ErrInvalidSensorIdentifier
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SensorDataRequest) Validate() error {
|
||||||
|
if r.SensorID == "" {
|
||||||
|
return ErrInvalidSensorIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.To == nil || *r.To == "" {
|
||||||
|
defaultTo := time.Now().Format(time.RFC3339)
|
||||||
|
r.To = &defaultTo
|
||||||
|
} else {
|
||||||
|
if _, err := time.Parse(time.RFC3339, *r.To); err != nil {
|
||||||
|
defaultTo := time.Now().Format(time.RFC3339)
|
||||||
|
r.To = &defaultTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.From == nil || *r.From == "" {
|
||||||
|
defaultFrom := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
|
||||||
|
r.From = &defaultFrom
|
||||||
|
} else {
|
||||||
|
if _, err := time.Parse(time.RFC3339, *r.From); err != nil {
|
||||||
|
defaultFrom := time.Now().AddDate(0, 0, -7).Format(time.RFC3339)
|
||||||
|
r.From = &defaultFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
ErrMissingValue = errors.New("sensor value no provided")
|
||||||
|
ErrSensorAlreadyExists = errors.New("sensor already exists")
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,802 @@
|
|||||||
|
package sensors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_SensorValidate(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
given Sensor
|
||||||
|
expected Sensor
|
||||||
|
expecErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
name: "success with all fields",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Hour * 24),
|
||||||
|
ThresholdAbove: ptr(50.0),
|
||||||
|
ThresholdBelow: ptr(-10.0),
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Hour * 24),
|
||||||
|
ThresholdAbove: ptr(50.0),
|
||||||
|
ThresholdBelow: ptr(-10.0),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when sensor_id is empty",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "",
|
||||||
|
SensorType: "temperature",
|
||||||
|
},
|
||||||
|
expecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when sensor_type is empty",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
SensorType: "",
|
||||||
|
},
|
||||||
|
expecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sensor type not in const",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
SensorType: "unknown",
|
||||||
|
},
|
||||||
|
expecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default sampling_interval when nil",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-002",
|
||||||
|
SensorType: "humidity",
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-002",
|
||||||
|
SensorType: "humidity",
|
||||||
|
SamplingInterval: ptr(time.Second * 3600),
|
||||||
|
ThresholdAbove: ptr(100.0),
|
||||||
|
ThresholdBelow: ptr(0.0),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default threshold_above when nil",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-003",
|
||||||
|
SensorType: "pressure",
|
||||||
|
SamplingInterval: ptr(time.Minute * 5),
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-003",
|
||||||
|
SensorType: "pressure",
|
||||||
|
SamplingInterval: ptr(time.Minute * 5),
|
||||||
|
ThresholdAbove: ptr(100.0),
|
||||||
|
ThresholdBelow: ptr(0.0),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default threshold_below when nil",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-004",
|
||||||
|
SensorType: "light",
|
||||||
|
SamplingInterval: ptr(time.Second * 30),
|
||||||
|
ThresholdAbove: ptr(200.0),
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-004",
|
||||||
|
SensorType: "light",
|
||||||
|
SamplingInterval: ptr(time.Second * 30),
|
||||||
|
ThresholdAbove: ptr(200.0),
|
||||||
|
ThresholdBelow: ptr(0.0),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero values are preserved",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-005",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Second * 10),
|
||||||
|
ThresholdAbove: ptr(0.0),
|
||||||
|
ThresholdBelow: ptr(0.0),
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-005",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Second * 10),
|
||||||
|
ThresholdAbove: ptr(0.0),
|
||||||
|
ThresholdBelow: ptr(0.0),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative threshold_below is valid",
|
||||||
|
given: Sensor{
|
||||||
|
SensorID: "temp-006",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Minute * 2),
|
||||||
|
ThresholdAbove: ptr(35.0),
|
||||||
|
ThresholdBelow: ptr(-20.5),
|
||||||
|
},
|
||||||
|
expected: Sensor{
|
||||||
|
SensorID: "temp-006",
|
||||||
|
SensorType: "temperature",
|
||||||
|
SamplingInterval: ptr(time.Minute * 2),
|
||||||
|
ThresholdAbove: ptr(35.0),
|
||||||
|
ThresholdBelow: ptr(-20.5),
|
||||||
|
},
|
||||||
|
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.SensorType != tt.expected.SensorType {
|
||||||
|
t.Errorf("expected %q, got %q", tt.expected.SensorType, tt.given.SensorType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.given.SamplingInterval == nil || tt.expected.SamplingInterval == nil {
|
||||||
|
if tt.given.SamplingInterval != tt.expected.SamplingInterval {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected.SamplingInterval, tt.given.SamplingInterval)
|
||||||
|
}
|
||||||
|
} else if *tt.given.SamplingInterval != *tt.expected.SamplingInterval {
|
||||||
|
t.Errorf("expected %v, got %v", *tt.expected.SamplingInterval, *tt.given.SamplingInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.given.ThresholdAbove == nil || tt.expected.ThresholdAbove == nil {
|
||||||
|
if tt.given.ThresholdAbove != tt.expected.ThresholdAbove {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected.ThresholdAbove, tt.given.ThresholdAbove)
|
||||||
|
}
|
||||||
|
} else if *tt.given.ThresholdAbove != *tt.expected.ThresholdAbove {
|
||||||
|
t.Errorf("expected %v, got %v", *tt.expected.ThresholdAbove, *tt.given.ThresholdAbove)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.given.ThresholdBelow == nil || tt.expected.ThresholdBelow == nil {
|
||||||
|
if tt.given.ThresholdBelow != tt.expected.ThresholdBelow {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected.ThresholdBelow, tt.given.ThresholdBelow)
|
||||||
|
}
|
||||||
|
} else if *tt.given.ThresholdBelow != *tt.expected.ThresholdBelow {
|
||||||
|
t.Errorf("expected %v, got %v", *tt.expected.ThresholdBelow, *tt.given.ThresholdBelow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
data SensorData
|
||||||
|
sensor Sensor
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
name: "value above threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(150.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdAbove: ptr(100.0),
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value below threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(50.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdAbove: ptr(100.0),
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value equal to threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(100.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdAbove: ptr(100.0),
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative value above negative threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(-5.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdAbove: ptr(-10.0),
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil values",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.data.IsOutOfRangeAbove(tt.sensor)
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SensorData_IsOutOfRangeBelow(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
data SensorData
|
||||||
|
sensor Sensor
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
name: "value below threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(5.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdBelow: ptr(10.0),
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value above threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(50.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdBelow: ptr(10.0),
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "value equal to threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(10.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdBelow: ptr(10.0),
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "negative value below threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(-15.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdBelow: ptr(-10.0),
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero value below positive threshold",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
Value: ptr(0.0),
|
||||||
|
Timestamp: ptr(time.Now()),
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
ThresholdBelow: ptr(5.0),
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil values",
|
||||||
|
data: SensorData{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
},
|
||||||
|
sensor: Sensor{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.data.IsOutOfRangeBelow(tt.sensor)
|
||||||
|
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SensorRequest_Validate(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
given SensorRequest
|
||||||
|
expecErr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
name: "valid request with sensor_id",
|
||||||
|
given: SensorRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when sensor_id is empty",
|
||||||
|
given: SensorRequest{
|
||||||
|
SensorID: "",
|
||||||
|
},
|
||||||
|
expecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid request with long sensor_id",
|
||||||
|
given: SensorRequest{
|
||||||
|
SensorID: "sensor-with-very-long-identifier-12345",
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid request with special characters",
|
||||||
|
given: SensorRequest{
|
||||||
|
SensorID: "sensor-001_test",
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SensorDataRequest_Validate(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
given SensorDataRequest
|
||||||
|
expecErr bool
|
||||||
|
checkFn func(t *testing.T, req SensorDataRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
weekAgo := now.AddDate(0, 0, -7)
|
||||||
|
validFrom := weekAgo.Format(time.RFC3339)
|
||||||
|
validTo := now.Format(time.RFC3339)
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{
|
||||||
|
name: "valid request with all fields",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr(validFrom),
|
||||||
|
To: ptr(validTo),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.From == nil || *req.From != validFrom {
|
||||||
|
t.Errorf("expected From to be %q, got %v", validFrom, req.From)
|
||||||
|
}
|
||||||
|
if req.To == nil || *req.To != validTo {
|
||||||
|
t.Errorf("expected To to be %q, got %v", validTo, req.To)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when sensor_id is empty",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "",
|
||||||
|
From: ptr(validFrom),
|
||||||
|
To: ptr(validTo),
|
||||||
|
},
|
||||||
|
expecErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default To when nil",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr(validFrom),
|
||||||
|
To: nil,
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.To == nil {
|
||||||
|
t.Error("expected To to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.To)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format, got error: %v", err)
|
||||||
|
}
|
||||||
|
if time.Since(parsed) > time.Minute {
|
||||||
|
t.Error("expected To to be approximately now")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default To when empty string",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr(validFrom),
|
||||||
|
To: ptr(""),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.To == nil {
|
||||||
|
t.Error("expected To to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.To)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format, got error: %v", err)
|
||||||
|
}
|
||||||
|
if time.Since(parsed) > time.Minute {
|
||||||
|
t.Error("expected To to be approximately now")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default From when nil",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: nil,
|
||||||
|
To: ptr(validTo),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.From == nil {
|
||||||
|
t.Error("expected From to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.From)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format, got error: %v", err)
|
||||||
|
}
|
||||||
|
expectedFrom := time.Now().AddDate(0, 0, -7)
|
||||||
|
diff := expectedFrom.Sub(parsed)
|
||||||
|
if diff > time.Hour || diff < -time.Hour {
|
||||||
|
t.Errorf("expected From to be approximately 7 days ago, got %v", parsed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default From when empty string",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr(""),
|
||||||
|
To: ptr(validTo),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.From == nil {
|
||||||
|
t.Error("expected From to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.From)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format, got error: %v", err)
|
||||||
|
}
|
||||||
|
expectedFrom := time.Now().AddDate(0, 0, -7)
|
||||||
|
diff := expectedFrom.Sub(parsed)
|
||||||
|
if diff > time.Hour || diff < -time.Hour {
|
||||||
|
t.Errorf("expected From to be approximately 7 days ago, got %v", parsed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid From format sets default",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr("invalid-date"),
|
||||||
|
To: ptr(validTo),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.From == nil {
|
||||||
|
t.Error("expected From to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.From)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format after correction, got error: %v", err)
|
||||||
|
}
|
||||||
|
expectedFrom := time.Now().AddDate(0, 0, -7)
|
||||||
|
diff := expectedFrom.Sub(parsed)
|
||||||
|
if diff > time.Hour || diff < -time.Hour {
|
||||||
|
t.Errorf("expected From to be approximately 7 days ago after correction, got %v", parsed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid To format sets default",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: ptr(validFrom),
|
||||||
|
To: ptr("not-a-date"),
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.To == nil {
|
||||||
|
t.Error("expected To to be set with default value")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsed, err := time.Parse(time.RFC3339, *req.To)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format after correction, got error: %v", err)
|
||||||
|
}
|
||||||
|
if time.Since(parsed) > time.Minute {
|
||||||
|
t.Error("expected To to be approximately now after correction")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all defaults when From and To are nil",
|
||||||
|
given: SensorDataRequest{
|
||||||
|
SensorID: "temp-001",
|
||||||
|
From: nil,
|
||||||
|
To: nil,
|
||||||
|
},
|
||||||
|
expecErr: false,
|
||||||
|
checkFn: func(t *testing.T, req SensorDataRequest) {
|
||||||
|
if req.From == nil || req.To == nil {
|
||||||
|
t.Error("expected both From and To to be set with defaults")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parsedFrom, err := time.Parse(time.RFC3339, *req.From)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format for From, got error: %v", err)
|
||||||
|
}
|
||||||
|
parsedTo, err := time.Parse(time.RFC3339, *req.To)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected valid RFC3339 format for To, got error: %v", err)
|
||||||
|
}
|
||||||
|
expectedFrom := time.Now().AddDate(0, 0, -7)
|
||||||
|
diff := expectedFrom.Sub(parsedFrom)
|
||||||
|
if diff > time.Hour || diff < -time.Hour {
|
||||||
|
t.Errorf("expected From to be approximately 7 days ago, got %v", parsedFrom)
|
||||||
|
}
|
||||||
|
if time.Since(parsedTo) > time.Minute {
|
||||||
|
t.Error("expected To to be approximately now")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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.checkFn != nil {
|
||||||
|
tt.checkFn(t, tt.given)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package sensors
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
"nats-app/internal/iot"
|
"nats-app/internal/iot"
|
||||||
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
@@ -13,25 +14,45 @@ const (
|
|||||||
subjectSensorsGet = "sensors.get"
|
subjectSensorsGet = "sensors.get"
|
||||||
subjectSensorsValuesGet = "sensors.values.get"
|
subjectSensorsValuesGet = "sensors.values.get"
|
||||||
subjectSensorsList = "sensors.list"
|
subjectSensorsList = "sensors.list"
|
||||||
|
subjectSensorsData = "sensors.data."
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
service *Service
|
service *Service
|
||||||
*iot.IoTDevice
|
*iot.IoTDevice
|
||||||
|
simulator *Simulator
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandlers(service *Service, iot *iot.IoTDevice) *Handlers {
|
func NewHandlers(service *Service, iot *iot.IoTDevice) *Handlers {
|
||||||
|
|
||||||
|
simulator := Start(iot.NATS)
|
||||||
|
activeSensors, err := service.repo.ReadAllSensors()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("reading all sensors", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sensor := range activeSensors {
|
||||||
|
go simulator.SimulateSensor(sensor)
|
||||||
|
slog.Info("started simulator for sensor", "sensor_id", sensor.SensorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
sensors, _ := service.repo.ReadAllSensors()
|
||||||
|
slog.Info("sensors", "sens", sensors)
|
||||||
|
|
||||||
return &Handlers{
|
return &Handlers{
|
||||||
service: service,
|
service: service,
|
||||||
IoTDevice: iot,
|
IoTDevice: iot,
|
||||||
|
simulator: simulator,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRequest[Req any, Res any](msg *nats.Msg, handler func(Req) (Res, error)) {
|
func handleRequest[Req any, Res any](msg *nats.Msg, handler func(Req) (Res, error)) {
|
||||||
var req Req
|
var req Req
|
||||||
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
if len(msg.Data) > 0 {
|
||||||
msg.Respond([]byte(`{"error":"invalid request"}`))
|
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
||||||
return
|
msg.Respond([]byte(`{"error":"invalid request"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := handler(req)
|
result, err := handler(req)
|
||||||
@@ -44,8 +65,23 @@ func handleRequest[Req any, Res any](msg *nats.Msg, handler func(Req) (Res, erro
|
|||||||
msg.Respond(response)
|
msg.Respond(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlePublish[Req any](msg *nats.Msg, handler func(Req) error) {
|
||||||
|
var req Req
|
||||||
|
if len(msg.Data) > 0 {
|
||||||
|
if err := json.Unmarshal(msg.Data, &req); err != nil {
|
||||||
|
slog.Error("failed to unmarshal message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handler(req); err != nil {
|
||||||
|
slog.Error("handler error", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handlers) SetupEndpoints() *Handlers {
|
func (h *Handlers) SetupEndpoints() *Handlers {
|
||||||
h.register()
|
h.register()
|
||||||
|
h.registerData()
|
||||||
h.update()
|
h.update()
|
||||||
h.get()
|
h.get()
|
||||||
h.getValues()
|
h.getValues()
|
||||||
@@ -56,17 +92,33 @@ 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) {
|
||||||
// service layer
|
if err := h.service.RegisterSensor(req); err != nil {
|
||||||
|
return Sensor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go h.simulator.SimulateSensor(req)
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handlers) registerData() {
|
||||||
|
h.NATS.Subscribe(subjectSensorsData+"*", func(msg *nats.Msg) {
|
||||||
|
handlePublish(msg, func(data SensorData) error {
|
||||||
|
return h.service.RegisterSensorData(data)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
// service layer
|
if err := h.service.UpdateSensor(req); err != nil {
|
||||||
|
return Sensor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.simulator.UpdateSensor(req)
|
||||||
|
|
||||||
return req, nil
|
return req, nil
|
||||||
})
|
})
|
||||||
@@ -75,26 +127,16 @@ 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 struct {
|
handleRequest(msg, func(req SensorRequest) (Sensor, error) {
|
||||||
SensorID string `json:"sensor_id"`
|
return h.service.GetSensor(req)
|
||||||
}) (Sensor, error) {
|
|
||||||
// service layer
|
|
||||||
|
|
||||||
return Sensor{}, nil
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 struct {
|
handleRequest(msg, func(req SensorDataRequest) ([]SensorData, error) {
|
||||||
SensorID string `json:"sensor_id"`
|
return h.service.GetValues(req)
|
||||||
From string `json:"from"`
|
|
||||||
To string `json:"to"`
|
|
||||||
}) ([]SensorData, error) {
|
|
||||||
// service layer
|
|
||||||
|
|
||||||
return []SensorData{}, nil
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -102,9 +144,7 @@ func (h *Handlers) getValues() {
|
|||||||
func (h *Handlers) list() {
|
func (h *Handlers) list() {
|
||||||
h.NATS.Subscribe(subjectSensorsList, func(msg *nats.Msg) {
|
h.NATS.Subscribe(subjectSensorsList, func(msg *nats.Msg) {
|
||||||
handleRequest(msg, func(req struct{}) ([]Sensor, error) {
|
handleRequest(msg, func(req struct{}) ([]Sensor, error) {
|
||||||
// service layer
|
return h.service.ListSensors()
|
||||||
|
|
||||||
return []Sensor{}, nil
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,26 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Sensor struct {
|
type Sensor struct {
|
||||||
SensorID string `json:"sensor_id"`
|
SensorID string `json:"sensor_id"`
|
||||||
SensorType SType `json:"sensor_type"`
|
SensorType SType `json:"sensor_type"`
|
||||||
SamplingInterval time.Duration `json:"sampling"`
|
SamplingInterval *time.Duration `json:"sampling"`
|
||||||
ThresholdAbove float64 `json:"thresoldabove"`
|
ThresholdAbove *float64 `json:"thresoldabove"`
|
||||||
ThresholdBelow float64 `json:"thresoldbelow"`
|
ThresholdBelow *float64 `json:"thresoldbelow"`
|
||||||
SensorData *[]SensorData `json:"sensor_data,omitempty"`
|
SensorData *map[int]SensorData `json:"sensor_data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SensorData struct {
|
type SensorData struct {
|
||||||
Value float64 `json:"value"`
|
SensorID string `json:"sensor_id"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Value *float64 `json:"value"`
|
||||||
|
Timestamp *time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SensorRequest struct {
|
||||||
|
SensorID string `json:"sensor_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SensorDataRequest struct {
|
||||||
|
SensorID string `json:"sensor_id"`
|
||||||
|
From *string `json:"from"`
|
||||||
|
To *string `json:"to"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package sensors
|
package sensors
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -9,41 +10,114 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
RegisterSensor(s Sensor) error
|
CreateSensor(s Sensor) error
|
||||||
UpdateSensorConfig(s Sensor) error
|
CreateSensorData(data SensorData) error
|
||||||
ReadSensor(id int) (Sensor, error)
|
UpdateSensor(s Sensor) error
|
||||||
ReadSensorValues(id int, from, to time.Time) ([]SensorData, error)
|
ReadSensor(sensorID string) (Sensor, error)
|
||||||
|
ReadSensorValues(sensorID string, from, to time.Time) ([]SensorData, error)
|
||||||
ReadAllSensors() ([]Sensor, error)
|
ReadAllSensors() ([]Sensor, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pgxRepo struct {
|
type pgxRepo struct {
|
||||||
pool *pgxpool.Pool
|
*pgxpool.Pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPGXRepo(pool *pgxpool.Pool) Repository {
|
func newPGXRepo(pool *pgxpool.Pool) Repository {
|
||||||
return &pgxRepo{
|
return &pgxRepo{
|
||||||
pool: pool,
|
pool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pgxRepo) ReadSensor(id int) (Sensor, error) {
|
const createSensorQuery = `insert into sensors (sensor_id, sensor_type, sampling_interval, threshold_above, threshold_below) values ($1, $2, $3, $4, $5)`
|
||||||
panic("unimplemented")
|
|
||||||
|
func (p *pgxRepo) CreateSensor(s Sensor) error {
|
||||||
|
_, err := p.Exec(context.Background(), createSensorQuery, s.SensorID, string(s.SensorType), s.SamplingInterval, s.ThresholdAbove, s.ThresholdBelow)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pgxRepo) UpdateSensorConfig(s Sensor) error {
|
const createSensorDataQuery = `insert into registry (sensor_id, value, created_at) values ($1, $2, $3)`
|
||||||
panic("unimplemented")
|
|
||||||
|
func (p *pgxRepo) CreateSensorData(s SensorData) error {
|
||||||
|
_, err := p.Exec(context.Background(), createSensorDataQuery, s.SensorID, s.Value, s.Timestamp)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pgxRepo) RegisterSensor(s Sensor) error {
|
const updateSensorQuery = `update sensors set sensor_type = $1, sampling_interval = $2, threshold_above = $3, threshold_below = $4 where sensor_id = $5`
|
||||||
panic("unimplemented")
|
|
||||||
|
func (p *pgxRepo) UpdateSensor(s Sensor) error {
|
||||||
|
_, err := p.Exec(context.Background(), updateSensorQuery, string(s.SensorType), s.SamplingInterval, s.ThresholdAbove, s.ThresholdBelow, s.SensorID)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pgxRepo) ReadSensorValues(id int, from time.Time, to time.Time) ([]SensorData, error) {
|
const readSensorBySensorID = `select sensor_id, sensor_type, sampling_interval, threshold_above, threshold_below from sensors where sensor_id = $1`
|
||||||
panic("unimplemented")
|
|
||||||
|
func (p *pgxRepo) ReadSensor(sensorID string) (Sensor, error) {
|
||||||
|
var s Sensor
|
||||||
|
err := p.QueryRow(context.Background(), readSensorBySensorID, sensorID).Scan(
|
||||||
|
&s.SensorID,
|
||||||
|
&s.SensorType,
|
||||||
|
&s.SamplingInterval,
|
||||||
|
&s.ThresholdAbove,
|
||||||
|
&s.ThresholdBelow,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return Sensor{}, ErrSensorNotFound
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const readSensorValuesBySensorID = `select sensor_id, value, created_at from registry where sensor_id = $1 and created_at >= $2 and created_at <= $3 order by created_at desc`
|
||||||
|
|
||||||
|
func (p *pgxRepo) ReadSensorValues(sensorID string, from, to time.Time) ([]SensorData, error) {
|
||||||
|
rows, err := p.Query(context.Background(), readSensorValuesBySensorID, sensorID, from, to)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
data := []SensorData{}
|
||||||
|
for rows.Next() {
|
||||||
|
var sd SensorData
|
||||||
|
if err := rows.Scan(&sd.SensorID, &sd.Value, &sd.Timestamp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data = append(data, sd)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const readAllSensorsQuery = `select sensor_id, sensor_type, sampling_interval, threshold_above, threshold_below from sensors order by created_at desc`
|
||||||
|
|
||||||
func (p *pgxRepo) ReadAllSensors() ([]Sensor, error) {
|
func (p *pgxRepo) ReadAllSensors() ([]Sensor, error) {
|
||||||
panic("unimplemented")
|
rows, err := p.Query(context.Background(), readAllSensorsQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
sensors := []Sensor{}
|
||||||
|
for rows.Next() {
|
||||||
|
var s Sensor
|
||||||
|
if err := rows.Scan(
|
||||||
|
&s.SensorID,
|
||||||
|
&s.SensorType,
|
||||||
|
&s.SamplingInterval,
|
||||||
|
&s.ThresholdAbove,
|
||||||
|
&s.ThresholdBelow,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sensors = append(sensors, s)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sensors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type inMemory struct {
|
type inMemory struct {
|
||||||
@@ -54,29 +128,116 @@ type inMemory struct {
|
|||||||
func newInMemoryRepo() Repository {
|
func newInMemoryRepo() Repository {
|
||||||
return &inMemory{
|
return &inMemory{
|
||||||
sensors: make(map[string]*Sensor),
|
sensors: make(map[string]*Sensor),
|
||||||
|
mu: &sync.Mutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inMemory) RegisterSensor(s Sensor) error {
|
func (i *inMemory) CreateSensor(s Sensor) error {
|
||||||
panic("unimplemented")
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
|
if _, exists := i.sensors[s.SensorID]; exists {
|
||||||
|
return ErrSensorAlreadyExists
|
||||||
|
}
|
||||||
|
|
||||||
|
sensorCopy := s
|
||||||
|
i.sensors[s.SensorID] = &sensorCopy
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inMemory) UpdateSensorConfig(s Sensor) error {
|
func (i *inMemory) CreateSensorData(data SensorData) error {
|
||||||
panic("unimplemented")
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
|
sensor, exists := i.sensors[data.SensorID]
|
||||||
|
if !exists {
|
||||||
|
return ErrSensorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if sensor.SensorData == nil {
|
||||||
|
sensor.SensorData = &map[int]SensorData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
key := int(data.Timestamp.Unix())
|
||||||
|
(*sensor.SensorData)[key] = data
|
||||||
|
|
||||||
|
if len(*sensor.SensorData) > 100 {
|
||||||
|
oldestKey := key
|
||||||
|
for k := range *sensor.SensorData {
|
||||||
|
if k < oldestKey {
|
||||||
|
oldestKey = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(*sensor.SensorData, oldestKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inMemory) ReadSensor(id int) (Sensor, error) {
|
func (i *inMemory) UpdateSensor(s Sensor) error {
|
||||||
panic("unimplemented")
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
|
sensor, exists := i.sensors[s.SensorID]
|
||||||
|
if !exists {
|
||||||
|
return ErrSensorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor.SensorType = s.SensorType
|
||||||
|
sensor.SamplingInterval = s.SamplingInterval
|
||||||
|
sensor.ThresholdAbove = s.ThresholdAbove
|
||||||
|
sensor.ThresholdBelow = s.ThresholdBelow
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inMemory) ReadSensorValues(id int, from time.Time, to time.Time) ([]SensorData, error) {
|
func (i *inMemory) ReadSensor(sensorID string) (Sensor, error) {
|
||||||
// holds only last 100 values for every sensor
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
panic("unimplemented")
|
sensor, exists := i.sensors[sensorID]
|
||||||
|
if !exists {
|
||||||
|
return Sensor{}, ErrSensorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return *sensor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *inMemory) ReadSensorValues(sensorID string, from time.Time, to time.Time) ([]SensorData, error) {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
|
sensor, exists := i.sensors[sensorID]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrSensorNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if sensor.SensorData == nil {
|
||||||
|
return []SensorData{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []SensorData{}
|
||||||
|
fromUnix := from.Unix()
|
||||||
|
toUnix := to.Unix()
|
||||||
|
|
||||||
|
for timestamp, sd := range *sensor.SensorData {
|
||||||
|
if int64(timestamp) >= fromUnix && int64(timestamp) <= toUnix {
|
||||||
|
data = append(data, sd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inMemory) ReadAllSensors() ([]Sensor, error) {
|
func (i *inMemory) ReadAllSensors() ([]Sensor, error) {
|
||||||
panic("unimplemented")
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
|
sensors := make([]Sensor, 0, len(i.sensors))
|
||||||
|
for _, s := range i.sensors {
|
||||||
|
sensors = append(sensors, *s)
|
||||||
|
}
|
||||||
|
return sensors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type DecoratorRepo struct {
|
type DecoratorRepo struct {
|
||||||
@@ -95,7 +256,7 @@ func NewDecoratorRepo(pool *pgxpool.Pool) Repository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range sensors {
|
for _, s := range sensors {
|
||||||
_ = memory.RegisterSensor(s)
|
_ = memory.CreateSensor(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DecoratorRepo{
|
return &DecoratorRepo{
|
||||||
@@ -104,47 +265,78 @@ func NewDecoratorRepo(pool *pgxpool.Pool) Repository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DecoratorRepo) RegisterSensor(s Sensor) error {
|
func (d *DecoratorRepo) CreateSensor(s Sensor) error {
|
||||||
if err := d.db.RegisterSensor(s); err != nil {
|
if err := d.db.CreateSensor(s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = d.memory.RegisterSensor(s)
|
_ = d.memory.CreateSensor(s)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DecoratorRepo) UpdateSensorConfig(s Sensor) error {
|
func (d *DecoratorRepo) CreateSensorData(data SensorData) error {
|
||||||
if err := d.db.UpdateSensorConfig(s); err != nil {
|
if err := d.db.CreateSensorData(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = d.memory.UpdateSensorConfig(s)
|
_ = d.memory.CreateSensorData(data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DecoratorRepo) ReadSensor(id int) (Sensor, error) {
|
func (d *DecoratorRepo) UpdateSensor(s Sensor) error {
|
||||||
sensor, err := d.memory.ReadSensor(id)
|
if err := d.db.UpdateSensor(s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = d.memory.UpdateSensor(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DecoratorRepo) ReadSensor(sensorID string) (Sensor, error) {
|
||||||
|
sensor, err := d.memory.ReadSensor(sensorID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return sensor, nil
|
return sensor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.db.ReadSensor(id)
|
return d.db.ReadSensor(sensorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DecoratorRepo) ReadSensorValues(id int, from, to time.Time) ([]SensorData, error) {
|
func (d *DecoratorRepo) ReadSensorValues(sensorID string, from, to time.Time) ([]SensorData, error) {
|
||||||
values, err := d.memory.ReadSensorValues(id, from, to)
|
values, err := d.memory.ReadSensorValues(sensorID, from, to)
|
||||||
if err == nil && len(values) > 0 {
|
if err != nil || len(values) == 0 {
|
||||||
return values, nil
|
return d.db.ReadSensorValues(sensorID, from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.db.ReadSensorValues(id, from, to)
|
var oldestTimestamp *time.Time
|
||||||
|
for _, v := range values {
|
||||||
|
if oldestTimestamp == nil || v.Timestamp.Before(*oldestTimestamp) {
|
||||||
|
oldestTimestamp = v.Timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if oldestTimestamp != nil && oldestTimestamp.After(from) {
|
||||||
|
return d.db.ReadSensorValues(sensorID, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DecoratorRepo) ReadAllSensors() ([]Sensor, error) {
|
func (d *DecoratorRepo) ReadAllSensors() ([]Sensor, error) {
|
||||||
|
var sensors []Sensor
|
||||||
|
|
||||||
sensors, err := d.memory.ReadAllSensors()
|
sensors, err := d.memory.ReadAllSensors()
|
||||||
if err == nil && len(sensors) > 0 {
|
if err == nil && len(sensors) > 0 {
|
||||||
return sensors, nil
|
return sensors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return d.db.ReadAllSensors()
|
sensors, err = d.db.ReadAllSensors()
|
||||||
|
if err != nil {
|
||||||
|
return []Sensor{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range sensors {
|
||||||
|
_ = d.memory.CreateSensor(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sensors, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: nats-app/internal/domains/sensors (interfaces: Repository)
|
||||||
|
//
|
||||||
|
// Generated by this command:
|
||||||
|
//
|
||||||
|
// mockgen -package sensors -destination internal/domains/sensors/repository_mock.go nats-app/internal/domains/sensors Repository
|
||||||
|
//
|
||||||
|
|
||||||
|
// Package sensors is a generated GoMock package.
|
||||||
|
package sensors
|
||||||
|
|
||||||
|
import (
|
||||||
|
reflect "reflect"
|
||||||
|
time "time"
|
||||||
|
|
||||||
|
gomock "go.uber.org/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockRepository is a mock of Repository interface.
|
||||||
|
type MockRepository struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockRepositoryMockRecorder
|
||||||
|
isgomock struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockRepositoryMockRecorder is the mock recorder for MockRepository.
|
||||||
|
type MockRepositoryMockRecorder struct {
|
||||||
|
mock *MockRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockRepository creates a new mock instance.
|
||||||
|
func NewMockRepository(ctrl *gomock.Controller) *MockRepository {
|
||||||
|
mock := &MockRepository{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockRepositoryMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSensor mocks base method.
|
||||||
|
func (m *MockRepository) CreateSensor(s Sensor) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CreateSensor", s)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSensor indicates an expected call of CreateSensor.
|
||||||
|
func (mr *MockRepositoryMockRecorder) CreateSensor(s any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
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() ([]Sensor, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReadAllSensors")
|
||||||
|
ret0, _ := ret[0].([]Sensor)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAllSensors indicates an expected call of ReadAllSensors.
|
||||||
|
func (mr *MockRepositoryMockRecorder) ReadAllSensors() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadAllSensors", reflect.TypeOf((*MockRepository)(nil).ReadAllSensors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSensor mocks base method.
|
||||||
|
func (m *MockRepository) ReadSensor(sensorID string) (Sensor, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "ReadSensor", sensorID)
|
||||||
|
ret0, _ := ret[0].(Sensor)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSensor indicates an expected call of ReadSensor.
|
||||||
|
func (mr *MockRepositoryMockRecorder) ReadSensor(sensorID any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadSensor", reflect.TypeOf((*MockRepository)(nil).ReadSensor), sensorID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSensorValues mocks base method.
|
||||||
|
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].([]SensorData)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadSensorValues indicates an expected call of ReadSensorValues.
|
||||||
|
func (mr *MockRepositoryMockRecorder) ReadSensorValues(sensorID, from, to any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadSensorValues", reflect.TypeOf((*MockRepository)(nil).ReadSensorValues), sensorID, from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSensor mocks base method.
|
||||||
|
func (m *MockRepository) UpdateSensor(s Sensor) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UpdateSensor", s)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSensor indicates an expected call of UpdateSensor.
|
||||||
|
func (mr *MockRepositoryMockRecorder) UpdateSensor(s any) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSensor", reflect.TypeOf((*MockRepository)(nil).UpdateSensor), s)
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
package sensors
|
package sensors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
repo Repository
|
repo Repository
|
||||||
}
|
}
|
||||||
@@ -9,3 +15,88 @@ func NewService(repo Repository) *Service {
|
|||||||
repo: repo,
|
repo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateSensor(sensor Sensor) error {
|
||||||
|
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(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) {
|
||||||
|
return s.repo.ReadAllSensors()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: ×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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,117 @@
|
|||||||
package sensors
|
package sensors
|
||||||
|
|
||||||
type Simulator struct{}
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"math/rand"
|
||||||
|
"nats-app/internal/broker"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Simulator struct {
|
||||||
|
*broker.NATS
|
||||||
|
stopChannels map[string]chan bool
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func Start(nats *broker.NATS) *Simulator {
|
||||||
|
return &Simulator{
|
||||||
|
NATS: nats,
|
||||||
|
stopChannels: make(map[string]chan bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
s.stopChannels[sensor.SensorID] = stopChan
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(*sensor.SamplingInterval * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopChan:
|
||||||
|
slog.Info("stopping simulator for sensor", "sensor_id", sensor.SensorID)
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
data := s.generateData(sensor)
|
||||||
|
|
||||||
|
if data.SensorID == "" {
|
||||||
|
slog.Warn("sensor data generation failed", "sensor_id", sensor.SensorID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to marshal sensor data", "error", err, "sensor_id", sensor.SensorID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := subjectSensorsData + sensor.SensorID
|
||||||
|
if err := s.Publish(subject, payload); err != nil {
|
||||||
|
slog.Error("failed to publish sensor data", "error", err, "subject", subject)
|
||||||
|
} else {
|
||||||
|
slog.Debug("sensor data published", "sensor_id", sensor.SensorID, "value", data.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
stopChan <- true
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.stopChannels, sensor.SensorID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
go s.SimulateSensor(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{
|
||||||
|
SensorID: sensor.SensorID,
|
||||||
|
Timestamp: &now,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rand.Float64() < 0.05 {
|
||||||
|
return SensorData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var value float64
|
||||||
|
switch sensor.SensorType {
|
||||||
|
case Temperature:
|
||||||
|
value = -20 + rand.Float64()*100
|
||||||
|
case Humidity:
|
||||||
|
value = 10 + rand.Float64()*90
|
||||||
|
case CarbonDioxide:
|
||||||
|
value = 980 + rand.Float64()*60
|
||||||
|
case Pressure:
|
||||||
|
value = 950 + rand.Float64()*100
|
||||||
|
case Proximity:
|
||||||
|
value = rand.Float64() * 400
|
||||||
|
case Light:
|
||||||
|
value = rand.Float64() * 10000
|
||||||
|
default:
|
||||||
|
value = rand.Float64() * 100
|
||||||
|
}
|
||||||
|
data.Value = &value
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user