Files
paragliding/backend/internal/client/openmeteo_test.go
2026-01-03 14:16:16 -08:00

329 lines
9.4 KiB
Go

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")
}
}