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: 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, }, } 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, }, } 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 }