improve validate and tests

This commit is contained in:
Pedro Pérez 2025-10-09 17:29:05 +02:00
parent 92640d1ad0
commit 6d3408f34c
2 changed files with 659 additions and 0 deletions

View File

@ -2,6 +2,7 @@ package sensors
import (
"errors"
"slices"
"time"
)
@ -13,6 +14,12 @@ func (s *Sensor) Validate() error {
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

View File

@ -0,0 +1,652 @@
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_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: 150.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdAbove: ptr(100.0),
},
expected: true,
},
{
name: "value below threshold",
data: SensorData{
SensorID: "temp-001",
Value: 50.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdAbove: ptr(100.0),
},
expected: false,
},
{
name: "value equal to threshold",
data: SensorData{
SensorID: "temp-001",
Value: 100.0,
Timestamp: 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: -5.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdAbove: ptr(-10.0),
},
expected: true,
},
}
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: 5.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdBelow: ptr(10.0),
},
expected: true,
},
{
name: "value above threshold",
data: SensorData{
SensorID: "temp-001",
Value: 50.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdBelow: ptr(10.0),
},
expected: false,
},
{
name: "value equal to threshold",
data: SensorData{
SensorID: "temp-001",
Value: 10.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdBelow: ptr(10.0),
},
expected: false,
},
{
name: "negative value below threshold",
data: SensorData{
SensorID: "temp-001",
Value: -15.0,
Timestamp: 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: 0.0,
Timestamp: time.Now(),
},
sensor: Sensor{
SensorID: "temp-001",
ThresholdBelow: ptr(5.0),
},
expected: true,
},
}
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
}