Skip to content

Commit c537260

Browse files
author
Guilherme Souza
committed
add tests
1 parent f08f731 commit c537260

File tree

6 files changed

+156
-42
lines changed

6 files changed

+156
-42
lines changed

Sources/Storage/MultipartFormData.swift

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
//
2626

2727
import Foundation
28-
import Helpers
2928
import HTTPTypes
29+
import Helpers
3030

3131
#if canImport(MobileCoreServices)
3232
import MobileCoreServices
@@ -59,21 +59,22 @@ class MultipartFormData {
5959
}
6060

6161
static func randomBoundary() -> String {
62-
let first = UInt32.random(in: UInt32.min ... UInt32.max)
63-
let second = UInt32.random(in: UInt32.min ... UInt32.max)
62+
let first = UInt32.random(in: UInt32.min...UInt32.max)
63+
let second = UInt32.random(in: UInt32.min...UInt32.max)
6464

6565
return String(format: "alamofire.boundary.%08x%08x", first, second)
6666
}
6767

6868
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
69-
let boundaryText = switch boundaryType {
70-
case .initial:
71-
"--\(boundary)\(EncodingCharacters.crlf)"
72-
case .encapsulated:
73-
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
74-
case .final:
75-
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
76-
}
69+
let boundaryText =
70+
switch boundaryType {
71+
case .initial:
72+
"--\(boundary)\(EncodingCharacters.crlf)"
73+
case .encapsulated:
74+
"\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
75+
case .final:
76+
"\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
77+
}
7778

