Skip to content

Commit

Permalink
Firebase Storage Configuration Component (#17)
Browse files Browse the repository at this point in the history
<!--

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

-->

# *Firebase Storage Configuration Component*

## ♻️ Current situation & Problem
At the moment, `SpeziFirebase` offers no Configuration component for
configuring the Firebase Storage functionality. This results in the need
for users to configure the Firebase Storage themselves, independent of
the Spezi ecosystem (which can potentially be dangerous!).


## ⚙️ Release Notes 
- `SpeziFirebase` now offers the `FirebaseStorageConfiguration`
component to configure the Firebase Storage.


## 📚 Documentation
Included


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [X] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
philippzagar authored Oct 16, 2023
1 parent 7e31453 commit 56dd7fb
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Spezi Firebase contributors
====================

* [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
* [Philipp Zagar](https://github.com/philippzagar)
11 changes: 10 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")),
Expand Down Expand Up @@ -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: [
Expand Down
51 changes: 51 additions & 0 deletions Sources/SpeziFirebaseStorage/FirebaseStorageConfiguration.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
2 changes: 2 additions & 0 deletions Tests/UITests/TestApp/Shared/TestAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Spezi
import SpeziAccount
import SpeziFirebaseAccount
import SpeziFirebaseStorage
import SpeziFirestore
import SwiftUI

Expand All @@ -23,6 +24,7 @@ class TestAppDelegate: SpeziAppDelegate {
])
Firestore(settings: .emulator)
FirebaseAccountConfiguration(emulatorSettings: (host: "localhost", port: 9099))
FirebaseStorageConfiguration(emulatorSettings: (host: "localhost", port: 9199))
}
}
}
3 changes: 3 additions & 0 deletions Tests/UITests/TestApp/TestApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct UITestsApp: App {
enum Tests: String, CaseIterable, Identifiable {
case firebaseAccount = "FirebaseAccount"
case firestoreDataStorage = "FirestoreDataStorage"
case firebaseStorage = "FirebaseStorage"


var id: RawValue {
Expand All @@ -29,6 +30,8 @@ struct UITestsApp: App {
FirebaseAccountTestsView()
case .firestoreDataStorage:
FirestoreDataStorageTestsView()
case .firebaseStorage:
FirebaseStorageTestsView()
}
}
}
Expand Down
104 changes: 104 additions & 0 deletions Tests/UITests/TestAppUITests/FirebaseStorageTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
27 changes: 27 additions & 0 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down Expand Up @@ -52,6 +56,9 @@
2FB926E42974B0FC008E7B03 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
2FC42FD7290ADD5E00B08F18 /* SpeziFirebase */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = SpeziFirebase; path = ../..; sourceTree = "<group>"; };
2FE62C3C2966074F00FCBE7F /* FirestoreDataStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirestoreDataStorageTests.swift; sourceTree = "<group>"; };
97359F632ADB27500080CB11 /* FirebaseStorageTestsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirebaseStorageTestsView.swift; sourceTree = "<group>"; };
97359F652ADB286D0080CB11 /* StorageMetadata+Sendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageMetadata+Sendable.swift"; sourceTree = "<group>"; };
978E198D2ADB40A300732324 /* FirebaseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseStorageTests.swift; sourceTree = "<group>"; };
A95D60CF2AA35E2200EB5968 /* FirebaseClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseClient.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand All @@ -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 */,
);
Expand Down Expand Up @@ -115,6 +123,7 @@
2FA7382B290ADFAA007ACEB9 /* TestApp.swift */,
2F148BFE298BB14100031B7F /* FirebaseAccountTests */,
2FE62C3B2966071400FCBE7F /* FirestoreDataStorageTests */,
97359F622ADB27430080CB11 /* FirebaseStorageTests */,
2F9F07ED29090AF500CDC598 /* Shared */,
2F6D139928F5F386007C25D6 /* Assets.xcassets */,
2F01E8CE291493560089C46B /* Info.plist */,
Expand All @@ -130,6 +139,7 @@
2F8CE160298C2C6D003799A8 /* FirebaseAccountTests.swift */,
2F2E4B8329749C5900FF710F /* FirestoreDataStorageTests.swift */,
A95D60CF2AA35E2200EB5968 /* FirebaseClient.swift */,
978E198D2ADB40A300732324 /* FirebaseStorageTests.swift */,
);
path = TestAppUITests;
sourceTree = "<group>";
Expand Down Expand Up @@ -158,6 +168,15 @@
path = FirestoreDataStorageTests;
sourceTree = "<group>";
};
97359F622ADB27430080CB11 /* FirebaseStorageTests */ = {
isa = PBXGroup;
children = (
97359F632ADB27500080CB11 /* FirebaseStorageTestsView.swift */,
97359F652ADB286D0080CB11 /* StorageMetadata+Sendable.swift */,
);
path = FirebaseStorageTests;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand All @@ -181,6 +200,7 @@
2FB0759C299DF96E00C0B37F /* Spezi */,
2F2E8EFB29E7366200D439B7 /* SpeziAccount */,
2F2E8EFE29E7369B00D439B7 /* SpeziViews */,
978DFE912ADB1E1600E2B9B5 /* SpeziFirebaseStorage */,
);
productName = Example;
productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */;
Expand Down Expand Up @@ -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;
};
Expand All @@ -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;
Expand Down Expand Up @@ -632,6 +655,10 @@
package = 2FB0758B299DF8F100C0B37F /* XCRemoteSwiftPackageReference "Spezi" */;
productName = Spezi;
};
978DFE912ADB1E1600E2B9B5 /* SpeziFirebaseStorage */ = {
isa = XCSwiftPackageProductDependency;
productName = SpeziFirebaseStorage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 2F6D138A28F5F384007C25D6 /* Project object */;
Expand Down
Loading

0 comments on commit 56dd7fb

Please sign in to comment.