package timezone_test import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "strings" "testing" "time" "github.com/fiskerinc/cloud-services/pkg/redis" "github.com/fiskerinc/cloud-services/pkg/redis/tester" "github.com/fiskerinc/cloud-services/pkg/testhelper" "github.com/fiskerinc/cloud-services/pkg/timezone" "github.com/fiskerinc/cloud-services/pkg/validator" ) var someTime, _ = time.Parse(time.RFC3339Nano, "2023-12-27T18:09:41.0701652Z") func mockTime(timezone string) time.Time { loc, _ := time.LoadLocation(timezone) return someTime.In(loc) } type MockClient struct { DoFunc func(req *http.Request) (*http.Response, error) } func (m *MockClient) Do(req *http.Request) (*http.Response, error) { return m.DoFunc(req) } var mocks = map[string]timezone.AzureTimezoneResult{ "52.520000,13.405000": { Version: "2023c", ReferenceUtcTimestamp: someTime, Timezones: []timezone.AzureTimezone{ { ID: "Europe/Berlin", ReferenceTime: timezone.AzureReferenceTime{ Tag: "CET", StandardOffset: "01:00:00", DaylightSavings: "00:00:00", WallTime: mockTime("Europe/Berlin"), }, }, }, }, "37.774899,122.419403": { Version: "2023c", ReferenceUtcTimestamp: someTime, Timezones: []timezone.AzureTimezone{ { ID: "Etc/GMT-8", ReferenceTime: timezone.AzureReferenceTime{ Tag: "Etc/GMT-8", StandardOffset: "-08:00:00", DaylightSavings: "01:00:00", WallTime: mockTime("America/Los_Angeles"), }, }, }, }, "-31.546177,159.083191": { Version: "2023d", ReferenceUtcTimestamp: someTime, Timezones: []timezone.AzureTimezone{ { ID: "Australia/Lord_Howe", ReferenceTime: timezone.AzureReferenceTime{ Tag: "+11", StandardOffset: "10:30:00", DaylightSavings: "00:30:00", WallTime: mockTime("Australia/Lord_Howe"), }, }, }, }, } type TestTimezone struct { Name string Latitude float32 Longitude float32 Response timezone.Timezone ExpectedError string } func TestGetTimezoneByCoordinate(t *testing.T) { mockAzureTimezoneAPI := &MockClient{ DoFunc: func(r *http.Request) (*http.Response, error) { query := r.URL.Query().Get("query") coords := strings.Split(query, ",") if payload, ok := mocks[query]; ok { resp, err := json.Marshal(payload) if err == nil { return &http.Response{ StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader(resp)), }, nil } } outOfBounds := fmt.Sprintf("latitude %s is out of bounds. longitude %s is out of bounds", coords[0], coords[1]) return &http.Response{ StatusCode: 400, Body: ioutil.NopCloser(bytes.NewBufferString(outOfBounds)), }, nil }, } timezone.SetTimezoneService(&timezone.TimezoneService{ Client: mockAzureTimezoneAPI, }) var tests = []TestTimezone{ { Name: "Berlin", Latitude: 52.5200, Longitude: 13.4050, Response: timezone.Timezone{ ID: "CET", Name: "Europe/Berlin", Offset: 3600, DST: false, }, }, { Name: "San Francisco", Latitude: 37.7749, Longitude: 122.4194, Response: timezone.Timezone{ ID: "Etc/GMT-8", Name: "Etc/GMT-8", Offset: -25200, DST: true, }, }, { Name: "Australia", Latitude: -31.5461769, Longitude: 159.0831909, Response: timezone.Timezone{ ID: "+11", Name: "Australia/Lord_Howe", Offset: 39600, DST: true, }, }, { Name: "Saturn", Latitude: 1832.7749, Longitude: 2378.4194, ExpectedError: "latitude 1832.774902 is out of bounds", }, } for _, tt := range tests { err := validator.ValidateStruct(tt) if err != nil { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Response, err) } actual, err := timezone.GetTimezoneService().GetTimezoneByCoordinate(tt.Latitude, tt.Longitude) if err != nil { if err.Error() == tt.ExpectedError { continue } t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Response, err) } compare(t, *actual, tt.Response) } } type TestTimezoneIntegration struct { Name string Latitude float32 Longitude float32 Timezone timezone.Timezone } func TestGetTimezoneByCoordinate_Integration(t *testing.T) { t.Skip() t.Setenv("AZURE_TIMEZONE_API_KEY", "") // Temporarily paste API key (see .env or README.md) var tests = []TestTimezoneIntegration{ { Name: "Berlin", Latitude: 52, Longitude: 13, Timezone: timezone.Timezone{ ID: "CET", Name: "Europe/Berlin", Offset: 1, DST: false, }, }, { Name: "San Francisco", Latitude: 37.7749, Longitude: 122.4194, Timezone: timezone.Timezone{ ID: "Etc/GMT-8", Name: "Etc/GMT-8", Offset: -8, DST: false, // data is not historical, this could fail on time of year }, }, } for _, tt := range tests { actual, err := timezone.GetTimezoneService().GetTimezoneByCoordinate(tt.Latitude, tt.Longitude) if err != nil { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Timezone, err) } compare(t, *actual, tt.Timezone) } } type TestTimezoneCache struct { Name string QuadKey string Timezone timezone.Timezone } func TestGetCachedTimezoneByQuadkey(t *testing.T) { conn := tester.NewRedisMock() conn.GetObjectResults = map[string]string{ "timezone:1321013300201": `{"ID":"Etc/GMT-8","Name":"Etc/GMT-8","Offset":-8,"DST":false}`, } tests := []TestTimezoneCache{ { Name: "San Francisco", QuadKey: "1321013300201123120", Timezone: timezone.Timezone{ ID: "Etc/GMT-8", Name: "Etc/GMT-8", Offset: -8, DST: false, }, }, } for _, tt := range tests { actual, err := timezone.GetTimezoneService().GetCachedTimezoneByQuadkey(conn, tt.QuadKey) if err != nil { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Timezone, err) } compare(t, *actual, tt.Timezone) } } func TestSetCachedTimezoneByQuadkey(t *testing.T) { conn := tester.NewRedisMock() tests := []TestTimezoneCache{ { Name: "San Francisco", QuadKey: "1321013300201123120", Timezone: timezone.Timezone{ ID: "Etc/GMT-8", Name: "Etc/GMT-8", Offset: -8, DST: false, }, }, } for _, tt := range tests { err := timezone.GetTimezoneService().SetCachedTimezoneByQuadkey(conn, tt.QuadKey, &tt.Timezone) if err != nil { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Timezone, err) } // lookup key set in redis mock key := redis.TimezoneQuadKey(tt.QuadKey, 13) value, ok := conn.FetchCache(key) if !ok { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Timezone, err) } // convert redis value back to timezone.Timezone var actual timezone.Timezone temp, _ := json.Marshal(value.Value) err = json.Unmarshal(temp, &actual) if err != nil { t.Errorf(testhelper.TestErrorTemplate, tt.Name, tt.Timezone, err) } compare(t, actual, tt.Timezone) } } func compare(t *testing.T, actual, expected timezone.Timezone) { if actual.Name != expected.Name { t.Errorf("expected name %s, got %s", expected.Name, actual.Name) } if actual.ID != expected.ID { t.Errorf("expected id %s, got %s", expected.ID, actual.ID) } if actual.Offset != expected.Offset { t.Errorf("expected offset %d, got %d", expected.Offset, actual.Offset) } if actual.DST != expected.DST { t.Errorf("expected dst %t, got %t", expected.DST, actual.DST) } }