gitbayTests/ReactionTests.swift
139 lines · 6527 bytes
12 symbols in this file
makeClientargvOfReactionModelTeststheVocabularyIsTheServersInItsOrderargvReactsToTheItemOrACommenttappingAReactionYouGaveRemovesItIssueReactionTestsissueShowDecodesReactionsOnTheIssueAndItsCommentsreactingToACommentSendsItsIDThenReloadsreactingWithOneYouGaveTakesItBackMRReactionTestsmrShowDecodesReactionsAndReactingSendsTheNoun
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}