1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import Foundation
@testable import Grain
import Testing
/// Deleting a gallery, which every screen showing a card can do.
///
/// Six screens used to run this inline with `try?` and drop the row locally
/// whether or not the server agreed, so a rejected delete was indistinguishable
/// from a successful one until the next refresh. These pin the reporting.
///
/// Written with Swift Testing rather than XCTest: it is the current default for
/// new tests, and the storage and session seams mean a test like this no longer
/// has to save and restore the real Keychain to be safe to run.
@MainActor
struct GalleryDeleteTests {
private static let uri = "at://did:plc:test/social.grain.gallery/abc"
@Test func aRejectedDeleteIsReportedRatherThanSwallowed() async {
await withGrainEnvironment {
let account = TestAccount()
defer { account.restore() }
let auth = AuthManager(session: MockURLProtocol.mockSession())
await account.activate(auth)
MockURLProtocol.respondWithError(statusCode: 500)
defer { MockURLProtocol.handler = nil }
let result = await GalleryService.delete(
galleryUri: Self.uri,
client: XRPCClient(baseURL: AuthManager.serverURL, session: MockURLProtocol.mockSession()),
auth: auth
)
switch result {
case .success:
Issue.record("a 500 must not report success — that is the bug this replaced")
case let .failure(error):
#expect(!error.localizedDescription.isEmpty, "the screen has to have something to show")
}
}
}
@Test func anAcceptedDeleteSucceeds() async {
await withGrainEnvironment {
let account = TestAccount()
defer { account.restore() }
let auth = AuthManager(session: MockURLProtocol.mockSession())
await account.activate(auth)
// An empty repo to list, and an empty answer to each write.
MockURLProtocol.respondWithJSON(#"{"records": []}"#)
defer { MockURLProtocol.handler = nil }
let result = await GalleryService.delete(
galleryUri: Self.uri,
client: XRPCClient(baseURL: AuthManager.serverURL, session: MockURLProtocol.mockSession()),
auth: auth
)
#expect(result.isSuccess)
}
}
/// Signed out is the one failure reachable before the network, and it has to
/// be distinguishable from a server error so the copy can differ.
@Test func deletingWhileSignedOutFailsWithoutAskingTheServer() async {
await withGrainEnvironment {
var sawRequest = false
MockURLProtocol.handler = { _ in
sawRequest = true
throw URLError(.badServerResponse)
}
defer { MockURLProtocol.handler = nil }
let auth = AuthManager(session: MockURLProtocol.mockSession())
let result = await GalleryService.delete(
galleryUri: Self.uri,
client: XRPCClient(baseURL: AuthManager.serverURL, session: MockURLProtocol.mockSession()),
auth: auth
)
guard case let .failure(error) = result else {
Issue.record("signed out cannot be a successful delete")
return
}
#expect(error as? GalleryDeleteError == .notSignedIn)
#expect(!sawRequest, "no point spending a round trip without credentials")
}
}
}
/// The state a gallery card's modals are driven by.
@MainActor
struct GalleryCardModalsTests {
@Test func confirmingADeleteRecordsBothTheTargetAndTheFlag() {
let modals = GalleryCardModals()
#expect(modals.pendingDeleteUri == nil)
#expect(!modals.isConfirmingDelete)
modals.confirmDelete("at://g")
// Two assignments that had to agree were made by hand on six screens.
#expect(modals.pendingDeleteUri == "at://g")
#expect(modals.isConfirmingDelete)
}
@Test func modalsStartClosed() {
let modals = GalleryCardModals()
#expect(modals.commentUri == nil)
#expect(modals.report == nil)
#expect(modals.storyAuthor == nil)
#expect(modals.deleteError == nil)
}
}
private extension Result {
var isSuccess: Bool {
if case .success = self {
return true
}
return false
}
}