@testable import ForgejoKit import Foundation import Testing struct NotificationTests { private func decoder() -> JSONDecoder { let jsonDecoder = JSONDecoder() jsonDecoder.dateDecodingStrategy = forgejoDateDecodingStrategy return jsonDecoder } // MARK: - Subject number extraction @Test func extractsIssueNumberFromSubjectUrl() { let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42" #expect(notificationSubjectNumber(from: url) == 42) } @Test func extractsPullRequestNumberFromSubjectUrl() { let url = "https://forgejo.example.com/api/v1/repos/user/repo/pulls/7" #expect(notificationSubjectNumber(from: url) == 7) } @Test func returnsNilForNilUrl() { #expect(notificationSubjectNumber(from: nil) == nil) } @Test func returnsNilForNonNumericLastComponent() { let url = "https://forgejo.example.com/api/v1/repos/org/app/commits/abc123" #expect(notificationSubjectNumber(from: url) == nil) } @Test func returnsNilForEmptyString() { #expect(notificationSubjectNumber(from: "") == nil) } @Test func returnsNilForInvalidUrl() { #expect(notificationSubjectNumber(from: "not a url at all %%%") == nil) } @Test func extractsNumberFromUrlWithTrailingSlash() { let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42/" #expect(notificationSubjectNumber(from: url) == 42) } @Test func extractsLargeNumber() { let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/99999" #expect(notificationSubjectNumber(from: url) == 99999) } @Test func extractsNumberFromUrlWithQueryParams() { let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42?foo=bar&baz=1" #expect(notificationSubjectNumber(from: url) == 42) } @Test func extractsNumberFromUrlWithFragment() { let url = "https://forgejo.example.com/api/v1/repos/org/app/pulls/7#section" #expect(notificationSubjectNumber(from: url) == 7) } @Test func extractsNumberFromUrlWithQueryAndFragment() { let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/15?ref=main#comment-1" #expect(notificationSubjectNumber(from: url) == 15) } // MARK: - Full API response decoding @Test func decodesNotificationArray() throws { let json = """ [ { "id": 1, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": { "type": "Issue", "title": "Bug report", "url": "https://forgejo.example.com/api/v1/repos/org/app/issues/1", "state": "open" }, "repository": {"id": 10, "name": "app", "full_name": "org/app"} }, { "id": 2, "unread": false, "pinned": true, "updated_at": "2024-06-14T10:00:00Z", "subject": { "type": "Pull", "title": "Add feature", "url": "https://forgejo.example.com/api/v1/repos/org/app/pulls/5", "state": "closed" }, "repository": {"id": 10, "name": "app", "full_name": "org/app"} } ] """ let notifications = try decoder().decode([NotificationThread].self, from: Data(json.utf8)) #expect(notifications.count == 2) #expect(notifications[0].id == 1) #expect(notifications[0].unread == true) #expect(notifications[0].subject.type == "Issue") #expect(notifications[1].id == 2) #expect(notifications[1].pinned == true) #expect(notifications[1].subject.type == "Pull") } @Test func decodesEmptyNotificationArray() throws { let json = "[]" let notifications = try decoder().decode([NotificationThread].self, from: Data(json.utf8)) #expect(notifications.isEmpty) } @Test func decodesNotificationCountZero() throws { let json = #"{"new": 0}"# let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8)) #expect(count.new == 0) } @Test func decodesNotificationCountLargeValue() throws { let json = #"{"new": 9999}"# let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8)) #expect(count.new == 9999) } // MARK: - Date handling in notifications @Test func decodesNotificationWithFractionalSecondsDate() throws { let json = """ { "id": 5, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00.123Z", "subject": {"type": "Issue", "title": "Test"}, "repository": {"id": 1, "name": "r", "full_name": "u/r"} } """ let notification = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) #expect(notification.id == 5) #expect(notification.updatedAt.timeIntervalSince1970 > 0) } // MARK: - Extra/unknown fields are ignored @Test func decodesNotificationIgnoringExtraFields() throws { let json = """ { "id": 6, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "some_future_field": "should be ignored", "subject": { "type": "Issue", "title": "Test", "some_other_field": 42 }, "repository": {"id": 1, "name": "r", "full_name": "u/r", "extra": true} } """ let notification = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) #expect(notification.id == 6) #expect(notification.subject.title == "Test") } // MARK: - Required fields validation @Test func failsDecodingNotificationMissingSubject() { let json = """ { "id": 7, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "repository": {"id": 1, "name": "r", "full_name": "u/r"} } """ #expect(throws: DecodingError.self) { _ = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) } } @Test func failsDecodingNotificationMissingRepository() { let json = """ { "id": 8, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": {"type": "Issue", "title": "Test"} } """ #expect(throws: DecodingError.self) { _ = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) } } @Test func failsDecodingNotificationMissingId() { let json = """ { "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": {"type": "Issue", "title": "Test"}, "repository": {"id": 1, "name": "r", "full_name": "u/r"} } """ #expect(throws: DecodingError.self) { _ = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) } } @Test func failsDecodingNotificationCountMissingNewField() { let json = #"{}"# #expect(throws: DecodingError.self) { _ = try decoder().decode(NotificationCount.self, from: Data(json.utf8)) } } @Test func failsDecodingSubjectMissingTitle() { let json = """ { "id": 9, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": {"type": "Issue"}, "repository": {"id": 1, "name": "r", "full_name": "u/r"} } """ #expect(throws: DecodingError.self) { _ = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) } } }