krz/hutch

an ios client for sourcehut

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

v3.2.1: 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            fields: .init(
114                id: 1,
115                rid: "rid-1",
116                service: .git,
117                name: "repo",
118                description: nil,
119                visibility: .publicVisibility,
120                updated: .now,
121                owner: Entity(canonicalName: "~owner"),
122                head: nil
123            )
124        )
125    }
126}
127
128@MainActor
129private final class MockRepositoryACLService: RepositoryACLServicing {
130    struct UpsertRequest: Equatable {
131        let repositoryId: Int
132        let entity: String
133        let mode: AccessMode
134    }
135
136    var fetchResponses: [[RepositoryACLEntry]] = []
137    var fetchError: Error?
138    var upsertResponse = RepositoryACLEntry(
139        id: 2,
140        mode: .ro,
141        entity: Entity(canonicalName: "~alice")
142    )
143    var upsertError: Error?
144    var deleteError: Error?
145
146    private(set) var fetchRequestRids: [String] = []
147    private(set) var upsertRequests: [UpsertRequest] = []
148    private(set) var deleteRequestIDs: [Int] = []
149
150    func fetchACLs(repositoryRid: String) async throws -> [RepositoryACLEntry] {
151        fetchRequestRids.append(repositoryRid)
152        if let fetchError {
153            throw fetchError
154        }
155        if !fetchResponses.isEmpty {
156            return fetchResponses.removeFirst()
157        }
158        return []
159    }
160
161    func upsertACL(repositoryId: Int, entity: String, mode: AccessMode) async throws -> RepositoryACLEntry {
162        upsertRequests.append(UpsertRequest(repositoryId: repositoryId, entity: entity, mode: mode))
163        if let upsertError {
164            throw upsertError
165        }
166        return RepositoryACLEntry(id: upsertResponse.id, mode: mode, entity: Entity(canonicalName: entity))
167    }
168
169    func deleteACL(entryId: Int) async throws {
170        deleteRequestIDs.append(entryId)
171        if let deleteError {
172            throw deleteError
173        }
174    }
175}