|
1 | | -import XCTest |
| 1 | +import Foundation |
| 2 | +import Testing |
2 | 3 | @testable import Kaset |
3 | 4 |
|
4 | 5 | /// Tests for APICache. |
| 6 | +@Suite(.serialized) |
5 | 7 | @MainActor |
6 | | -final class APICacheTests: XCTestCase { |
7 | | - var cache: APICache! |
| 8 | +struct APICacheTests { |
| 9 | + var cache: APICache |
8 | 10 |
|
9 | | - override func setUp() async throws { |
| 11 | + init() { |
10 | 12 | self.cache = APICache.shared |
11 | 13 | self.cache.invalidateAll() |
12 | 14 | } |
13 | 15 |
|
14 | | - override func tearDown() async throws { |
15 | | - self.cache.invalidateAll() |
16 | | - self.cache = nil |
17 | | - } |
18 | | - |
19 | | - func testCacheSetAndGet() { |
| 16 | + @Test("Cache set and get") |
| 17 | + func cacheSetAndGet() { |
20 | 18 | let data: [String: Any] = ["key": "value", "number": 42] |
21 | | - self.cache.set(key: "test_key", data: data, ttl: 60) |
| 19 | + cache.set(key: "test_key", data: data, ttl: 60) |
22 | 20 |
|
23 | | - let retrieved = self.cache.get(key: "test_key") |
24 | | - XCTAssertNotNil(retrieved) |
25 | | - XCTAssertEqual(retrieved?["key"] as? String, "value") |
26 | | - XCTAssertEqual(retrieved?["number"] as? Int, 42) |
| 21 | + let retrieved = cache.get(key: "test_key") |
| 22 | + #expect(retrieved != nil) |
| 23 | + #expect(retrieved?["key"] as? String == "value") |
| 24 | + #expect(retrieved?["number"] as? Int == 42) |
27 | 25 | } |
28 | 26 |
|
29 | | - func testCacheGetNonexistent() { |
30 | | - let retrieved = self.cache.get(key: "nonexistent_key") |
31 | | - XCTAssertNil(retrieved) |
| 27 | + @Test("Cache get nonexistent returns nil") |
| 28 | + func cacheGetNonexistent() { |
| 29 | + let retrieved = cache.get(key: "nonexistent_key") |
| 30 | + #expect(retrieved == nil) |
32 | 31 | } |
33 | 32 |
|
34 | | - func testCacheInvalidateAll() { |
35 | | - self.cache.set(key: "key1", data: ["a": 1], ttl: 60) |
36 | | - self.cache.set(key: "key2", data: ["b": 2], ttl: 60) |
| 33 | + @Test("Cache invalidate all") |
| 34 | + func cacheInvalidateAll() { |
| 35 | + cache.set(key: "key1", data: ["a": 1], ttl: 60) |
| 36 | + cache.set(key: "key2", data: ["b": 2], ttl: 60) |
37 | 37 |
|
38 | | - XCTAssertNotNil(self.cache.get(key: "key1")) |
39 | | - XCTAssertNotNil(self.cache.get(key: "key2")) |
| 38 | + #expect(cache.get(key: "key1") != nil) |
| 39 | + #expect(cache.get(key: "key2") != nil) |
40 | 40 |
|
41 | | - self.cache.invalidateAll() |
| 41 | + cache.invalidateAll() |
42 | 42 |
|
43 | | - XCTAssertNil(self.cache.get(key: "key1")) |
44 | | - XCTAssertNil(self.cache.get(key: "key2")) |
| 43 | + #expect(cache.get(key: "key1") == nil) |
| 44 | + #expect(cache.get(key: "key2") == nil) |
45 | 45 | } |
46 | 46 |
|
47 | | - func testCacheInvalidateMatchingPrefix() { |
48 | | - self.cache.set(key: "home_section1", data: ["a": 1], ttl: 60) |
49 | | - self.cache.set(key: "home_section2", data: ["b": 2], ttl: 60) |
50 | | - self.cache.set(key: "search_results", data: ["c": 3], ttl: 60) |
| 47 | + @Test("Cache invalidate matching prefix") |
| 48 | + func cacheInvalidateMatchingPrefix() { |
| 49 | + cache.set(key: "home_section1", data: ["a": 1], ttl: 60) |
| 50 | + cache.set(key: "home_section2", data: ["b": 2], ttl: 60) |
| 51 | + cache.set(key: "search_results", data: ["c": 3], ttl: 60) |
51 | 52 |
|
52 | | - self.cache.invalidate(matching: "home_") |
| 53 | + cache.invalidate(matching: "home_") |
53 | 54 |
|
54 | | - XCTAssertNil(self.cache.get(key: "home_section1")) |
55 | | - XCTAssertNil(self.cache.get(key: "home_section2")) |
56 | | - XCTAssertNotNil(self.cache.get(key: "search_results")) |
| 55 | + #expect(cache.get(key: "home_section1") == nil) |
| 56 | + #expect(cache.get(key: "home_section2") == nil) |
| 57 | + #expect(cache.get(key: "search_results") != nil) |
57 | 58 | } |
58 | 59 |
|
59 | | - func testCacheEntryExpiration() async throws { |
60 | | - // Set with a very short TTL |
61 | | - self.cache.set(key: "short_lived", data: ["test": true], ttl: 0.1) |
| 60 | + @Test("Cache entry expiration") |
| 61 | + func cacheEntryExpiration() async throws { |
| 62 | + cache.set(key: "short_lived", data: ["test": true], ttl: 0.1) |
62 | 63 |
|
63 | | - // Should exist immediately |
64 | | - XCTAssertNotNil(self.cache.get(key: "short_lived")) |
| 64 | + #expect(cache.get(key: "short_lived") != nil) |
65 | 65 |
|
66 | | - // Wait for expiration |
67 | 66 | try await Task.sleep(for: .milliseconds(150)) |
68 | 67 |
|
69 | | - // Should be expired |
70 | | - XCTAssertNil(self.cache.get(key: "short_lived")) |
| 68 | + #expect(cache.get(key: "short_lived") == nil) |
71 | 69 | } |
72 | 70 |
|
73 | | - func testCacheOverwrite() { |
74 | | - self.cache.set(key: "key", data: ["value": 1], ttl: 60) |
75 | | - XCTAssertEqual(self.cache.get(key: "key")?["value"] as? Int, 1) |
| 71 | + @Test("Cache overwrite") |
| 72 | + func cacheOverwrite() { |
| 73 | + cache.set(key: "key", data: ["value": 1], ttl: 60) |
| 74 | + #expect(cache.get(key: "key")?["value"] as? Int == 1) |
76 | 75 |
|
77 | | - self.cache.set(key: "key", data: ["value": 2], ttl: 60) |
78 | | - XCTAssertEqual(self.cache.get(key: "key")?["value"] as? Int, 2) |
| 76 | + cache.set(key: "key", data: ["value": 2], ttl: 60) |
| 77 | + #expect(cache.get(key: "key")?["value"] as? Int == 2) |
79 | 78 | } |
80 | 79 |
|
81 | | - func testCacheTTLConstants() { |
82 | | - XCTAssertEqual(APICache.TTL.home, 5 * 60) // 5 minutes |
83 | | - XCTAssertEqual(APICache.TTL.playlist, 30 * 60) // 30 minutes |
84 | | - XCTAssertEqual(APICache.TTL.artist, 60 * 60) // 1 hour |
85 | | - XCTAssertEqual(APICache.TTL.search, 2 * 60) // 2 minutes |
86 | | - XCTAssertEqual(APICache.TTL.library, 5 * 60) // 5 minutes |
87 | | - XCTAssertEqual(APICache.TTL.lyrics, 24 * 60 * 60) // 24 hours |
88 | | - XCTAssertEqual(APICache.TTL.songMetadata, 30 * 60) // 30 minutes |
| 80 | + @Test("Cache TTL constants are correct") |
| 81 | + func cacheTTLConstants() { |
| 82 | + #expect(APICache.TTL.home == 5 * 60) // 5 minutes |
| 83 | + #expect(APICache.TTL.playlist == 30 * 60) // 30 minutes |
| 84 | + #expect(APICache.TTL.artist == 60 * 60) // 1 hour |
| 85 | + #expect(APICache.TTL.search == 2 * 60) // 2 minutes |
| 86 | + #expect(APICache.TTL.library == 5 * 60) // 5 minutes |
| 87 | + #expect(APICache.TTL.lyrics == 24 * 60 * 60) // 24 hours |
| 88 | + #expect(APICache.TTL.songMetadata == 30 * 60) // 30 minutes |
89 | 89 | } |
90 | 90 |
|
91 | | - func testLyricsCacheNotInvalidatedByMutations() { |
92 | | - // Lyrics use browse: prefix but should NOT be invalidated by mutation operations |
93 | | - // This test verifies that invalidating next: prefix doesn't affect lyrics |
94 | | - self.cache.set(key: "browse:lyrics_abc123", data: ["text": "lyrics content"], ttl: APICache.TTL.lyrics) |
95 | | - self.cache.set(key: "next:song_abc123", data: ["title": "song"], ttl: APICache.TTL.songMetadata) |
| 91 | + @Test("Lyrics cache not invalidated by mutations") |
| 92 | + func lyricsCacheNotInvalidatedByMutations() { |
| 93 | + cache.set(key: "browse:lyrics_abc123", data: ["text": "lyrics content"], ttl: APICache.TTL.lyrics) |
| 94 | + cache.set(key: "next:song_abc123", data: ["title": "song"], ttl: APICache.TTL.songMetadata) |
96 | 95 |
|
97 | | - // Simulate mutation invalidation (like rateSong would do) |
98 | | - self.cache.invalidate(matching: "next:") |
| 96 | + cache.invalidate(matching: "next:") |
99 | 97 |
|
100 | | - // Lyrics should still be cached (browse: prefix not invalidated) |
101 | | - XCTAssertNotNil(self.cache.get(key: "browse:lyrics_abc123")) |
102 | | - // Song metadata should be invalidated |
103 | | - XCTAssertNil(self.cache.get(key: "next:song_abc123")) |
| 98 | + #expect(cache.get(key: "browse:lyrics_abc123") != nil) |
| 99 | + #expect(cache.get(key: "next:song_abc123") == nil) |
104 | 100 | } |
105 | 101 |
|
106 | | - func testSongMetadataCacheInvalidatedByMutations() { |
107 | | - // Song metadata uses next: prefix and should be invalidated by mutations |
108 | | - self.cache.set(key: "next:song_abc123", data: ["title": "song"], ttl: APICache.TTL.songMetadata) |
109 | | - self.cache.set(key: "browse:home_section", data: ["section": "home"], ttl: APICache.TTL.home) |
| 102 | + @Test("Song metadata cache invalidated by mutations") |
| 103 | + func songMetadataCacheInvalidatedByMutations() { |
| 104 | + cache.set(key: "next:song_abc123", data: ["title": "song"], ttl: APICache.TTL.songMetadata) |
| 105 | + cache.set(key: "browse:home_section", data: ["section": "home"], ttl: APICache.TTL.home) |
110 | 106 |
|
111 | | - // Simulate mutation invalidation for both prefixes |
112 | | - self.cache.invalidate(matching: "browse:") |
113 | | - self.cache.invalidate(matching: "next:") |
| 107 | + cache.invalidate(matching: "browse:") |
| 108 | + cache.invalidate(matching: "next:") |
114 | 109 |
|
115 | | - // Both should be invalidated |
116 | | - XCTAssertNil(self.cache.get(key: "next:song_abc123")) |
117 | | - XCTAssertNil(self.cache.get(key: "browse:home_section")) |
| 110 | + #expect(cache.get(key: "next:song_abc123") == nil) |
| 111 | + #expect(cache.get(key: "browse:home_section") == nil) |
118 | 112 | } |
119 | 113 |
|
120 | | - func testCacheEntryIsExpired() { |
| 114 | + @Test("Cache entry isExpired property") |
| 115 | + func cacheEntryIsExpired() { |
121 | 116 | let freshEntry = APICache.CacheEntry( |
122 | 117 | data: [:], |
123 | 118 | timestamp: Date(), |
124 | 119 | ttl: 60 |
125 | 120 | ) |
126 | | - XCTAssertFalse(freshEntry.isExpired) |
| 121 | + #expect(freshEntry.isExpired == false) |
127 | 122 |
|
128 | 123 | let expiredEntry = APICache.CacheEntry( |
129 | 124 | data: [:], |
130 | 125 | timestamp: Date().addingTimeInterval(-120), |
131 | 126 | ttl: 60 |
132 | 127 | ) |
133 | | - XCTAssertTrue(expiredEntry.isExpired) |
| 128 | + #expect(expiredEntry.isExpired == true) |
134 | 129 | } |
135 | 130 |
|
136 | | - func testCacheSharedInstance() { |
137 | | - XCTAssertNotNil(APICache.shared) |
138 | | - // Test that it's truly a singleton |
| 131 | + @Test("Cache shared instance is singleton") |
| 132 | + func cacheSharedInstance() { |
| 133 | + #expect(APICache.shared != nil) |
139 | 134 | let instance1 = APICache.shared |
140 | 135 | let instance2 = APICache.shared |
141 | | - XCTAssertTrue(instance1 === instance2) |
| 136 | + #expect(instance1 === instance2) |
142 | 137 | } |
143 | 138 | } |
0 commit comments