krz/hutch

an ios client for sourcehut

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

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