krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.5.0: 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}