summaryrefslogtreecommitdiff
path: root/SoraTests
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-23 09:14:04 -0800
committerFuwn <[email protected]>2026-02-23 13:33:42 -0800
commit0f53ed0fc04952fb2fb43518be11c545da535f5b (patch)
tree8b16594291ff7a5e7dd289193eaa1998030edefe /SoraTests
parent(no commit message) (diff)
downloadsora-testing-0f53ed0fc04952fb2fb43518be11c545da535f5b.tar.xz
sora-testing-0f53ed0fc04952fb2fb43518be11c545da535f5b.zip
fix: harden danbooru decoding and pagination flow
Diffstat (limited to 'SoraTests')
-rw-r--r--SoraTests/ViewDerivedDataTests.swift189
1 files changed, 189 insertions, 0 deletions
diff --git a/SoraTests/ViewDerivedDataTests.swift b/SoraTests/ViewDerivedDataTests.swift
index ed83654..6572b70 100644
--- a/SoraTests/ViewDerivedDataTests.swift
+++ b/SoraTests/ViewDerivedDataTests.swift
@@ -1,3 +1,5 @@
+// swiftlint:disable file_length
+
import XCTest
final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_body_length
@@ -296,6 +298,193 @@ final class ViewDerivedDataTests: XCTestCase { // swiftlint:disable:this type_b
)
}
+ func testBooruManagerDanbooruPostsIncludeLimitParameter() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let urlBuilderSection = try extractFunction(
+ named: "func url(forPosts page: Int, limit: Int, tags: [String]) -> URL?",
+ from: source
+ )
+ let danbooruCaseStart = try XCTUnwrap(urlBuilderSection.range(of: "case .danbooru:")?.lowerBound)
+ let danbooruCaseEnd = try XCTUnwrap(
+ urlBuilderSection.range(of: "case .moebooru:", range: danbooruCaseStart..<urlBuilderSection.endIndex)?
+ .lowerBound
+ )
+ let danbooruSection = String(urlBuilderSection[danbooruCaseStart..<danbooruCaseEnd])
+ let danbooruLimitQueryCount = tokenCount(
+ matching: #"URLQueryItem\(name:\s*"limit""#,
+ in: danbooruSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ danbooruLimitQueryCount,
+ 0,
+ "Danbooru requests should include a `limit` query parameter."
+ )
+ }
+
+ func testBooruManagerDanbooruPostsUseBeforeCursorPagination() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let urlBuilderSection = try extractFunction(
+ named: "func url(forPosts page: Int, limit: Int, tags: [String]) -> URL?",
+ from: source
+ )
+ let danbooruCaseStart = try XCTUnwrap(urlBuilderSection.range(of: "case .danbooru:")?.lowerBound)
+ let danbooruCaseEnd = try XCTUnwrap(
+ urlBuilderSection.range(of: "case .moebooru:", range: danbooruCaseStart..<urlBuilderSection.endIndex)?
+ .lowerBound
+ )
+ let danbooruSection = String(urlBuilderSection[danbooruCaseStart..<danbooruCaseEnd])
+ let pageTokenFunctionSection = try extractFunction(
+ named: "private func danbooruPageToken(for page: Int) -> String",
+ from: source
+ )
+ let danbooruCursorPageQueryCount = tokenCount(
+ matching: #"URLQueryItem\(name:\s*"page",\s*value:\s*danbooruPageToken\(for:\s*page\)\)"#,
+ in: danbooruSection
+ )
+ let beforeCursorCount = tokenCount(
+ matching: #""b\\\#\("#,
+ in: pageTokenFunctionSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ danbooruCursorPageQueryCount,
+ 0,
+ "Danbooru requests should derive the `page` query using a cursor-aware token helper."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ beforeCursorCount,
+ 0,
+ "Danbooru cursor pagination should build `page=b<id>` for subsequent pages."
+ )
+ }
+
+ func testBooruManagerValidatesHTTPStatusCodesForRequests() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let requestURLSection = try extractFunction(
+ named: "func requestURL(_ url: URL) async throws -> Data",
+ from: source
+ )
+ let statusValidationCount = tokenCount(
+ matching: #"\.validate\(statusCode:\s*200\.\.<300\)"#,
+ in: requestURLSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ statusValidationCount,
+ 0,
+ "BooruManager should validate HTTP status codes to avoid silent API failures."
+ )
+ }
+
+ func testBooruManagerDanbooruFallsBackToUnauthenticatedRequestsOnUnauthorized() throws {
+ let source = try loadSource(at: "Sora/Data/Booru/BooruManager.swift")
+ let retrySection = try extractFunction(
+ named: "private func fetchPostsWithRetry(url: URL) async -> [BooruPost]",
+ from: source
+ )
+ let credentialStripperSection = try extractFunction(
+ named: "private static func removingDanbooruCredentials(from url: URL) -> URL?",
+ from: source
+ )
+ let unauthorizedResponseCheckCount = tokenCount(
+ matching: #"responseCode\s*==\s*401"#,
+ in: retrySection
+ )
+ let fallbackInvocationCount = tokenCount(
+ matching: #"removingDanbooruCredentials\(from:\s*requestURL\)"#,
+ in: retrySection
+ )
+ let strippedQueryItemCount = tokenCount(
+ matching: #"queryItem\.name\s*!=\s*"login"\s*&&\s*queryItem\.name\s*!=\s*"api_key""#,
+ in: credentialStripperSection
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ unauthorizedResponseCheckCount,
+ 0,
+ "Danbooru fetch retries should detect unauthorized responses."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ fallbackInvocationCount,
+ 0,
+ "Danbooru fetch retries should retry without credentials after unauthorized responses."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ strippedQueryItemCount,
+ 0,
+ "Credential fallback should remove both `login` and `api_key` query parameters."
+ )
+ }
+
+ func testDanbooruPostModelUsesOptionalVisibilityDependentFields() throws {
+ let source = try loadSource(at: "Sora/Data/Danbooru/DanbooruPost.swift")
+ let optionalFileURLCount = tokenCount(matching: #"let\s+fileURL:\s+String\?"#, in: source)
+ let optionalLargeFileURLCount = tokenCount(matching: #"let\s+largeFileURL:\s+String\?"#, in: source)
+ let optionalPreviewURLCount = tokenCount(
+ matching: #"let\s+previewFileURL:\s+String\?"#,
+ in: source
+ )
+ let optionalMD5Count = tokenCount(matching: #"let\s+md5:\s+String\?"#, in: source)
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ optionalFileURLCount,
+ 0,
+ "Danbooru model should decode visibility-dependent file URLs as optional."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ optionalLargeFileURLCount,
+ 0,
+ "Danbooru model should decode visibility-dependent large file URLs as optional."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ optionalPreviewURLCount,
+ 0,
+ "Danbooru model should decode visibility-dependent preview URLs as optional."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ optionalMD5Count,
+ 0,
+ "Danbooru model should decode visibility-dependent MD5 values as optional."
+ )
+ }
+
+ func testDanbooruParserUsesLossyDecodingForMixedRecordValidity() throws {
+ let source = try loadSource(at: "Sora/Data/Danbooru/DanbooruPostParser.swift")
+ let lossyDecoderUsageCount = tokenCount(
+ matching: #"\[FailableDecodable<DanbooruPost>\]\.self"#,
+ in: source
+ )
+ let malformedRecordDebugCount = tokenCount(
+ matching: #"dropped\s*\\\#\(droppedRecordCount\)\s*malformed records"#,
+ in: source
+ )
+
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ lossyDecoderUsageCount,
+ 0,
+ "Danbooru parser should decode lossily so one malformed item doesn't drop the entire page."
+ )
+ // swiftlint:disable:next prefer_nimble
+ XCTAssertGreaterThan(
+ malformedRecordDebugCount,
+ 0,
+ "Danbooru parser should log dropped malformed records for observability."
+ )
+ }
+
func testThumbnailGridViewAvoidsDirectColumnArrayIndexing() throws {
let source = try loadSource(at: "Sora/Views/Shared/ThumbnailGridView.swift")
let normalizedSource = strippingCommentsAndStrings(from: source)