diff options
Diffstat (limited to 'HutchTests')
| -rw-r--r-- | HutchTests/RepositoryACLViewModelTests.swift | 173 | ||||
| -rw-r--r-- | HutchTests/RepositorySettingsViewModelTests.swift | 6 |
2 files changed, 176 insertions, 3 deletions
diff --git a/HutchTests/RepositoryACLViewModelTests.swift b/HutchTests/RepositoryACLViewModelTests.swift new file mode 100644 index 0000000..9c3f929 --- /dev/null +++ b/HutchTests/RepositoryACLViewModelTests.swift @@ -0,0 +1,173 @@ +import Foundation +import Testing +@testable import Hutch + +struct RepositoryACLViewModelTests { + + @Test + @MainActor + func addValidationRejectsOwnerAndDuplicates() async { + let service = MockRepositoryACLService() + service.fetchResponses = [[ + RepositoryACLEntry( + id: 2, + mode: .ro, + entity: Entity(canonicalName: "~alice") + ) + ]] + let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service) + viewModel.addUsername = "~owner" + #expect(viewModel.addValidationMessage == "The repository owner already has access.") + + await viewModel.load() + viewModel.addUsername = "alice" + #expect(viewModel.addValidationMessage == "That user already has access.") + } + + @Test + @MainActor + func addEntryRefreshesListAfterSuccess() async { + let service = MockRepositoryACLService() + service.fetchResponses = [[ + RepositoryACLEntry( + id: 2, + mode: .rw, + entity: Entity(canonicalName: "~alice") + ) + ]] + + let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service) + viewModel.addUsername = "alice" + viewModel.addMode = .rw + + let didAdd = await viewModel.addEntry() + + #expect(didAdd) + #expect(service.upsertRequests == [MockRepositoryACLService.UpsertRequest(repositoryId: 1, entity: "~alice", mode: .rw)]) + #expect(service.fetchRequestRids == ["rid-1"]) + #expect(viewModel.visibleEntries.map(\.entity.canonicalName) == ["~alice"]) + #expect(viewModel.addUsername.isEmpty) + } + + @Test + @MainActor + func permissionUpdateFailureLeavesExistingEntryUntouched() async { + let service = MockRepositoryACLService() + service.upsertError = SRHTError.httpError(500) + + let entry = RepositoryACLEntry( + id: 2, + mode: .ro, + entity: Entity(canonicalName: "~alice") + ) + let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service) + service.fetchResponses = [[entry]] + await viewModel.load() + + await viewModel.updatePermission(for: entry, to: .rw) + + #expect(viewModel.visibleEntries.first?.mode == .ro) + #expect(viewModel.updatingEntryIDs.isEmpty) + #expect(viewModel.error != nil) + } + + @Test + @MainActor + func removeFailureKeepsEntryVisible() async { + let service = MockRepositoryACLService() + service.deleteError = SRHTError.httpError(500) + + let entry = RepositoryACLEntry( + id: 2, + mode: .ro, + entity: Entity(canonicalName: "~alice") + ) + let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service) + service.fetchResponses = [[entry]] + await viewModel.load() + + await viewModel.removeEntry(entry) + + #expect(viewModel.visibleEntries.map(\.id) == [2]) + #expect(viewModel.deletingEntryIDs.isEmpty) + #expect(viewModel.error != nil) + } + + @Test + @MainActor + func initialLoadFailureSetsBlockingErrorState() async { + let service = MockRepositoryACLService() + service.fetchError = SRHTError.httpError(500) + + let viewModel = RepositoryACLViewModel(repository: makeRepository(), service: service) + + await viewModel.load() + + #expect(viewModel.loadError != nil) + #expect(viewModel.visibleEntries.isEmpty) + } + + @MainActor + private func makeRepository() -> RepositorySummary { + RepositorySummary( + id: 1, + rid: "rid-1", + service: .git, + name: "repo", + description: nil, + visibility: .public, + updated: .now, + owner: Entity(canonicalName: "~owner"), + head: nil + ) + } +} + +@MainActor +private final class MockRepositoryACLService: RepositoryACLServicing { + struct UpsertRequest: Equatable { + let repositoryId: Int + let entity: String + let mode: AccessMode + } + + var fetchResponses: [[RepositoryACLEntry]] = [] + var fetchError: Error? + var upsertResponse = RepositoryACLEntry( + id: 2, + mode: .ro, + entity: Entity(canonicalName: "~alice") + ) + var upsertError: Error? + var deleteError: Error? + + private(set) var fetchRequestRids: [String] = [] + private(set) var upsertRequests: [UpsertRequest] = [] + private(set) var deleteRequestIDs: [Int] = [] + + func fetchACLs(repositoryRid: String) async throws -> [RepositoryACLEntry] { + fetchRequestRids.append(repositoryRid) + if let fetchError { + throw fetchError + } + if !fetchResponses.isEmpty { + return fetchResponses.removeFirst() + } + return [] + } + + func upsertACL(repositoryId: Int, entity: String, mode: AccessMode) async throws -> RepositoryACLEntry { + upsertRequests.append(UpsertRequest(repositoryId: repositoryId, entity: entity, mode: mode)) + if let upsertError { + throw upsertError + } + return RepositoryACLEntry(id: upsertResponse.id, mode: mode, entity: Entity(canonicalName: entity)) + } + + func deleteACL(entryId: Int) async throws { + deleteRequestIDs.append(entryId) + if let deleteError { + throw deleteError + } + } +} diff --git a/HutchTests/RepositorySettingsViewModelTests.swift b/HutchTests/RepositorySettingsViewModelTests.swift index f211bab..5b06539 100644 --- a/HutchTests/RepositorySettingsViewModelTests.swift +++ b/HutchTests/RepositorySettingsViewModelTests.swift @@ -35,9 +35,9 @@ struct RepositorySettingsViewModelTests { @Test @MainActor func gitCanonicalEntityAddsMissingTilde() { - #expect(RepositorySettingsViewModel.gitCanonicalEntity(from: "alice") == "~alice") - #expect(RepositorySettingsViewModel.gitCanonicalEntity(from: "~alice") == "~alice") - #expect(RepositorySettingsViewModel.gitCanonicalEntity(from: " alice ") == "~alice") + #expect(RepositoryACLViewModel.canonicalEntity(from: "alice") == "~alice") + #expect(RepositoryACLViewModel.canonicalEntity(from: "~alice") == "~alice") + #expect(RepositoryACLViewModel.canonicalEntity(from: " alice ") == "~alice") } @Test |
