init
This commit is contained in:
328
backend/internal/client/openmeteo_test.go
Normal file
328
backend/internal/client/openmeteo_test.go
Normal file
@@ -0,0 +1,328 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user