mirror of
https://codeberg.org/secana/ForgejoKit.git
synced 2026-06-16 05:13:53 -07:00
249 lines
8.6 KiB
Swift
249 lines
8.6 KiB
Swift
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))
|
|
}
|
|
}
|
|
}
|