Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 145 additions & 30 deletions Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,153 @@

@Suite("JSONDecoder.podium")
struct JSONDecoderPodiumTests {
private func date(from string: String, format: String) -> Date? {
let formatter = DateFormatter()
formatter.dateFormat = format
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter.date(from: string)
}
private func date(from string: String, format: String) -> Date? {
let formatter = DateFormatter()
formatter.dateFormat = format
formatter.timeZone = TimeZone(secondsFromGMT: 0)
return formatter.date(from: string)
}

@Test("Decodes microsecond ISO8601 with timezone offset")
func decodesMicrosecondISO8601WithTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123456+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123456+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))
#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}
@Test("Decodes microsecond ISO8601 with timezone offset")
func decodesMicrosecondISO8601WithTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123456+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123456+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 34 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters
#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes second precision ISO8601 with timezone offset")
func decodesSecondPrecisionISO8601WithTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33+00:00", format: "yyyy-MM-dd'T'HH:mm:ssXXXXX"))
#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}
@Test("Decodes second precision ISO8601 with timezone offset")
func decodesSecondPrecisionISO8601WithTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33+00:00", format: "yyyy-MM-dd'T'HH:mm:ssXXXXX"))
#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Throws on unsupported format")
func throwsOnUnsupportedFormat() {
let jsonString = #"{"date":"03/11/2025 14:22:33"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try? JSONDecoder.podium.decode(DecodableDate.self, from: data)
@Test("Throws on unsupported format")
func throwsOnUnsupportedFormat() {
let jsonString = #"{"date":"03/11/2025 14:22:33"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try? JSONDecoder.podium.decode(DecodableDate.self, from: data)

#expect(decoded == nil)
}
}
#expect(decoded == nil)
}

@Test("Decodes millisecond ISO8601 with timezone offset")
func decodesMillisecondISO8601WithTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"))

Check warning on line 61 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 126 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes ISO8601 with Z timezone")
func decodesISO8601WithZTimezone() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123456Z"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123456+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 71 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes ISO8601 with non-zero timezone offset")
func decodesISO8601WithNonZeroTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123+05:30"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123+05:30", format: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"))

Check warning on line 81 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 126 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes ISO8601 with negative timezone offset")
func decodesISO8601WithNegativeTimezoneOffset() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123-07:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-11-03T14:22:33.123-07:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"))

