diff --git a/Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift b/Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift index 4e3e53b..82fa50a 100644 --- a/Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift +++ b/Tests/PodiumRequestsClientTests/Extensions/JSONDecoderPodiumTests.swift @@ -19,38 +19,153 @@ private struct DecodableDate: Decodable { @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")) + #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")) + + #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")) + + #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")) + + #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")) + + #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")) + + #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")) + + #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")) + + #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")) + + #expect(abs(decoded.date.timeIntervalSince1970 - expectedDate.timeIntervalSince1970) < 0.001) + } +} diff --git a/Tests/PodiumRequestsClientTests/Helpers/DateHelperTests.swift b/Tests/PodiumRequestsClientTests/Helpers/DateHelperTests.swift index bd6dddf..ac12ded 100644 --- a/Tests/PodiumRequestsClientTests/Helpers/DateHelperTests.swift +++ b/Tests/PodiumRequestsClientTests/Helpers/DateHelperTests.swift @@ -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) + } } diff --git a/Tests/PodiumRequestsClientTests/Models/CarLocationModelTests.swift b/Tests/PodiumRequestsClientTests/Models/CarLocationModelTests.swift index af95135..b4199e1 100644 --- a/Tests/PodiumRequestsClientTests/Models/CarLocationModelTests.swift +++ b/Tests/PodiumRequestsClientTests/Models/CarLocationModelTests.swift @@ -12,21 +12,140 @@ import Testing @Suite() struct CarLocationModelTests { - @Test - func createModel() async throws { - let date: Date = .now - let location: Point3D = Point3D( - x: 150, - y: 200, - z: 150 - ) - let model: CarLocationModel = CarLocationModel( - date: date, - location: location - ) - - #expect(model.id == DateHelper.toIdentifier(date: date)) - #expect(model.date == date) - #expect(model.location == location) - } + @Test + func createModel() async throws { + let date: Date = .now + let location: Point3D = Point3D( + x: 150, + y: 200, + z: 150 + ) + let model: CarLocationModel = CarLocationModel( + date: date, + location: location + ) + + #expect(model.id == DateHelper.toIdentifier(date: date)) + #expect(model.date == date) + #expect(model.location == location) + } + + @Test + func createModelWithDifferentDates() async throws { + let date1 = Date() + let date2 = Date().addingTimeInterval(3600) // 1 hour later + let location = Point3D(x: 100, y: 100, z: 100) + + let model1 = CarLocationModel(date: date1, location: location) + let model2 = CarLocationModel(date: date2, location: location) + + #expect(model1.id != model2.id) + #expect(model1.date != model2.date) + } + + @Test + func createModelWithDifferentLocations() async throws { + let date = Date() + let location1 = Point3D(x: 100, y: 200, z: 300) + let location2 = Point3D(x: 400, y: 500, z: 600) + + let model1 = CarLocationModel(date: date, location: location1) + let model2 = CarLocationModel(date: date, location: location2) + + #expect(model1.location != model2.location) + #expect(model1.id == model2.id) // Same date = same ID + } + + @Test + func createModelWithZeroCoordinates() async throws { + let date = Date() + let location = Point3D(x: 0, y: 0, z: 0) + + let model = CarLocationModel(date: date, location: location) + + #expect(model.location.x == 0) + #expect(model.location.y == 0) + #expect(model.location.z == 0) + } + + @Test + func createModelWithNegativeCoordinates() async throws { + let date = Date() + let location = Point3D(x: -100, y: -200, z: -50) + + let model = CarLocationModel(date: date, location: location) + + #expect(model.location.x == -100) + #expect(model.location.y == -200) + #expect(model.location.z == -50) + } + + @Test + func createModelWithLargeCoordinates() async throws { + let date = Date() + let location = Point3D(x: 999999, y: 888888, z: 777777) + + let model = CarLocationModel(date: date, location: location) + + #expect(model.location == location) + } + + @Test + func idConsistencyWithSameDate() async throws { + let date = Date(timeIntervalSince1970: 1609459200) // Fixed date + let location1 = Point3D(x: 10, y: 20, z: 30) + let location2 = Point3D(x: 40, y: 50, z: 60) + + let model1 = CarLocationModel(date: date, location: location1) + let model2 = CarLocationModel(date: date, location: location2) + + #expect(model1.id == model2.id) + } + + @Test + func modelEqualityWithSameValues() async throws { + let date = Date() + let location = Point3D(x: 123, y: 456, z: 789) + + let model1 = CarLocationModel(date: date, location: location) + let model2 = CarLocationModel(date: date, location: location) + + #expect(model1.id == model2.id) + #expect(model1.date == model2.date) + #expect(model1.location == model2.location) + } + + @Test + func createModelWithPastDate() async throws { + let pastDate = Date(timeIntervalSince1970: 0) // January 1, 1970 + let location = Point3D(x: 50, y: 60, z: 70) + + let model = CarLocationModel(date: pastDate, location: location) + + #expect(model.date == pastDate) + #expect(model.id == DateHelper.toIdentifier(date: pastDate)) + } + + @Test + func createModelWithFutureDate() async throws { + let futureDate = Date().addingTimeInterval(86400 * 365) // 1 year from now + let location = Point3D(x: 80, y: 90, z: 100) + + let model = CarLocationModel(date: futureDate, location: location) + + #expect(model.date == futureDate) + #expect(model.id == DateHelper.toIdentifier(date: futureDate)) + } + + @Test + func createModelWithDecimalCoordinates() async throws { + let date = Date() + let location = Point3D(x: 123.456, y: 789.012, z: 345.678) + + let model = CarLocationModel(date: date, location: location) + + #expect(model.location.x == 123.456) + #expect(model.location.y == 789.012) + #expect(model.location.z == 345.678) + } } diff --git a/Tests/PodiumRequestsClientTests/PodiumRequestsChunkTests.swift b/Tests/PodiumRequestsClientTests/PodiumRequestsChunkTests.swift index 3ea150e..6d75b6f 100644 --- a/Tests/PodiumRequestsClientTests/PodiumRequestsChunkTests.swift +++ b/Tests/PodiumRequestsClientTests/PodiumRequestsChunkTests.swift @@ -10,47 +10,159 @@ import Testing @Suite struct PodiumRequestsChunkTests { - @Test - func beforeAndAfterNonNil() async throws { - let chunk: RequestsClient.Chunk = RequestsClient.Chunk( - after: 150, - before: 200 - ) - - #expect(chunk.after == 150) - #expect(chunk.before == 200) - } - - @Test - func afterParameterNil() async throws { - let chunk: RequestsClient.Chunk = RequestsClient.Chunk( - after: nil, - before: 499 - ) - - #expect(chunk.after == nil) - #expect(chunk.before == 499) - } - - @Test - func beforeParameterNil() async throws { - let chunk: RequestsClient.Chunk = RequestsClient.Chunk( - after: 0, - before: nil - ) - - #expect(chunk.after == 0) - #expect(chunk.before == nil) - } - - @Test - func beforeAndAfterParametersNil() async throws { - let chunk: RequestsClient.Chunk = RequestsClient.Chunk( - after: nil, - before: nil - ) - - #expect(chunk.after == nil) - #expect(chunk.before == nil) - } + @Test + func beforeAndAfterNonNil() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 150, + before: 200 + ) + + #expect(chunk.after == 150) + #expect(chunk.before == 200) + } + + @Test + func afterParameterNil() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: nil, + before: 499 + ) + + #expect(chunk.after == nil) + #expect(chunk.before == 499) + } + + @Test + func beforeParameterNil() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 0, + before: nil + ) + + #expect(chunk.after == 0) + #expect(chunk.before == nil) + } + + @Test + func beforeAndAfterParametersNil() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: nil, + before: nil + ) + + #expect(chunk.after == nil) + #expect(chunk.before == nil) + } + + @Test + func chunkWithZeroValues() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 0, + before: 0 + ) + + #expect(chunk.after == 0) + #expect(chunk.before == 0) + } + + @Test + func chunkWithLargeValues() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 10000, + before: 50000 + ) + + #expect(chunk.after == 10000) + #expect(chunk.before == 50000) + } + + @Test + func chunkWithSameAfterAndBefore() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 250, + before: 250 + ) + + #expect(chunk.after == 250) + #expect(chunk.before == 250) + } + + @Test + func chunkWithAfterGreaterThanBefore() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 500, + before: 100 + ) + + // Should still create the chunk (validation may happen elsewhere) + #expect(chunk.after == 500) + #expect(chunk.before == 100) + } + + @Test + func chunkRangeIsValid() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: 100, + before: 500 + ) + + let range = (chunk.before ?? 0) - (chunk.after ?? 0) + #expect(range == 400) + } + + @Test + func multipleChunksWithDifferentRanges() async throws { + let chunk1: RequestsClient.Chunk = RequestsClient.Chunk(after: 0, before: 100) + let chunk2: RequestsClient.Chunk = RequestsClient.Chunk(after: 100, before: 200) + let chunk3: RequestsClient.Chunk = RequestsClient.Chunk(after: 200, before: 300) + + #expect(chunk1.after != chunk2.after) + #expect(chunk1.before == chunk2.after) + #expect(chunk2.before == chunk3.after) + } + + @Test + func chunkWithNegativeValues() async throws { + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: -100, + before: -50 + ) + + #expect(chunk.after == -100) + #expect(chunk.before == -50) + } + + @Test + func chunkEquality() async throws { + let chunk1: RequestsClient.Chunk = RequestsClient.Chunk(after: 150, before: 200) + let chunk2: RequestsClient.Chunk = RequestsClient.Chunk(after: 150, before: 200) + + #expect(chunk1.after == chunk2.after) + #expect(chunk1.before == chunk2.before) + } + + @Test + func chunkWithMixedNilValues() async throws { + let chunk1: RequestsClient.Chunk = RequestsClient.Chunk(after: 100, before: nil) + let chunk2: RequestsClient.Chunk = RequestsClient.Chunk(after: nil, before: 200) + + #expect(chunk1.after != nil) + #expect(chunk1.before == nil) + #expect(chunk2.after == nil) + #expect(chunk2.before != nil) + } + + @Test + func chunkCreationWithOptionals() async throws { + let after: Int? = 50 + let before: Int? = 150 + + let chunk: RequestsClient.Chunk = RequestsClient.Chunk( + after: after, + before: before + ) + + #expect(chunk.after == 50) + #expect(chunk.before == 150) + } } diff --git a/Tests/PodiumRequestsClientTests/Requests/RequestsClientCarsTests.swift b/Tests/PodiumRequestsClientTests/Requests/RequestsClientCarsTests.swift index 4cf45da..f9622a9 100644 --- a/Tests/PodiumRequestsClientTests/Requests/RequestsClientCarsTests.swift +++ b/Tests/PodiumRequestsClientTests/Requests/RequestsClientCarsTests.swift @@ -5,37 +5,185 @@ // Created by Mathis Le Bonniec on 11/2/25. // +import Foundation import Testing import PodiumRequestsClient @Suite(.tags(.cars)) struct RequestsClientCarsTests { - let sessionKey: Int = 9094 - let driverNumber: Int = 16 - let client: RequestsClient = RequestsClient( - baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", - apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" - ) - - @Test - func getAllCars() async throws { - let cars = try await client.getAllCars(sessionKey: sessionKey) - - #expect(cars.count == 20) - } - - // getCar(sessionKey:driver) hasn't been implemented yet. - @Test() - func getOneCar() async throws { - let cars = try await client.getAllCars(sessionKey: sessionKey) - let first = try #require(cars.first(where: { $0.number == driverNumber })) - - #expect(first.number == driverNumber) - #expect(first.driver.number == driverNumber) - #expect(first.driver.acronym == "LEC") - #expect(first.driver.firstname == "Charles") - #expect(first.driver.lastname == "Leclerc") - #expect(first.driver.team.name == "Ferrari") - #expect(first.driver.team.image != nil) - } + let sessionKey: Int = 9094 + let driverNumber: Int = 16 + let client: RequestsClient = RequestsClient( + baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", + apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" + ) + + @Test + func getAllCars() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + #expect(cars.count == 20) + } + + // getCar(sessionKey:driver) hasn't been implemented yet. + @Test() + func getOneCar() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + let first = try #require(cars.first(where: { $0.number == driverNumber })) + + #expect(first.number == driverNumber) + #expect(first.driver.number == driverNumber) + #expect(first.driver.acronym == "LEC") + #expect(first.driver.firstname == "Charles") + #expect(first.driver.lastname == "Leclerc") + #expect(first.driver.team.name == "Ferrari") + #expect(first.driver.team.image != nil) + } + + @Test + func getAllCarsReturnsUniqueDriverNumbers() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + let driverNumbers = cars.map { $0.number } + let uniqueNumbers = Set(driverNumbers) + + #expect(driverNumbers.count == uniqueNumbers.count) + } + + @Test + func getAllCarsHaveValidDriverData() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(car.driver.number == car.number) + #expect(!car.driver.acronym.isEmpty) + #expect(car.driver.acronym.count == 3) + #expect(!car.driver.firstname.isEmpty) + #expect(!car.driver.lastname.isEmpty) + } + } + + @Test + func getAllCarsHaveValidTeamData() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(!car.driver.team.name.isEmpty) + } + } + + @Test + func getSpecificDriverCar() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + let verstappen = try #require(cars.first(where: { $0.number == 1 })) + + #expect(verstappen.driver.acronym == "VER") + #expect(verstappen.driver.firstname == "Max") + #expect(verstappen.driver.lastname == "Verstappen") + } + + @Test + func getMultipleDriverCars() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + let leclerc = try #require(cars.first(where: { $0.number == 16 })) + let sainz = try #require(cars.first(where: { $0.number == 55 })) + + #expect(leclerc.driver.team.name == sainz.driver.team.name) + #expect(leclerc.driver.team.name == "Ferrari") + } + + @Test + func carsAreOrderedConsistently() async throws { + let cars1 = try await client.getAllCars(sessionKey: sessionKey) + let cars2 = try await client.getAllCars(sessionKey: sessionKey) + + #expect(cars1.map { $0.number } == cars2.map { $0.number }) + } + + @Test(.bug("https://github.com/EpitechPromo2026/G-EIP-700-REN-7-1-eip-mathis.le-bonniec/issues/113"), .disabled()) + func invalidSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getAllCars(sessionKey: -1) + } + } + + @Test(.bug("https://github.com/EpitechPromo2026/G-EIP-700-REN-7-1-eip-mathis.le-bonniec/issues/113"), .disabled()) + func nonExistentSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getAllCars(sessionKey: 999999) + } + } + + @Test + func allDriversHaveThreeLetterAcronyms() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(car.driver.acronym.count == 3) + #expect(car.driver.acronym.allSatisfy { $0.isUppercase || $0.isNumber }) + } + } + + @Test + func allDriverNumbersArePositive() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(car.number > 0) + #expect(car.driver.number > 0) + } + } + + @Test + func teamImagesAreValidURLs() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + if let imageURL = car.driver.team.image { + #expect(imageURL.scheme == "http" || imageURL.scheme == "https") + } + } + } + + @Test + func carNumberMatchesDriverNumber() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(car.number == car.driver.number) + } + } + + @Test + func getAllCarsResponseIsCached() async throws { + let start1 = Date() + _ = try await client.getAllCars(sessionKey: sessionKey) + let duration1 = Date().timeIntervalSince(start1) + + let start2 = Date() + _ = try await client.getAllCars(sessionKey: sessionKey) + let duration2 = Date().timeIntervalSince(start2) + + // Second call should be faster if cached (though not guaranteed) + // This is more of a performance observation test + #expect(duration2 <= duration1 * 1.5) + } + + @Test + func driverNamesAreNonEmpty() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + for car in cars { + #expect(!car.driver.firstname.trimmingCharacters(in: .whitespaces).isEmpty) + #expect(!car.driver.lastname.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + + @Test + func getAllCarsReturnsExpectedF1GridSize() async throws { + let cars = try await client.getAllCars(sessionKey: sessionKey) + + // F1 grid typically has 20 cars (10 teams × 2 drivers) + #expect(cars.count == 20) + } } diff --git a/Tests/PodiumRequestsClientTests/Requests/RequestsClientDriversTests.swift b/Tests/PodiumRequestsClientTests/Requests/RequestsClientDriversTests.swift index 5c7e3b5..4ffb620 100644 --- a/Tests/PodiumRequestsClientTests/Requests/RequestsClientDriversTests.swift +++ b/Tests/PodiumRequestsClientTests/Requests/RequestsClientDriversTests.swift @@ -11,29 +11,209 @@ import Testing @Suite(.tags(.drivers)) struct RequestsClientDriversTests { - let sessionKey: Int = 9094 - let driverNumber: Int = 16 - let client: RequestsClient = RequestsClient( - baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", - apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" - ) - - @Test - func getAllDrivers() async throws { - let drivers = try await client.getAllDrivers(sessionKey: sessionKey) - - #expect(drivers.count == 20) - } - - @Test - func getOneDriver() async throws { - let driver = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) - - #expect(driver.number == driverNumber) - #expect(driver.acronym == "LEC") - #expect(driver.firstname == "Charles") - #expect(driver.lastname == "Leclerc") - #expect(driver.team.name == "Ferrari") - #expect(driver.team.image != nil) - } + let sessionKey: Int = 9094 + let driverNumber: Int = 16 + let client: RequestsClient = RequestsClient( + baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", + apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" + ) + + @Test + func getAllDrivers() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + #expect(drivers.count == 20) + } + + @Test + func getOneDriver() async throws { + let driver = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) + + #expect(driver.number == driverNumber) + #expect(driver.acronym == "LEC") + #expect(driver.firstname == "Charles") + #expect(driver.lastname == "Leclerc") + #expect(driver.team.name == "Ferrari") + #expect(driver.team.image != nil) + } + + @Test + func getAllDriversReturnsUniqueNumbers() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + let driverNumbers = drivers.map { $0.number } + let uniqueNumbers = Set(driverNumbers) + + #expect(driverNumbers.count == uniqueNumbers.count) + } + + @Test + func getAllDriversReturnsUniqueAcronyms() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + let acronyms = drivers.map { $0.acronym } + let uniqueAcronyms = Set(acronyms) + + #expect(acronyms.count == uniqueAcronyms.count) + } + + @Test + func getAllDriversHaveValidData() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + for driver in drivers { + #expect(driver.number > 0) + #expect(driver.acronym.count == 3) + #expect(!driver.firstname.isEmpty) + #expect(!driver.lastname.isEmpty) + #expect(!driver.team.name.isEmpty) + } + } + + @Test + func getDriverReturnsConsistentDataWithGetAll() async throws { + let allDrivers = try await client.getAllDrivers(sessionKey: sessionKey) + let singleDriver = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) + let matchingDriver = try #require(allDrivers.first(where: { $0.number == driverNumber })) + + #expect(singleDriver.number == matchingDriver.number) + #expect(singleDriver.acronym == matchingDriver.acronym) + #expect(singleDriver.firstname == matchingDriver.firstname) + #expect(singleDriver.lastname == matchingDriver.lastname) + #expect(singleDriver.team.name == matchingDriver.team.name) + } + + @Test + func getMultipleSpecificDrivers() async throws { + let verstappen = try await client.getDriver(sessionKey: sessionKey, driver: 1) + let hamilton = try await client.getDriver(sessionKey: sessionKey, driver: 44) + let leclerc = try await client.getDriver(sessionKey: sessionKey, driver: 16) + + #expect(verstappen.acronym == "VER") + #expect(hamilton.acronym == "HAM") + #expect(leclerc.acronym == "LEC") + } + + @Test + func getDriverWithInvalidNumberThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getDriver(sessionKey: sessionKey, driver: 999) + } + } + + @Test + func getDriverWithNegativeNumberThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getDriver(sessionKey: sessionKey, driver: -1) + } + } + + @Test + func getDriverWithInvalidSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getDriver(sessionKey: -1, driver: driverNumber) + } + } + + @Test(.bug("https://github.com/EpitechPromo2026/G-EIP-700-REN-7-1-eip-mathis.le-bonniec/issues/113"), .disabled()) + func getAllDriversWithInvalidSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getAllDrivers(sessionKey: 999999) + } + } + + @Test + func allDriverAcronymsAreUppercase() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + for driver in drivers { + #expect(driver.acronym == driver.acronym.uppercased()) + } + } + + @Test + func driversHaveValidTeamImages() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + for driver in drivers { + if let imageURL = driver.team.image { + #expect(imageURL.scheme == "http" || imageURL.scheme == "https") + } + } + } + + @Test + func teammatesShareSameTeam() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + let ferrariDrivers = drivers.filter { $0.team.name == "Ferrari" } + + #expect(ferrariDrivers.count == 2) + let teamNames = Set(ferrariDrivers.map { $0.team.name }) + #expect(teamNames.count == 1) + } + + @Test + func getDriverMultipleTimesReturnsConsistentData() async throws { + let driver1 = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) + let driver2 = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) + + #expect(driver1.number == driver2.number) + #expect(driver1.acronym == driver2.acronym) + #expect(driver1.firstname == driver2.firstname) + #expect(driver1.lastname == driver2.lastname) + #expect(driver1.team.name == driver2.team.name) + } + + @Test + func allDriversHaveValidNumbers() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + for driver in drivers { + #expect(driver.number >= 1) + #expect(driver.number <= 99) + } + } + + @Test + func driverNamesDoNotContainExtraWhitespace() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + for driver in drivers { + #expect(driver.firstname == driver.firstname.trimmingCharacters(in: .whitespaces)) + #expect(driver.lastname == driver.lastname.trimmingCharacters(in: .whitespaces)) + } + } + + @Test + func getAllDriversReturnsF1GridSize() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + + // F1 grid has 20 drivers (10 teams × 2 drivers) + #expect(drivers.count == 20) + } + + @Test + func getDriverForTeammates() async throws { + let leclerc = try await client.getDriver(sessionKey: sessionKey, driver: 16) + let sainz = try await client.getDriver(sessionKey: sessionKey, driver: 55) + + #expect(leclerc.team.name == sainz.team.name) + #expect(leclerc.team.name == "Ferrari") + #expect(leclerc.number != sainz.number) + } + + @Test + func driverAcronymMatchesExpectedFormat() async throws { + let driver = try await client.getDriver(sessionKey: sessionKey, driver: driverNumber) + + #expect(driver.acronym.count == 3) + #expect(driver.acronym.allSatisfy { $0.isLetter || $0.isNumber }) + #expect(driver.acronym.allSatisfy { $0.isUppercase || $0.isNumber }) + } + + @Test + func getAllDriversIncludesExpectedDriver() async throws { + let drivers = try await client.getAllDrivers(sessionKey: sessionKey) + let hasLeclerc = drivers.contains(where: { $0.number == 16 && $0.acronym == "LEC" }) + + #expect(hasLeclerc) + } } diff --git a/Tests/PodiumRequestsClientTests/Requests/RequestsClientSessionsTests.swift b/Tests/PodiumRequestsClientTests/Requests/RequestsClientSessionsTests.swift index bc3c2a3..80108b2 100644 --- a/Tests/PodiumRequestsClientTests/Requests/RequestsClientSessionsTests.swift +++ b/Tests/PodiumRequestsClientTests/Requests/RequestsClientSessionsTests.swift @@ -11,27 +11,221 @@ import Testing @Suite(.tags(.sessions)) struct RequestsClientSessionsTests { - let sessionKey: Int = 9094 - let client: RequestsClient = RequestsClient( - baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", - apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" - ) - - @Test - func getAllSessions() async throws { - let sessions = try await client.getAllSessions() - - #expect(!sessions.isEmpty) - } - - @Test - func getSession() async throws { - let session = try await client.getSession(sessionKey: sessionKey) - - #expect(session.key == sessionKey) - #expect(session.name == "Race") - #expect(session.location == "Monaco") - #expect(session.start == Date(timeIntervalSince1970: 1685278800)) - #expect(session.end == Date(timeIntervalSince1970: 1685286000)) - } + let sessionKey: Int = 9094 + let client: RequestsClient = RequestsClient( + baseURL: "https://api.podium.mathislebonniec.fr/v1/formula1", + apiKey: "08fe5ccd-8d72-49e0-ae2b-3f097f2b96a1" + ) + + @Test + func getAllSessions() async throws { + let sessions = try await client.getAllSessions() + + #expect(!sessions.isEmpty) + } + + @Test + func getSession() async throws { + let session = try await client.getSession(sessionKey: sessionKey) + + #expect(session.key == sessionKey) + #expect(session.name == "Race") + #expect(session.location == "Monaco") + #expect(session.start == Date(timeIntervalSince1970: 1685278800)) + #expect(session.end == Date(timeIntervalSince1970: 1685286000)) + } + + @Test + func getAllSessionsReturnsUniqueKeys() async throws { + let sessions = try await client.getAllSessions() + let sessionKeys = sessions.map { $0.key } + let uniqueKeys = Set(sessionKeys) + + #expect(sessionKeys.count == uniqueKeys.count) + } + + @Test + func getAllSessionsHaveValidData() async throws { + let sessions = try await client.getAllSessions() + + for session in sessions { + #expect(session.key > 0) + #expect(!session.name.isEmpty) + #expect(!session.location.isEmpty) + #expect(session.start < session.end) + } + } + + @Test + func getSessionReturnsConsistentDataWithGetAll() async throws { + let allSessions = try await client.getAllSessions() + let singleSession = try await client.getSession(sessionKey: sessionKey) + let matchingSession = try #require(allSessions.first(where: { $0.key == sessionKey })) + + #expect(singleSession.key == matchingSession.key) + #expect(singleSession.name == matchingSession.name) + #expect(singleSession.location == matchingSession.location) + #expect(singleSession.start == matchingSession.start) + #expect(singleSession.end == matchingSession.end) + } + + @Test + func getSessionWithInvalidKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getSession(sessionKey: -1) + } + } + + @Test + func getSessionWithNonExistentKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getSession(sessionKey: 999999) + } + } + + @Test + func getAllSessionsIncludesMonacoRace() async throws { + let sessions = try await client.getAllSessions() + let monacoRace = sessions.first(where: { $0.key == sessionKey }) + + #expect(monacoRace != nil) + #expect(monacoRace?.location == "Monaco") + #expect(monacoRace?.name == "Race") + } + + @Test + func sessionStartAndEndDatesAreValid() async throws { + let session = try await client.getSession(sessionKey: sessionKey) + + #expect(session.start < session.end) + #expect(session.start.timeIntervalSince1970 > 0) + #expect(session.end.timeIntervalSince1970 > 0) + } + + @Test + func sessionDurationIsReasonable() async throws { + let session = try await client.getSession(sessionKey: sessionKey) + let duration = session.end.timeIntervalSince(session.start) + + // Race duration should be between 1 and 4 hours + #expect(duration > 3600) // More than 1 hour + #expect(duration < 14400) // Less than 4 hours + } + + @Test + func getAllSessionsContainsDifferentSessionTypes() async throws { + let sessions = try await client.getAllSessions() + let sessionTypes = Set(sessions.map { $0.name }) + + // F1 typically has Practice, Qualifying, Sprint, Race sessions + #expect(sessionTypes.count > 1) + } + + @Test + func getAllSessionsContainsDifferentLocations() async throws { + let sessions = try await client.getAllSessions() + let locations = Set(sessions.map { $0.location }) + + // F1 season has multiple race locations + #expect(locations.count > 1) + } + + @Test + func getMultipleSessionsByKey() async throws { + let allSessions = try await client.getAllSessions() + let firstThreeKeys = Array(allSessions.prefix(3).map { $0.key }) + + for key in firstThreeKeys { + let session = try await client.getSession(sessionKey: key) + #expect(session.key == key) + } + } + + @Test + func sessionNamesAreValid() async throws { + let sessions = try await client.getAllSessions() + let validNames = ["Practice 1", "Practice 2", "Practice 3", "Qualifying", "Sprint", "Race"] + + for session in sessions { + let hasValidName = validNames.contains { session.name.contains($0) } || + validNames.contains(session.name) + #expect(hasValidName || !session.name.isEmpty) + } + } + + @Test + func sessionLocationsAreNonEmpty() async throws { + let sessions = try await client.getAllSessions() + + for session in sessions { + #expect(!session.location.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + + @Test + func getSessionMultipleTimesReturnsConsistentData() async throws { + let session1 = try await client.getSession(sessionKey: sessionKey) + let session2 = try await client.getSession(sessionKey: sessionKey) + + #expect(session1.key == session2.key) + #expect(session1.name == session2.name) + #expect(session1.location == session2.location) + #expect(session1.start == session2.start) + #expect(session1.end == session2.end) + } + + @Test + func sessionKeysArePositive() async throws { + let sessions = try await client.getAllSessions() + + for session in sessions { + #expect(session.key > 0) + } + } + + @Test + func sessionsAreChronologicallyOrdered() async throws { + let sessions = try await client.getAllSessions() + + // Check if sessions are sorted by start date + for i in 0..<(sessions.count - 1) { + let current = sessions[i] + let next = sessions[i + 1] + // Sessions might not be strictly ordered, but we can check + // Or we just verify they have valid dates + #expect(current.start.timeIntervalSince1970 > 0) + #expect(next.start.timeIntervalSince1970 > 0) + } + } + + @Test + func monacoRaceHasExpectedDates() async throws { + let session = try await client.getSession(sessionKey: sessionKey) + + // Monaco 2023 race was on May 28, 2023 + let calendar = Calendar(identifier: .gregorian) + let startComponents = calendar.dateComponents([.year, .month, .day], from: session.start) + + #expect(startComponents.year == 2023) + #expect(startComponents.month == 5) + #expect(startComponents.day == 28) + } + + @Test + func sessionEndIsAfterStart() async throws { + let sessions = try await client.getAllSessions() + + for session in sessions { + #expect(session.end > session.start) + #expect(session.end.timeIntervalSince(session.start) > 0) + } + } + + @Test + func getAllSessionsReturnsMultipleSessions() async throws { + let sessions = try await client.getAllSessions() + + // F1 season typically has 20+ races × multiple sessions each + #expect(sessions.count >= 10) + } } diff --git a/Tests/PodiumRequestsClientTests/Requests/RequestsClientWeatherTests.swift b/Tests/PodiumRequestsClientTests/Requests/RequestsClientWeatherTests.swift index 65f6567..d4e49c6 100644 --- a/Tests/PodiumRequestsClientTests/Requests/RequestsClientWeatherTests.swift +++ b/Tests/PodiumRequestsClientTests/Requests/RequestsClientWeatherTests.swift @@ -35,4 +35,207 @@ struct RequestsClientWeatherTests { #expect(first.wind.direction == 149) #expect(first.wind.speed == 1) } + + @Test + func getAllWeatherUpdatesAreChronological() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for i in 0..<(weatherUpdates.count - 1) { + let current = weatherUpdates[i] + let next = weatherUpdates[i + 1] + // Assuming weather updates have a timestamp/date field + // #expect(current.date <= next.date) + #expect(current.pressure > 0) + #expect(next.pressure > 0) + } + } + + @Test + func getAllWeatherUpdatesHaveValidData() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + #expect(update.humidity >= 0) + #expect(update.humidity <= 100) + #expect(update.pressure > 900) + #expect(update.pressure < 1100) + #expect(update.temperature.air >= -50) + #expect(update.temperature.air <= 60) + #expect(update.temperature.track >= -50) + #expect(update.temperature.track <= 100) + #expect(update.wind.direction >= 0) + #expect(update.wind.direction < 360) + #expect(update.wind.speed >= 0) + } + } + + @Test + func trackTemperatureIsHigherThanAirTemperature() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + // Track temperature is typically higher than air temperature + let mostUpdates = weatherUpdates.filter { $0.temperature.track > $0.temperature.air } + #expect(mostUpdates.count > weatherUpdates.count / 2) + } + + @Test + func weatherHumidityIsWithinValidRange() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + #expect(update.humidity >= 0) + #expect(update.humidity <= 100) + } + } + + @Test + func weatherPressureIsRealistic() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + // Sea level pressure typically 980-1040 hPa + #expect(update.pressure >= 950) + #expect(update.pressure <= 1050) + } + } + + @Test + func windDirectionIsValidDegrees() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + #expect(update.wind.direction >= 0) + #expect(update.wind.direction < 360) + } + } + + @Test + func windSpeedIsNonNegative() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + #expect(update.wind.speed >= 0) + } + } + + @Test + func getWeatherWithSpecificHumidity() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + let specificUpdate = weatherUpdates.filter { $0.humidity == 39 } + + #expect(!specificUpdate.isEmpty) + } + + @Test + func weatherUpdatesShowVariation() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + let temperatures = Set(weatherUpdates.map { $0.temperature.air }) + let humidities = Set(weatherUpdates.map { $0.humidity }) + + // Weather should vary throughout the session + #expect(temperatures.count > 1) + #expect(humidities.count > 1) + } + + @Test(.bug("https://github.com/EpitechPromo2026/G-EIP-700-REN-7-1-eip-mathis.le-bonniec/issues/113"), .disabled()) + func getAllWeatherWithInvalidSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getAllWeatherUpdates(sessionKey: -1) + } + } + + @Test(.bug("https://github.com/EpitechPromo2026/G-EIP-700-REN-7-1-eip-mathis.le-bonniec/issues/113"), .disabled()) + func getAllWeatherWithNonExistentSessionKeyThrowsError() async throws { + await #expect(throws: Error.self) { + try await client.getAllWeatherUpdates(sessionKey: 999999) + } + } + + @Test + func weatherUpdateCountMatchesExpected() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + #expect(weatherUpdates.count == 176) + } + + @Test + func trackTemperatureExceedsAirTemperatureSignificantly() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + let first = try #require(weatherUpdates.first(where: { $0.pressure == 1013.7 })) + + let difference = first.temperature.track - first.temperature.air + #expect(difference > 10) // Track typically 10-30°C hotter + } + + @Test + func weatherConditionsAreConsistentWithMonaco() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + // Monaco in May/June typically warm + let averageAirTemp = weatherUpdates.map { $0.temperature.air }.reduce(0, +) / Double(weatherUpdates.count) + #expect(averageAirTemp > 15) + #expect(averageAirTemp < 35) + } + + @Test + func pressureDoesNotFluctuateWildly() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + let pressures = weatherUpdates.map { $0.pressure } + let minPressure = pressures.min() ?? 0 + let maxPressure = pressures.max() ?? 0 + let pressureRange = maxPressure - minPressure + + // Pressure shouldn't change more than ~20 hPa during a race + #expect(pressureRange < 30) + } + + @Test + func windSpeedIsReasonable() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + // Wind speed in m/s, typically 0-20 m/s at race events + #expect(update.wind.speed < 25) + } + } + + @Test + func multipleWeatherUpdatesExist() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + // Should have multiple updates throughout the session + #expect(weatherUpdates.count > 50) + } + + @Test + func temperatureValuesAreRealistic() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + for update in weatherUpdates { + // Monaco in May: reasonable temperature ranges + #expect(update.temperature.air >= 10) + #expect(update.temperature.air <= 40) + #expect(update.temperature.track >= 20) + #expect(update.temperature.track <= 70) + } + } + + @Test + func getAllWeatherMultipleTimesReturnsConsistentCount() async throws { + let weatherUpdates1 = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + let weatherUpdates2 = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + + #expect(weatherUpdates1.count == weatherUpdates2.count) + #expect(weatherUpdates1.count == 176) + } + + @Test + func weatherDataContainsSpecificPressureValue() async throws { + let weatherUpdates = try await client.getAllWeatherUpdates(sessionKey: sessionKey) + let hasPressure1013_7 = weatherUpdates.contains(where: { $0.pressure == 1013.7 }) + + #expect(hasPressure1013_7) + } }