diff --git a/.env.example b/.env.example index 13a1fa5e..d95f4f19 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,4 @@ SUPABASE_URL=https://mysupabasereference.supabase.co SUPABASE_ANON_KEY=my.supabase.anon.key +SUPABASE_SERVICE_ROLE_KEY=my.supabase.service.role.key \ No newline at end of file diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a8ee1893..618bb356 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -26,4 +26,4 @@ jobs: env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} - SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }} + SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} diff --git a/Makefile b/Makefile index ebeb0613..2f0c535e 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ define SECRETS enum DotEnv { static let SUPABASE_URL = "$(SUPABASE_URL)" static let SUPABASE_ANON_KEY = "$(SUPABASE_ANON_KEY)" + static let SUPABASE_SERVICE_ROLE_KEY = "$(SUPABASE_SERVICE_ROLE_KEY)" } endef diff --git a/Package.swift b/Package.swift index 2832c6f5..c9e8ca6d 100644 --- a/Package.swift +++ b/Package.swift @@ -84,6 +84,7 @@ let package = Package( "TestHelpers", "PostgREST", "Realtime", + "Storage", ] ), .target( diff --git a/Sources/PostgREST/Types.swift b/Sources/PostgREST/Types.swift index 818f9f72..b2c37888 100644 --- a/Sources/PostgREST/Types.swift +++ b/Sources/PostgREST/Types.swift @@ -32,7 +32,7 @@ public struct PostgrestResponse: Sendable { self.count = count self.value = value } - + /// Returns the response converting the returned Data into Unicode characters using a given encoding. public func string(encoding: String.Encoding = .utf8) -> String? { String(data: data, encoding: encoding) diff --git a/Sources/Storage/BucketOptions.swift b/Sources/Storage/BucketOptions.swift index aac5cc52..e11e59ef 100644 --- a/Sources/Storage/BucketOptions.swift +++ b/Sources/Storage/BucketOptions.swift @@ -2,10 +2,10 @@ import Foundation public struct BucketOptions: Sendable { public let `public`: Bool - public let fileSizeLimit: Int? + public let fileSizeLimit: String? public let allowedMimeTypes: [String]? - public init(public: Bool = false, fileSizeLimit: Int? = nil, allowedMimeTypes: [String]? = nil) { + public init(public: Bool = false, fileSizeLimit: String? = nil, allowedMimeTypes: [String]? = nil) { self.public = `public` self.fileSizeLimit = fileSizeLimit self.allowedMimeTypes = allowedMimeTypes diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift index c4ffccc6..d5ec8d5d 100644 --- a/Sources/Storage/Deprecated.swift +++ b/Sources/Storage/Deprecated.swift @@ -30,3 +30,39 @@ extension StorageClientConfiguration { ) } } + +extension StorageFileApi { + @_disfavoredOverload + @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") + @discardableResult + public func upload( + path: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> String { + try await upload(path: path, file: file, options: options).fullPath + } + + @_disfavoredOverload + @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") + @discardableResult + public func update( + path: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> String { + try await update(path: path, file: file, options: options).fullPath + } + + @_disfavoredOverload + @available(*, deprecated, message: "Please use method that returns FileUploadResponse.") + @discardableResult + public func uploadToSignedURL( + path: String, + token: String, + file: Data, + options: FileOptions = FileOptions() + ) async throws -> String { + try await uploadToSignedURL(path: path, token: token, file: file, options: options).fullPath + } +} diff --git a/Sources/Storage/StorageBucketApi.swift b/Sources/Storage/StorageBucketApi.swift index 9ae3ae21..c99f5141 100644 --- a/Sources/Storage/StorageBucketApi.swift +++ b/Sources/Storage/StorageBucketApi.swift @@ -35,7 +35,7 @@ public class StorageBucketApi: StorageApi { var id: String var name: String var `public`: Bool - var fileSizeLimit: Int? + var fileSizeLimit: String? var allowedMimeTypes: [String]? } diff --git a/Sources/Storage/StorageFileApi.swift b/Sources/Storage/StorageFileApi.swift index 97f31276..81a94d0e 100644 --- a/Sources/Storage/StorageFileApi.swift +++ b/Sources/Storage/StorageFileApi.swift @@ -24,10 +24,6 @@ public class StorageFileApi: StorageApi { super.init(configuration: configuration) } - private struct UploadResponse: Decodable { - let Key: String - } - private struct MoveResponse: Decodable { let message: String } @@ -41,7 +37,7 @@ public class StorageFileApi: StorageApi { path: String, file: Data, options: FileOptions - ) async throws -> String { + ) async throws -> FileUploadResponse { let contentType = options.contentType var headers = HTTPHeaders([ "x-upsert": "\(options.upsert)", @@ -56,7 +52,12 @@ public class StorageFileApi: StorageApi { file: File(name: fileName, data: file, fileName: fileName, contentType: contentType) ) - return try await execute( + struct UploadResponse: Decodable { + let Key: String + let Id: String + } + + let response = try await execute( HTTPRequest( url: configuration.url.appendingPathComponent("object/\(bucketId)/\(path)"), method: method, @@ -66,7 +67,13 @@ public class StorageFileApi: StorageApi { headers: headers ) ) - .decoded(as: UploadResponse.self, decoder: configuration.decoder).Key + .decoded(as: UploadResponse.self, decoder: configuration.decoder) + + return FileUploadResponse( + id: response.Id, + path: path, + fullPath: response.Key + ) } /// Uploads a file to an existing bucket. @@ -80,7 +87,7 @@ public class StorageFileApi: StorageApi { path: String, file: Data, options: FileOptions = FileOptions() - ) async throws -> String { + ) async throws -> FileUploadResponse { try await uploadOrUpdate(method: .post, path: path, file: file, options: options) } @@ -95,16 +102,20 @@ public class StorageFileApi: StorageApi { path: String, file: Data, options: FileOptions = FileOptions() - ) async throws -> String { + ) async throws -> FileUploadResponse { try await uploadOrUpdate(method: .put, path: path, file: file, options: options) } /// Moves an existing file, optionally renaming it at the same time. /// - Parameters: - /// - from: The original file path, including the current file name. For example - /// `folder/image.png`. - /// - to: The new file path, including the new file name. For example `folder/image-copy.png`. - public func move(from source: String, to destination: String) async throws { + /// - source: The original file path, including the current file name. For example `folder/image.png`. + /// - destination: The new file path, including the new file name. For example `folder/image-copy.png`. + /// - options: The destination options. + public func move( + from source: String, + to destination: String, + options: DestinationOptions? = nil + ) async throws { try await execute( HTTPRequest( url: configuration.url.appendingPathComponent("object/move"), @@ -114,6 +125,7 @@ public class StorageFileApi: StorageApi { "bucketId": bucketId, "sourceKey": source, "destinationKey": destination, + "destinationBucket": options?.destinationBucket ] ) ) @@ -122,12 +134,20 @@ public class StorageFileApi: StorageApi { /// Copies an existing file to a new path in the same bucket. /// - Parameters: - /// - from: The original file path, including the current file name. For example - /// `folder/image.png`. - /// - to: The new file path, including the new file name. For example `folder/image-copy.png`. + /// - source: The original file path, including the current file name. For example `folder/image.png`. + /// - destination: The new file path, including the new file name. For example `folder/image-copy.png`. + /// - options: The destination options. @discardableResult - public func copy(from source: String, to destination: String) async throws -> String { - try await execute( + public func copy( + from source: String, + to destination: String, + options: DestinationOptions? = nil + ) async throws -> String { + struct UploadResponse: Decodable { + let Key: String + } + + return try await execute( HTTPRequest( url: configuration.url.appendingPathComponent("object/copy"), method: .post, @@ -136,6 +156,7 @@ public class StorageFileApi: StorageApi { "bucketId": bucketId, "sourceKey": source, "destinationKey": destination, + "destinationBucket": options?.destinationBucket ] ) ) @@ -393,15 +414,24 @@ public class StorageFileApi: StorageApi { /// /// - Note: Signed upload URLs can be used to upload files to the bucket without further /// authentication. They are valid for 2 hours. - public func createSignedUploadURL(path: String) async throws -> SignedUploadURL { + public func createSignedUploadURL( + path: String, + options: CreateSignedUploadURLOptions? = nil + ) async throws -> SignedUploadURL { struct Response: Decodable { let url: URL } + var headers = HTTPHeaders() + if let upsert = options?.upsert, upsert { + headers["x-upsert"] = "true" + } + let response = try await execute( HTTPRequest( url: configuration.url.appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), - method: .post + method: .post, + headers: headers ) ) .decoded(as: Response.self, decoder: configuration.decoder) @@ -441,7 +471,7 @@ public class StorageFileApi: StorageApi { token: String, file: Data, options: FileOptions = FileOptions() - ) async throws -> String { + ) async throws -> SignedURLUploadResponse { let contentType = options.contentType var headers = HTTPHeaders([ "x-upsert": "\(options.upsert)", @@ -458,7 +488,11 @@ public class StorageFileApi: StorageApi { contentType: contentType )) - return try await execute( + struct UploadResponse: Decodable { + let Key: String + } + + let fullPath = try await execute( HTTPRequest( url: configuration.url .appendingPathComponent("object/upload/sign/\(bucketId)/\(path)"), @@ -471,6 +505,8 @@ public class StorageFileApi: StorageApi { ) .decoded(as: UploadResponse.self, decoder: configuration.decoder) .Key + + return SignedURLUploadResponse(path: path, fullPath: fullPath) } } diff --git a/Sources/Storage/TransformOptions.swift b/Sources/Storage/TransformOptions.swift index f5550b42..ff42d5dd 100644 --- a/Sources/Storage/TransformOptions.swift +++ b/Sources/Storage/TransformOptions.swift @@ -10,9 +10,9 @@ public struct TransformOptions: Encodable, Sendable { public init( width: Int? = nil, height: Int? = nil, - resize: String? = "cover", + resize: String? = nil, quality: Int? = 80, - format: String? = "origin" + format: String? = nil ) { self.width = width self.height = height diff --git a/Sources/Storage/Types.swift b/Sources/Storage/Types.swift index efc564ed..0e887210 100644 --- a/Sources/Storage/Types.swift +++ b/Sources/Storage/Types.swift @@ -92,3 +92,30 @@ public struct SignedUploadURL: Sendable { public let path: String public let token: String } + +public struct FileUploadResponse: Sendable { + public let id: String + public let path: String + public let fullPath: String +} + +public struct SignedURLUploadResponse: Sendable { + public let path: String + public let fullPath: String +} + +public struct CreateSignedUploadURLOptions: Sendable { + public var upsert: Bool + + public init(upsert: Bool) { + self.upsert = upsert + } +} + +public struct DestinationOptions: Sendable { + public var destinationBucket: String? + + public init(destinationBucket: String? = nil) { + self.destinationBucket = destinationBucket + } +} diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index 67bdbab9..fc060548 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -62,7 +62,7 @@ final class FunctionsClientTests: XCTestCase { let http = HTTPClientMock().any { _ in try .stub(body: Empty()) } let sut = FunctionsClient( - url: self.url, + url: url, headers: ["Apikey": apiKey], region: nil, http: http diff --git a/Tests/IntegrationTests/Fixtures/Upload/file-2.txt b/Tests/IntegrationTests/Fixtures/Upload/file-2.txt new file mode 100644 index 00000000..55ed72d4 --- /dev/null +++ b/Tests/IntegrationTests/Fixtures/Upload/file-2.txt @@ -0,0 +1 @@ +supabase txt file 2 diff --git a/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg b/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg new file mode 100644 index 00000000..859aa4c3 Binary files /dev/null and b/Tests/IntegrationTests/Fixtures/Upload/sadcat.jpg differ diff --git a/Tests/IntegrationTests/StorageClientIntegrationTests.swift b/Tests/IntegrationTests/StorageClientIntegrationTests.swift new file mode 100644 index 00000000..2d073eab --- /dev/null +++ b/Tests/IntegrationTests/StorageClientIntegrationTests.swift @@ -0,0 +1,68 @@ +// +// StorageClientIntegrationTests.swift +// +// +// Created by Guilherme Souza on 07/05/24. +// + +import InlineSnapshotTesting +import Storage +import XCTest + +final class StorageClientIntegrationTests: XCTestCase { + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!, + headers: [ + "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)", + ], + logger: nil + ) + ) + + func testBucket_CRUD() async throws { + let bucketName = "test-bucket" + + var buckets = try await storage.listBuckets() + XCTAssertFalse(buckets.contains(where: { $0.name == bucketName })) + + try await storage.createBucket(bucketName, options: .init(public: true)) + + var bucket = try await storage.getBucket(bucketName) + XCTAssertEqual(bucket.name, bucketName) + XCTAssertEqual(bucket.id, bucketName) + XCTAssertEqual(bucket.isPublic, true) + + buckets = try await storage.listBuckets() + XCTAssertTrue(buckets.contains { $0.id == bucket.id }) + + try await storage.updateBucket(bucketName, options: BucketOptions(allowedMimeTypes: ["image/jpeg"])) + + bucket = try await storage.getBucket(bucketName) + XCTAssertEqual(bucket.allowedMimeTypes, ["image/jpeg"]) + + try await storage.deleteBucket(bucketName) + + buckets = try await storage.listBuckets() + XCTAssertFalse(buckets.contains { $0.id == bucket.id }) + } + + func testGetBucketWithWrongId() async { + do { + _ = try await storage.getBucket("not-exist-id") + XCTFail("Unexpected success") + } catch { + assertInlineSnapshot(of: error, as: .dump) { + """ + ▿ StorageError + ▿ error: Optional + - some: "Bucket not found" + - message: "Bucket not found" + ▿ statusCode: Optional + - some: "404" + + """ + } + } + } +} diff --git a/Tests/IntegrationTests/StorageFileIntegrationTests.swift b/Tests/IntegrationTests/StorageFileIntegrationTests.swift new file mode 100644 index 00000000..735de890 --- /dev/null +++ b/Tests/IntegrationTests/StorageFileIntegrationTests.swift @@ -0,0 +1,344 @@ +// +// StorageFileIntegrationTests.swift +// +// +// Created by Guilherme Souza on 07/05/24. +// + +import InlineSnapshotTesting +import Storage +import XCTest + +final class StorageFileIntegrationTests: XCTestCase { + let storage = SupabaseStorageClient( + configuration: StorageClientConfiguration( + url: URL(string: "\(DotEnv.SUPABASE_URL)/storage/v1")!, + headers: [ + "Authorization": "Bearer \(DotEnv.SUPABASE_SERVICE_ROLE_KEY)", + ], + logger: nil + ) + ) + + var bucketName = "" + var file = Data() + var uploadPath = "" + + override func setUp() async throws { + try await super.setUp() + + bucketName = try await newBucket() + file = try Data(contentsOf: uploadFileURL("sadcat.jpg")) + uploadPath = "testpath/file-\(UUID().uuidString).jpg" + } + + override func tearDown() async throws { + try? await storage.emptyBucket(bucketName) + try? await storage.deleteBucket(bucketName) + + try await super.tearDown() + } + + func testGetPublicURL() throws { + let publicURL = try storage.from(bucketName).getPublicURL(path: uploadPath) + XCTAssertEqual( + publicURL.absoluteString, + "\(DotEnv.SUPABASE_URL)/storage/v1/object/public/\(bucketName)/\(uploadPath)" + ) + } + + func testGetPublicURLWithDownloadQueryString() throws { + let publicURL = try storage.from(bucketName).getPublicURL(path: uploadPath, download: true) + XCTAssertEqual( + publicURL.absoluteString, + "\(DotEnv.SUPABASE_URL)/storage/v1/object/public/\(bucketName)/\(uploadPath)?download=" + ) + } + + func testGetPublicURLWithCustomDownload() throws { + let publicURL = try storage.from(bucketName).getPublicURL(path: uploadPath, download: "test.jpg") + XCTAssertEqual( + publicURL.absoluteString, + "\(DotEnv.SUPABASE_URL)/storage/v1/object/public/\(bucketName)/\(uploadPath)?download=test.jpg" + ) + } + + func testSignURL() async throws { + _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000) + XCTAssertTrue( + url.absoluteString.contains("\(DotEnv.SUPABASE_URL)/storage/v1/object/sign/\(bucketName)/\(uploadPath)") + ) + } + + func testSignURL_withDownloadQueryString() async throws { + _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000, download: true) + XCTAssertTrue( + url.absoluteString.contains("\(DotEnv.SUPABASE_URL)/storage/v1/object/sign/\(bucketName)/\(uploadPath)") + ) + XCTAssertTrue(url.absoluteString.contains("&download=")) + } + + func testSignURL_withCustomFilenameForDownload() async throws { + _ = try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let url = try await storage.from(bucketName).createSignedURL(path: uploadPath, expiresIn: 2000, download: "test.jpg") + XCTAssertTrue( + url.absoluteString.contains("\(DotEnv.SUPABASE_URL)/storage/v1/object/sign/\(bucketName)/\(uploadPath)") + ) + XCTAssertTrue(url.absoluteString.contains("&download=test.jpg")) + } + + func testUploadAndUpdateFile() async throws { + let file2 = try Data(contentsOf: uploadFileURL("file-2.txt")) + + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let res = try await storage.from(bucketName).update(path: uploadPath, file: file2) + XCTAssertEqual(res.path, uploadPath) + } + + func testUploadFileWithinFileSizeLimit() async throws { + bucketName = try await newBucket( + prefix: "with-limit", + options: BucketOptions(public: true, fileSizeLimit: "1mb") + ) + + try await storage.from(bucketName).upload(path: uploadPath, file: file) + } + + func testUploadFileThatExceedFileSizeLimit() async throws { + bucketName = try await newBucket( + prefix: "with-limit", + options: BucketOptions(public: true, fileSizeLimit: "1kb") + ) + + do { + try await storage.from(bucketName).upload(path: uploadPath, file: file) + XCTFail("Unexpected success") + } catch { + assertInlineSnapshot(of: error, as: .dump) { + """ + ▿ StorageError + ▿ error: Optional + - some: "Payload too large" + - message: "The object exceeded the maximum allowed size" + ▿ statusCode: Optional + - some: "413" + + """ + } + } + } + + func testUploadFileWithValidMimeType() async throws { + bucketName = try await newBucket( + prefix: "with-mimetype", + options: BucketOptions(public: true, allowedMimeTypes: ["image/jpeg"]) + ) + + try await storage.from(bucketName).upload( + path: uploadPath, + file: file, + options: FileOptions( + contentType: "image/jpeg" + ) + ) + } + + func testUploadFileWithInvalidMimeType() async throws { + bucketName = try await newBucket( + prefix: "with-mimetype", + options: BucketOptions(public: true, allowedMimeTypes: ["image/png"]) + ) + + do { + try await storage.from(bucketName).upload( + path: uploadPath, + file: file, + options: FileOptions( + contentType: "image/jpeg" + ) + ) + XCTFail("Unexpected success") + } catch { + assertInlineSnapshot(of: error, as: .dump) { + """ + ▿ StorageError + ▿ error: Optional + - some: "invalid_mime_type" + - message: "mime type image/jpeg is not supported" + ▿ statusCode: Optional + - some: "415" + + """ + } + } + } + + func testSignedURLForUpload() async throws { + let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath) + XCTAssertEqual(res.path, uploadPath) + XCTAssertTrue( + res.signedURL.absoluteString.contains( + "\(DotEnv.SUPABASE_URL)/storage/v1/object/upload/sign/\(bucketName)/\(uploadPath)" + ) + ) + } + + func testCanUploadWithSignedURLForUpload() async throws { + let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath) + + let uploadRes = try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + XCTAssertEqual(uploadRes.path, uploadPath) + } + + func testCanUploadOverwritingFilesWithSignedURL() async throws { + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath, options: CreateSignedUploadURLOptions(upsert: true)) + let uploadRes = try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + XCTAssertEqual(uploadRes.path, uploadPath) + } + + func testCannotUploadToSignedURLTwice() async throws { + let res = try await storage.from(bucketName).createSignedUploadURL(path: uploadPath) + + try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + + do { + try await storage.from(bucketName).uploadToSignedURL(path: res.path, token: res.token, file: file) + XCTFail("Unexpected success") + } catch { + assertInlineSnapshot(of: error, as: .dump) { + """ + ▿ StorageError + ▿ error: Optional + - some: "Duplicate" + - message: "The resource already exists" + ▿ statusCode: Optional + - some: "409" + + """ + } + } + } + + func testListObjects() async throws { + try await storage.from(bucketName).upload(path: uploadPath, file: file) + let res = try await storage.from(bucketName).list(path: "testpath") + + XCTAssertEqual(res.count, 1) + XCTAssertEqual(res[0].name, uploadPath.replacingOccurrences(of: "testpath/", with: "")) + } + + func testMoveObjectToDifferentPath() async throws { + let newPath = "testpath/file-moved-\(UUID().uuidString).txt" + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + try await storage.from(bucketName).move(from: uploadPath, to: newPath) + } + + func testMoveObjectsAcrossBucketsInDifferentPath() async throws { + let newBucketName = "bucket-move" + try await findOrCreateBucket(name: newBucketName) + + let newPath = "testpath/file-to-move-\(UUID().uuidString).txt" + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + try await storage.from(bucketName).move( + from: uploadPath, + to: newPath, + options: DestinationOptions(destinationBucket: newBucketName) + ) + + _ = try await storage.from(newBucketName).download(path: newPath) + } + + func testCopyObjectToDifferentPath() async throws { + let newPath = "testpath/file-moved-\(UUID().uuidString).txt" + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + try await storage.from(bucketName).copy(from: uploadPath, to: newPath) + } + + func testCopyObjectsAcrossBucketsInDifferentPath() async throws { + let newBucketName = "bucket-copy" + try await findOrCreateBucket(name: newBucketName) + + let newPath = "testpath/file-to-copy-\(UUID().uuidString).txt" + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + try await storage.from(bucketName).copy( + from: uploadPath, + to: newPath, + options: DestinationOptions(destinationBucket: newBucketName) + ) + + _ = try await storage.from(newBucketName).download(path: newPath) + } + + func testDownloadsAnObject() async throws { + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let res = try await storage.from(bucketName).download(path: uploadPath) + XCTAssertGreaterThan(res.count, 0) + } + + func testRemovesAnObject() async throws { + try await storage.from(bucketName).upload(path: uploadPath, file: file) + + let res = try await storage.from(bucketName).remove(paths: [uploadPath]) + XCTAssertEqual(res.count, 1) + XCTAssertEqual(res[0].bucketId, bucketName) + XCTAssertEqual(res[0].name, uploadPath) + } + + func testGetPublishURLWithTransformationOptions() throws { + let res = try storage.from(bucketName).getPublicURL( + path: uploadPath, + options: TransformOptions( + width: 700, + height: 300, + quality: 70 + ) + ) + + XCTAssertEqual( + res.absoluteString, + "\(DotEnv.SUPABASE_URL)/storage/v1/render/image/public/\(bucketName)/\(uploadPath)?width=700&height=300&quality=70" + ) + } + + private func newBucket( + prefix: String = "", + options: BucketOptions = BucketOptions(public: true) + ) async throws -> String { + let bucketName = "\(!prefix.isEmpty ? prefix + "-" : "")bucket-\(UUID().uuidString)" + return try await findOrCreateBucket(name: bucketName, options: options) + } + + @discardableResult + private func findOrCreateBucket( + name: String, + options: BucketOptions = BucketOptions(public: true) + ) async throws -> String { + do { + _ = try await storage.getBucket(name) + } catch { + try await storage.createBucket(name, options: options) + } + + return name + } + + private func uploadFileURL(_ fileName: String) -> URL { + URL(fileURLWithPath: #file) + .deletingLastPathComponent() + .appendingPathComponent("Fixtures/Upload") + .appendingPathComponent(fileName) + } +} diff --git a/Tests/StorageTests/StorageClientIntegrationTests.swift b/Tests/StorageTests/StorageClientIntegrationTests.swift deleted file mode 100644 index 3f120190..00000000 --- a/Tests/StorageTests/StorageClientIntegrationTests.swift +++ /dev/null @@ -1,168 +0,0 @@ -// -// StorageClientIntegrationTests.swift -// -// -// Created by Guilherme Souza on 04/11/23. -// - -@testable import Storage -import XCTest - -final class StorageClientIntegrationTests: XCTestCase { - static var apiKey: String { - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU" - } - - static var supabaseURL: String { - "http://localhost:54321/storage/v1" - } - - let bucketId = "tests" - - let storage = SupabaseStorageClient.test(supabaseURL: supabaseURL, apiKey: apiKey) - - let uploadData = try? Data( - contentsOf: URL( - string: "https://raw.githubusercontent.com/supabase-community/storage-swift/main/README.md" - )! - ) - - override func setUp() async throws { - try await super.setUp() - - try XCTSkipUnless( - ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil, - "INTEGRATION_TESTS not defined." - ) - - try? await storage.emptyBucket(bucketId) - try? await storage.deleteBucket(bucketId) - - try await storage.createBucket(bucketId, options: BucketOptions(public: true)) - } - - func testUpdateBucket() async throws { - var bucket = try await storage.getBucket(bucketId) - XCTAssertTrue(bucket.isPublic) - - try await storage.updateBucket(bucketId, options: BucketOptions(public: false)) - bucket = try await storage.getBucket(bucketId) - XCTAssertFalse(bucket.isPublic) - } - - func testListBuckets() async throws { - let buckets = try await storage.listBuckets() - XCTAssertTrue(buckets.contains { $0.id == bucketId }) - } - - func testFileIntegration() async throws { - var files = try await storage.from(bucketId).list() - XCTAssertTrue(files.isEmpty) - - try await uploadTestData() - - files = try await storage.from(bucketId).list() - XCTAssertEqual(files.map(\.name), ["README.md"]) - - let downloadedData = try await storage.from(bucketId).download(path: "README.md") - XCTAssertEqual(downloadedData, uploadData) - - try await storage.from(bucketId).move(from: "README.md", to: "README_2.md") - - var searchedFiles = try await storage.from(bucketId) - .list(options: .init(search: "README.md")) - XCTAssertTrue(searchedFiles.isEmpty) - - try await storage.from(bucketId).copy(from: "README_2.md", to: "README.md") - searchedFiles = try await storage.from(bucketId).list(options: .init(search: "README.md")) - XCTAssertEqual(searchedFiles.map(\.name), ["README.md"]) - - let removedFiles = try await storage.from(bucketId).remove(paths: ["README_2.md"]) - XCTAssertEqual(removedFiles.map(\.name), ["README_2.md"]) - } - - func testGetPublicURL() async throws { - try await uploadTestData() - - let path = "README.md" - - let baseUrl = try storage.from(bucketId).getPublicURL(path: path) - XCTAssertEqual(baseUrl.absoluteString, "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)") - - let baseUrlWithDownload = try storage.from(bucketId).getPublicURL( - path: path, - download: true - ) - XCTAssertEqual( - baseUrlWithDownload.absoluteString, - "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=" - ) - - let baseUrlWithDownloadAndFileName = try storage.from(bucketId).getPublicURL( - path: path, download: "test" - ) - XCTAssertEqual( - baseUrlWithDownloadAndFileName.absoluteString, - "\(Self.supabaseURL)/object/public/\(bucketId)/\(path)?download=test" - ) - - let baseUrlWithAllOptions = try storage.from(bucketId).getPublicURL( - path: path, download: "test", - options: TransformOptions(width: 300, height: 300) - ) - XCTAssertEqual( - baseUrlWithAllOptions.absoluteString, - "\(Self.supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" - ) - } - - func testCreateSignedURL() async throws { - try await uploadTestData() - - let path = "README.md" - - let url = try await storage.from(bucketId).createSignedURL( - path: path, - expiresIn: 60, - download: "README_local.md" - ) - let components = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true)) - - let downloadQuery = components.queryItems?.first(where: { $0.name == "download" }) - XCTAssertEqual(downloadQuery?.value, "README_local.md") - XCTAssertEqual(components.path, "/storage/v1/object/sign/\(bucketId)/\(path)") - } - - func testUpdate() async throws { - try await uploadTestData() - - let dataToUpdate = try? Data( - contentsOf: URL( - string: "https://raw.githubusercontent.com/supabase-community/supabase-swift/master/README.md" - )! - ) - - try await storage.from(bucketId).update( - path: "README.md", - file: dataToUpdate ?? Data() - ) - } - - func testCreateAndUploadToSignedUploadURL() async throws { - let path = "README-\(UUID().uuidString).md" - let url = try await storage.from(bucketId).createSignedUploadURL(path: path) - let key = try await storage.from(bucketId).uploadToSignedURL( - path: url.path, - token: url.token, - file: uploadData ?? Data() - ) - - XCTAssertEqual(key, "\(bucketId)/\(path)") - } - - private func uploadTestData() async throws { - _ = try await storage.from(bucketId).upload( - path: "README.md", file: uploadData ?? Data(), options: FileOptions(cacheControl: "3600") - ) - } -} diff --git a/Tests/StorageTests/SupabaseStorageTests.swift b/Tests/StorageTests/SupabaseStorageTests.swift index 22deab51..fb49d2aa 100644 --- a/Tests/StorageTests/SupabaseStorageTests.swift +++ b/Tests/StorageTests/SupabaseStorageTests.swift @@ -48,7 +48,7 @@ final class SupabaseStorageTests: XCTestCase { ) XCTAssertEqual( baseUrlWithAllOptions.absoluteString, - "\(supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&resize=cover&quality=80&format=origin" + "\(supabaseURL)/render/image/public/\(bucketId)/\(path)?download=test&width=300&height=300&quality=80" ) }