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}