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