ForgejoKit/Tests/ForgejoKitTests/NotificationTests.swift
Stefan Hausotte b78c57ffd9 feat: add Attachment model and image upload support
- Add Attachment model with isImage computed property
- Add assets field to Issue, IssueComment, and PullRequest
- Add multipart/form-data upload support to ForgejoClient
- Add uploadIssueAttachment and uploadCommentAttachment to IssueService
2026-06-15 09:48:47 +02:00

237 lines
8.1 KiB
Swift

@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 `extracts issue number from subject url`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42"
#expect(notificationSubjectNumber(from: url) == 42)
}
@Test func `extracts pull request number from subject url`() {
let url = "https://forgejo.example.com/api/v1/repos/user/repo/pulls/7"
#expect(notificationSubjectNumber(from: url) == 7)
}
@Test func `returns nil for nil url`() {
#expect(notificationSubjectNumber(from: nil) == nil)
}
@Test func `returns nil for non numeric last component`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/commits/abc123"
#expect(notificationSubjectNumber(from: url) == nil)
}
@Test func `returns nil for empty string`() {
#expect(notificationSubjectNumber(from: "") == nil)
}
@Test func `returns nil for invalid url`() {
#expect(notificationSubjectNumber(from: "not a url at all %%%") == nil)
}
@Test func `extracts number from url with trailing slash`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42/"
#expect(notificationSubjectNumber(from: url) == 42)
}
@Test func `extracts large number`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/99999"
#expect(notificationSubjectNumber(from: url) == 99999)
}
@Test func `extracts number from url with query params`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/issues/42?foo=bar&baz=1"
#expect(notificationSubjectNumber(from: url) == 42)
}
@Test func `extracts number from url with fragment`() {
let url = "https://forgejo.example.com/api/v1/repos/org/app/pulls/7#section"
#expect(notificationSubjectNumber(from: url) == 7)
}
@Test func `extracts number from url with query and fragment`() {
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 `decodes notification array`() 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 `decodes empty notification array`() throws {
let json = "[]"
let notifications = try decoder().decode([NotificationThread].self, from: Data(json.utf8))
#expect(notifications.isEmpty)
}
@Test func `decodes notification count zero`() throws {
let json = #"{"new": 0}"#
let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
#expect(count.new == 0)
}
@Test func `decodes notification count large value`() 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 `decodes notification with fractional seconds date`() 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 `decodes notification ignoring extra fields`() 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 `fails decoding notification missing subject`() {
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 `fails decoding notification missing repository`() {
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 `fails decoding notification missing id`() {
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 `fails decoding notification count missing new field`() {
let json = #"{}"#
#expect(throws: DecodingError.self) {
_ = try decoder().decode(NotificationCount.self, from: Data(json.utf8))
}
}
@Test func `fails decoding subject missing title`() {
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))
}
}
}