Skip to content

Commit 3118542

Browse files
authored
Merge pull request #90 from CompositeCode/chore/apiclient-dead-endpoint-sweep
chore(api): delete five unwired endpoints and guard against new ones
2 parents c6458b6 + 56dc98d commit 3118542

7 files changed

Lines changed: 36 additions & 206 deletions

File tree

.claude/commands/ios-review.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,35 @@ Perform a focused code review of the Swift/SwiftUI changes on the current branch
4343
### Code quality
4444
- [ ] No unnecessary comments (only "why", never "what")
4545
- [ ] No dead code, unused variables, or leftover `TODO` without a tracking issue
46+
- [ ] **Every new `APIClient` function has a call site outside `Services/APIClient*`.** An endpoint
47+
consumed only by a function no screen calls looks like coverage in an endpoint diff while
48+
being a missing feature — that is how "documents shared with me", "mute", "add org member"
49+
and "trash a DM" stayed invisible for a year. If it is deliberately unwired, say why in a
50+
comment on the function.
51+
- [ ] **Request-body keys match what the route destructures.** `PATCH /api/user/update` and friends
52+
ignore unknown keys and still answer `200`, so a mismatch is silent data loss, not an error.
53+
Read the route, don't infer the name.
4654

47-
4. **Summarize findings** as:
55+
4. **Run the zero-call-site sweep** (cheap, catches the above mechanically):
56+
```bash
57+
for f in InterlinedList/Services/APIClient*.swift; do
58+
grep -oE '^\s+(@discardableResult\s+)?func [a-zA-Z0-9_]+' "$f" | sed -E 's/.*func //'
59+
done | sort -u | while read -r fn; do
60+
n=$(grep -rn "\.${fn}(" InterlinedList/ | grep -vc "InterlinedList/Services/APIClient")
61+
[ "$n" -eq 0 ] && echo "zero app call sites: $fn"
62+
done
63+
```
64+
Transport helpers (`get`, `post*`, `put*`, `patch*`, `delete*`, `checkResponse`,
65+
`postMultipartRawData`, `pathSegment`, `serverErrorMessage`) are called via `Self.` inside the
66+
client and will always show up here — ignore them. Anything else needs wiring, deleting, or a
67+
comment explaining why it is kept.
68+
69+
5. **Summarize findings** as:
4870
- Blockers (must fix before merge)
4971
- Suggestions (non-blocking improvements)
5072
- Positives (good patterns worth noting)
5173

