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}