gitbayTests/ReactionTests.swift

v1.11.0
gitbay-ios/gitbayTests/ReactionTests.swift history · blame · raw

139 lines · 6527 bytes

  1import Foundation
  2import Testing
  3@testable import gitbay
  4
  5private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
  6    let box = StubProtocol.box()
  7    let client = GitbayClient(
  8        instance: try GitbayInstance(url: "https://gitbay.org"),
  9        token: "test-token",
 10        session: box.session()
 11    )
 12    return (client, box)
 13}
 14
 15private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] {
 16    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 17    return try #require(body["argv"] as? [String])
 18}
 19
 20private let issueShowJSON = """
 21    {"protocol_version":1,"data":{"number":11,"title":"iOS app","state":"open",\
 22    "author":"krz","body":"Build it.","created_at":"2026-08-20T10:00:00.000Z",\
 23    "reactions":[{"reaction":"+1","count":2,"me":true},{"reaction":"eyes","count":1,"me":false}],\
 24    "comments":[{"id":7,"author":"cmc","body":"started","created_at":"2026-08-22T09:00:00.000Z",\
 25    "reactions":[{"reaction":"heart","count":1,"me":false}]}]},"exit_code":0}
 26    """
 27
 28private let mrShowJSON = """
 29    {"protocol_version":1,"data":{"number":3,"title":"t","state":"open","author":"krz",\
 30    "source":"feat","target_ref":"main","head_sha":"abc","created_at":"2026-08-20T10:00:00.000Z",\
 31    "reactions":[{"reaction":"rocket","count":1,"me":true}],\
 32    "comments":[{"id":9,"author":"cmc","body":"lgtm","created_at":"2026-08-22T09:00:00.000Z"},\
 33    {"id":10,"author":"cmc","body":"queued the merge","kind":"system","created_at":"2026-08-22T09:01:00.000Z"},\
 34    {"id":11,"author":"system","body":"retargeted","created_at":"2026-08-22T09:02:00.000Z"}]},\
 35    "exit_code":0}
 36    """
 37
 38private let ok = #"{"protocol_version":1,"data":{"reaction":"+1"},"exit_code":0}"#
 39
 40struct ReactionModelTests {
 41
 42    @Test func theVocabularyIsTheServersInItsOrder() {
 43        #expect(Reaction.all.map(\.name) == ["+1", "-1", "laugh", "hooray", "confused", "heart", "rocket", "eyes"])
 44        #expect(Reaction.emoji(for: "hooray") == "🎉")
 45        // An unknown name from a newer server still draws.
 46        #expect(Reaction.emoji(for: "sparkles") == "sparkles")
 47        #expect(Reaction.spoken(for: "+1") == "thumbs up")
 48    }
 49
 50    @Test func argvReactsToTheItemOrAComment() {
 51        #expect(Reaction.argv(noun: "issue", repo: "krz/gitbay", number: 11, comment: nil, name: "+1", remove: false)
 52            == ["issue", "react", "krz/gitbay", "11", "+1"])
 53        #expect(Reaction.argv(noun: "mr", repo: "krz/gitbay", number: 3, comment: 9, name: "eyes", remove: true)
 54            == ["mr", "react", "krz/gitbay", "3", "--comment", "9", "eyes", "--remove"])
 55    }
 56
 57    /// Every name the server knows is offered, and the ones already given
 58    /// are offered too: picking one of those takes it back.
 59    @Test func tappingAReactionYouGaveRemovesIt() {
 60        let given = [ReactionCount(reaction: "+1", count: 2, me: true), ReactionCount(reaction: "eyes", count: 1, me: false)]
 61        #expect(Reaction.remove("+1", given: given))
 62        #expect(!Reaction.remove("eyes", given: given))
 63        #expect(!Reaction.remove("heart", given: given))
 64    }
 65}
 66
 67@MainActor
 68struct IssueReactionTests {
 69
 70    @Test func issueShowDecodesReactionsOnTheIssueAndItsComments() async throws {
 71        let (client, stub) = try makeClient()
 72        stub.enqueue(.init(status: 200, json: issueShowJSON))
 73        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
 74
 75        await model.load()
 76
 77        let issue = try #require(model.state.value)
 78        #expect(issue.reactions == [ReactionCount(reaction: "+1", count: 2, me: true),
 79                                    ReactionCount(reaction: "eyes", count: 1, me: false)])
 80        #expect(issue.comments?.first?.commentID == 7)
 81        #expect(issue.comments?.first?.reactions?.first?.reaction == "heart")
 82    }
 83
 84    @Test func reactingToACommentSendsItsIDThenReloads() async throws {
 85        let (client, stub) = try makeClient()
 86        stub.enqueue(.init(status: 200, json: issueShowJSON, match: "argv=show"))
 87        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
 88        stub.enqueue(.init(status: 200, json: issueShowJSON, match: "argv=show"))
 89        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
 90        await model.load()
 91
 92        await model.react("heart", comment: 7)
 93
 94        let post = try #require(stub.seen.first { $0.method == "POST" })
 95        #expect(try argvOf(post) == ["issue", "react", "krz/gitbay", "11", "--comment", "7", "heart"])
 96        #expect(stub.seen.filter { $0.url.absoluteString.contains("argv=show") }.count == 2)
 97    }
 98
 99    @Test func reactingWithOneYouGaveTakesItBack() async throws {
100        let (client, stub) = try makeClient()
101        stub.enqueue(.init(status: 200, json: issueShowJSON, match: "argv=show"))
102        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
103        stub.enqueue(.init(status: 200, json: issueShowJSON, match: "argv=show"))
104        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
105        await model.load()
106
107        await model.react("+1", comment: nil)
108
109        let post = try #require(stub.seen.first { $0.method == "POST" })
110        #expect(try argvOf(post) == ["issue", "react", "krz/gitbay", "11", "+1", "--remove"])
111    }
112}
113
114@MainActor
115struct MRReactionTests {
116
117    @Test func mrShowDecodesReactionsAndReactingSendsTheNoun() async throws {
118        let (client, stub) = try makeClient()
119        stub.enqueue(.init(status: 200, json: mrShowJSON, match: "argv=show"))
120        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
121        stub.enqueue(.init(status: 200, json: mrShowJSON, match: "argv=show"))
122        let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 3)
123        await model.load()
124
125        let mr = try #require(model.state.value)
126        #expect(mr.reactions == [ReactionCount(reaction: "rocket", count: 1, me: true)])
127        #expect(mr.comments?.first?.commentID == 9)
128        #expect(mr.comments?.first?.reactions == nil)
129        #expect(mr.comments?.first?.isSystem == false)
130        #expect(mr.comments?[1].isSystem == true)
131        // An older server sends no kind; its system lines are authored "system".
132        #expect(mr.comments?.last?.isSystem == true)
133
134        await model.react("eyes", comment: 9)
135
136        let post = try #require(stub.seen.first { $0.method == "POST" })
137        #expect(try argvOf(post) == ["mr", "react", "krz/gitbay", "3", "--comment", "9", "eyes"])
138    }
139}