diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 7e5a6a874..ba9d14d7a 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -955,7 +955,7 @@ public class StorageFileApi: StorageApi, @unchecked Sendable { file: FileUpload, options: FileOptions? ) async throws -> SignedURLUploadResponse { - let options = options ?? defaultFileOptions + let options = options ?? FileOptions() var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields() headers[.xUpsert] = "\(options.upsert)" diff --git a/Tests/StorageTests/StorageFileAPITests.swift b/Tests/StorageTests/StorageFileAPITests.swift index 0be414046..b2ce4eaaf 100644 --- a/Tests/StorageTests/StorageFileAPITests.swift +++ b/Tests/StorageTests/StorageFileAPITests.swift @@ -45,6 +45,37 @@ extension StorageMockerTests { ) } + /// A client whose transport records the request body, for asserting the emitted multipart + /// headers. + private func makeBodyCapturingSUT(body: LockIsolated) -> SupabaseStorageClient { + let respond: @Sendable (URLRequest) -> (Data, URLResponse) = { request in + ( + Data(#"{"Key":"bucket/\#(request.url!.lastPathComponent)"}"#.utf8), + HTTPURLResponse( + url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil + )! + ) + } + + return SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: url, + headers: [:], + session: StorageHTTPSession( + fetch: { request in + body.setValue(request.httpBody ?? Data()) + return respond(request) + }, + upload: { request, data in + body.setValue(data) + return respond(request) + } + ), + logger: nil + ) + ) + } + @Test func listFiles() async throws { let storage = makeSUT() @@ -1410,7 +1441,7 @@ extension StorageMockerTests { curl \ --request PUT \ --header "Cache-Control: max-age=3600" \ - --header "Content-Length: 297" \ + --header "Content-Length: 283" \ --header "Content-Type: multipart/form-data; boundary=alamofire.boundary.e56f43407f772505" \ --header "X-Client-Info: storage-swift/0.0.0" \ --header "apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" \ @@ -1421,7 +1452,7 @@ extension StorageMockerTests { 3600\#r --alamofire.boundary.e56f43407f772505\#r Content-Disposition: form-data; name=\"\"; filename=\"file.txt\"\#r - Content-Type: text/plain;charset=UTF-8\#r + Content-Type: text/plain\#r \#r hello world\#r --alamofire.boundary.e56f43407f772505--\#r @@ -1438,6 +1469,38 @@ extension StorageMockerTests { #expect(response.fullPath == "bucket/file.txt") } + @Test + func uploadToSignedURLDerivesContentTypeFromPathExtensionWhenOptionsOmitted() async throws { + let body = LockIsolated(Data()) + let storage = makeBodyCapturingSUT(body: body) + + _ = try await storage.from("bucket") + .uploadToSignedURL( + "cat.png", + token: "abc.def.ghi", + data: Data("not-really-a-png".utf8) + ) + + #expect(body.value.containsBytes(of: "Content-Type: image/png")) + #expect(!body.value.containsBytes(of: "text/plain")) + } + + @Test + func uploadToSignedURLFromFileURLDerivesContentTypeWhenOptionsOmitted() async throws { + let body = LockIsolated(Data()) + let storage = makeBodyCapturingSUT(body: body) + + _ = try await storage.from("bucket") + .uploadToSignedURL( + "cat.jpg", + token: "abc.def.ghi", + fileURL: Bundle.module.url(forResource: "sadcat", withExtension: "jpg")! + ) + + #expect(body.value.containsBytes(of: "Content-Type: image/jpeg")) + #expect(!body.value.containsBytes(of: "text/plain")) + } + @Test func uploadToSignedURL_fromFileURL() async throws { let storage = makeSUT() @@ -1603,3 +1666,10 @@ extension StorageMockerTests { } } } + +extension Data { + /// Whether the raw bytes contain `string`, for bodies that are not valid UTF-8 as a whole. + fileprivate func containsBytes(of string: String) -> Bool { + range(of: Data(string.utf8)) != nil + } +}