krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.0.0: HutchTests/RepositoryACLViewModelTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5struct RepositoryACLViewModelTests {
6
7 @Test
8 @MainActor
9 func addValidationRejectsOwnerAndDuplicates() async {
10 let service = MockRepositoryACLService()
11 service.fetchResponses = [[
12 RepositoryACLEntry(
13 id: 2,
14 mode: .ro,
15 entity: Entity(canonicalName: "~alice")
16 )
17 ]]
18 let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service)
19 viewModel.addUsername = "~owner"
20 #expect(viewModel.addValidationMessage == "The repository owner already has access.")
21
22 await viewModel.load()
23 viewModel.addUsername = "alice"
24 #expect(viewModel.addValidationMessage == "That user already has access.")
25 }
26
27 @Test
28 @MainActor
29 func addEntryRefreshesListAfterSuccess() async {
30 let service = MockRepositoryACLService()
31 service.fetchResponses = [[
32 RepositoryACLEntry(
33 id: 2,
34 mode: .rw,
35 entity: Entity(canonicalName: "~alice")
36 )
37 ]]
38
39 let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service)
40 viewModel.addUsername = "alice"
41 viewModel.addMode = .rw
42
43 let didAdd = await viewModel.addEntry()
44
45 #expect(didAdd)
46 #expect(service.upsertRequests == [MockRepositoryACLService.UpsertRequest(repositoryId: 1, entity: "~alice", mode: .rw)])
47 #expect(service.fetchRequestRids == ["rid-1"])
48 #expect(viewModel.visibleEntries.map(\.entity.canonicalName) == ["~alice"])
49 #expect(viewModel.addUsername.isEmpty)
50 }
51
52 @Test
53 @MainActor
54 func permissionUpdateFailureLeavesExistingEntryUntouched() async {
55 let service = MockRepositoryACLService()
56 service.upsertError = SRHTError.httpError(500)
57
58 let entry = RepositoryACLEntry(
59 id: 2,
60 mode: .ro,
61 entity: Entity(canonicalName: "~alice")
62 )
63 let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service)
64 service.fetchResponses = [[entry]]
65 await viewModel.load()
66
67 await viewModel.updatePermission(for: entry, to: .rw)
68
69 #expect(viewModel.visibleEntries.first?.mode == .ro)
70 #expect(viewModel.updatingEntryIDs.isEmpty)
71 #expect(viewModel.error != nil)
72 }
73
74 @Test
75 @MainActor
76 func removeFailureKeepsEntryVisible() async {
77 let service = MockRepositoryACLService()
78 service.deleteError = SRHTError.httpError(500)
79
80 let entry = RepositoryACLEntry(
81 id: 2,
82 mode: .ro,
83 entity: Entity(canonicalName: "~alice")
84 )
85 let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service)
86 service.fetchResponses = [[entry]]
87 await viewModel.load()
88
89 await viewModel.removeEntry(entry)
90
91 #expect(viewModel.visibleEntries.map(\.id) == [2])
92 #expect(viewModel.deletingEntryIDs.isEmpty)
93 #expect(viewModel.error != nil)
94 }
95
96 @Test
97 @MainActor
98 func initialLoadFailureSetsBlockingErrorState() async {
99 let service = MockRepositoryACLService()
100 service.fetchError = SRHTError.httpError(500)
101
102 let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service)
103
104 await viewModel.load()
105
106 #expect(viewModel.loadError != nil)
107 #expect(viewModel.visibleEntries.isEmpty)
108 }
109
110 @MainActor
111 private func makeRepository() -> RepositorySummary {
112 RepositorySummary(
113 id: 1,
114 rid: "rid-1",
115 service: .git,
116 name: "repo",
117 description: nil,
118 visibility: .public,
119 updated: .now,
120 owner: Entity(canonicalName: "~owner"),
121 head: nil
122 )
123 }
124}
125
126@MainActor
127private final class MockRepositoryACLService: RepositoryACLServicing {
128 struct UpsertRequest: Equatable {
129 let repositoryId: Int
130 let entity: String
131 let mode: AccessMode
132 }
133
134 var fetchResponses: [[RepositoryACLEntry]] = []
135 var fetchError: Error?
136 var upsertResponse = RepositoryACLEntry(
137 id: 2,
138 mode: .ro,
139 entity: Entity(canonicalName: "~alice")
140 )
141 var upsertError: Error?
142 var deleteError: Error?
143
144 private(set) var fetchRequestRids: [String] = []
145 private(set) var upsertRequests: [UpsertRequest] = []
146 private(set) var deleteRequestIDs: [Int] = []
147
148 func fetchACLs(repositoryRid: String) async throws -> [RepositoryACLEntry] {
149 fetchRequestRids.append(repositoryRid)
150 if let fetchError {
151 throw fetchError
152 }
153 if !fetchResponses.isEmpty {
154 return fetchResponses.removeFirst()
155 }
156 return []
157 }
158
159 func upsertACL(repositoryId: Int, entity: String, mode: AccessMode) async throws -> RepositoryACLEntry {
160 upsertRequests.append(UpsertRequest(repositoryId: repositoryId, entity: entity, mode: mode))
161 if let upsertError {
162 throw upsertError
163 }
164 return RepositoryACLEntry(id: upsertResponse.id, mode: mode, entity: Entity(canonicalName: entity))
165 }
166
167 func deleteACL(entryId: Int) async throws {
168 deleteRequestIDs.append(entryId)
169 if let deleteError {
170 throw deleteError
171 }
172 }
173}