ForgejoKit/Tests/ForgejoKitTests/NotificationTests.swift

250 lines
8.6 KiB
Swift
Raw Normal View History

import Foundation
import Testing
@testable import ForgejoKit
struct NotificationTests {
private func decoder() -> JSONDecoder {
let d = JSONDecoder()
d.dateDecodingStrategy = forgejoDateDecodingStrategy
return d
}
// 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: - ServiceError descriptions
@Test func serviceErrorDescriptions() {
#expect(ServiceError.noActiveInstance.errorDescription == "No active Forgejo instance")
#expect(ServiceError.invalidURL.errorDescription == "Invalid URL")
#expect(ServiceError.invalidResponse.errorDescription == "Invalid response from server")
#expect(ServiceError.httpError(statusCode: 404).errorDescription == "HTTP error: 404")
#expect(ServiceError.httpError(statusCode: 500).errorDescription == "HTTP error: 500")
#expect(ServiceError.decodingFailed(detail: "missing key").errorDescription == "Decoding failed: missing key")
}
// 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))
}
}
}