diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7cffe39..23df400 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -32,7 +32,7 @@ jobs: setupfirebaseemulator: true path: Tests/UITests customcommand: | - firebase emulators:exec 'set -o pipefail && xcodebuild test -project UITests.xcodeproj -scheme TestApp -destination "name=iPhone 14 Pro Max" -resultBundlePath UITests.xcresult -derivedDataPath ".derivedData" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcpretty' + firebase emulators:exec 'set -o pipefail && xcodebuild test -project UITests.xcodeproj -scheme TestApp -destination "platform=iOS Simulator,name=iPhone 15 Pro" -resultBundlePath UITests.xcresult -derivedDataPath ".derivedData" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO | xcpretty' uploadcoveragereport: name: Upload Coverage Report needs: [buildandtest, buildandtestuitests] diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 58bf870..9683c2a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,3 +12,4 @@ Spezi Firebase contributors ==================== * [Paul Schmiedmayer](https://github.com/PSchmiedmayer) +* [Philipp Zagar](https://github.com/philippzagar) \ No newline at end of file diff --git a/Package.swift b/Package.swift index 75482d0..d67fea0 100644 --- a/Package.swift +++ b/Package.swift @@ -20,7 +20,8 @@ let package = Package( products: [ .library(name: "SpeziFirebaseAccount", targets: ["SpeziFirebaseAccount"]), .library(name: "SpeziFirebaseConfiguration", targets: ["SpeziFirebaseConfiguration"]), - .library(name: "SpeziFirestore", targets: ["SpeziFirestore"]) + .library(name: "SpeziFirestore", targets: ["SpeziFirestore"]), + .library(name: "SpeziFirebaseStorage", targets: ["SpeziFirebaseStorage"]) ], dependencies: [ .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.0")), @@ -55,6 +56,14 @@ let package = Package( .product(name: "FirebaseFirestoreSwift", package: "firebase-ios-sdk") ] ), + .target( + name: "SpeziFirebaseStorage", + dependencies: [ + .target(name: "SpeziFirebaseConfiguration"), + .product(name: "Spezi", package: "Spezi"), + .product(name: "FirebaseStorage", package: "firebase-ios-sdk") + ] + ), .testTarget( name: "SpeziFirebaseTests", dependencies: [ diff --git a/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift b/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift new file mode 100644 index 0000000..419a654 --- /dev/null +++ b/Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift @@ -0,0 +1,51 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import FirebaseStorage +import Spezi +import SpeziFirebaseConfiguration + + +/// Configures the Firebase Storage that can then be used within any application via `Storage.storage()`. +/// +/// The ``FirebaseStorageConfiguration`` can be used to connect to the Firebase Storage emulator: +/// ``` +/// class ExampleAppDelegate: SpeziAppDelegate { +/// override var configuration: Configuration { +/// Configuration(standard: /* ... */) { +/// FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) +/// // ... +/// } +/// } +/// } +/// ``` +public final class FirebaseStorageConfiguration: Component, DefaultInitializable { + @Dependency private var configureFirebaseApp: ConfigureFirebaseApp + + private let emulatorSettings: (host: String, port: Int)? + + + public required convenience init() { + self.init(emulatorSettings: nil) + } + + /// - Parameters: + /// - emulatorSettings: The emulator settings. The default value is `nil`, connecting the FirebaseStorage module to the FirebaseStorage cloud instance. + public init( + emulatorSettings: (host: String, port: Int)? = nil + ) { + self.emulatorSettings = emulatorSettings + } + + + public func configure() { + if let emulatorSettings { + Storage.storage().useEmulator(withHost: emulatorSettings.host, port: emulatorSettings.port) + } + } +} diff --git a/Tests/UITests/TestApp/FirebaseStorageTests/FirebaseStorageTestsView.swift b/Tests/UITests/TestApp/FirebaseStorageTests/FirebaseStorageTestsView.swift new file mode 100644 index 0000000..429eed4 --- /dev/null +++ b/Tests/UITests/TestApp/FirebaseStorageTests/FirebaseStorageTestsView.swift @@ -0,0 +1,45 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import FirebaseStorage +import PDFKit +import SpeziViews +import SwiftUI + + +struct FirebaseStorageTestsView: View { + @State private var viewState: ViewState = .idle + + + var body: some View { + Button("Upload") { + uploadFile() + } + .buttonStyle(.borderedProminent) + .disabled(viewState == .processing) + .viewStateAlert(state: $viewState) + .navigationTitle("FirestoreDataStorage") + } + + + @MainActor + private func uploadFile() { + viewState = .processing + Task { + do { + let ref = Storage.storage().reference().child("test.txt") + let metadata = StorageMetadata() + metadata.contentType = "text/plain" + _ = try await ref.putDataAsync("Hello World!".data(using: .utf8) ?? .init(), metadata: metadata) + viewState = .idle + } catch { + viewState = .error(AnyLocalizedError(error: error)) + } + } + } +} diff --git a/Tests/UITests/TestApp/FirebaseStorageTests/StorageMetadata+Sendable.swift b/Tests/UITests/TestApp/FirebaseStorageTests/StorageMetadata+Sendable.swift new file mode 100644 index 0000000..f0a2a59 --- /dev/null +++ b/Tests/UITests/TestApp/FirebaseStorageTests/StorageMetadata+Sendable.swift @@ -0,0 +1,12 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import FirebaseStorage + + +extension StorageMetadata: @unchecked Sendable {} diff --git a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift index c3657e6..a1beb8e 100644 --- a/Tests/UITests/TestApp/Shared/TestAppDelegate.swift +++ b/Tests/UITests/TestApp/Shared/TestAppDelegate.swift @@ -9,6 +9,7 @@ import Spezi import SpeziAccount import SpeziFirebaseAccount +import SpeziFirebaseStorage import SpeziFirestore import SwiftUI @@ -23,6 +24,7 @@ class TestAppDelegate: SpeziAppDelegate { ]) Firestore(settings: .emulator) FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099)) + FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199)) } } } diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 7259d50..5a0da1e 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -14,6 +14,7 @@ struct UITestsApp: App { enum Tests: String, CaseIterable, Identifiable { case firebaseAccount = "FirebaseAccount" case firestoreDataStorage = "FirestoreDataStorage" + case firebaseStorage = "FirebaseStorage" var id: RawValue { @@ -29,6 +30,8 @@ struct UITestsApp: App { FirebaseAccountTestsView() case .firestoreDataStorage: FirestoreDataStorageTestsView() + case .firebaseStorage: + FirebaseStorageTestsView() } } } diff --git a/Tests/UITests/TestAppUITests/FirebaseStorageTests.swift b/Tests/UITests/TestAppUITests/FirebaseStorageTests.swift new file mode 100644 index 0000000..d9c9507 --- /dev/null +++ b/Tests/UITests/TestAppUITests/FirebaseStorageTests.swift @@ -0,0 +1,104 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest +import XCTestExtensions + + +/// The `FirebaseStorageTests` require the Firebase Storage Emulator to run at port 9199. +/// +/// Refer to https://firebase.google.com/docs/emulator-suite#storage about more information about the +/// Firebase Local Emulator Suite. +final class FirebaseStorageTests: XCTestCase { + struct FirebaseStorageItem: Decodable { + let name: String + let bucket: String + } + + + @MainActor + override func setUp() async throws { + try await super.setUp() + + try await deleteAllFiles() + try await Task.sleep(for: .seconds(0.5)) + } + + @MainActor + func testFirebaseStorageFileUpload() async throws { + let app = XCUIApplication() + app.launch() + XCTAssert(app.buttons["FirebaseStorage"].waitForExistence(timeout: 2.0)) + app.buttons["FirebaseStorage"].tap() + + var documents = try await getAllFiles() + XCTAssert(documents.isEmpty) + + XCTAssert(app.buttons["Upload"].waitForExistence(timeout: 2.0)) + app.buttons["Upload"].tap() + + try await Task.sleep(for: .seconds(2.0)) + documents = try await getAllFiles() + XCTAssertEqual(documents.count, 1) + } + + private func getAllFiles() async throws -> [FirebaseStorageItem] { + let documentsURL = try XCTUnwrap( + URL(string: "http://localhost:9199/v0/b/STORAGE_BUCKET/o") + ) + let (data, response) = try await URLSession.shared.data(from: documentsURL) + + guard let urlResponse = response as? HTTPURLResponse, + 200...299 ~= urlResponse.statusCode else { + print( + """ + The `FirebaseStorageTests` require the Firebase Storage Emulator to run at port 9199. + + Refer to https://firebase.google.com/docs/emulator-suite#storage about more information about the + Firebase Local Emulator Suite. + """ + ) + throw URLError(.fileDoesNotExist) + } + + struct ResponseWrapper: Decodable { + let items: [FirebaseStorageItem] + } + + do { + return try JSONDecoder().decode(ResponseWrapper.self, from: data).items + } catch { + return [] + } + } + + private func deleteAllFiles() async throws { + for storageItem in try await getAllFiles() { + let url = try XCTUnwrap( + URL(string: "http://localhost:9199/v0/b/STORAGE_BUCKET/o/\(storageItem.name)") + ) + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let urlResponse = response as? HTTPURLResponse, + 200...299 ~= urlResponse.statusCode else { + print( + """ + The `FirebaseStorageTests` require the Firebase Storage Emulator to run at port 9199. + + Refer to https://firebase.google.com/docs/emulator-suite#storage about more information about the + Firebase Local Emulator Suite. + """ + ) + throw URLError(.fileDoesNotExist) + } + } + } +} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 668c344..6a48002 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ 2FB07597299DF96E00C0B37F /* SpeziFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB07596299DF96E00C0B37F /* SpeziFirestore */; }; 2FB0759D299DF96E00C0B37F /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2FB0759C299DF96E00C0B37F /* Spezi */; }; 2FE62C3D2966074F00FCBE7F /* FirestoreDataStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE62C3C2966074F00FCBE7F /* FirestoreDataStorageTests.swift */; }; + 97359F642ADB27500080CB11 /* FirebaseStorageTestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97359F632ADB27500080CB11 /* FirebaseStorageTestsView.swift */; }; + 97359F662ADB286D0080CB11 /* StorageMetadata+Sendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97359F652ADB286D0080CB11 /* StorageMetadata+Sendable.swift */; }; + 978DFE922ADB1E1600E2B9B5 /* SpeziFirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 978DFE912ADB1E1600E2B9B5 /* SpeziFirebaseStorage */; }; + 978E198E2ADB40A300732324 /* FirebaseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 978E198D2ADB40A300732324 /* FirebaseStorageTests.swift */; }; A95D60D02AA35E2200EB5968 /* FirebaseClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A95D60CF2AA35E2200EB5968 /* FirebaseClient.swift */; }; /* End PBXBuildFile section */ @@ -52,6 +56,9 @@ 2FB926E42974B0FC008E7B03 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 2FC42FD7290ADD5E00B08F18 /* SpeziFirebase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziFirebase; path = ../..; sourceTree = ""; }; 2FE62C3C2966074F00FCBE7F /* FirestoreDataStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreDataStorageTests.swift; sourceTree = ""; }; + 97359F632ADB27500080CB11 /* FirebaseStorageTestsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirebaseStorageTestsView.swift; sourceTree = ""; }; + 97359F652ADB286D0080CB11 /* StorageMetadata+Sendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageMetadata+Sendable.swift"; sourceTree = ""; }; + 978E198D2ADB40A300732324 /* FirebaseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseStorageTests.swift; sourceTree = ""; }; A95D60CF2AA35E2200EB5968 /* FirebaseClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseClient.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -64,6 +71,7 @@ 2FB07597299DF96E00C0B37F /* SpeziFirestore in Frameworks */, 2F2E8EFF29E7369B00D439B7 /* SpeziViews in Frameworks */, 2F2E8EFC29E7366200D439B7 /* SpeziAccount in Frameworks */, + 978DFE922ADB1E1600E2B9B5 /* SpeziFirebaseStorage in Frameworks */, 2FB0759D299DF96E00C0B37F /* Spezi in Frameworks */, 2FB07593299DF96E00C0B37F /* SpeziFirebaseAccount in Frameworks */, ); @@ -115,6 +123,7 @@ 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2F148BFE298BB14100031B7F /* FirebaseAccountTests */, 2FE62C3B2966071400FCBE7F /* FirestoreDataStorageTests */, + 97359F622ADB27430080CB11 /* FirebaseStorageTests */, 2F9F07ED29090AF500CDC598 /* Shared */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, 2F01E8CE291493560089C46B /* Info.plist */, @@ -130,6 +139,7 @@ 2F8CE160298C2C6D003799A8 /* FirebaseAccountTests.swift */, 2F2E4B8329749C5900FF710F /* FirestoreDataStorageTests.swift */, A95D60CF2AA35E2200EB5968 /* FirebaseClient.swift */, + 978E198D2ADB40A300732324 /* FirebaseStorageTests.swift */, ); path = TestAppUITests; sourceTree = ""; @@ -158,6 +168,15 @@ path = FirestoreDataStorageTests; sourceTree = ""; }; + 97359F622ADB27430080CB11 /* FirebaseStorageTests */ = { + isa = PBXGroup; + children = ( + 97359F632ADB27500080CB11 /* FirebaseStorageTestsView.swift */, + 97359F652ADB286D0080CB11 /* StorageMetadata+Sendable.swift */, + ); + path = FirebaseStorageTests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -181,6 +200,7 @@ 2FB0759C299DF96E00C0B37F /* Spezi */, 2F2E8EFB29E7366200D439B7 /* SpeziAccount */, 2F2E8EFE29E7369B00D439B7 /* SpeziViews */, + 978DFE912ADB1E1600E2B9B5 /* SpeziFirebaseStorage */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -275,11 +295,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 97359F662ADB286D0080CB11 /* StorageMetadata+Sendable.swift in Sources */, 2F148C00298BB15900031B7F /* FirebaseAccountTestsView.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, 2FE62C3D2966074F00FCBE7F /* FirestoreDataStorageTests.swift in Sources */, 2F8A431729130BBC005D2B8F /* TestAppType.swift in Sources */, 2F9F07F129090B0500CDC598 /* TestAppDelegate.swift in Sources */, + 97359F642ADB27500080CB11 /* FirebaseStorageTestsView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -289,6 +311,7 @@ files = ( A95D60D02AA35E2200EB5968 /* FirebaseClient.swift in Sources */, 2F2E4B8429749C5900FF710F /* FirestoreDataStorageTests.swift in Sources */, + 978E198E2ADB40A300732324 /* FirebaseStorageTests.swift in Sources */, 2F8CE161298C2C6D003799A8 /* FirebaseAccountTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -632,6 +655,10 @@ package = 2FB0758B299DF8F100C0B37F /* XCRemoteSwiftPackageReference "Spezi" */; productName = Spezi; }; + 978DFE912ADB1E1600E2B9B5 /* SpeziFirebaseStorage */ = { + isa = XCSwiftPackageProductDependency; + productName = SpeziFirebaseStorage; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2F6D138A28F5F384007C25D6 /* Project object */; diff --git a/Tests/UITests/firebase.json b/Tests/UITests/firebase.json index 657a3fd..0c3efd5 100644 --- a/Tests/UITests/firebase.json +++ b/Tests/UITests/firebase.json @@ -1,4 +1,7 @@ { + "storage": { + "rules": "firebasestorage.rules" + }, "emulators": { "auth": { "port": 9099 @@ -6,10 +9,13 @@ "firestore": { "port": 8080 }, + "storage": { + "port": 9199 + }, "ui": { "enabled": true, "port": 4000 }, "singleProjectMode": true } -} +} \ No newline at end of file diff --git a/Tests/UITests/firebasestorage.rules b/Tests/UITests/firebasestorage.rules new file mode 100644 index 0000000..41df88a --- /dev/null +++ b/Tests/UITests/firebasestorage.rules @@ -0,0 +1,8 @@ +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if true; + } + } +} \ No newline at end of file diff --git a/Tests/UITests/firebasestorage.rules.license b/Tests/UITests/firebasestorage.rules.license new file mode 100644 index 0000000..6917c4b --- /dev/null +++ b/Tests/UITests/firebasestorage.rules.license @@ -0,0 +1,5 @@ +This source file is part of the Stanford Spezi open-source project + +SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) + +SPDX-License-Identifier: MIT