spotpricehistory_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package aws
  2. import (
  3. "errors"
  4. "sync"
  5. "sync/atomic"
  6. "testing"
  7. "time"
  8. )
  9. type mockSpotPriceHistoryFetcher struct {
  10. fetchFunc func(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error)
  11. }
  12. func (m *mockSpotPriceHistoryFetcher) FetchSpotPrice(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error) {
  13. if m.fetchFunc != nil {
  14. return m.fetchFunc(key)
  15. }
  16. return &SpotPriceHistoryEntry{
  17. SpotPrice: 0.05,
  18. Timestamp: time.Now(),
  19. RetrievedAt: time.Now(),
  20. }, nil
  21. }
  22. func TestSpotPriceHistoryCache_GetSpotPrice_CacheHit(t *testing.T) {
  23. mockFetcher := &mockSpotPriceHistoryFetcher{}
  24. cache := NewSpotPriceHistoryCache(mockFetcher)
  25. region := "us-west-2"
  26. instanceType := "m5.large"
  27. availabilityZone := "us-west-2a"
  28. key := SpotPriceHistoryKey{
  29. Region: region,
  30. InstanceType: instanceType,
  31. AvailabilityZone: availabilityZone,
  32. }
  33. cachedEntry := &SpotPriceHistoryEntry{
  34. SpotPrice: 0.08,
  35. Timestamp: time.Now(),
  36. RetrievedAt: time.Now(),
  37. }
  38. cache.cache[key] = cachedEntry
  39. entry, err := cache.GetSpotPrice(region, instanceType, availabilityZone)
  40. if err != nil {
  41. t.Errorf("Expected no error, got %v", err)
  42. }
  43. if entry.SpotPrice != 0.08 {
  44. t.Errorf("Expected spot price 0.08, got %f", entry.SpotPrice)
  45. }
  46. }
  47. func TestSpotPriceHistoryCache_GetSpotPrice_CacheMiss(t *testing.T) {
  48. fetchCalled := false
  49. mockFetcher := &mockSpotPriceHistoryFetcher{
  50. fetchFunc: func(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error) {
  51. fetchCalled = true
  52. return &SpotPriceHistoryEntry{
  53. SpotPrice: 0.12,
  54. Timestamp: time.Now(),
  55. RetrievedAt: time.Now(),
  56. }, nil
  57. },
  58. }
  59. cache := NewSpotPriceHistoryCache(mockFetcher)
  60. entry, err := cache.GetSpotPrice("us-west-2", "m5.large", "us-west-2a")
  61. if err != nil {
  62. t.Errorf("Expected no error, got %v", err)
  63. }
  64. if !fetchCalled {
  65. t.Error("Expected fetcher to be called for cache miss")
  66. }
  67. if entry.SpotPrice != 0.12 {
  68. t.Errorf("Expected spot price 0.12, got %f", entry.SpotPrice)
  69. }
  70. }
  71. func TestSpotPriceHistoryCache_GetSpotPrice_ConcurrentSameKey(t *testing.T) {
  72. var fetchCount atomic.Int32
  73. mockFetcher := &mockSpotPriceHistoryFetcher{
  74. fetchFunc: func(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error) {
  75. fetchCount.Add(1)
  76. // Simulate slow API call to increase chance of concurrent access
  77. time.Sleep(50 * time.Millisecond)
  78. return &SpotPriceHistoryEntry{
  79. SpotPrice: 0.07,
  80. Timestamp: time.Now(),
  81. RetrievedAt: time.Now(),
  82. }, nil
  83. },
  84. }
  85. cache := NewSpotPriceHistoryCache(mockFetcher)
  86. const goroutines = 10
  87. var wg sync.WaitGroup
  88. wg.Add(goroutines)
  89. for i := 0; i < goroutines; i++ {
  90. go func() {
  91. defer wg.Done()
  92. entry, err := cache.GetSpotPrice("us-west-2", "m5.large", "us-west-2a")
  93. if err != nil {
  94. t.Errorf("Expected no error, got %v", err)
  95. }
  96. if entry.SpotPrice != 0.07 {
  97. t.Errorf("Expected spot price 0.07, got %f", entry.SpotPrice)
  98. }
  99. }()
  100. }
  101. wg.Wait()
  102. if count := fetchCount.Load(); count != 1 {
  103. t.Errorf("Expected exactly 1 fetch call, got %d", count)
  104. }
  105. }
  106. func TestSpotPriceHistoryCache_GetSpotPrice_StaleEntry(t *testing.T) {
  107. fetchCalled := false
  108. mockFetcher := &mockSpotPriceHistoryFetcher{
  109. fetchFunc: func(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error) {
  110. fetchCalled = true
  111. return &SpotPriceHistoryEntry{
  112. SpotPrice: 0.15,
  113. Timestamp: time.Now(),
  114. RetrievedAt: time.Now(),
  115. }, nil
  116. },
  117. }
  118. cache := NewSpotPriceHistoryCache(mockFetcher)
  119. key := SpotPriceHistoryKey{
  120. Region: "us-west-2",
  121. InstanceType: "m5.large",
  122. AvailabilityZone: "us-west-2a",
  123. }
  124. staleEntry := &SpotPriceHistoryEntry{
  125. SpotPrice: 0.08,
  126. Timestamp: time.Now(),
  127. RetrievedAt: time.Now().Add(-2 * time.Hour),
  128. }
  129. cache.cache[key] = staleEntry
  130. entry, err := cache.GetSpotPrice("us-west-2", "m5.large", "us-west-2a")
  131. if err != nil {
  132. t.Errorf("Expected no error, got %v", err)
  133. }
  134. if !fetchCalled {
  135. t.Error("Expected fetcher to be called for stale entry")
  136. }
  137. if entry.SpotPrice != 0.15 {
  138. t.Errorf("Expected refreshed spot price 0.15, got %f", entry.SpotPrice)
  139. }
  140. }
  141. func TestSpotPriceHistoryCache_GetSpotPrice_FetchError(t *testing.T) {
  142. expectedError := errors.New("fetch failed")
  143. mockFetcher := &mockSpotPriceHistoryFetcher{
  144. fetchFunc: func(key SpotPriceHistoryKey) (*SpotPriceHistoryEntry, error) {
  145. return nil, expectedError
  146. },
  147. }
  148. cache := NewSpotPriceHistoryCache(mockFetcher)
  149. _, err := cache.GetSpotPrice("us-west-2", "m5.large", "us-west-2a")
  150. if err == nil {
  151. t.Error("Expected error from failed fetch")
  152. }
  153. if !errors.Is(err, expectedError) {
  154. t.Errorf("Expected error %v, got %v", expectedError, err)
  155. }
  156. key := SpotPriceHistoryKey{
  157. Region: "us-west-2",
  158. InstanceType: "m5.large",
  159. AvailabilityZone: "us-west-2a",
  160. }
  161. cachedEntry := cache.cache[key]
  162. if !errors.Is(cachedEntry.Error, expectedError) {
  163. t.Errorf("Expected cached entry error %v, got %v", expectedError, cachedEntry.Error)
  164. }
  165. }
  166. func TestSpotPriceHistoryEntry_shouldRefresh(t *testing.T) {
  167. now := time.Now()
  168. tests := []struct {
  169. name string
  170. retrievedAt time.Time
  171. expected bool
  172. }{
  173. {
  174. name: "fresh entry",
  175. retrievedAt: now,
  176. expected: false,
  177. },
  178. {
  179. name: "stale entry",
  180. retrievedAt: now.Add(-2 * time.Hour),
  181. expected: true,
  182. },
  183. {
  184. name: "borderline entry",
  185. retrievedAt: now.Add(-SpotPriceHistoryCacheAge + 1*time.Minute),
  186. expected: false,
  187. },
  188. {
  189. name: "expired entry",
  190. retrievedAt: now.Add(-SpotPriceHistoryCacheAge - 1*time.Minute),
  191. expected: true,
  192. },
  193. }
  194. for _, tt := range tests {
  195. t.Run(tt.name, func(t *testing.T) {
  196. entry := SpotPriceHistoryEntry{
  197. RetrievedAt: tt.retrievedAt,
  198. }
  199. if got := entry.shouldRefresh(); got != tt.expected {
  200. t.Errorf("shouldRefresh() = %v, want %v", got, tt.expected)
  201. }
  202. })
  203. }
  204. }
  205. func TestSpotPriceHistoryKey_String(t *testing.T) {
  206. key := SpotPriceHistoryKey{
  207. Region: "us-west-2",
  208. InstanceType: "m5.large",
  209. AvailabilityZone: "us-west-2a",
  210. }
  211. expected := "us-west-2/m5.large/us-west-2a"
  212. if got := key.String(); got != expected {
  213. t.Errorf("String() = %v, want %v", got, expected)
  214. }
  215. }