diff --git a/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/RFC3339Strategy.swift b/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/RFC3339Strategy.swift index bc7fcd1..fe6c3bf 100644 --- a/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/RFC3339Strategy.swift +++ b/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/RFC3339Strategy.swift @@ -20,7 +20,7 @@ public struct RFC3339Strategy: DateValueCodableStrategy { let dateFormatter = DateFormatter() dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" return dateFormatter }() diff --git a/Tests/KarrotCodableKitTests/BetterCodable/DateValue/RFC3339StrategyTests.swift b/Tests/KarrotCodableKitTests/BetterCodable/DateValue/RFC3339StrategyTests.swift new file mode 100644 index 0000000..147591a --- /dev/null +++ b/Tests/KarrotCodableKitTests/BetterCodable/DateValue/RFC3339StrategyTests.swift @@ -0,0 +1,99 @@ +// +// RFC3339StrategyTests.swift +// KarrotCodableKit +// +// Created by Elon on 7/13/26. +// + +import Foundation +import Testing + +import KarrotCodableKit + +@Suite("RFC3339Strategy") +struct RFC3339StrategyTests { + + private struct Fixture: Codable { + @DateValue var date: Date + } +} + +// MARK: - Encoding a Date + +extension RFC3339StrategyTests { + + @Test + func encodesUTCDateWithZOffsetInsteadOfRFC822Offset() { + // given + let date = Date(timeIntervalSince1970: 1715082540) // 2024-05-07T11:49:00 UTC + + // when + let result = RFC3339Strategy.encode(date) + + // then + // RFC 3339 allows only "Z" or "+00:00" for UTC, never the colon-less "+0000". + #expect(result == "2024-05-07T11:49:00Z") + } + + @Test + func serializesUTCDateWithZOffsetViaJSONEncoder() throws { + // given + let date = Date(timeIntervalSince1970: 1715082540) // 2024-05-07T11:49:00 UTC + let fixture = Fixture(date: date) + + // when + let data = try JSONEncoder().encode(fixture) + let json = try #require(String(bytes: data, encoding: .utf8)) + + // then + #expect(json.contains("\"2024-05-07T11:49:00Z\"")) + #expect(!json.contains("+0000")) + } +} + +// MARK: - Decoding an RFC 3339 string + +extension RFC3339StrategyTests { + + @Test(arguments: [ + "2024-05-07T11:49:00Z", + "2024-05-07T11:49:00+00:00", + "2024-05-07T11:49:00+0000" + ]) + func decodesEveryUTCOffsetFormToSameInstant(input: String) throws { + // when + let date = try RFC3339Strategy.decode(input) + + // then + // "ZZZZZ" still parses every offset form the old "Z" pattern accepted. + #expect(date == Date(timeIntervalSince1970: 1715082540)) + } + + @Test(arguments: [ + "1996-12-19T16:39:57-08:00", + "1996-12-19T16:39:57-0800" + ]) + func decodesNonUTCOffsetsWithOrWithoutColon(input: String) throws { + // when + let date = try RFC3339Strategy.decode(input) + + // then + #expect(date == Date(timeIntervalSince1970: 851042397)) + } +} + +// MARK: - Decoding a malformed string + +extension RFC3339StrategyTests { + + @Test + func throwsDataCorruptedErrorForMalformedString() { + // when / then + #expect { + try RFC3339Strategy.decode("not-a-valid-date") + } throws: { error in + guard case DecodingError.dataCorrupted = error else { return false } + return true + } + } +}