gitbayTests/SavedQueryTests.swift

v1.14.0
gitbay-ios/gitbayTests/SavedQueryTests.swift history · blame · raw

153 lines · 6724 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 posted(_ stub: StubProtocol.Box) throws -> [[String]] {
 16    try stub.seen.filter { $0.method == "POST" }.map { seen in
 17        let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 18        return try #require(body["argv"] as? [String])
 19    }
 20}
 21
 22private func argv(_ url: URL) -> [String] {
 23    URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.compactMap(\.value) ?? []
 24}
 25
 26private let listJSON = """
 27    {"protocol_version":1,"data":[{"name":"mine","query":"is:open author:@me","pinned":true},\
 28    {"name":"bugs","query":"is:issue label:bug","pinned":false}],"exit_code":0}
 29    """
 30
 31private let ok = #"{"protocol_version":1,"data":{},"exit_code":0}"#
 32
 33@MainActor
 34struct SavedQueriesViewModelTests {
 35
 36    private func loaded() async throws -> (SavedQueriesViewModel, StubProtocol.Box) {
 37        let (client, stub) = try makeClient()
 38        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 39        let model = SavedQueriesViewModel(client: client)
 40        await model.load()
 41        return (model, stub)
 42    }
 43
 44    @Test func listsSavedQueries() async throws {
 45        let (model, stub) = try await loaded()
 46        #expect(model.state.value?.map(\.name) == ["mine", "bugs"])
 47        #expect(model.state.value?.first?.pinned == true)
 48        #expect(argv(try #require(stub.seen.first).url) == ["query", "list"])
 49    }
 50
 51    @Test func savingSendsTheNameAndTheQueryAsOneWord() async throws {
 52        let (model, stub) = try await loaded()
 53        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
 54        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 55
 56        let saved = await model.save(name: " review ", query: #" is:mr label:"needs review" "#, replace: false)
 57
 58        #expect(saved)
 59        #expect(try posted(stub) == [["query", "save", "--", "review", #"is:mr label:"needs review""#]])
 60    }
 61
 62    @Test func replacingAddsForce() async throws {
 63        let (model, stub) = try await loaded()
 64        stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
 65        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 66
 67        _ = await model.save(name: "bugs", query: "is:issue label:bug is:open", replace: true)
 68
 69        #expect(try posted(stub) == [["query", "save", "--force", "--", "bugs", "is:issue label:bug is:open"]])
 70    }
 71
 72    @Test func aRefusedSaveKeepsTheSheetAndSaysWhy() async throws {
 73        let (model, stub) = try await loaded()
 74        // A failure is retried once before it surfaces.
 75        for _ in 0..<2 {
 76            stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"you already have a query named bugs; pass --force to replace it","exit_code":1}"#, match: "cmd"))
 77        }
 78
 79        let saved = await model.save(name: "bugs", query: "x", replace: false)
 80
 81        #expect(!saved)
 82        #expect(model.actionError == "you already have a query named bugs; pass --force to replace it")
 83    }
 84
 85    /// The query is typed, so a syntax refusal says what to fix rather
 86    /// than reading as the app's own mistake.
 87    @Test func aQueryTheServerCannotParseShowsItsMessage() async throws {
 88        let (model, stub) = try await loaded()
 89        stub.enqueue(.init(status: 400, json: #"{"protocol_version":1,"error":"unknown qualifier; the qualifiers are repo:, owner:, is:, label:, no:, milestone:, assignee: and author:","exit_code":2}"#, match: "cmd"))
 90
 91        #expect(!(await model.save(name: "x", query: "colour:red", replace: false)))
 92        #expect(model.actionError?.hasPrefix("unknown qualifier") == true)
 93    }
 94
 95    @Test func pinUnpinAndRemoveNameTheQuery() async throws {
 96        let (model, stub) = try await loaded()
 97        for _ in 0..<3 {
 98            stub.enqueue(.init(status: 200, json: ok, match: "cmd"))
 99            stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
100        }
101        let queries = try #require(model.state.value)
102
103        await model.setPinned(queries[1], true)
104        await model.setPinned(queries[0], false)
105        await model.remove(queries[1])
106
107        #expect(try posted(stub) == [["query", "pin", "bugs"], ["query", "unpin", "mine"], ["query", "remove", "bugs"]])
108    }
109}
110
111@MainActor
112struct QueryResultsTests {
113
114    @Test func resultsPageThroughQueryRun() async throws {
115        let (client, stub) = try makeClient()
116        stub.enqueue(.init(status: 200, json: """
117            {"protocol_version":1,"data":{"items":[\
118            {"kind":"mr","repo":"krz/gitbay","number":12,"title":"Add it","state":"open","draft":true,\
119            "author":"cmc","created_at":"2026-08-20T10:00:00.000Z","updated_at":"2026-08-21T10:00:00.000Z"},\
120            {"kind":"issue","repo":"krz/app","number":3,"title":"Bug","state":"closed",\
121            "author":"dee","milestone":"m1","created_at":"2026-08-20T10:00:00.000Z","updated_at":"2026-08-21T10:00:00.000Z"}],\
122            "next":"c1"},"exit_code":0}
123            """))
124        let model = QueryResultsViewModel(client: client, name: "mine")
125
126        await model.load()
127
128        #expect(argv(try #require(stub.seen.first).url) == ["query", "run", "mine", "--limit", "50"])
129        let items = try #require(model.list.state.value)
130        #expect(items.map(\.ref) == ["krz/gitbay!12", "krz/app#3"])
131        #expect(items.first?.draft == true)
132        #expect(model.list.hasMore)
133    }
134}
135
136struct DashboardQueryTests {
137
138    @Test func pinnedQueriesDecodeWithTheirRowsAndCount() throws {
139        let json = """
140            {"queries":[{"name":"mine","query":"is:open author:@me","count":7,"items":[\
141            {"kind":"issue","repo":"krz/app","number":3,"title":"Bug","state":"open",\
142            "author":"dee","created_at":"2026-08-20T10:00:00.000Z","updated_at":"2026-08-21T10:00:00.000Z"}]},\
143            {"name":"broken","query":"label:","count":0,"items":[],"error":"label: needs a value"}]}
144            """
145        let data = try GitbayClient.decoder().decode(DashboardData.self, from: Data(json.utf8))
146        #expect(data.queries.map(\.name) == ["mine", "broken"])
147        #expect(data.queries.first?.count == 7)
148        #expect(data.queries.last?.error == "label: needs a value")
149        // A server from before saved queries sends none.
150        let old = try GitbayClient.decoder().decode(DashboardData.self, from: Data("{}".utf8))
151        #expect(old.queries.isEmpty)
152    }
153}