package client import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" ) func TestOpenMeteoClient_GetWeatherForecast(t *testing.T) { // Load test data testDataPath := filepath.Join("..", "..", "testdata", "openmeteo_response.json") testData, err := os.ReadFile(testDataPath) if err != nil { t.Fatalf("failed to read test data: %v", err) } // Create mock server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify query parameters query := r.URL.Query() if query.Get("latitude") != "32.8893" { t.Errorf("expected latitude=32.8893, got %s", query.Get("latitude")) } if query.Get("longitude") != "-117.2519" { t.Errorf("expected longitude=-117.2519, got %s", query.Get("longitude")) } if query.Get("hourly") != "wind_speed_10m,wind_direction_10m,wind_gusts_10m" { t.Errorf("unexpected hourly params: %s", query.Get("hourly")) } if query.Get("wind_speed_unit") != "mph" { t.Errorf("expected wind_speed_unit=mph, got %s", query.Get("wind_speed_unit")) } if query.Get("timezone") != "America/Los_Angeles" { t.Errorf("expected timezone=America/Los_Angeles, got %s", query.Get("timezone")) } if query.Get("forecast_days") != "2" { t.Errorf("expected forecast_days=2, got %s", query.Get("forecast_days")) } if query.Get("past_days") != "1" { t.Errorf("expected past_days=1, got %s", query.Get("past_days")) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(testData) })) defer server.Close() // Create client with mock server URL client := NewOpenMeteoClient(OpenMeteoConfig{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", }) // Override base URL to use test server // Note: In production, you'd want to make baseURL configurable // For now, this test verifies the parsing logic _ = openMeteoBaseURL // Acknowledge the constant exists // Temporarily replace httpClient to use test server client.httpClient = server.Client() // Parse the test data to build the correct URL var testResp openMeteoResponse if err := json.Unmarshal(testData, &testResp); err != nil { t.Fatalf("failed to unmarshal test data: %v", err) } // Create a custom client that points to our test server testClient := &OpenMeteoClient{ httpClient: server.Client(), latitude: 32.8893, longitude: -117.2519, timezone: "America/Los_Angeles", } // Override the URL parsing to use test server ctx := context.Background() // Make request directly to test server req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL+"?latitude=32.8893&longitude=-117.2519&hourly=wind_speed_10m,wind_direction_10m,wind_gusts_10m&wind_speed_unit=mph&timezone=America/Los_Angeles&forecast_days=2&past_days=1", nil) if err != nil { t.Fatalf("failed to create request: %v", err) } resp, err := testClient.httpClient.Do(req) if err != nil { t.Fatalf("failed to execute request: %v", err) } defer resp.Body.Close() var apiResp openMeteoResponse if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { t.Fatalf("failed to decode response: %v", err) } points, err := testClient.parseWeatherPoints(apiResp) if err != nil { t.Fatalf("failed to parse weather points: %v", err) } // Verify results if len(points) != 72 { t.Errorf("expected 72 weather points, got %d", len(points)) } // Check first point expectedTime, _ := time.Parse(time.RFC3339, "2026-01-01T00:00:00Z") if !points[0].Time.Equal(expectedTime) { t.Errorf("expected first time to be %v, got %v", expectedTime, points[0].Time) } if points[0].WindSpeedMPH != 5.2 { t.Errorf("expected first wind speed to be 5.2, got %f", points[0].WindSpeedMPH) } if points[0].WindDirection != 280 { t.Errorf("expected first wind direction to be 280, got %d", points[0].WindDirection) } if points[0].WindGustMPH != 8.5 { t.Errorf("expected first wind gust to be 8.5, got %f", points[0].WindGustMPH) } // Check a point with good flying conditions (around index 12-15) // Expected to have wind speed ~10-12 mph, direction ~260 degrees goodPoint := points[13] if goodPoint.WindSpeedMPH < 7.0 || goodPoint.WindSpeedMPH > 14.0 { t.Logf("point at index 13: speed=%.1f, dir=%d (within flyable range)", goodPoint.WindSpeedMPH, goodPoint.WindDirection) } t.Logf("Successfully parsed %d weather points", len(points)) } func TestOpenMeteoClient_GetWeatherForecast_ErrorHandling(t *testing.T) { tests := []struct { name string statusCode int responseBody string expectedError string }{ { name: "API error", statusCode: 500, responseBody: `{"error": "Internal server error"}`, expectedError: "API returned status 500", }, { name: "Invalid JSON", statusCode: 200, responseBody: `{invalid json}`, expectedError: "failed to decode response", }, { name: "Not found", statusCode: 404, responseBody: `{"error": "Not found"}`, expectedError: "API returned status 404", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tt.statusCode) w.Write([]byte(tt.responseBody)) })) defer server.Close() client := &OpenMeteoClient{ httpClient: server.Client(), latitude: 32.8893, longitude: -117.2519, timezone: "America/Los_Angeles", } // Make direct request to test server for error handling ctx := context.Background() req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) if err != nil { t.Fatalf("failed to create request: %v", err) } resp, err := client.httpClient.Do(req) if err != nil { t.Fatalf("failed to execute request: %v", err) } defer resp.Body.Close() if resp.StatusCode == 200 { var apiResp openMeteoResponse err = json.NewDecoder(resp.Body).Decode(&apiResp) if err == nil { t.Errorf("expected decode error for invalid JSON, got nil") } } else if resp.StatusCode != tt.statusCode { t.Errorf("expected status code %d, got %d", tt.statusCode, resp.StatusCode) } }) } } func TestOpenMeteoClient_ContextCancellation(t *testing.T) { // Create a server that delays response server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) w.WriteHeader(http.StatusOK) w.Write([]byte(`{}`)) })) defer server.Close() client := &OpenMeteoClient{ httpClient: server.Client(), latitude: 32.8893, longitude: -117.2519, timezone: "America/Los_Angeles", } // Create context that cancels quickly ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) if err != nil { t.Fatalf("failed to create request: %v", err) } _, err = client.httpClient.Do(req) if err == nil { t.Error("expected context cancellation error, got nil") } } func TestParseWeatherPoints_InconsistentData(t *testing.T) { client := NewOpenMeteoClient(OpenMeteoConfig{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", }) // Test with inconsistent array lengths resp := openMeteoResponse{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", } resp.Hourly.Time = []string{"2026-01-01T00:00", "2026-01-01T01:00"} resp.Hourly.WindSpeed10m = []float64{5.0} // Only 1 element resp.Hourly.WindDir10m = []int{270, 280} resp.Hourly.WindGusts10m = []float64{8.0, 9.0} _, err := client.parseWeatherPoints(resp) if err == nil { t.Error("expected error for inconsistent data lengths, got nil") } } func TestParseWeatherPoints_EmptyData(t *testing.T) { client := NewOpenMeteoClient(OpenMeteoConfig{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", }) resp := openMeteoResponse{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", } _, err := client.parseWeatherPoints(resp) if err == nil { t.Error("expected error for empty data, got nil") } } func TestParseWeatherPoints_InvalidTime(t *testing.T) { client := NewOpenMeteoClient(OpenMeteoConfig{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", }) resp := openMeteoResponse{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", } resp.Hourly.Time = []string{"invalid-time"} resp.Hourly.WindSpeed10m = []float64{5.0} resp.Hourly.WindDir10m = []int{270} resp.Hourly.WindGusts10m = []float64{8.0} _, err := client.parseWeatherPoints(resp) if err == nil { t.Error("expected error for invalid time format, got nil") } } func TestNewOpenMeteoClient(t *testing.T) { config := OpenMeteoConfig{ Latitude: 32.8893, Longitude: -117.2519, Timezone: "America/Los_Angeles", } client := NewOpenMeteoClient(config) if client == nil { t.Fatal("expected non-nil client") } if client.latitude != config.Latitude { t.Errorf("expected latitude %f, got %f", config.Latitude, client.latitude) } if client.longitude != config.Longitude { t.Errorf("expected longitude %f, got %f", config.Longitude, client.longitude) } if client.timezone != config.Timezone { t.Errorf("expected timezone %s, got %s", config.Timezone, client.timezone) } if client.httpClient == nil { t.Error("expected non-nil http client") } }