diff --git a/internal/domains/sensors/domain.go b/internal/domains/sensors/domain.go index 240c663..30144e2 100644 --- a/internal/domains/sensors/domain.go +++ b/internal/domains/sensors/domain.go @@ -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 diff --git a/internal/domains/sensors/domain_test.go b/internal/domains/sensors/domain_test.go new file mode 100644 index 0000000..860da70 --- /dev/null +++ b/internal/domains/sensors/domain_test.go @@ -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 +}