krz/hutch

an ios client for sourcehut

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

v3.2.1: HutchTests/RepositorySettingsViewModelTests.swift · raw

  1import Foundation
  2import Testing
  3@testable import Hutch
  4
  5private struct UpdateRepositoryInfoEnvelope: Decodable {
  6    let updateRepository: UpdateRepositoryInfoPayload
  7}
  8
  9private struct UpdateRepositoryInfoPayload: Decodable {
 10    let id: Int
 11}
 12
 13struct RepositorySettingsViewModelTests {
 14
 15    @Test
 16    func saveInfoResponseDecodesMinimalRepositoryPayload() throws {
 17        let json = """
 18        {
 19            "data": {
 20                "updateRepository": {
 21                    "id": 42
 22                }
 23            }
 24        }
 25        """
 26
 27        let decoded = try JSONDecoder().decode(
 28            GraphQLResponse<UpdateRepositoryInfoEnvelope>.self,
 29            from: Data(json.utf8)
 30        )
 31
 32        #expect(decoded.data?.updateRepository.id == 42)
 33    }
 34
 35    @Test
 36    @MainActor
 37    func gitCanonicalEntityAddsMissingTilde() {
 38        #expect(RepositoryACLViewModel.canonicalEntity(from: "alice") == "~alice")
 39        #expect(RepositoryACLViewModel.canonicalEntity(from: "~alice") == "~alice")
 40        #expect(RepositoryACLViewModel.canonicalEntity(from: "  alice  ") == "~alice")
 41    }
 42
 43    @Test
 44    @MainActor
 45    func gitHeadReferenceUsesFullBranchRef() {
 46        #expect(RepositorySettingsViewModel.gitHeadReference(from: "main") == "refs/heads/main")
 47        #expect(RepositorySettingsViewModel.gitHeadReference(from: " refs/heads/dev ") == "refs/heads/dev")
 48    }
 49
 50    @Test
 51    @MainActor
 52    func unchangedHeadIsOmittedFromSaveInput() {
 53        let viewModel = RepositorySettingsViewModel(
 54            repository: makeRepository(headName: "refs/heads/main"),
 55            branches: [ReferenceDetail(name: "refs/heads/main", target: nil, date: nil)],
 56            client: SRHTClient(token: "test-token")
 57        )
 58
 59        #expect(viewModel.selectedHeadReferenceForSave() == nil)
 60    }
 61
 62    @Test
 63    @MainActor
 64    func changedHeadUsesSelectedBranchReference() {
 65        let viewModel = RepositorySettingsViewModel(
 66            repository: makeRepository(headName: "refs/heads/main"),
 67            branches: [
 68                ReferenceDetail(name: "refs/heads/main", target: nil, date: nil),
 69                ReferenceDetail(name: "refs/heads/dev", target: nil, date: nil)
 70            ],
 71            client: SRHTClient(token: "test-token")
 72        )
 73        viewModel.editedHead = "dev"
 74
 75        #expect(viewModel.selectedHeadReferenceForSave() == "refs/heads/dev")
 76    }
 77
 78    @Test
 79    @MainActor
 80    func bareRepositoryOmitsHeadWhenNoBranchExists() {
 81        let viewModel = RepositorySettingsViewModel(
 82            repository: makeRepository(headName: nil),
 83            branches: [],
 84            client: SRHTClient(token: "test-token")
 85        )
 86
 87        #expect(viewModel.selectedHeadReferenceForSave() == nil)
 88        viewModel.editedHead = "main"
 89        #expect(viewModel.selectedHeadReferenceForSave() == nil)
 90    }
 91
 92    @Test
 93    @MainActor
 94    func metadataValidationRejectsEmptyOrInvalidNames() {
 95        #expect(RepositorySettingsViewModel.metadataValidationMessage(for: "") == "Enter a repository name.")
 96        #expect(RepositorySettingsViewModel.metadataValidationMessage(for: "repo/name") == "Repository names can't contain '/'.")
 97        #expect(RepositorySettingsViewModel.metadataValidationMessage(for: "repo name") == "Repository names can't contain spaces.")
 98    }
 99
100    @Test
101    @MainActor
102    func metadataDirtyStateTracksNormalizedInputs() {
103        let viewModel = RepositorySettingsViewModel(
104            repository: makeRepository(headName: "refs/heads/main"),
105            branches: [ReferenceDetail(name: "refs/heads/main", target: nil, date: nil)],
106            client: SRHTClient(token: "test-token")
107        )
108
109        #expect(viewModel.isMetadataDirty == false)
110        viewModel.editedDescription = "  updated  "
111        #expect(viewModel.isMetadataDirty)
112
113        viewModel.editedDescription = "desc"
114        viewModel.editedName = "  repo  "
115        #expect(viewModel.isMetadataDirty == false)
116    }
117
118    @Test
119    @MainActor
120    func metadataInputOmitsUnchangedNameWhenOnlyDescriptionChanges() {
121        let viewModel = RepositorySettingsViewModel(
122            repository: makeRepository(headName: "refs/heads/main"),
123            branches: [ReferenceDetail(name: "refs/heads/main", target: nil, date: nil)],
124            client: SRHTClient(token: "test-token")
125        )
126        viewModel.editedDescription = "an ios client for sourcehut"
127
128        let input = viewModel.metadataInputForSave()
129
130        #expect(input["name"] == nil)
131        #expect(input["description"] as? String == "an ios client for sourcehut")
132    }
133
134    @Test
135    @MainActor
136    func metadataInputUsesNilToClearDescription() {
137        let viewModel = RepositorySettingsViewModel(
138            repository: makeRepository(headName: "refs/heads/main"),
139            branches: [ReferenceDetail(name: "refs/heads/main", target: nil, date: nil)],
140            client: SRHTClient(token: "test-token")
141        )
142        viewModel.editedDescription = "   "
143
144        let input = viewModel.metadataInputForSave()
145
146        #expect(input["name"] == nil)
147        #expect(input.keys.contains("description"))
148        #expect(input["description"] as? String == nil)
149    }
150
151    @MainActor
152    private func makeRepository(headName: String?) -> RepositorySummary {
153        RepositorySummary(
154            fields: .init(
155                id: 1,
156                rid: "rid-1",
157                service: .git,
158                name: "repo",
159                description: "desc",
160                visibility: .publicVisibility,
161                updated: .now,
162                owner: Entity(canonicalName: "~owner"),
163                head: headName.map { Reference(name: $0, target: nil) }
164            )
165        )
166    }
167}