krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.4: 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}