Check warning on line 91 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 126 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes midnight date")
func decodesMidnightDate() throws {
let jsonString = #"{"date":"2025-01-01T00:00:00.000000+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-01-01T00:00:00.000000+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 101 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes end of day date")
func decodesEndOfDayDate() throws {
let jsonString = #"{"date":"2025-12-31T23:59:59.999999+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-12-31T23:59:59.999999+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 111 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes leap year date")
func decodesLeapYearDate() throws {
let jsonString = #"{"date":"2024-02-29T12:00:00.000000+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2024-02-29T12:00:00.000000+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 121 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}

@Test("Decodes nanosecond precision if supported")
func decodesNanosecondPrecision() throws {
let jsonString = #"{"date":"2025-11-03T14:22:33.123456789+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)

#expect(decoded.date.timeIntervalSince1970 > 0)
}

@Test("Throws on invalid date format")
func throwsOnInvalidDateFormat() {
let jsonString = #"{"date":"2025-13-45T14:22:33+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try? JSONDecoder.podium.decode(DecodableDate.self, from: data)

#expect(decoded == nil)
}

@Test("Throws on malformed JSON")
func throwsOnMalformedJSON() {
let jsonString = #"{"date":"2025-11-03T14:22:33+00:00""#
let data = jsonString.data(using: .utf8)!
let decoded = try? JSONDecoder.podium.decode(DecodableDate.self, from: data)

#expect(decoded == nil)
}

@Test("Throws on non-string date value")
func throwsOnNonStringDateValue() {
let jsonString = #"{"date":1730642553}"#
let data = jsonString.data(using: .utf8)!
let decoded = try? JSONDecoder.podium.decode(DecodableDate.self, from: data)

#expect(decoded == nil)
}

@Test("Decodes single digit month and day")
func decodesSingleDigitMonthAndDay() throws {
let jsonString = #"{"date":"2025-01-05T09:08:07.123456+00:00"}"#
let data = jsonString.data(using: .utf8)!
let decoded = try JSONDecoder.podium.decode(DecodableDate.self, from: data)
let expectedDate = try #require(date(from: "2025-01-05T09:08:07.123456+00:00", format: "yyyy-MM-dd'T'HH:mm:ss.SSSSSSXXXXX"))

Check warning on line 167 in Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift

View workflow job for this annotation

GitHub Actions / Lint Code

Line Length (line_length)

Line should be 120 characters or less; currently it has 132 characters

#expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001)
}
}
158 changes: 146 additions & 12 deletions Tests/PodiumRequestsClientTests/Helpers/DateHelperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,153 @@ import Testing

@Suite
struct DateHelperTests {
@Test
func dateToStringIdentifier() async throws {
let date: Date = try Date("2003-03-06T05:30:35Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)
@Test
func dateToStringIdentifier() async throws {
let date: Date = try Date("2003-03-06T05:30:35Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "2003-03-06T05:30:35.000Z")
}
#expect(identifier == "2003-03-06T05:30:35.000Z")
}

@Test
func missingFractionsSeconds() async throws {
let date: Date = try Date("2003-03-06T05:30:35Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)
@Test
func missingFractionsSeconds() async throws {
let date: Date = try Date("2003-03-06T05:30:35Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier != "2003-03-06T05:30:35Z")
}
#expect(identifier != "2003-03-06T05:30:35Z")
}

@Test
func midnightDate() async throws {
let date: Date = try Date("2020-06-15T00:00:00Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "2020-06-15T00:00:00.000Z")
}

@Test
func endOfDayDate() async throws {
let date: Date = try Date("2021-08-20T23:59:59Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "2021-08-20T23:59:59.000Z")
}

@Test
func epochDate() async throws {
let date = Date(timeIntervalSince1970: 0)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "1970-01-01T00:00:00.000Z")
}

@Test
func leapYearDate() async throws {
let date: Date = try Date("2024-02-29T14:30:00Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "2024-02-29T14:30:00.000Z")
}

@Test
func currentDateFormat() async throws {
let date = Date()
let identifier: String = DateHelper.toIdentifier(date: date)

// Verify format structure
#expect(identifier.contains("T"))
#expect(identifier.hasSuffix("Z"))
#expect(identifier.contains("."))
}

@Test
func differentDatesHaveDifferentIdentifiers() async throws {
let date1: Date = try Date("2022-01-01T10:00:00Z", strategy: .iso8601)
let date2: Date = try Date("2022-01-01T10:00:01Z", strategy: .iso8601)

let identifier1 = DateHelper.toIdentifier(date: date1)
let identifier2 = DateHelper.toIdentifier(date: date2)

#expect(identifier1 != identifier2)
}

@Test
func sameDateHasSameIdentifier() async throws {
let date: Date = try Date("2023-07-04T16:45:30Z", strategy: .iso8601)

let identifier1 = DateHelper.toIdentifier(date: date)
let identifier2 = DateHelper.toIdentifier(date: date)

#expect(identifier1 == identifier2)
}

@Test
func identifierLengthConsistency() async throws {
let dates = [
try Date("2000-01-01T00:00:00Z", strategy: .iso8601),
try Date("2015-06-15T12:30:45Z", strategy: .iso8601),
try Date("2025-12-31T23:59:59Z", strategy: .iso8601)
]

let identifiers = dates.map { DateHelper.toIdentifier(date: $0) }
let lengths = Set(identifiers.map { $0.count })

// All identifiers should have the same length
#expect(lengths.count == 1)
}

@Test
func identifierAlwaysHasThreeDecimalPlaces() async throws {
let date: Date = try Date("2023-05-10T08:15:22Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

// Extract fractional seconds part
let components = identifier.split(separator: ".")
#expect(components.count == 2)

let fractionalPart = String(components[1].dropLast()) // Remove 'Z'
#expect(fractionalPart.count == 3)
}

@Test
func veryOldDate() async throws {
let date: Date = try Date("1900-01-01T00:00:00Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "1900-01-01T00:00:00.000Z")
}

@Test
func farFutureDate() async throws {
let date: Date = try Date("2099-12-31T23:59:59Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

#expect(identifier == "2099-12-31T23:59:59.000Z")
}

@Test
func singleDigitMonthAndDay() async throws {
let date: Date = try Date("2023-01-05T09:08:07Z", strategy: .iso8601)
let identifier: String = DateHelper.toIdentifier(date: date)

// Should have zero-padding
#expect(identifier.contains("-01-"))
#expect(identifier.contains("-05T"))
}

@Test
func identifierSortability() async throws {
let date1: Date = try Date("2020-05-10T10:00:00Z", strategy: .iso8601)
let date2: Date = try Date("2021-03-15T14:30:00Z", strategy: .iso8601)
let date3: Date = try Date("2022-11-20T08:45:00Z", strategy: .iso8601)

let id1 = DateHelper.toIdentifier(date: date1)
let id2 = DateHelper.toIdentifier(date: date2)
let id3 = DateHelper.toIdentifier(date: date3)

// Identifiers should be lexicographically sortable
#expect(id1 < id2)
#expect(id2 < id3)
#expect(id1 < id3)
}
}
Loading