package usecase import ( "errors" "testing" "time" "users_management/m/model/dto/req" "users_management/m/model/entity" "github.com/google/uuid" ) type mockNearestDeviceRepo struct { GetNearestDevicesFunc func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) GetDeviceByIDWithConnectionsFunc func(id uuid.UUID) (entity.Device, error) CountConnectionsByDeviceIDFunc func(deviceID uuid.UUID) (int, int, int, error) } func (m *mockNearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) { if m.GetNearestDevicesFunc != nil { return m.GetNearestDevicesFunc(longitude, latitude, radius, limit, province, city, district, deviceType) } return nil, nil } func (m *mockNearestDeviceRepo) GetDeviceByIDWithConnections(id uuid.UUID) (entity.Device, error) { if m.GetDeviceByIDWithConnectionsFunc != nil { return m.GetDeviceByIDWithConnectionsFunc(id) } return entity.Device{}, nil } func (m *mockNearestDeviceRepo) GetBackbonesByDeviceID(deviceID uuid.UUID) ([]entity.Backbone, error) { return nil, nil } func (m *mockNearestDeviceRepo) GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) { return nil, nil } func (m *mockNearestDeviceRepo) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) { return nil, nil } func (m *mockNearestDeviceRepo) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) { if m.CountConnectionsByDeviceIDFunc != nil { return m.CountConnectionsByDeviceIDFunc(deviceID) } return 0, 0, 0, nil } func (m *mockNearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) { return nil, nil } func (m *mockNearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) { return entity.Tower{}, nil } type mockGeocoder struct{} func (m *mockGeocoder) GetAddressFromCoordinates(lat, lng float64) (string, error) { return "Mock Address, City", nil } func TestGetNearestDevices_Success(t *testing.T) { deviceID := uuid.New() provinceStr := "Jakarta" cityStr := "Jakarta Selatan" mockRepo := &mockNearestDeviceRepo{ GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, prov, cty, district, deviceType *string) ([]entity.DeviceWithDistance, error) { if longitude != 106.8 { t.Errorf("expected longitude 106.8, got %f", longitude) } if latitude != -6.2 { t.Errorf("expected latitude -6.2, got %f", latitude) } if radius != 5.0 { t.Errorf("expected radius 5.0, got %f", radius) } if limit != 10 { t.Errorf("expected limit 10, got %d", limit) } _ = prov _ = cty return []entity.DeviceWithDistance{ { ID: deviceID, DeviceCode: "ODP-001", DeviceType: entity.ODP, Longitude: 106.81, Latitude: -6.21, PortAmount: 8, Status: entity.ActiveDev, Province: &provinceStr, City: &cityStr, Distance: 1.5, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, }, nil }, CountConnectionsByDeviceIDFunc: func(deviceID uuid.UUID) (int, int, int, error) { return 2, 3, 1, nil }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 106.8, Latitude: -6.2, Radius: 0, Limit: 0, } devices, err := uc.GetNearestDevices(request) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(devices) != 1 { t.Fatalf("expected 1 device, got %d", len(devices)) } if devices[0].DeviceCode != "ODP-001" { t.Errorf("expected device code ODP-001, got %s", devices[0].DeviceCode) } } func TestGetNearestDevices_WithDeviceTypeFilter(t *testing.T) { deviceType := "ODP" mockRepo := &mockNearestDeviceRepo{ GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) { if deviceType == nil || *deviceType != "ODP" { t.Errorf("expected deviceType filter to be ODP") } return []entity.DeviceWithDistance{ { ID: uuid.New(), DeviceCode: "ODP-002", DeviceType: entity.ODP, Longitude: 106.82, Latitude: -6.22, PortAmount: 16, Status: entity.ActiveDev, Distance: 2.5, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, }, nil }, CountConnectionsByDeviceIDFunc: func(deviceID uuid.UUID) (int, int, int, error) { return 1, 2, 0, nil }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 106.8, Latitude: -6.2, Radius: 5.0, Limit: 10, DeviceType: &deviceType, } devices, err := uc.GetNearestDevices(request) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(devices) != 1 { t.Fatalf("expected 1 device, got %d", len(devices)) } if devices[0].DeviceType != "ODP" { t.Errorf("expected device type ODP, got %s", devices[0].DeviceType) } } func TestGetNearestDevices_RepositoryError(t *testing.T) { mockRepo := &mockNearestDeviceRepo{ GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) { return nil, errors.New("database connection failed") }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 106.8, Latitude: -6.2, } _, err := uc.GetNearestDevices(request) if err == nil { t.Fatal("expected error, got nil") } } func TestGetNearestDevices_EmptyResult(t *testing.T) { mockRepo := &mockNearestDeviceRepo{ GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) { return []entity.DeviceWithDistance{}, nil }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 106.8, Latitude: -6.2, } devices, err := uc.GetNearestDevices(request) if err != nil { t.Fatalf("expected no error, got %v", err) } if len(devices) != 0 { t.Errorf("expected 0 devices, got %d", len(devices)) } } func TestGetNearestDevices_ValidationError(t *testing.T) { mockRepo := &mockNearestDeviceRepo{} uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 0, Latitude: 0, } _, err := uc.GetNearestDevices(request) if err == nil { t.Fatal("expected validation error for missing required fields") } } func TestGetNearestDevices_CustomRadiusAndLimit(t *testing.T) { customRadius := 0.3 customLimit := 5 mockRepo := &mockNearestDeviceRepo{ GetNearestDevicesFunc: func(longitude, latitude, radius float64, limit int, province, city, district, deviceType *string) ([]entity.DeviceWithDistance, error) { if radius != customRadius { t.Errorf("expected radius %f, got %f", customRadius, radius) } if limit != customLimit { t.Errorf("expected limit %d, got %d", customLimit, limit) } return []entity.DeviceWithDistance{}, nil }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) request := req.NearestDeviceDTO{ Longitude: 106.8, Latitude: -6.2, Radius: customRadius, Limit: customLimit, } _, err := uc.GetNearestDevices(request) if err != nil { t.Fatalf("expected no error, got %v", err) } } func TestCalculateDistance(t *testing.T) { tests := []struct { name string lat1 float64 lng1 float64 lat2 float64 lng2 float64 expected float64 delta float64 }{ { name: "Same point", lat1: -6.2, lng1: 106.8, lat2: -6.2, lng2: 106.8, expected: 0, delta: 0.001, }, { name: "Jakarta to Bandung", lat1: -6.2088, lng1: 106.8456, lat2: -6.9175, lng2: 107.6191, expected: 115.0, delta: 5.0, }, { name: "Short distance ~350m", lat1: -6.2, lng1: 106.8, lat2: -6.20225, lng2: 106.80225, expected: 0.35, delta: 0.05, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := calculateDistance(tt.lat1, tt.lng1, tt.lat2, tt.lng2) if result < tt.expected-tt.delta || result > tt.expected+tt.delta { t.Errorf("calculateDistance(%f, %f, %f, %f) = %f, expected %f ± %f", tt.lat1, tt.lng1, tt.lat2, tt.lng2, result, tt.expected, tt.delta) } }) } } func TestGetNearestDeviceByID_Success(t *testing.T) { deviceID := uuid.New() province := "Jakarta" city := "Jakarta" mockRepo := &mockNearestDeviceRepo{ GetDeviceByIDWithConnectionsFunc: func(id uuid.UUID) (entity.Device, error) { if id != deviceID { t.Errorf("expected device ID %s, got %s", deviceID, id) } return entity.Device{ ID: deviceID, DeviceCode: "ODP-003", DeviceType: entity.ODP, Longitude: 106.81, Latitude: -6.21, PortAmount: 8, Status: entity.ActiveDev, Province: &province, City: &city, CreatedAt: time.Now(), UpdatedAt: time.Now(), }, nil }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) device, err := uc.GetNearestDeviceByID(deviceID, -6.2, 106.8) if err != nil { t.Fatalf("expected no error, got %v", err) } if device.DeviceCode != "ODP-003" { t.Errorf("expected device code ODP-003, got %s", device.DeviceCode) } if device.Distance <= 0 { t.Errorf("expected positive distance, got %f", device.Distance) } } func TestGetNearestDeviceByID_NotFound(t *testing.T) { deviceID := uuid.New() mockRepo := &mockNearestDeviceRepo{ GetDeviceByIDWithConnectionsFunc: func(id uuid.UUID) (entity.Device, error) { return entity.Device{}, errors.New("record not found") }, } uc := NewNearestDeviceUseCase(mockRepo, &mockGeocoder{}) _, err := uc.GetNearestDeviceByID(deviceID, -6.2, 106.8) if err == nil { t.Fatal("expected error for not found device") } }