import Foundation import Testing @testable import ForgejoKit struct ModelDecodingTests { private func decoder() -> JSONDecoder { let d = JSONDecoder() d.dateDecodingStrategy = forgejoDateDecodingStrategy return d } // MARK: - User @Test func decodesUser() throws { let json = """ { "id": 1, "login": "admin", "full_name": "Admin User", "email": "admin@example.com", "avatar_url": "https://example.com/avatar.png", "is_admin": true, "created": "2024-01-01T00:00:00Z" } """ let user = try decoder().decode(User.self, from: Data(json.utf8)) #expect(user.id == 1) #expect(user.login == "admin") #expect(user.fullName == "Admin User") #expect(user.email == "admin@example.com") #expect(user.isAdmin == true) } @Test func decodesUserWithMinimalFields() throws { let json = """ { "id": 2, "login": "bot" } """ let user = try decoder().decode(User.self, from: Data(json.utf8)) #expect(user.id == 2) #expect(user.login == "bot") #expect(user.fullName == nil) #expect(user.email == nil) } // MARK: - IssueLabel @Test func decodesIssueLabel() throws { let json = """ { "id": 10, "name": "bug", "color": "ee0701", "description": "Something is broken" } """ let label = try decoder().decode(IssueLabel.self, from: Data(json.utf8)) #expect(label.id == 10) #expect(label.name == "bug") #expect(label.color == "ee0701") } // MARK: - IssueComment @Test func decodesIssueComment() throws { let json = """ { "id": 100, "body": "Looks good!", "user": {"id": 1, "login": "reviewer"}, "created_at": "2024-05-01T12:00:00Z", "updated_at": "2024-05-01T12:00:00Z" } """ let comment = try decoder().decode(IssueComment.self, from: Data(json.utf8)) #expect(comment.id == 100) #expect(comment.body == "Looks good!") #expect(comment.user.login == "reviewer") } // MARK: - Issue @Test func decodesIssue() throws { let json = """ { "id": 50, "number": 7, "title": "Fix login bug", "body": "The login button does not work", "state": "open", "user": {"id": 1, "login": "reporter"}, "labels": [{"id": 1, "name": "bug", "color": "ff0000"}], "assignees": [], "created_at": "2024-02-01T09:00:00Z", "updated_at": "2024-02-02T10:00:00Z", "comments": 3 } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 7) #expect(issue.title == "Fix login bug") #expect(issue.state == "open") #expect(issue.labels.count == 1) #expect(issue.comments == 3) #expect(issue.repository == nil) } @Test func decodesIssueWithMilestoneAndAssignees() throws { let json = """ { "id": 56, "number": 13, "title": "Issue with metadata", "body": "Has milestone and assignees", "state": "open", "user": {"id": 1, "login": "reporter"}, "labels": [{"id": 1, "name": "bug", "color": "ff0000"}], "milestone": { "id": 5, "title": "v1.0", "description": "First release", "state": "open", "due_on": "2026-03-01T00:00:00Z" }, "assignees": [ {"id": 2, "login": "dev1", "full_name": "Developer One"}, {"id": 3, "login": "dev2"} ], "created_at": "2024-02-01T09:00:00Z", "updated_at": "2024-02-02T10:00:00Z", "comments": 0 } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 13) #expect(issue.labels.count == 1) #expect(issue.milestone?.id == 5) #expect(issue.milestone?.title == "v1.0") #expect(issue.milestone?.description == "First release") #expect(issue.milestone?.state == "open") #expect(issue.milestone?.dueOn != nil) #expect(issue.assignees?.count == 2) #expect(issue.assignees?[0].login == "dev1") #expect(issue.assignees?[0].fullName == "Developer One") #expect(issue.assignees?[1].login == "dev2") } @Test func decodesIssueWithNilMilestoneAndEmptyAssignees() throws { let json = """ { "id": 57, "number": 14, "title": "Issue without metadata", "state": "open", "user": {"id": 1, "login": "reporter"}, "labels": [], "milestone": null, "assignees": [], "created_at": "2024-02-01T09:00:00Z", "updated_at": "2024-02-02T10:00:00Z", "comments": 0 } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.milestone == nil) #expect(issue.assignees?.isEmpty == true) } @Test func decodesIssueMilestone() throws { let json = """ { "id": 1, "title": "v2.0", "description": "Second major release", "state": "open", "due_on": "2026-06-15T00:00:00Z", "closed_at": null } """ let milestone = try decoder().decode(IssueMilestone.self, from: Data(json.utf8)) #expect(milestone.id == 1) #expect(milestone.title == "v2.0") #expect(milestone.description == "Second major release") #expect(milestone.state == "open") #expect(milestone.dueOn != nil) #expect(milestone.closedAt == nil) } @Test func decodesIssueMilestoneWithMinimalFields() throws { let json = """ { "id": 2, "title": "Backlog", "state": "open" } """ let milestone = try decoder().decode(IssueMilestone.self, from: Data(json.utf8)) #expect(milestone.id == 2) #expect(milestone.title == "Backlog") #expect(milestone.description == nil) #expect(milestone.dueOn == nil) #expect(milestone.closedAt == nil) } @Test func decodesIssueWithMinimalRepository() throws { let json = """ { "id": 52, "number": 9, "title": "Search result issue", "state": "open", "user": {"id": 1, "login": "reporter"}, "labels": [], "created_at": "2024-02-01T09:00:00Z", "updated_at": "2024-02-02T10:00:00Z", "comments": 1, "repository": { "id": 10, "name": "backend", "full_name": "org/backend" } } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 9) #expect(issue.repository?.id == 10) #expect(issue.repository?.fullName == "org/backend") #expect(issue.repository?.name == "backend") #expect(issue.repository?.empty == nil) #expect(issue.repository?.starsCount == nil) #expect(issue.repository?.defaultBranch == nil) } @Test func decodesIssueWithRepository() throws { let json = """ { "id": 51, "number": 8, "title": "Add dark mode", "state": "open", "user": {"id": 1, "login": "reporter"}, "labels": [], "created_at": "2024-02-01T09:00:00Z", "updated_at": "2024-02-02T10:00:00Z", "comments": 0, "repository": { "id": 1, "name": "my-repo", "full_name": "user/my-repo", "empty": false, "private": false, "fork": false, "mirror": false, "size": 1024, "html_url": "https://forgejo.example.com/user/my-repo", "ssh_url": "git@forgejo.example.com:user/my-repo.git", "clone_url": "https://forgejo.example.com/user/my-repo.git", "stars_count": 5, "forks_count": 2, "watchers_count": 3, "open_issues_count": 1, "open_pr_counter": 0, "release_counter": 1, "default_branch": "main", "archived": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-06-01T00:00:00Z", "has_issues": true, "has_wiki": false, "has_pull_requests": true, "has_projects": false, "has_releases": true, "has_packages": false, "has_actions": false, "template": false } } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 8) #expect(issue.title == "Add dark mode") #expect(issue.repository?.fullName == "user/my-repo") } @Test func decodesIssueWithMergedPullRequestField() throws { let json = """ { "id": 53, "number": 10, "title": "Add dark mode support", "state": "closed", "user": {"id": 1, "login": "dev"}, "labels": [], "created_at": "2024-03-01T09:00:00Z", "updated_at": "2024-03-05T10:00:00Z", "comments": 2, "pull_request": { "merged": true, "merged_at": "2024-03-05T10:00:00Z" }, "repository": { "id": 10, "name": "app", "full_name": "org/app" } } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 10) #expect(issue.state == "closed") #expect(issue.pullRequest?.merged == true) #expect(issue.pullRequest?.mergedAt != nil) #expect(issue.repository?.fullName == "org/app") } @Test func decodesIssueWithOpenPullRequestField() throws { let json = """ { "id": 54, "number": 11, "title": "WIP: refactor auth", "state": "open", "user": {"id": 2, "login": "contributor"}, "labels": [{"id": 1, "name": "WIP", "color": "fbca04"}], "created_at": "2024-04-01T09:00:00Z", "updated_at": "2024-04-02T10:00:00Z", "comments": 0, "pull_request": { "merged": false }, "repository": { "id": 10, "name": "app", "full_name": "org/app" } } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 11) #expect(issue.state == "open") #expect(issue.pullRequest?.merged == false) #expect(issue.pullRequest?.mergedAt == nil) #expect(issue.labels.count == 1) #expect(issue.comments == 0) } @Test func decodesIssueWithMinimalPullRequestField() throws { let json = """ { "id": 55, "number": 12, "title": "Quick fix", "state": "closed", "user": {"id": 1, "login": "dev"}, "labels": [], "created_at": "2024-04-10T09:00:00Z", "updated_at": "2024-04-10T10:00:00Z", "comments": 1, "pull_request": {} } """ let issue = try decoder().decode(Issue.self, from: Data(json.utf8)) #expect(issue.number == 12) #expect(issue.pullRequest != nil) #expect(issue.pullRequest?.merged == nil) #expect(issue.pullRequest?.mergedAt == nil) #expect(issue.repository == nil) } // MARK: - PullRequest @Test func decodesPullRequest() throws { let json = """ { "id": 200, "number": 42, "title": "Add feature X", "body": "Implements feature X", "state": "open", "user": {"id": 1, "login": "dev"}, "labels": [], "head": { "label": "dev:feature-x", "ref": "feature-x", "sha": "abc123" }, "base": { "label": "dev:main", "ref": "main", "sha": "def456" }, "mergeable": true, "merged": false, "draft": false, "comments": 1, "created_at": "2024-03-01T08:00:00Z", "updated_at": "2024-03-02T09:00:00Z" } """ let pr = try decoder().decode(PullRequest.self, from: Data(json.utf8)) #expect(pr.number == 42) #expect(pr.title == "Add feature X") #expect(pr.head.ref == "feature-x") #expect(pr.base.ref == "main") #expect(pr.mergeable == true) #expect(pr.merged == false) #expect(pr.draft == false) } @Test func decodesPullRequestWithFullMetadata() throws { let json = """ { "id": 204, "number": 46, "title": "PR with full metadata", "body": "Has labels, milestone, assignees, and reviewers", "state": "open", "user": {"id": 1, "login": "dev"}, "labels": [ {"id": 1, "name": "enhancement", "color": "0075ca"}, {"id": 2, "name": "review-needed", "color": "e4e669"} ], "milestone": { "id": 3, "title": "v1.0", "description": "First release", "state": "open", "due_on": "2026-03-01T00:00:00Z" }, "assignees": [ {"id": 2, "login": "assignee1", "full_name": "Assignee One"}, {"id": 3, "login": "assignee2"} ], "requested_reviewers": [ {"id": 4, "login": "reviewer1"} ], "head": {"label": "dev:feature", "ref": "feature", "sha": "aaa"}, "base": {"label": "dev:main", "ref": "main", "sha": "bbb"}, "mergeable": true, "merged": false, "draft": false, "comments": 0, "created_at": "2024-03-01T08:00:00Z", "updated_at": "2024-03-02T09:00:00Z" } """ let pr = try decoder().decode(PullRequest.self, from: Data(json.utf8)) #expect(pr.number == 46) #expect(pr.labels.count == 2) #expect(pr.labels[0].name == "enhancement") #expect(pr.milestone?.id == 3) #expect(pr.milestone?.title == "v1.0") #expect(pr.milestone?.dueOn != nil) #expect(pr.assignees?.count == 2) #expect(pr.assignees?[0].login == "assignee1") #expect(pr.assignees?[0].fullName == "Assignee One") #expect(pr.requestedReviewers?.count == 1) #expect(pr.requestedReviewers?[0].login == "reviewer1") } @Test func decodesPullRequestWithRequestedReviewers() throws { let json = """ { "id": 202, "number": 44, "title": "PR with reviewers", "state": "open", "user": {"id": 1, "login": "dev"}, "labels": [], "head": {"label": "dev:feature", "ref": "feature", "sha": "aaa"}, "base": {"label": "dev:main", "ref": "main", "sha": "bbb"}, "mergeable": true, "merged": false, "draft": false, "comments": 0, "created_at": "2024-03-01T08:00:00Z", "updated_at": "2024-03-02T09:00:00Z", "requested_reviewers": [ {"id": 10, "login": "reviewer1"}, {"id": 11, "login": "reviewer2", "full_name": "Reviewer Two"} ] } """ let pr = try decoder().decode(PullRequest.self, from: Data(json.utf8)) #expect(pr.number == 44) #expect(pr.requestedReviewers?.count == 2) #expect(pr.requestedReviewers?[0].login == "reviewer1") #expect(pr.requestedReviewers?[1].fullName == "Reviewer Two") } @Test func decodesPullRequestWithoutRequestedReviewers() throws { let json = """ { "id": 203, "number": 45, "title": "PR without reviewers", "state": "open", "user": {"id": 1, "login": "dev"}, "labels": [], "head": {"label": "dev:fix", "ref": "fix", "sha": "ccc"}, "base": {"label": "dev:main", "ref": "main", "sha": "ddd"}, "comments": 0, "created_at": "2024-03-01T08:00:00Z", "updated_at": "2024-03-02T09:00:00Z" } """ let pr = try decoder().decode(PullRequest.self, from: Data(json.utf8)) #expect(pr.number == 45) #expect(pr.requestedReviewers == nil) } @Test func decodesMergedPullRequest() throws { let json = """ { "id": 201, "number": 43, "title": "Merged PR", "state": "closed", "user": {"id": 1, "login": "dev"}, "labels": [], "head": {"label": "dev:feat", "ref": "feat", "sha": "aaa"}, "base": {"label": "dev:main", "ref": "main", "sha": "bbb"}, "merged": true, "merged_by": {"id": 2, "login": "maintainer"}, "merged_at": "2024-04-01T12:00:00Z", "comments": 0, "created_at": "2024-03-01T08:00:00Z", "updated_at": "2024-04-01T12:00:00Z" } """ let pr = try decoder().decode(PullRequest.self, from: Data(json.utf8)) #expect(pr.merged == true) #expect(pr.mergedBy?.login == "maintainer") #expect(pr.mergedAt != nil) } // MARK: - PullRequestReview @Test func decodesPullRequestReview() throws { let json = """ { "id": 300, "body": "Approved with minor comments", "user": {"id": 5, "login": "reviewer"}, "state": "APPROVED", "comments_count": 2, "submitted_at": "2024-04-10T15:00:00Z", "updated_at": "2024-04-10T15:00:00Z", "commit_id": "abc123", "stale": false, "official": true, "dismissed": false } """ let review = try decoder().decode(PullRequestReview.self, from: Data(json.utf8)) #expect(review.id == 300) #expect(review.state == "APPROVED") #expect(review.user?.login == "reviewer") #expect(review.commentsCount == 2) #expect(review.official == true) } @Test func decodesReviewWithNullableFields() throws { let json = """ { "id": 301, "state": "COMMENT" } """ let review = try decoder().decode(PullRequestReview.self, from: Data(json.utf8)) #expect(review.id == 301) #expect(review.body == nil) #expect(review.user == nil) #expect(review.commentsCount == nil) } // MARK: - ReviewComment @Test func decodesReviewComment() throws { let json = """ { "id": 400, "body": "This line needs a guard clause", "user": {"id": 5, "login": "reviewer"}, "path": "Sources/main.swift", "diff_hunk": "@@ -10,5 +10,7 @@", "position": 15, "original_position": 10, "created_at": "2024-04-10T15:30:00Z", "updated_at": "2024-04-10T15:30:00Z" } """ let comment = try decoder().decode(ReviewComment.self, from: Data(json.utf8)) #expect(comment.id == 400) #expect(comment.path == "Sources/main.swift") #expect(comment.position == 15) #expect(comment.originalPosition == 10) } // MARK: - CreateReviewComment encoding @Test func encodesCreateReviewComment() throws { let comment = CreateReviewComment( body: "Fix this", path: "file.swift", oldPosition: 5, newPosition: 7 ) let data = try JSONEncoder().encode(comment) let dict = try JSONSerialization.jsonObject(with: data) as! [String: Any] #expect(dict["body"] as? String == "Fix this") #expect(dict["path"] as? String == "file.swift") #expect(dict["old_position"] as? Int == 5) #expect(dict["new_position"] as? Int == 7) } @Test func encodesCreateReviewCommentWithNilPositions() throws { let comment = CreateReviewComment( body: "Note", path: "readme.md", oldPosition: nil, newPosition: nil ) let data = try JSONEncoder().encode(comment) let dict = try JSONSerialization.jsonObject(with: data) as! [String: Any] #expect(dict["old_position"] == nil) #expect(dict["new_position"] == nil) } // MARK: - Repository @Test func decodesRepository() throws { let json = """ { "id": 1, "name": "my-repo", "full_name": "user/my-repo", "empty": false, "private": false, "fork": false, "mirror": false, "size": 1024, "html_url": "https://forgejo.example.com/user/my-repo", "ssh_url": "git@forgejo.example.com:user/my-repo.git", "clone_url": "https://forgejo.example.com/user/my-repo.git", "stars_count": 5, "forks_count": 2, "watchers_count": 3, "open_issues_count": 1, "open_pr_counter": 0, "release_counter": 1, "default_branch": "main", "archived": false, "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-06-01T00:00:00Z", "has_issues": true, "has_wiki": false, "has_pull_requests": true, "has_projects": false, "has_releases": true, "has_packages": false, "has_actions": false, "template": false } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.name == "my-repo") #expect(repo.fullName == "user/my-repo") #expect(repo.empty != nil) #expect(repo.starsCount != nil) #expect(repo.starsCount == 5) #expect(repo.defaultBranch == "main") #expect(repo.hasPullRequests == true) #expect(repo.createdAt != nil) #expect(repo.updatedAt != nil) } @Test func decodesRepositoryWithMinimalFields() throws { let json = """ { "id": 10, "name": "backend", "full_name": "org/backend" } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.id == 10) #expect(repo.name == "backend") #expect(repo.fullName == "org/backend") #expect(repo.empty == nil) #expect(repo.private == nil) #expect(repo.fork == nil) #expect(repo.mirror == nil) #expect(repo.size == nil) #expect(repo.htmlUrl == nil) #expect(repo.sshUrl == nil) #expect(repo.cloneUrl == nil) #expect(repo.starsCount == nil) #expect(repo.forksCount == nil) #expect(repo.watchersCount == nil) #expect(repo.openIssuesCount == nil) #expect(repo.defaultBranch == nil) #expect(repo.archived == nil) #expect(repo.createdAt == nil) #expect(repo.updatedAt == nil) #expect(repo.hasIssues == nil) #expect(repo.hasPullRequests == nil) #expect(repo.hasReleases == nil) #expect(repo.hasPackages == nil) #expect(repo.hasActions == nil) #expect(repo.template == nil) } @Test func decodesRepositoryWithPartialFields() throws { let json = """ { "id": 11, "name": "frontend", "full_name": "org/frontend", "private": true, "html_url": "https://forgejo.example.com/org/frontend", "stars_count": 12, "default_branch": "develop" } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.id == 11) #expect(repo.fullName == "org/frontend") #expect(repo.private == true) #expect(repo.htmlUrl == "https://forgejo.example.com/org/frontend") #expect(repo.starsCount == 12) #expect(repo.defaultBranch == "develop") #expect(repo.empty == nil) #expect(repo.forksCount == nil) } // MARK: - NotificationThread @Test func decodesNotificationThread() throws { let json = """ { "id": 1, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "url": "https://forgejo.example.com/api/v1/notifications/threads/1", "subject": { "type": "Issue", "title": "Fix login bug", "url": "https://forgejo.example.com/api/v1/repos/org/app/issues/42", "html_url": "https://forgejo.example.com/org/app/issues/42", "state": "open", "latest_comment_url": "https://forgejo.example.com/api/v1/repos/org/app/issues/comments/100", "latest_comment_html_url": "https://forgejo.example.com/org/app/issues/42#issuecomment-100" }, "repository": { "id": 10, "name": "app", "full_name": "org/app" } } """ let notification = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) #expect(notification.id == 1) #expect(notification.unread == true) #expect(notification.pinned == false) #expect(notification.subject.type == "Issue") #expect(notification.subject.title == "Fix login bug") #expect(notification.subject.state == "open") #expect(notification.subject.htmlUrl == "https://forgejo.example.com/org/app/issues/42") #expect(notification.subject.latestCommentUrl != nil) #expect(notification.subject.latestCommentHtmlUrl != nil) #expect(notification.repository.fullName == "org/app") } @Test func decodesNotificationSubjectTypes() throws { let issueJson = """ { "id": 2, "unread": true, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": { "type": "Issue", "title": "Bug report", "state": "open" }, "repository": {"id": 1, "name": "repo", "full_name": "user/repo"} } """ let pullJson = """ { "id": 3, "unread": false, "pinned": false, "updated_at": "2024-06-15T15:00:00Z", "subject": { "type": "Pull", "title": "Add feature", "state": "closed" }, "repository": {"id": 1, "name": "repo", "full_name": "user/repo"} } """ let commitJson = """ { "id": 4, "unread": true, "pinned": false, "updated_at": "2024-06-15T16:00:00Z", "subject": { "type": "Commit", "title": "abc123" }, "repository": {"id": 1, "name": "repo", "full_name": "user/repo"} } """ let issue = try decoder().decode(NotificationThread.self, from: Data(issueJson.utf8)) #expect(issue.subject.type == "Issue") #expect(issue.subject.state == "open") let pull = try decoder().decode(NotificationThread.self, from: Data(pullJson.utf8)) #expect(pull.subject.type == "Pull") #expect(pull.subject.state == "closed") #expect(pull.unread == false) let commit = try decoder().decode(NotificationThread.self, from: Data(commitJson.utf8)) #expect(commit.subject.type == "Commit") #expect(commit.subject.state == nil) } @Test func decodesNotificationCount() throws { let json = """ {"new": 5} """ let count = try decoder().decode(NotificationCount.self, from: Data(json.utf8)) #expect(count.new == 5) } @Test func decodesNotificationWithMinimalFields() throws { let json = """ { "id": 10, "unread": false, "pinned": false, "updated_at": "2024-06-15T14:30:00Z", "subject": { "type": "Repository", "title": "New release" }, "repository": {"id": 5, "name": "lib", "full_name": "org/lib"} } """ let notification = try decoder().decode(NotificationThread.self, from: Data(json.utf8)) #expect(notification.id == 10) #expect(notification.unread == false) #expect(notification.url == nil) #expect(notification.subject.url == nil) #expect(notification.subject.htmlUrl == nil) #expect(notification.subject.state == nil) #expect(notification.subject.latestCommentUrl == nil) #expect(notification.subject.latestCommentHtmlUrl == nil) } // MARK: - Commit @Test func decodesCommit() throws { let json = """ { "sha": "abc123def456", "url": "https://forgejo.example.com/api/v1/repos/user/repo/git/commits/abc123def456", "html_url": "https://forgejo.example.com/user/repo/commit/abc123def456", "commit": { "url": "https://forgejo.example.com/api/v1/repos/user/repo/git/commits/abc123def456", "message": "Add new feature\\n\\nDetailed description of the change.", "author": { "name": "Test User", "email": "test@example.com", "date": "2024-06-01T10:30:00Z" }, "committer": { "name": "Test User", "email": "test@example.com", "date": "2024-06-01T10:30:00Z" } }, "author": {"id": 1, "login": "testuser"}, "committer": {"id": 1, "login": "testuser"}, "parents": [ { "sha": "parent123", "url": "https://forgejo.example.com/api/v1/repos/user/repo/git/commits/parent123", "html_url": "https://forgejo.example.com/user/repo/commit/parent123" } ] } """ let commit = try decoder().decode(Commit.self, from: Data(json.utf8)) #expect(commit.sha == "abc123def456") #expect(commit.htmlUrl == "https://forgejo.example.com/user/repo/commit/abc123def456") #expect(commit.commit.message == "Add new feature\n\nDetailed description of the change.") #expect(commit.commit.author?.name == "Test User") #expect(commit.commit.author?.email == "test@example.com") #expect(commit.commit.author?.date != nil) #expect(commit.commit.committer?.name == "Test User") #expect(commit.author?.login == "testuser") #expect(commit.committer?.login == "testuser") #expect(commit.parents?.count == 1) #expect(commit.parents?[0].sha == "parent123") #expect(commit.id == "abc123def456") } @Test func decodesCommitWithMinimalFields() throws { let json = """ { "sha": "minimal123", "commit": { "message": "Quick fix" } } """ let commit = try decoder().decode(Commit.self, from: Data(json.utf8)) #expect(commit.sha == "minimal123") #expect(commit.commit.message == "Quick fix") #expect(commit.commit.author == nil) #expect(commit.commit.committer == nil) #expect(commit.author == nil) #expect(commit.committer == nil) #expect(commit.parents == nil) #expect(commit.htmlUrl == nil) #expect(commit.url == nil) } @Test func decodesCommitWithNullAuthor() throws { let json = """ { "sha": "noauthor456", "commit": { "message": "Automated commit", "author": { "name": "bot", "email": "bot@noreply.example.com", "date": "2024-07-15T08:00:00Z" } }, "author": null, "committer": null, "parents": [] } """ let commit = try decoder().decode(Commit.self, from: Data(json.utf8)) #expect(commit.sha == "noauthor456") #expect(commit.commit.author?.name == "bot") #expect(commit.author == nil) #expect(commit.committer == nil) #expect(commit.parents?.isEmpty == true) } @Test func decodesCommitList() throws { let json = """ [ { "sha": "first111", "commit": {"message": "First commit"}, "parents": [] }, { "sha": "second222", "commit": {"message": "Second commit"}, "parents": [{"sha": "first111"}] } ] """ let commits = try decoder().decode([Commit].self, from: Data(json.utf8)) #expect(commits.count == 2) #expect(commits[0].sha == "first111") #expect(commits[1].sha == "second222") #expect(commits[1].parents?[0].sha == "first111") } // MARK: - RepositoryContent @Test func decodesRepositoryContent() throws { let json = """ { "name": "README.md", "path": "README.md", "sha": "abc123", "size": 256, "url": "https://example.com/api/v1/repos/u/r/contents/README.md", "html_url": "https://example.com/u/r/src/branch/main/README.md", "git_url": "https://example.com/api/v1/repos/u/r/git/blobs/abc123", "type": "file" } """ let content = try decoder().decode(RepositoryContent.self, from: Data(json.utf8)) #expect(content.name == "README.md") #expect(content.type == .file) #expect(content.id == "README.md") } @Test func decodesDirectoryContent() throws { let json = """ { "name": "src", "path": "src", "sha": "def456", "size": 0, "url": "https://example.com/api", "html_url": "https://example.com/html", "git_url": "https://example.com/git", "type": "dir" } """ let content = try decoder().decode(RepositoryContent.self, from: Data(json.utf8)) #expect(content.type == .dir) } // MARK: - Branch @Test func decodesBranch() throws { let json = """ { "name": "main", "commit": { "id": "abc123", "message": "Initial commit", "url": "https://example.com/commit/abc123" }, "protected": true } """ let branch = try decoder().decode(Branch.self, from: Data(json.utf8)) #expect(branch.name == "main") #expect(branch.commit.message == "Initial commit") #expect(branch.protected == true) #expect(branch.id == "main") } // MARK: - Repository Equatable / Hashable @Test func repositoriesWithSameIdAreEqual() throws { let json1 = """ {"id": 42, "name": "repo-a", "full_name": "owner/repo-a", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo-a", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"} """ let json2 = """ {"id": 42, "name": "repo-b", "full_name": "owner/repo-b", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo-b", "description": "different", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 5, "forks_count": 1, "open_issues_count": 3, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"} """ let repo1 = try decoder().decode(Repository.self, from: Data(json1.utf8)) let repo2 = try decoder().decode(Repository.self, from: Data(json2.utf8)) #expect(repo1 == repo2) #expect(repo1.hashValue == repo2.hashValue) } @Test func repositoriesWithDifferentIdsAreNotEqual() throws { let json1 = """ {"id": 1, "name": "repo", "full_name": "owner/repo", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"} """ let json2 = """ {"id": 2, "name": "repo", "full_name": "owner/repo", "owner": {"id": 1, "login": "owner"}, "html_url": "https://example.com/owner/repo", "description": "", "empty": false, "private": false, "fork": false, "mirror": false, "archived": false, "stars_count": 0, "forks_count": 0, "open_issues_count": 0, "open_pr_counter": 0, "has_issues": true, "has_pull_requests": true, "default_branch": "main", "created_at": "2024-01-01T00:00:00Z", "updated_at": "2024-01-01T00:00:00Z"} """ let repo1 = try decoder().decode(Repository.self, from: Data(json1.utf8)) let repo2 = try decoder().decode(Repository.self, from: Data(json2.utf8)) #expect(repo1 != repo2) } // MARK: - RepositoryPermissions @Test func decodesRepositoryWithFullPermissions() throws { let json = """ { "id": 20, "name": "collab-repo", "full_name": "org/collab-repo", "permissions": { "admin": true, "push": true, "pull": true } } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.permissions?.admin == true) #expect(repo.permissions?.push == true) #expect(repo.permissions?.pull == true) } @Test func decodesRepositoryWithReadOnlyPermissions() throws { let json = """ { "id": 21, "name": "public-repo", "full_name": "org/public-repo", "permissions": { "admin": false, "push": false, "pull": true } } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.permissions?.admin == false) #expect(repo.permissions?.push == false) #expect(repo.permissions?.pull == true) } @Test func decodesRepositoryWithMissingPermissions() throws { let json = """ { "id": 22, "name": "no-perms", "full_name": "org/no-perms" } """ let repo = try decoder().decode(Repository.self, from: Data(json.utf8)) #expect(repo.permissions == nil) } // MARK: - DecodingError description @Test func decodingErrorAtRootShowsRoot() { // Decoding a String where an object is expected produces a root-level error let json = #""not-an-object""# do { _ = try decoder().decode(User.self, from: Data(json.utf8)) Issue.record("Expected decoding to fail") } catch let error as DecodingError { let description = describeDecodingError(error) #expect(description.contains("root")) } catch { Issue.record("Unexpected error type: \(error)") } } }