7879
return Data(boundaryText.utf8)
7980
}
@@ -96,7 +97,7 @@ class MultipartFormData {
9697
// MARK: - Properties
9798

9899
/// Default memory threshold used when encoding `MultipartFormData`, in bytes.
99-
static let encodingMemoryThreshold: UInt64 = 10000000
100+
static let encodingMemoryThreshold: UInt64 = 10_000_000
100101

101102
/// The `Content-Type` header value containing the boundary used to generate the `multipart/form-data`.
102103
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
@@ -402,8 +403,8 @@ class MultipartFormData {
402403
private func encodeHeaders(for bodyPart: BodyPart) -> Data {
403404
let headerText =
404405
bodyPart.headers.map { "\($0.name): \($0.value)\(EncodingCharacters.crlf)" }
405-
.joined()
406-
+ EncodingCharacters.crlf
406+
.joined()
407+
+ EncodingCharacters.crlf
407408

408409
return Data(headerText.utf8)
409410
}
@@ -481,7 +482,7 @@ class MultipartFormData {
481482

482483
if bytesRead > 0 {
483484
if buffer.count != bytesRead {
484-
buffer = Array(buffer[0 ..< bytesRead])
485+
buffer = Array(buffer[0..<bytesRead])
485486
}
486487

487488
try write(&buffer, to: outputStream)
@@ -492,7 +493,8 @@ class MultipartFormData {
492493
}
493494
}
494495

495-
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws {
496+
private func writeFinalBoundaryData(for bodyPart: BodyPart, to outputStream: OutputStream) throws
497+
{
496498
if bodyPart.hasFinalBoundary {
497499
try write(finalBoundaryData(), to: outputStream)
498500
}
@@ -520,7 +522,7 @@ class MultipartFormData {
520522
bytesToWrite -= bytesWritten
521523

522524
if bytesToWrite > 0 {
523-
buffer = Array(buffer[bytesWritten ..< buffer.count])
525+
buffer = Array(buffer[bytesWritten..<buffer.count])
524526
}
525527
}
526528
}
@@ -577,7 +579,7 @@ class MultipartFormData {
577579
kUTTagClassFilenameExtension, pathExtension as CFString, nil
578580
)?.takeRetainedValue(),
579581
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
580-
.takeRetainedValue()
582+
.takeRetainedValue()
581583
{
582584
return contentType as String
583585
}
@@ -593,7 +595,7 @@ class MultipartFormData {
593595
kUTTagClassFilenameExtension, pathExtension as CFString, nil
594596
)?.takeRetainedValue(),
595597
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
596-
.takeRetainedValue()
598+
.takeRetainedValue()
597599
{
598600
return contentType as String
599601
}
@@ -615,7 +617,7 @@ class MultipartFormData {
615617
kUTTagClassFilenameExtension, pathExtension as CFString, nil
616618
)?.takeRetainedValue(),
617619
let contentType = UTTypeCopyPreferredTagWithClass(id, kUTTagClassMIMEType)?
618-
.takeRetainedValue()
620+
.takeRetainedValue()
619621
{
620622
return contentType as String
621623
}
@@ -650,37 +652,37 @@ enum MultipartFormDataError: Error {
650652
var underlyingError: (any Error)? {
651653
switch self {
652654
case let .bodyPartFileNotReachableWithError(_, error),
653-
let .bodyPartFileSizeQueryFailedWithError(_, error),
654-
let .inputStreamReadFailed(error),
655-
let .outputStreamWriteFailed(error):
655+
let .bodyPartFileSizeQueryFailedWithError(_, error),
656+
let .inputStreamReadFailed(error),
657+
let .outputStreamWriteFailed(error):
656658
error
657659

658660
case .bodyPartURLInvalid,
659-
.bodyPartFilenameInvalid,
660-
.bodyPartFileNotReachable,
661-
.bodyPartFileIsDirectory,
662-
.bodyPartFileSizeNotAvailable,
663-
.bodyPartInputStreamCreationFailed,
664-
.outputStreamFileAlreadyExists,
665-
.outputStreamURLInvalid,
666-
.outputStreamCreationFailed:
661+
.bodyPartFilenameInvalid,
662+
.bodyPartFileNotReachable,
663+
.bodyPartFileIsDirectory,
664+
.bodyPartFileSizeNotAvailable,
665+
.bodyPartInputStreamCreationFailed,
666+
.outputStreamFileAlreadyExists,
667+
.outputStreamURLInvalid,
668+
.outputStreamCreationFailed:
667669
nil
668670
}
669671
}
670672

671673
var url: URL? {
672674
switch self {
673675
case let .bodyPartURLInvalid(url),
674-
let .bodyPartFilenameInvalid(url),
675-
let .bodyPartFileNotReachable(url),
676-
let .bodyPartFileNotReachableWithError(url, _),
677-
let .bodyPartFileIsDirectory(url),
678-
let .bodyPartFileSizeNotAvailable(url),
679-
let .bodyPartFileSizeQueryFailedWithError(url, _),
680-
let .bodyPartInputStreamCreationFailed(url),
681-
let .outputStreamFileAlreadyExists(url),
682-
let .outputStreamURLInvalid(url),
683-
let .outputStreamCreationFailed(url):
676+
let .bodyPartFilenameInvalid(url),
677+
let .bodyPartFileNotReachable(url),
678+
let .bodyPartFileNotReachableWithError(url, _),
679+
let .bodyPartFileIsDirectory(url),
680+
let .bodyPartFileSizeNotAvailable(url),
681+
let .bodyPartFileSizeQueryFailedWithError(url, _),
682+
let .bodyPartInputStreamCreationFailed(url),
683+
let .outputStreamFileAlreadyExists(url),
684+
let .outputStreamURLInvalid(url),
685+
let .outputStreamCreationFailed(url):
684686
url
685687

686688
case .inputStreamReadFailed, .outputStreamWriteFailed:

Sources/Storage/StorageFileApi.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ enum FileUpload {
5050
}
5151
}
5252

53+
#if DEBUG
54+
// It is safe to mark it as unsafe, since this property is only used in tests for overriding the
55+
// boundary value, instead of using the random one.
56+
nonisolated(unsafe) var testingBoundary: String?
57+
#endif
58+
5359
/// Supabase Storage File API
5460
public class StorageFileApi: StorageApi, @unchecked Sendable {
5561
/// The bucket id to operate on.
@@ -83,7 +89,11 @@ public class StorageFileApi: StorageApi, @unchecked Sendable {
8389

8490
headers[.duplex] = options.duplex
8591

86-
let formData = MultipartFormData()
92+
#if DEBUG
93+
let formData = MultipartFormData(boundary: testingBoundary)
94+
#else
95+
let formData = MultipartFormData()
96+
#endif
8797
file.encode(to: formData, withPath: path, options: options)
8898

8999
struct UploadResponse: Decodable {

Tests/IntegrationTests/StorageFileIntegrationTests.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@ final class StorageFileIntegrationTests: XCTestCase {
380380
XCTAssertEqual(cacheControl, "public, max-age=14400")
381381
}
382382

383+
func testUploadWithFileURL() async throws {
384+
try await storage.from(bucketName)
385+
.upload(uploadPath, fileURL: uploadFileURL("sadcat.jpg"))
386+
387+
let uploadedFile = try await storage.from(bucketName).download(path: uploadPath)
388+
389+
XCTAssertEqual(uploadedFile, file)
390+
}
391+
383392
private func newBucket(
384393
prefix: String = "",
385394
options: BucketOptions = BucketOptions(public: true)

Tests/StorageTests/SupabaseStorageClient+Test.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ extension SupabaseStorageClient {
2020
headers: [
2121
"Authorization": "Bearer \(apiKey)",
2222
"Apikey": apiKey,
23+
"X-Client-Info": "storage-swift/x.y.z",
2324
],
2425
session: session,
2526
logger: nil

Tests/StorageTests/SupabaseStorageTests.swift

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ final class SupabaseStorageTests: XCTestCase {
1818
upload: unimplemented("StorageHTTPSession.upload")
1919
)
2020

21+
override func invokeTest() {
22+
withSnapshotTesting(record: .missing) {
23+
super.invokeTest()
24+
}
25+
}
26+
2127
func testGetPublicURL() throws {
2228
let sut = makeSUT()
2329

@@ -91,11 +97,97 @@ final class SupabaseStorageTests: XCTestCase {
9197
}
9298
}
9399

100+
func testUploadData() async {
101+
testingBoundary = "alamofire.boundary.c21f947c1c7b0c57"
102+
103+
sessionMock.fetch = { request in
104+
assertInlineSnapshot(of: request, as: .curl) {
105+
#"""
106+
curl \
107+
--request POST \
108+
--header "Apikey: test.api.key" \
109+
--header "Authorization: Bearer test.api.key" \
110+
--header "Cache-Control: max-age=14400" \
111+
--header "Content-Type: multipart/form-data; boundary=alamofire.boundary.c21f947c1c7b0c57" \
112+
--header "X-Client-Info: storage-swift/x.y.z" \
113+
--header "x-upsert: false" \
114+
--data "--alamofire.boundary.c21f947c1c7b0c57\#r
115+
Content-Disposition: form-data; name=\"cacheControl\"\#r
116+
\#r
117+
14400\#r
118+
--alamofire.boundary.c21f947c1c7b0c57\#r
119+
Content-Disposition: form-data; name=\"metadata\"\#r
120+
\#r
121+
{\"key\":\"value\"}\#r
122+
--alamofire.boundary.c21f947c1c7b0c57\#r
123+
Content-Disposition: form-data; name=\"\"; filename=\"file1.txt\"\#r
124+
Content-Type: text/plain\#r
125+
\#r
126+
test data\#r
127+
--alamofire.boundary.c21f947c1c7b0c57--\#r
128+
" \
129+
"http://localhost:54321/storage/v1/object/tests/file1.txt"
130+
"""#
131+
}
132+
throw UnimplementedFailure(description: "unimplemented")
133+
}
134+
135+
let sut = makeSUT()
136+
137+
_ = try? await sut.from(bucketId)
138+
.upload(
139+
"file1.txt",
140+
data: "test data".data(using: .utf8)!,
141+
options: FileOptions(
142+
cacheControl: "14400",
143+
metadata: ["key": "value"]
144+
)
145+
)
146+
}
147+
148+
func testUploadFileURL() async {
149+
testingBoundary = "alamofire.boundary.c21f947c1c7b0c57"
150+
151+
sessionMock.fetch = { request in
152+
assertInlineSnapshot(of: request, as: .curl) {
153+
#"""
154+
curl \
155+
--request POST \
156+
--header "Apikey: test.api.key" \
157+
--header "Authorization: Bearer test.api.key" \
158+
--header "Cache-Control: max-age=3600" \
159+
--header "Content-Type: multipart/form-data; boundary=alamofire.boundary.c21f947c1c7b0c57" \
160+
--header "X-Client-Info: storage-swift/x.y.z" \
161+
--header "x-upsert: false" \
162+
"http://localhost:54321/storage/v1/object/tests/sadcat.jpg"
163+
"""#
164+
}
165+
throw UnimplementedFailure(description: "unimplemented")
166+
}
167+
168+
let sut = makeSUT()
169+
170+
_ = try? await sut.from(bucketId)
171+
.upload(
172+
"sadcat.jpg",
173+
fileURL: uploadFileURL("sadcat.jpg"),
174+
options: FileOptions(
175+
metadata: ["key": "value"]
176+
)
177+
)
178+
}
179+
94180
private func makeSUT() -> SupabaseStorageClient {
95181
SupabaseStorageClient.test(
96182
supabaseURL: supabaseURL.absoluteString,
97183
apiKey: "test.api.key",
98184
session: sessionMock
99185
)
100186
}
187+
188+
private func uploadFileURL(_ fileName: String) -> URL {
189+
URL(fileURLWithPath: #file)
190+
.deletingLastPathComponent()
191+
.appendingPathComponent(fileName)
192+
}
101193
}

Tests/StorageTests/sadcat.jpg

28.8 KB
Loading

0 commit comments

Comments
 (0)