mount public data from the atmosphere to a virtual filesystem (macos only) pdfs.at
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(atproto/oauth): Collection.from smart constructor + parser edge-case tests

+74 -9
+19 -9
Packages/ATProto/Sources/ATProto/OAuth/Scope.swift
··· 15 15 case wildcard 16 16 case nsid(String) 17 17 18 + /// Smart constructor: normalizes `"*"` to `.wildcard` so semantic 19 + /// containment works correctly regardless of which surface form the 20 + /// caller passes. Returns nil for empty strings. 21 + public static func from(_ raw: String) -> Collection? { 22 + if raw == "*" { return .wildcard } 23 + guard !raw.isEmpty else { return nil } 24 + return .nsid(raw) 25 + } 26 + 18 27 var rawValue: String { 19 28 switch self { 20 29 case .wildcard: return "*" ··· 23 32 } 24 33 25 34 static func parse(_ s: String) -> Collection? { 26 - if s == "*" { return .wildcard } 27 - guard !s.isEmpty else { return nil } 28 - return .nsid(s) 35 + Self.from(s) 29 36 } 30 37 } 31 38 ··· 75 82 if let queryActions = query["action"] { 76 83 var parsed: Set<Action> = [] 77 84 for qa in queryActions { 78 - guard let a = Action(rawValue: qa) else { return nil } 85 + guard !qa.isEmpty, let a = Action(rawValue: qa) else { return nil } 79 86 parsed.insert(a) 80 87 } 81 88 actions = parsed.isEmpty ? nil : parsed ··· 245 252 var query: [String: [String]] = [:] 246 253 if !queryString.isEmpty { 247 254 for pair in queryString.split(separator: "&") { 248 - let kv = pair.split(separator: "=", maxSplits: 1).map(String.init) 249 - guard kv.count == 2 else { continue } 250 - let name = kv[0].removingPercentEncoding ?? kv[0] 251 - let value = kv[1].removingPercentEncoding ?? kv[1] 252 - query[name, default: []].append(value) 255 + // Use manual index-based split to preserve trailing empty strings 256 + // (e.g. "action=" → ["action", ""]) 257 + guard let eqIdx = pair.firstIndex(of: "=") else { continue } 258 + let name = String(pair[..<eqIdx]) 259 + let value = String(pair[pair.index(after: eqIdx)...]) 260 + let decodedName = name.removingPercentEncoding ?? name 261 + let decodedValue = value.removingPercentEncoding ?? value 262 + query[decodedName, default: []].append(decodedValue) 253 263 } 254 264 } 255 265 return (resource, positional, query)
+55
Packages/ATProto/Tests/ATProtoTests/OAuth/ScopeTests.swift
··· 229 229 let requested: Set<Scope> = [.blob(accepts: ["video/mp4"])] 230 230 #expect(!Scope.satisfies(granted: granted, requested: requested)) 231 231 } 232 + 233 + // MARK: - Parser edge cases 234 + 235 + @Test("parser accepts trailing ? with no query") 236 + func parseTrailingQuestion() { 237 + // Accepted: `?` with empty query is harmless; common from lazy callers. 238 + let scope = Scope("repo:app.bsky.feed.post?") 239 + guard case let .repo(collections, actions) = scope else { 240 + Issue.record("expected .repo") 241 + return 242 + } 243 + #expect(collections == [.nsid("app.bsky.feed.post")]) 244 + #expect(actions == nil) 245 + } 246 + 247 + @Test("parser rejects empty action value") 248 + func parseEmptyActionValue() { 249 + // "action=" yields a value of "" which is not a valid Action rawValue. 250 + #expect(Scope("repo:app.bsky.feed.post?action=") == nil) 251 + } 252 + 253 + @Test("parser rejects empty collection in query") 254 + func parseEmptyCollectionValue() { 255 + #expect(Scope("repo?collection=") == nil) 256 + } 257 + 258 + @Test("parser percent-decodes MIME with reserved characters") 259 + func parsePercentEncodedMIME() { 260 + // application/vnd.api+json — `+` percent-encoded as %2B. 261 + let scope = Scope("blob:application/vnd.api%2Bjson") 262 + guard case let .blob(accepts) = scope else { 263 + Issue.record("expected .blob") 264 + return 265 + } 266 + #expect(accepts == ["application/vnd.api%2Bjson"]) 267 + // Note: positional segments don't percent-decode per our parser; 268 + // only query-string values do. This pins the current behavior. 269 + } 270 + 271 + @Test("parser percent-decodes query values") 272 + func parsePercentEncodedQuery() { 273 + let scope = Scope("blob?accept=application/vnd.api%2Bjson") 274 + guard case let .blob(accepts) = scope else { 275 + Issue.record("expected .blob") 276 + return 277 + } 278 + #expect(accepts == ["application/vnd.api+json"]) 279 + } 280 + 281 + @Test("Collection.from(\"*\") normalizes to wildcard") 282 + func collectionFromWildcard() { 283 + #expect(Scope.Collection.from("*") == .wildcard) 284 + #expect(Scope.Collection.from("app.bsky.feed.post") == .nsid("app.bsky.feed.post")) 285 + #expect(Scope.Collection.from("") == nil) 286 + } 232 287 }