52-
5. **Run a build** to confirm there are no compilation errors. Prefer XcodeBuildMCP `build_sim` (after `session_show_defaults`); raw fallback pins a concrete UDID (`name=iPhone 16` alone is ambiguous across runtimes):
74+
6. **Run a build** to confirm there are no compilation errors. Prefer XcodeBuildMCP `build_sim` (after `session_show_defaults`); raw fallback pins a concrete UDID (`name=iPhone 16` alone is ambiguous across runtimes):
5375
```bash
5476
xcodebuild -scheme InterlinedList \
5577
-destination 'platform=iOS Simulator,id=<SIM_UDID>' \

InterlinedList.xcodeproj/project.pbxproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,6 @@
240240
T1E5T1E5T1E5P003 /* APIClientSearchDocumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P004 /* APIClientSearchDocumentsTests.swift */; };
241241
T1E5T1E5T1E5P005 /* APIClientSearchListsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P006 /* APIClientSearchListsTests.swift */; };
242242
T1E5T1E5T1E5P007 /* ListSchemaDraftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P008 /* ListSchemaDraftTests.swift */; };
243-
T1E5T1E5T1E5P009 /* APIClientUpdateListSchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P010 /* APIClientUpdateListSchemaTests.swift */; };
244243
T1E5T1E5T1E5P011 /* EnvLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P012 /* EnvLoader.swift */; };
245244
T1E5T1E5T1E5P013 /* E2EReadOnlyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P014 /* E2EReadOnlyTests.swift */; };
246245
T1E5T1E5T1E5P015 /* KeychainServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = T1E5T1E5T1E5P016 /* KeychainServiceTests.swift */; };
@@ -505,7 +504,6 @@
505504
T1E5T1E5T1E5P004 /* APIClientSearchDocumentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientSearchDocumentsTests.swift; sourceTree = "<group>"; };
506505
T1E5T1E5T1E5P006 /* APIClientSearchListsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientSearchListsTests.swift; sourceTree = "<group>"; };
507506
T1E5T1E5T1E5P008 /* ListSchemaDraftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSchemaDraftTests.swift; sourceTree = "<group>"; };
508-
T1E5T1E5T1E5P010 /* APIClientUpdateListSchemaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClientUpdateListSchemaTests.swift; sourceTree = "<group>"; };
509507
T1E5T1E5T1E5P012 /* EnvLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvLoader.swift; sourceTree = "<group>"; };
510508
T1E5T1E5T1E5P014 /* E2EReadOnlyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = E2EReadOnlyTests.swift; sourceTree = "<group>"; };
511509
T1E5T1E5T1E5P016 /* KeychainServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainServiceTests.swift; sourceTree = "<group>"; };
@@ -780,7 +778,6 @@
780778
T1E5T1E5T1E5P002 /* APIClientListFolderTests.swift */,
781779
T1E5T1E5T1E5P004 /* APIClientSearchDocumentsTests.swift */,
782780
T1E5T1E5T1E5P006 /* APIClientSearchListsTests.swift */,
783-
T1E5T1E5T1E5P010 /* APIClientUpdateListSchemaTests.swift */,
784781
T1E5T1E5T1E5P018 /* APIClientPasswordResetTests.swift */,
785782
T1E5T1E5T1E5P01A /* APIClientEmailVerificationTests.swift */,
786783
T1E5T1E5T1E5P01C /* APIClientIdentitiesTests.swift */,
@@ -1154,7 +1151,6 @@
11541151
T1E5T1E5T1E5P003 /* APIClientSearchDocumentsTests.swift in Sources */,
11551152
T1E5T1E5T1E5P005 /* APIClientSearchListsTests.swift in Sources */,
11561153
T1E5T1E5T1E5P007 /* ListSchemaDraftTests.swift in Sources */,
1157-
T1E5T1E5T1E5P009 /* APIClientUpdateListSchemaTests.swift in Sources */,
11581154
T1E5T1E5T1E5P011 /* EnvLoader.swift in Sources */,
11591155
T1E5T1E5T1E5P013 /* E2EReadOnlyTests.swift in Sources */,
11601156
T1E5T1E5T1E5P015 /* KeychainServiceTests.swift in Sources */,

InterlinedList/Services/APIClient.swift

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@ final class APIClient {
174174
try await deleteCamel("/api/user/identities", body: Body(provider: provider, providerId: providerId))
175175
}
176176

177+
/// Actively verifies a stored OAuth credential — the only route that reports real
178+
/// token health. The five `/status` routes report whether a *provider* is
179+
/// configured server-side, not whether this account's token still works, which is
180+
/// why `LinkedIdentitiesView` cannot show a "reconnect needed" state today. Kept
181+
/// as the primitive that closes that gap; no caller yet.
182+
///
183+
/// Note: the route reads only `provider` — `providerId` is accepted and ignored.
177184
func verifyIdentity(provider: String, providerId: String) async throws {
178185
struct Body: Encodable { let provider: String; let providerId: String }
179186
struct Response: Decodable { let ok: Bool? }
@@ -366,6 +373,11 @@ final class APIClient {
366373
}
367374

368375
/// Fetch/refresh OpenGraph link-preview metadata for a message's links.
376+
///
377+
/// No caller yet, deliberately. The web fires this after publishing and after an
378+
/// edit (`MessageInput.tsx:511`) so a new post's link previews populate without a
379+
/// reload. Wiring it adds a request to the publish path, so it is tracked
380+
/// separately rather than slipped into a hygiene sweep.
369381
@discardableResult
370382
func refreshMessageMetadata(messageId: String) async throws -> [MessageLinkPreview] {
371383
struct Response: Decodable {
@@ -717,16 +729,6 @@ final class APIClient {
717729
return list
718730
}
719731

720-
func updateListSchema(listId: String, schemaDSL: String) async throws -> [ListPropertyDef] {
721-
struct Body: Encodable { let schema: String }
722-
// Response shape isn't documented; tolerate missing `properties` (e.g. {"ok":true}).
723-
struct Response: Decodable { let properties: [ListPropertyDef]? }
724-
let encoded = listId.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? listId
725-
let response: Response = try await putCamel("/api/lists/\(encoded)/schema",
726-
body: Body(schema: schemaDSL))
727-
return response.properties ?? []
728-
}
729-
730732
func searchLists(q: String, limit: Int = 20, offset: Int = 0) async throws -> ([UserList], Pagination?) {
731733
let qEncoded = q.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? q
732734
struct Response: Decodable { let lists: [UserList]; let pagination: Pagination? }
@@ -743,18 +745,6 @@ final class APIClient {
743745
return try await get("/api/github/repos")
744746
}
745747

746-
/// Open (or `state`) issues for a repo (`GET /api/github/issues?repo=owner/repo`,
747-
/// Bearer). Raw GitHub REST array; decode defensively.
748-
func githubIssues(repo: String, state: String = "open") async throws -> [GitHubIssue] {
749-
var components = URLComponents(string: baseURL + "/api/github/issues")
750-
components?.queryItems = [
751-
URLQueryItem(name: "repo", value: repo),
752-
URLQueryItem(name: "state", value: state),
753-
]
754-
let query = components?.percentEncodedQuery.map { "?" + $0 } ?? ""
755-
return try await get("/api/github/issues" + query)
756-
}
757-
758748
/// Re-syncs a GitHub-backed list's cached rows from GitHub issues
759749
/// (`POST /api/lists/:id/refresh`, Bearer). 400 if the list isn't github-backed
760750
/// or its repo is missing.
@@ -1290,11 +1280,6 @@ final class APIClient {
12901280

12911281
// MARK: - Organizations (Phase 8)
12921282

1293-
func organizations(limit: Int = 30, offset: Int = 0) async throws -> (orgs: [Organization], pagination: Pagination?) {
1294-
let response: OrganizationsResponse = try await get("/api/organizations?limit=\(limit)&offset=\(offset)")
1295-
return (response.organizations, response.pagination)
1296-
}
1297-
12981283
func organization(id: String) async throws -> Organization {
12991284
let encoded = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
13001285
let response: OrganizationResponse = try await get("/api/organizations/\(encoded)")
@@ -1467,20 +1452,6 @@ final class APIClient {
14671452
return response.message
14681453
}
14691454

1470-
func directMessage(id: String) async throws -> DMMessage {
1471-
let encoded = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
1472-
let response: DMMessageResponse = try await get("/api/dm/\(encoded)")
1473-
return response.message
1474-
}
1475-
1476-
@discardableResult
1477-
func markDMRead(id: String) async throws -> Int {
1478-
struct Empty: Encodable {}
1479-
let encoded = id.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? id
1480-
let response: DMUpdatedResponse = try await post("/api/dm/\(encoded)/read", body: Empty())
1481-
return response.updated
1482-
}
1483-
14841455
func trashDM(id: String) async throws {
14851456
struct Empty: Encodable {}
14861457
struct Response: Decodable { let ok: Bool? }

InterlinedListTests/APIClientTests/APIClientDirectMessagesTests.swift

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,26 +123,6 @@ final class APIClientDirectMessagesTests: XCTestCase {
123123
}
124124
}
125125

126-
// MARK: directMessage(id:)
127-
128-
func test_directMessage_sendsGetToIdPath() async throws {
129-
session.stub(json: "{\"message\":\(dmMessageJSON)}")
130-
let message = try await sut.directMessage(id: "m1")
131-
XCTAssertEqual(session.lastRequest?.httpMethod, "GET")
132-
XCTAssertEqual(session.lastRequest?.url?.path, "/api/dm/m1")
133-
XCTAssertEqual(message.id, "m1")
134-
}
135-
136-
// MARK: markDMRead()
137-
138-
func test_markDMRead_sendsPostToReadPath() async throws {
139-
session.stub(json: #"{"updated":1}"#)
140-
let updated = try await sut.markDMRead(id: "m1")
141-
XCTAssertEqual(session.lastRequest?.httpMethod, "POST")
142-
XCTAssertEqual(session.lastRequest?.url?.path, "/api/dm/m1/read")
143-
XCTAssertEqual(updated, 1)
144-
}
145-
146126
// MARK: trashDM() / restoreDM()
147127

148128
func test_trashDM_sendsPostToTrashPath() async throws {

InterlinedListTests/APIClientTests/APIClientGapPhasesTests.swift

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,13 +180,6 @@ final class APIClientGapPhasesTests: XCTestCase {
180180

181181
// MARK: - Phase 8: Organizations
182182

183-
func test_organizations_decodesList() async throws {
184-
session.stub(json: #"{"organizations":[{"id":"o1","name":"Acme"}],"pagination":null}"#)
185-
let (orgs, _) = try await sut.organizations()
186-
XCTAssertTrue(session.lastRequest?.url?.path.hasSuffix("/api/organizations") == true)
187-
XCTAssertEqual(orgs.first?.name, "Acme")
188-
}
189-
190183
func test_organization_decodesRole() async throws {
191184
session.stub(json: #"{"organization":{"id":"o1","name":"Acme","isPublic":false,"memberCount":3,"userRole":"owner"}}"#)
192185
let org = try await sut.organization(id: "o1")

InterlinedListTests/APIClientTests/APIClientGitHubTests.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,20 +91,6 @@ final class APIClientGitHubTests: XCTestCase {
9191
}
9292
}
9393

94-
// MARK: - githubIssues()
95-
96-
func test_githubIssues_sendsRepoAndStateQuery() async throws {
97-
session.stub(json: #"[{"number":1,"title":"Bug","state":"open","html_url":"https://github.com/o/r/issues/1"}]"#)
98-
let issues = try await sut.githubIssues(repo: "octocat/Hello-World", state: "open")
99-
XCTAssertEqual(session.lastRequest?.url?.path, "/api/github/issues")
100-
let query = session.lastRequest?.url?.query ?? ""
101-
XCTAssertTrue(query.contains("repo=octocat/Hello-World") || query.contains("repo=octocat%2FHello-World"))
102-
XCTAssertTrue(query.contains("state=open"))
103-
XCTAssertEqual(issues.first?.number, 1)
104-
XCTAssertEqual(issues.first?.title, "Bug")
105-
XCTAssertEqual(issues.first?.htmlUrl, "https://github.com/o/r/issues/1")
106-
}
107-
10894
// MARK: - refreshList()
10995

11096
func test_refreshList_sendsPostToCorrectPath() async throws {

InterlinedListTests/APIClientTests/APIClientUpdateListSchemaTests.swift

Lines changed: 0 additions & 118 deletions
This file was deleted.

0 commit comments

Comments
 (0)