From af7fa1d35bba80b87b905b03ea98f144ae1770d1 Mon Sep 17 00:00:00 2001 From: Jeroen Wesbeek Date: Wed, 4 Dec 2024 17:27:19 +0100 Subject: [PATCH] - Extend PullRequest to read and create pull request review comments (regular comments as well as comments to specific commits / line numbers) - Add support for fetching requested reviewers (users and teams) for a Pull Request - Fix lint warnings --- OctoKit/Issue.swift | 54 +-- OctoKit/PullRequest.swift | 316 ++++++++++++++++-- OctoKit/Team.swift | 83 +++++ .../Fixtures/pull_request_comment.json | 56 ++++ .../Fixtures/pull_request_comments.json | 58 ++++ ...sts_files.json => pull_request_files.json} | 0 .../pull_request_requested_reviewers.json | 41 +++ Tests/OctoKitTests/PullRequestTests.swift | 178 +++++++++- 8 files changed, 730 insertions(+), 56 deletions(-) create mode 100644 OctoKit/Team.swift create mode 100644 Tests/OctoKitTests/Fixtures/pull_request_comment.json create mode 100644 Tests/OctoKitTests/Fixtures/pull_request_comments.json rename Tests/OctoKitTests/Fixtures/{pull_requests_files.json => pull_request_files.json} (100%) create mode 100644 Tests/OctoKitTests/Fixtures/pull_request_requested_reviewers.json diff --git a/OctoKit/Issue.swift b/OctoKit/Issue.swift index ffb4dfe3..0d7fa0b6 100644 --- a/OctoKit/Issue.swift +++ b/OctoKit/Issue.swift @@ -104,21 +104,23 @@ open class Issue: Codable { } } -public struct Comment: Codable { - public let id: Int - public let url: URL - public let htmlURL: URL - public let body: String - public let user: User - public let createdAt: Date - public let updatedAt: Date - public let reactions: Reactions? +public extension Issue { + struct Comment: Codable { + public let id: Int + public let url: URL + public let htmlURL: URL + public let body: String + public let user: User + public let createdAt: Date + public let updatedAt: Date + public let reactions: Reactions? - enum CodingKeys: String, CodingKey { - case id, url, body, user, reactions - case htmlURL = "html_url" - case createdAt = "created_at" - case updatedAt = "updated_at" + enum CodingKeys: String, CodingKey { + case id, url, body, user, reactions + case htmlURL = "html_url" + case createdAt = "created_at" + case updatedAt = "updated_at" + } } } @@ -382,11 +384,11 @@ public extension Octokit { repository: String, number: Int, body: String, - completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { + completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { let router = IssueRouter.commentIssue(configuration, owner, repository, number, body) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) - return router.post(session, decoder: decoder, expectedResultType: Comment.self) { issue, error in + return router.post(session, decoder: decoder, expectedResultType: Issue.Comment.self) { issue, error in if let error = error { completion(.failure(error)) } else { @@ -406,11 +408,11 @@ public extension Octokit { /// - body: The contents of the comment. /// - completion: Callback for the comment that is created. @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) - func commentIssue(owner: String, repository: String, number: Int, body: String) async throws -> Comment { + func commentIssue(owner: String, repository: String, number: Int, body: String) async throws -> Issue.Comment { let router = IssueRouter.commentIssue(configuration, owner, repository, number, body) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) - return try await router.post(session, decoder: decoder, expectedResultType: Comment.self) + return try await router.post(session, decoder: decoder, expectedResultType: Issue.Comment.self) } #endif @@ -428,9 +430,9 @@ public extension Octokit { number: Int, page: String = "1", perPage: String = "100", - completion: @escaping (_ response: Result<[Comment], Error>) -> Void) -> URLSessionDataTaskProtocol? { + completion: @escaping (_ response: Result<[Issue.Comment], Error>) -> Void) -> URLSessionDataTaskProtocol? { let router = IssueRouter.readIssueComments(configuration, owner, repository, number, page, perPage) - return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Comment].self) { comments, error in + return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Issue.Comment].self) { comments, error in if let error = error { completion(.failure(error)) } else { @@ -455,9 +457,9 @@ public extension Octokit { repository: String, number: Int, page: String = "1", - perPage: String = "100") async throws -> [Comment] { + perPage: String = "100") async throws -> [Issue.Comment] { let router = IssueRouter.readIssueComments(configuration, owner, repository, number, page, perPage) - return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Comment].self) + return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [Issue.Comment].self) } #endif @@ -473,11 +475,11 @@ public extension Octokit { repository: String, number: Int, body: String, - completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { + completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { let router = IssueRouter.patchIssueComment(configuration, owner, repository, number, body) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) - return router.post(session, decoder: decoder, expectedResultType: Comment.self) { issue, error in + return router.post(session, decoder: decoder, expectedResultType: Issue.Comment.self) { issue, error in if let error = error { completion(.failure(error)) } else { @@ -497,11 +499,11 @@ public extension Octokit { /// - body: The contents of the comment. /// - completion: Callback for the comment that is created. @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) - func patchIssueComment(owner: String, repository: String, number: Int, body: String) async throws -> Comment { + func patchIssueComment(owner: String, repository: String, number: Int, body: String) async throws -> Issue.Comment { let router = IssueRouter.patchIssueComment(configuration, owner, repository, number, body) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) - return try await router.post(session, decoder: decoder, expectedResultType: Comment.self) + return try await router.post(session, decoder: decoder, expectedResultType: Issue.Comment.self) } #endif } diff --git a/OctoKit/PullRequest.swift b/OctoKit/PullRequest.swift index 85584fd5..5dcd71ea 100644 --- a/OctoKit/PullRequest.swift +++ b/OctoKit/PullRequest.swift @@ -180,6 +180,58 @@ extension PullRequest.File { } } +public extension PullRequest { + struct Comment: Codable { + public let id: Int + public let url: URL + public let htmlURL: URL + public let body: String + public let user: User + public let createdAt: Date + public let updatedAt: Date + public let reactions: Reactions? + + /// The SHA of the commit this comment is associated with. + public private(set) var commitId: String? + /// The line of the blob in the pull request diff that the comment applies to. For a multi-line comment, the last line of the range that your comment applies to. + public private(set) var line: Int? + /// The first line in the pull request diff that the multi-line comment applies to. + public private(set) var startLine: Int? + /// The starting side of the diff that the comment applies to. + public private(set) var side: Side? + /// The level at which the comment is targeted. + public private(set) var subjectType: SubjectType? + /// The ID of the review comment this comment replied to. + public private(set) var inReplyToId: Int? + + enum CodingKeys: String, CodingKey { + case id, url, body, user, reactions + case htmlURL = "html_url" + case createdAt = "created_at" + case updatedAt = "updated_at" + + case line, side + case commitId = "commit_id" + case startLine = "start_line" + case inReplyToId = "in_reply_to_id" + } + } +} + +public extension PullRequest.Comment { + enum Side: String, Codable { + case left = "LEFT" + case right = "RIGHT" + case side + } +} + +public extension PullRequest.Comment { + enum SubjectType: String, Codable { + case line, file + } +} + // MARK: Request public extension Octokit { @@ -213,10 +265,8 @@ public extension Octokit { return router.post(session, decoder: decoder, expectedResultType: PullRequest.self) { pullRequest, error in if let error = error { completion(.failure(error)) - } else { - if let pullRequest = pullRequest { - completion(.success(pullRequest)) - } + } else if let pullRequest { + completion(.success(pullRequest)) } } } @@ -268,10 +318,8 @@ public extension Octokit { return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: PullRequest.self) { pullRequest, error in if let error = error { completion(.failure(error)) - } else { - if let pullRequest = pullRequest { - completion(.success(pullRequest)) - } + } else if let pullRequest { + completion(.success(pullRequest)) } } } @@ -319,10 +367,8 @@ public extension Octokit { return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [PullRequest].self) { pullRequests, error in if let error = error { completion(.failure(error)) - } else { - if let pullRequests = pullRequests { - completion(.success(pullRequests)) - } + } else if let pullRequests { + completion(.success(pullRequests)) } } } @@ -381,10 +427,8 @@ public extension Octokit { return router.post(session, decoder: decoder, expectedResultType: PullRequest.self) { pullRequest, error in if let error = error { completion(.failure(error)) - } else { - if let pullRequest = pullRequest { - completion(.success(pullRequest)) - } + } else if let pullRequest { + completion(.success(pullRequest)) } } } @@ -416,7 +460,6 @@ public extension Octokit { decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) return try await router.post(session, decoder: decoder, expectedResultType: PullRequest.self) } - #endif func listPullRequestsFiles(owner: String, @@ -429,10 +472,8 @@ public extension Octokit { return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [PullRequest.File].self) { files, error in if let error = error { completion(.failure(error)) - } else { - if let files = files { - completion(.success(files)) - } + } else if let files { + completion(.success(files)) } } } @@ -448,6 +489,196 @@ public extension Octokit { return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [PullRequest.File].self) } #endif + + /// Fetches all review comments for a pull request. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - page: Current page for comments pagination. `1` by default. + /// - perPage: Number of comments per page. `100` by default. + /// - completion: Callback for the outcome of the fetch. + @discardableResult + func readPullRequestReviewComments(owner: String, + repository: String, + number: Int, + page: Int = 1, + perPage: Int = 100, + completion: @escaping (_ response: Result<[PullRequest.Comment], Error>) -> Void) -> URLSessionDataTaskProtocol? { + let router = PullRequestRouter.readPullRequestReviewComments(configuration, owner, repository, number, page, perPage) + return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [PullRequest.Comment].self) { comments, error in + if let error = error { + completion(.failure(error)) + } else if let comments { + completion(.success(comments)) + } + } + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /// Fetches all review comments for a pull request. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - page: Current page for comments pagination. `1` by default. + /// - perPage: Number of comments per page. `100` by default. + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func readPullRequestReviewComments(owner: String, + repository: String, + number: Int, + page: Int = 1, + perPage: Int = 100) async throws -> [PullRequest.Comment] { + let router = PullRequestRouter.readPullRequestReviewComments(configuration, owner, repository, number, page, perPage) + return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: [PullRequest.Comment].self) + } + #endif + + /// Posts a review comment on a pull request using the given body. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - commitId: The SHA of the commit needing a comment. + /// - path: The relative path to the file that necessitates a comment. + /// - line: The line of the blob in the pull request diff that the comment applies to. + /// - body: The text of the review comment. + /// - completion: Callback for the comment that is created. + @discardableResult + func createPullRequestReviewComment(owner: String, + repository: String, + number: Int, + commitId: String, + path: String, + line: Int, + body: String, + completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { + let router = PullRequestRouter.createPullRequestReviewComment(configuration, owner, repository, number, commitId, path, line, body) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) + return router.post(session, decoder: decoder, expectedResultType: PullRequest.Comment.self) { issue, error in + if let error = error { + completion(.failure(error)) + } else if let issue { + completion(.success(issue)) + } + } + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /// Posts a review comment on a pull request using the given body. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - commitId: The SHA of the commit needing a comment. + /// - path: The relative path to the file that necessitates a comment. + /// - line: The line of the blob in the pull request diff that the comment applies to. + /// - body: The contents of the comment. + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func createPullRequestReviewComment(owner: String, + repository: String, + number: Int, + commitId: String, + path: String, + line: Int, + body: String) async throws -> PullRequest.Comment { + let router = PullRequestRouter.createPullRequestReviewComment(configuration, owner, repository, number, commitId, path, line, body) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(Time.rfc3339DateFormatter) + return try await router.post(session, decoder: decoder, expectedResultType: PullRequest.Comment.self) + } + #endif + + /// Posts a review comment on a pull request using the given body. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - body: The text of the review comment. + /// - completion: Callback for the comment that is created. + @discardableResult + func createPullRequestReviewComment(owner: String, + repository: String, + number: Int, + body: String, + completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { + /// To add a regular comment to a pull request timeline, the Issue Comment API should be used. + /// See: https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-review-comment-for-a-pull-request + commentIssue(owner: owner, + repository: repository, + number: number, body: body, + completion: completion) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /// Posts a review comment on a pull request using the given body. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - body: The contents of the comment. + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func createPullRequestReviewComment(owner: String, + repository: String, + number: Int, + body: String) async throws -> Issue.Comment { + /// To add a regular comment to a pull request timeline, the Issue Comment API should be used. + /// See: https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-review-comment-for-a-pull-request + try await commentIssue(owner: owner, repository: repository, number: number, body: body) + } + #endif + + /// Fetches all reviewers for a pull request. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - page: Current page for comments pagination. `1` by default. + /// - perPage: Number of comments per page. `100` by default. + /// - completion: Callback for the outcome of the fetch. + @discardableResult + func readPullRequestRequestedReviewers(owner: String, + repository: String, + number: Int, + page: Int = 1, + perPage: Int = 100, + completion: @escaping (_ response: Result) -> Void) -> URLSessionDataTaskProtocol? { + let router = PullRequestRouter.readPullRequestRequestedReviewers(configuration, owner, repository, number, page, perPage) + return router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: PullRequest.RequestedReviewers.self) { requestedReviewers, error in + if let error = error { + completion(.failure(error)) + } else if let requestedReviewers { + completion(.success(requestedReviewers)) + } + } + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + /// Fetches all reviewers for a pull request. + /// - Parameters: + /// - owner: The user or organization that owns the repository. + /// - repository: The name of the repository. + /// - number: The number of the pull request. + /// - page: Current page for comments pagination. `1` by default. + /// - perPage: Number of comments per page. `100` by default. + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func readPullRequestRequestedReviewers(owner: String, + repository: String, + number: Int, + page: Int = 1, + perPage: Int = 100) async throws -> PullRequest.RequestedReviewers { + let router = PullRequestRouter.readPullRequestRequestedReviewers(configuration, owner, repository, number, page, perPage) + return try await router.load(session, dateDecodingStrategy: .formatted(Time.rfc3339DateFormatter), expectedResultType: PullRequest.RequestedReviewers.self) + } + #endif +} + +public extension PullRequest { + struct RequestedReviewers: Codable { + public private(set) var users = [User]() + public private(set) var teams = [Team]() + } } // MARK: Router @@ -458,14 +689,20 @@ enum PullRequestRouter: JSONPostRouter { case createPullRequest(Configuration, String, String, String, String, String?, String, String?, Bool?, Bool?) case patchPullRequest(Configuration, String, String, String, String, String, Openness, String?, Bool?) case listPullRequestsFiles(Configuration, String, String, Int, Int?, Int?) + case readPullRequestReviewComments(Configuration, String, String, Int, Int?, Int?) + case createPullRequestReviewComment(Configuration, String, String, Int, String, String, Int, String) + case readPullRequestRequestedReviewers(Configuration, String, String, Int, Int?, Int?) var method: HTTPMethod { switch self { - case .createPullRequest: + case .createPullRequest, + .createPullRequestReviewComment: return .POST case .readPullRequest, .readPullRequests, - .listPullRequestsFiles: + .listPullRequestsFiles, + .readPullRequestReviewComments, + .readPullRequestRequestedReviewers: return .GET case .patchPullRequest: return .PATCH @@ -474,7 +711,9 @@ enum PullRequestRouter: JSONPostRouter { var encoding: HTTPEncoding { switch self { - case .patchPullRequest, .createPullRequest: + case .patchPullRequest, + .createPullRequest, + .createPullRequestReviewComment: return .json default: return .url @@ -488,6 +727,9 @@ enum PullRequestRouter: JSONPostRouter { case let .patchPullRequest(config, _, _, _, _, _, _, _, _): return config case let .createPullRequest(config, _, _, _, _, _, _, _, _, _): return config case let .listPullRequestsFiles(config, _, _, _, _, _): return config + case let .readPullRequestReviewComments(config, _, _, _, _, _): return config + case let .createPullRequestReviewComment(config, _, _, _, _, _, _, _): return config + case let .readPullRequestRequestedReviewers(config, _, _, _, _, _): return config } } @@ -552,15 +794,24 @@ enum PullRequestRouter: JSONPostRouter { parameters["draft"] = draft } return parameters - case let .listPullRequestsFiles(_, _, _, _, perPage, page): + case let .listPullRequestsFiles(_, _, _, _, perPage, page), + let .readPullRequestReviewComments(_, _, _, _, page, perPage), + let .readPullRequestRequestedReviewers(_, _, _, _, page, perPage): var parameters: [String: Any] = [:] - if let perPage = perPage { + if let perPage { parameters["per_page"] = perPage } - if let page = page { + if let page { parameters["page"] = page } return parameters + case let .createPullRequestReviewComment(_, _, _, _, commitId, path, line, body): + return [ + "body": body, + "commit_id": commitId, + "path": path, + "line": line + ] } } @@ -575,7 +826,16 @@ enum PullRequestRouter: JSONPostRouter { case let .createPullRequest(_, owner, repository, _, _, _, _, _, _, _): return "repos/\(owner)/\(repository)/pulls" case let .listPullRequestsFiles(_, owner, repository, number, _, _): - return "/repos/\(owner)/\(repository)/pulls/\(number)/files" + return "repos/\(owner)/\(repository)/pulls/\(number)/files" + case let .readPullRequestReviewComments(_, owner, repository, number, _, _): + /// See: https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#list-review-comments-on-a-pull-request + return "repos/\(owner)/\(repository)/pulls/\(number)/comments" + case let .createPullRequestReviewComment(_, owner, repository, number, _, _, _, _): + /// See: https://docs.github.com/en/rest/pulls/comments?apiVersion=2022-11-28#create-a-review-comment-for-a-pull-request + return "repos/\(owner)/\(repository)/pulls/\(number)/comments" + case let .readPullRequestRequestedReviewers(_, owner, repository, number, _, _): + /// See: https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#get-all-requested-reviewers-for-a-pull-request + return "repos/\(owner)/\(repository)/pulls/\(number)/requested_reviewers" } } } diff --git a/OctoKit/Team.swift b/OctoKit/Team.swift new file mode 100644 index 00000000..9b09125c --- /dev/null +++ b/OctoKit/Team.swift @@ -0,0 +1,83 @@ +// +// Team.swift +// OctoKit +// +// Created by Jeroen Wesbeek on 04/12/2024. +// + +import Foundation +import RequestKit +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +// MARK: model + +open class Team: Codable { + open internal(set) var id: Int + open var nodeID: String + open var name: String + open var slug: String + open var description: String? + open var privacy: Privacy? + open var notificationSetting: NotificationSetting? + open var url: String? + open var htmlURL: String? + open var membersURL: String? + open var repositoriesURL: String? + + public init(id: Int = -1, + nodeID: String, + name: String, + slug: String, + description: String, + privacy: Privacy? = nil, + notificationSetting: NotificationSetting? = nil, + url: String? = nil, + htmlURL: String? = nil, + membersURL: String? = nil, + repositoriesURL: String? = nil) { + self.id = id + self.nodeID = nodeID + self.name = name + self.slug = slug + self.description = description + self.privacy = privacy + self.notificationSetting = notificationSetting + self.url = url + self.htmlURL = htmlURL + self.membersURL = membersURL + self.repositoriesURL = repositoriesURL + } + + enum CodingKeys: String, CodingKey { + case id, name, slug, description, privacy, url + case nodeID = "node_id" + case htmlURL = "html_url" + case membersURL = "members_url" + case repositoriesURL = "repositories_url" + } +} + +public extension Team { + /// The level of privacy of a team. + enum Privacy: String, Codable { + /// Only visible to organization owners and members of this team. + case secret + /// Visible to all members of this organization. + case closed + } +} + +public extension Team { + enum Permission: String, Codable { + case pull, push, triage, maintain, admin + } +} + +public extension Team { + enum NotificationSetting: String, Codable { + case enabled = "notifications_enabled" + case disabled = "notifications_disabled" + } +} diff --git a/Tests/OctoKitTests/Fixtures/pull_request_comment.json b/Tests/OctoKitTests/Fixtures/pull_request_comment.json new file mode 100644 index 00000000..2d390f67 --- /dev/null +++ b/Tests/OctoKitTests/Fixtures/pull_request_comment.json @@ -0,0 +1,56 @@ +{ + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + }, + "start_line": 1, + "original_start_line": 1, + "start_side": "RIGHT", + "line": 2, + "original_line": 2, + "side": "RIGHT" +} diff --git a/Tests/OctoKitTests/Fixtures/pull_request_comments.json b/Tests/OctoKitTests/Fixtures/pull_request_comments.json new file mode 100644 index 00000000..231a17eb --- /dev/null +++ b/Tests/OctoKitTests/Fixtures/pull_request_comments.json @@ -0,0 +1,58 @@ +[ + { + "url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1", + "pull_request_review_id": 42, + "id": 10, + "node_id": "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDEw", + "diff_hunk": "@@ -16,33 +16,40 @@ public class Connection : IConnection...", + "path": "file1.txt", + "position": 1, + "original_position": 4, + "commit_id": "6dcb09b5b57875f334f61aebed695e2e4193db5e", + "original_commit_id": "9c48853fa3dc5c1c3d6f1f1cd1f2743e72652840", + "in_reply_to_id": 8, + "user": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "Great stuff!", + "created_at": "2011-04-14T16:00:49Z", + "updated_at": "2011-04-14T16:00:49Z", + "html_url": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1", + "pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1", + "author_association": "NONE", + "_links": { + "self": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments/1" + }, + "html": { + "href": "https://github.com/octocat/Hello-World/pull/1#discussion-diff-1" + }, + "pull_request": { + "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1" + } + }, + "start_line": 1, + "original_start_line": 1, + "start_side": "RIGHT", + "line": 2, + "original_line": 2, + "side": "RIGHT" + } +] diff --git a/Tests/OctoKitTests/Fixtures/pull_requests_files.json b/Tests/OctoKitTests/Fixtures/pull_request_files.json similarity index 100% rename from Tests/OctoKitTests/Fixtures/pull_requests_files.json rename to Tests/OctoKitTests/Fixtures/pull_request_files.json diff --git a/Tests/OctoKitTests/Fixtures/pull_request_requested_reviewers.json b/Tests/OctoKitTests/Fixtures/pull_request_requested_reviewers.json new file mode 100644 index 00000000..780f0555 --- /dev/null +++ b/Tests/OctoKitTests/Fixtures/pull_request_requested_reviewers.json @@ -0,0 +1,41 @@ +{ + "users": [ + { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + } + ], + "teams": [ + { + "id": 1, + "node_id": "MDQ6VGVhbTE=", + "url": "https://api.github.com/teams/1", + "html_url": "https://github.com/orgs/github/teams/justice-league", + "name": "Justice League", + "slug": "justice-league", + "description": "A great team.", + "privacy": "closed", + "notification_setting": "notifications_enabled", + "permission": "admin", + "members_url": "https://api.github.com/teams/1/members{/member}", + "repositories_url": "https://api.github.com/teams/1/repos", + "parent": null + } + ] +} diff --git a/Tests/OctoKitTests/PullRequestTests.swift b/Tests/OctoKitTests/PullRequestTests.swift index acac3f14..56fb8b29 100644 --- a/Tests/OctoKitTests/PullRequestTests.swift +++ b/Tests/OctoKitTests/PullRequestTests.swift @@ -370,7 +370,7 @@ class PullRequestTests: XCTestCase { func testListPullRequestsFiles() { let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octocat/Hello-World/pulls/1347/files", expectedHTTPMethod: "GET", - jsonFile: "pull_requests_files", + jsonFile: "pull_request_files", statusCode: 200) let task = Octokit(session: session) @@ -405,7 +405,7 @@ class PullRequestTests: XCTestCase { func testListPullRequestsFilesAsync() async throws { let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octocat/Hello-World/pulls/1347/files", expectedHTTPMethod: "GET", - jsonFile: "pull_requests_files", + jsonFile: "pull_request_files", statusCode: 200) let files = try await Octokit(session: session) @@ -429,4 +429,178 @@ class PullRequestTests: XCTestCase { XCTAssertTrue(session.wasCalled) } #endif + + func testReadPullRequestReviewComments() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/comments?", + expectedHTTPMethod: "GET", + jsonFile: "pull_request_comments", + statusCode: 200) + let task = Octokit(session: session).readPullRequestReviewComments(owner: "octokat", + repository: "Hello-World", + number: 1347) { response in + switch response { + case let .success(comments): + XCTAssertEqual(comments.count, 1) + let comment = comments.first + XCTAssertNotNil(comment) + XCTAssertEqual(comment?.body, "Great stuff!") + case let .failure(error): + XCTAssertNil(error) + } + } + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func testReadPullRequestReviewCommentsAsync() async throws { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/comments?", + expectedHTTPMethod: "GET", + jsonFile: "pull_request_comments", + statusCode: 200) + let comments = try await Octokit(session: session).readPullRequestReviewComments(owner: "octokat", + repository: "Hello-World", + number: 1347) + XCTAssertEqual(comments.count, 1) + let comment = try XCTUnwrap(comments.first) + XCTAssertNotNil(comment) + XCTAssertEqual(comment.body, "Great stuff!") + XCTAssertTrue(session.wasCalled) + } + #endif + + func testCreatePullRequestReviewComment() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/comments", + expectedHTTPMethod: "POST", + jsonFile: "pull_request_comment", + statusCode: 200) + let task = Octokit(session: session).createPullRequestReviewComment(owner: "octokat", + repository: "Hello-World", + number: 1347, + commitId: "6dcb09b5b57875f334f61aebed695e2e4193db5e", + path: "file1.txt", + line: 2, + body: "Great stuff!") { response in + switch response { + case let .success(comment): + XCTAssertEqual(comment.body, "Great stuff!") + XCTAssertEqual(comment.commitId, "6dcb09b5b57875f334f61aebed695e2e4193db5e") + XCTAssertEqual(comment.line, 2) + XCTAssertEqual(comment.startLine, 1) + XCTAssertEqual(comment.side, .right) + XCTAssertNil(comment.subjectType) + XCTAssertEqual(comment.inReplyToId, 8) + case let .failure(error): + XCTAssertNil(error) + } + } + + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func testCreatePullRequestReviewCommentAsync() async throws { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/comments", + expectedHTTPMethod: "POST", + jsonFile: "pull_request_comment", + statusCode: 200) + let comment = try await Octokit(session: session).createPullRequestReviewComment(owner: "octokat", + repository: "Hello-World", + number: 1347, + commitId: "6dcb09b5b57875f334f61aebed695e2e4193db5e", + path: "file1.txt", + line: 2, + body: "Great stuff!") + XCTAssertEqual(comment.body, "Great stuff!") + XCTAssertEqual(comment.commitId, "6dcb09b5b57875f334f61aebed695e2e4193db5e") + XCTAssertEqual(comment.line, 2) + XCTAssertEqual(comment.startLine, 1) + XCTAssertEqual(comment.side, .right) + XCTAssertNil(comment.subjectType) + XCTAssertEqual(comment.inReplyToId, 8) + XCTAssertTrue(session.wasCalled) + } + #endif + + func testCreatePullRequestRegularComment() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/issues/1347/comments", + expectedHTTPMethod: "POST", + jsonFile: "pull_request_comment", + statusCode: 200) + let task = Octokit(session: session).createPullRequestReviewComment(owner: "octokat", + repository: "Hello-World", + number: 1347, + body: "Great stuff!") { response in + switch response { + case let .success(comment): + XCTAssertEqual(comment.body, "Great stuff!") + case let .failure(error): + XCTAssertNil(error) + } + } + + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func testCreatePullRequestRegularCommentAsync() async throws { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/issues/1347/comments", + expectedHTTPMethod: "POST", + jsonFile: "pull_request_comment", + statusCode: 200) + let comment = try await Octokit(session: session).createPullRequestReviewComment(owner: "octokat", + repository: "Hello-World", + number: 1347, + body: "Great stuff!") + XCTAssertEqual(comment.body, "Great stuff!") + XCTAssertTrue(session.wasCalled) + } + #endif + + func testReadPullRequestRequestedReviewers() { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/requested_reviewers?", + expectedHTTPMethod: "GET", + jsonFile: "pull_request_requested_reviewers", + statusCode: 200) + let task = Octokit(session: session).readPullRequestRequestedReviewers(owner: "octokat", + repository: "Hello-World", + number: 1347) { response in + switch response { + case let .success(requestedReviewers): + XCTAssertEqual(requestedReviewers.users.count, 1) + XCTAssertEqual(requestedReviewers.users.first?.login, "octocat") + XCTAssertEqual(requestedReviewers.teams.count, 1) + XCTAssertEqual(requestedReviewers.teams.first?.name, "Justice League") + case let .failure(error): + XCTAssertNil(error) + } + } + XCTAssertNotNil(task) + XCTAssertTrue(session.wasCalled) + } + + #if compiler(>=5.5.2) && canImport(_Concurrency) + @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) + func testReadPullRequestRequestedReviewersAsync() async throws { + let session = OctoKitURLTestSession(expectedURL: "https://api.github.com/repos/octokat/Hello-World/pulls/1347/requested_reviewers?", + expectedHTTPMethod: "GET", + jsonFile: "pull_request_requested_reviewers", + statusCode: 200) + let requestedReviewers = try await Octokit(session: session).readPullRequestRequestedReviewers(owner: "octokat", + repository: "Hello-World", + number: 1347) + XCTAssertEqual(requestedReviewers.users.count, 1) + let user = try XCTUnwrap(requestedReviewers.users.first) + XCTAssertEqual(user.login, "octocat") + XCTAssertEqual(requestedReviewers.teams.count, 1) + let team = try XCTUnwrap(requestedReviewers.teams.first) + XCTAssertEqual(team.name, "Justice League") + XCTAssertTrue(session.wasCalled) + } + #endif }