diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml new file mode 100644 index 0000000..1ffa391 --- /dev/null +++ b/.github/workflows/swift.yml @@ -0,0 +1,14 @@ +name: test + +on: + pull_request: { types: [opened, reopened, synchronize, ready_for_review] } + push: { branches: [ master ] } + +jobs: + build: + runs-on: ubuntu-latest + container: swift:5.9-jammy + steps: + - uses: actions/checkout@v4 + - name: Build + run: swift build -v diff --git a/README.md b/README.md index 255e3d5..12eb5eb 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,10 @@ dependencies: [ ## Setup +### Legacy setup + 1. Ferno uses an access token to read and write to your database. First we will need to get your service account information. + * Log into the Firebase console * Click the settings gear next to `Project Overview` * Select `Project settings` @@ -34,10 +37,23 @@ dependencies: [ 2. Register `Ferno` as a Provider and import `Ferno`. This is usually done in `configure.swift` +### FirebaseSDK setup + + 1. Log into the Firebase console + 2. Click the settings gear next to `Project Overview` + 3. Select `Project settings` + 4. Select the `SERVICE ACCOUNTS` tab + 5. Select `Firebase Admin SDK` option + 6. Click the button `Generate new private key` to download the json file. + +### Ferno setup + +You can use the content of json file information to fill out the `FernoDefaultConfiguration` or use the json file to preper the `FernoServiceJsonConfiguration`. + ```swift import Ferno -let fernoConfiguration = FernoConfiguration( +let fernoConfiguration = FernoDefaultConfiguration( basePath: "database-url", email: "service-account-email", privateKey: "private-key" @@ -45,13 +61,45 @@ let fernoConfiguration = FernoConfiguration( app.ferno.use(.default(fernoConfiguration)) ``` +If you prefer to use the Firebase `ServiceAccount.json` follow the example below. + +```swift +import Ferno + +// option 1 +let fernoConfiguration = try FernoServiceJsonConfiguration(json: Data) + +// option 2 +let fernoConfiguration = try FernoServiceJsonConfiguration(path: URL) + +// option 3 +let fernoConfiguration = FernoServiceJsonConfiguration( + type: String, + projectId: String, + privateKeyId: String, + privateKey: String, + clientEmail: String, + clientId: String, + authUri: String, + tokenUri: String, + authProviderX509CertUrl: String, + clientX509CertUrl: String, + universeDomain: String +) + +app.ferno.use(.serviceAccountKey(fernoConfiguration)) +``` + ## Parameters + There are some custom parameters to pass into functions. I want to go over all the parameters you will need to know. ### [FernoQuery] + In GET requests, you might want to query on your data. This is what `[FernoQuery]` is for. `FernoQuery` is an enum with: + 1. `case shallow(Bool)` 2. `case orderBy(FernoValue)` 3. `case limitToFirst(FernoValue)` @@ -63,52 +111,69 @@ In GET requests, you might want to query on your data. This is what `[FernoQuery These are all the possible queries that are allowed on Firebase according to the [docs](https://firebase.google.com/docs/reference/rest/database/#section-query-parameters) #### NOTES on [FernoQuery] + - `shallow(Bool)` cannot be mixed with any other query parameters. - you usually use `orderBy(FernoValue)` in conjunction with enums `3-7` - using `orderBy(FernoValue)` alone will just order the data returned #### FernoValue + You will notice most cases in `FernoQuery` have a value of `FernoValue`. `FernoValue` is just a wrapper for `Bool, String, Int, Double, Float`. So you can just do `.startAt(5)` and everything will work. #### Examples of [FernoQuery] -Just using shallow: + +Just using shallow: + ```swift [.shallow(true)] ``` + Filter data to only return data that matches `"age": 21`: + ```swift [.orderBy("age"), .equalTo(21)] ``` Just orderBy(returns data in ascending order): + ```swift [.orderBy("age")] ``` ## Usage + There are 6 functions that allow you to interact with your Firebase realtime database. ### GET + There are four functions that allow you get your data. + ```swift app.ferno.retrieve(_ path: [String], queryItems: [FernoQuery] = []) ``` + ```swift app.ferno.retrieve(_ path: String..., queryItems: [FernoQuery] = []) ``` + ```swift app.ferno.retrieveMany(_ path: [String], queryItems: [FernoQuery] = []) ``` + ```swift app.ferno.retrieveMany(_ path: String..., queryItems: [FernoQuery] = []) ``` + The only difference between `retrieve` and `retrieveMany` is the return type. + - `retrieve` returns -> `F` where `F` is of type `Decodable` - `retrieveMany` returns -> `[String: F]` where `F` is of type `Decodable` and `String` is the key #### Example -1. Define the value you want the data converted. + +1. Define the value you want the data converted. + ```swift struct Developer: Content { var name: String @@ -118,19 +183,24 @@ struct Developer: Content { ``` 2. Make the request. Make sure you set the type of the response so Ferno knows what to convert. + ```swift let developers: [String: Developer] = try await app.ferno.retrieveMany("developers") let developer: Developer = try await app.ferno.retrieve(["developers", "dev1"]) ``` ### POST + Used to create a new entry in your database + ```swift app.ferno.create(_ path: [String], body: T) try await -> FernoChild ``` + ```swift app.ferno.create(_ path: String..., body: T) try await -> FernoChild ``` + - `body: T` is of type `Content`. - `FernoChild` is a struct: @@ -143,37 +213,48 @@ struct FernoChild: Content { - `FernoChild` is returned, because the API request returns the key from the newly created child. #### Example + ```swift let newDeveloper = Developer(name: "Elon", favLanguage: "Python", age: 46) // conforms to Content let newDeveloperKey: FernoChild = try await app.ferno.create("developers", body: newDeveloper) ``` ### DELETE + Used to delete an entry in your database + ```swift app.ferno.delete(_ path: [String]) try await -> Bool ``` + ```swift app.ferno.delete(_ path: String...) try await -> Bool ``` + - the delete method will return a boolean depending on if the delete was successful #### Example + ```swift let successfulDelete: Bool = try await app.ferno.delete(["developers", "dev-1"]) ``` ### PATCH + Update values at a specific location, but omitted values won't get removed + ```swift app.ferno.update(_ path: [String], body: T) try await -> T ``` + ```swift app.ferno.update(_ path: String..., body: T) try await -> T ``` + - the update method will return the body ### Example + ```swift struct UpdateDeveloperName: Content { var name: String @@ -184,14 +265,19 @@ let updatedDeveloperName: UpdateDeveloperName = try await app.ferno.update(["dev ``` ### PUT + Overwrite the current location with data you are passing in + ```swift client.ferno.overwrite(_ path: [String], body: T) try await -> T ``` + ```swift client.ferno.overwrite(_ path: String..., body: T) try await -> T ``` + #### Example + ```swift struct LeadDeveloper: Content { var name: String @@ -225,4 +311,3 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md * Vapor Discord (for helping me with all my issues <3) * Stripe Provider as a great template! [stripe-provider](https://github.com/vapor-community/stripe-provider) - diff --git a/Sources/Ferno/Application+Ferno.swift b/Sources/Ferno/Application+Ferno.swift index 8f33ef5..95c33e5 100644 --- a/Sources/Ferno/Application+Ferno.swift +++ b/Sources/Ferno/Application+Ferno.swift @@ -4,38 +4,52 @@ import JWT extension Application { /// The `Ferno` object public var ferno: Ferno { .init(application: self) } - + public struct Ferno { - + struct Key: StorageKey { typealias Value = Storage } - + /// The provider of the `Ferno` configuration public struct Provider { - let run: (Application) -> () + let run: (Application) -> Void - public init(_ run: @escaping (Application) -> ()) { + public init(_ run: @escaping (Application) -> Void) { self.run = run } - + @available(*, deprecated) public static func `default`(_ configuration: FernoConfiguration) -> Self { .init { app in app.ferno.use(configuration) - app.ferno.use(custom: DefaultFernoDriver(client: app.client)) + app.ferno.use(custom: FernoDefaultDriver(client: app.client)) + } + } + + public static func `default`(_ configuration: FernoDefaultConfiguration) -> Self { + .init { app in + app.ferno.use(configuration) + app.ferno.use(custom: FernoDefaultDriver(client: app.client)) + } + } + + public static func serviceAccountKey(_ configuration: FernoServiceJsonConfiguration) -> Self { + .init { app in + app.ferno.use(configuration) + app.ferno.use(custom: FernoServiceAccountKeyDriver(client: app.client)) } } } - + final class Storage { - public var configuration: FernoConfiguration + public var configuration: FernoConfigurationProvider public var driver: FernoDriver? - public init(config: FernoConfiguration) { + public init(config: FernoConfigurationProvider) { self.configuration = config } } - + struct Lifecycle: LifecycleHandler { func shutdown(_ application: Application) { if let driver = application.ferno.storage.driver { @@ -43,11 +57,11 @@ extension Application { } } } - + public let application: Application - + /// The `FernoConfiguration` object - public var configuration: FernoConfiguration { + public var configuration: any FernoConfigurationProvider { get { self.storage.configuration } nonmutating set { self.storage.configuration = newValue } } @@ -59,38 +73,38 @@ extension Application { } return driver } - + var storage: Storage { guard let storage = self.application.storage[Key.self] else { fatalError("No Ferno configuration found. Configure with app.ferno.use(...)") } return storage } - + var client: FernoClient { driver.makeClient(with: configuration) } - + func initialize() { self.application.lifecycle.use(Lifecycle()) } - + public func use(_ provider: Provider) { provider.run(self.application) } - - public func use(_ config: FernoConfiguration) { + + public func use(_ config: FernoConfigurationProvider) { self.application.storage[Key.self] = .init(config: config) self.initialize() } - + public func use(custom driver: FernoDriver) { self.storage.driver = driver } - + } } extension Application.Ferno { - + /// Deletes everything public func delete(_ path: String...) async throws -> Bool { try await self.delete(path) @@ -120,9 +134,8 @@ extension Application.Ferno { } } - extension Application.Ferno { - + /// Deletes everything public func delete(_ path: [String]) async throws -> Bool { try await self.client.delete(method: .DELETE, path: path) @@ -139,7 +152,6 @@ extension Application.Ferno { ) } - /// Overwrites everything at that location with the data public func overwrite(_ path: [String], body: T) async throws -> T { try await self.client.send( diff --git a/Sources/Ferno/FernoClient.swift b/Sources/Ferno/FernoClient.swift index 5cd8efc..b2d8044 100644 --- a/Sources/Ferno/FernoClient.swift +++ b/Sources/Ferno/FernoClient.swift @@ -3,14 +3,25 @@ import JWT import NIOConcurrencyHelpers struct OAuthBody: Content { - var grant_type: String + var grantType: String var assertion: String + + private enum CodingKeys: String, CodingKey { + case grantType = "grant_type" + case assertion + } } struct OAuthResponse: Content { - var access_token: String - var token_type: String - var expires_in: Int + var accessToken: String + var tokenType: String + var expiresIn: Int + + private enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case tokenType = "token_type" + case expiresIn = "expires_in" + } } public protocol FernoClient { @@ -18,7 +29,7 @@ public protocol FernoClient { method: HTTPMethod, path: [String] ) async throws -> Bool - + func send( method: HTTPMethod, path: [String], @@ -26,7 +37,7 @@ public protocol FernoClient { body: T, headers: HTTPHeaders ) async throws -> F - + func sendMany( method: HTTPMethod, path: [String], @@ -39,10 +50,10 @@ public protocol FernoClient { final class FernoAPIClient: FernoClient { private let decoder = JSONDecoder() private let client: Client - private(set) public var configuration: FernoConfiguration + private(set) public var configuration: FernoConfigurationProvider private let lock = NIOLock() - public init(configuration: FernoConfiguration, client: Client) { + public init(configuration: FernoConfigurationProvider, client: Client) { self.configuration = configuration self.client = client } @@ -111,20 +122,20 @@ extension FernoAPIClient { "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/firebase.database" ].joined(separator: " "), - aud: "https://www.googleapis.com/oauth2/v4/token", + aud: configuration.tokenURI, exp: .init(value: expirationDate), iat: .init(value: currentDate) ) - + return try privateSigner.sign(payload) } private func getAccessToken() async throws -> String { - + if let cachedToken = lock.withLock({ if let accessToken = configuration.accessToken, - let tokenExpriationDate = configuration.tokenExpirationDate, - Date().timeIntervalSince(tokenExpriationDate) > 30*60 { // should be valid for 1 hour + let tokenExpirationDate = configuration.tokenExpirationDate, + Date().timeIntervalSince(tokenExpirationDate) > 30*60 { // should be valid for 1 hour return accessToken } else { return nil @@ -136,24 +147,24 @@ extension FernoAPIClient { configuration.logger.debug("Going to get accessToken") let jwt = try createJWT() configuration.logger.debug("JWT created") - + var headers = HTTPHeaders() headers.add(name: .contentType, value: "application/x-www-form-urlencoded") - let oauthBody = OAuthBody(grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt) + let oauthBody = OAuthBody(grantType: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt) var req = ClientRequest( method: .POST, - url: URI(string: "https://www.googleapis.com/oauth2/v4/token"), + url: URI(string: configuration.tokenURI), headers: headers, body: nil ) try req.content.encode(oauthBody, as: .urlEncodedForm) - + let res = try await client.send(req).content.decode(OAuthResponse.self) lock.withLockVoid { - self.configuration.accessToken = res.access_token - self.configuration.tokenExpirationDate = Date().addingTimeInterval(TimeInterval(res.expires_in)) + self.configuration.accessToken = res.accessToken + self.configuration.tokenExpirationDate = Date().addingTimeInterval(TimeInterval(res.expiresIn)) } self.configuration.logger.debug("Access token received") - return res.access_token + return res.accessToken } } diff --git a/Sources/Ferno/FernoConfiguration.swift b/Sources/Ferno/FernoConfiguration.swift deleted file mode 100644 index 15aed01..0000000 --- a/Sources/Ferno/FernoConfiguration.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// FernoConfiguration.swift -// Ferno -// -// Created by Maxim Krouk on 6/12/20. -// - -import Vapor - -public struct FernoConfiguration { - public let logger: Logger - public let basePath: String - public let email: String - public let privateKey: String - public var accessToken: String? - public var tokenExpirationDate: Date? - - public init( - basePath: String, - email: String, - privateKey: String, - accessToken: String? = nil, - tokenExpirationDate: Date? = nil, - logger: Logger = .init(label: "codes.vapor.ferno") - ) { - self.basePath = basePath - self.email = email - self.privateKey = privateKey - self.accessToken = accessToken - self.tokenExpirationDate = tokenExpirationDate - self.logger = logger - } -} diff --git a/Sources/Ferno/FernoDefaultConfiguration.swift b/Sources/Ferno/FernoDefaultConfiguration.swift new file mode 100644 index 0000000..50b9575 --- /dev/null +++ b/Sources/Ferno/FernoDefaultConfiguration.swift @@ -0,0 +1,70 @@ +// +// FernoConfiguration.swift +// Ferno +// +// Created by Maxim Krouk on 6/12/20. +// + +import Vapor + +public protocol FernoConfigurationProvider { + var logger: Logger { get } + var basePath: String { get } + var email: String { get } + var privateKey: String { get } + var accessToken: String? { get set } + var tokenExpirationDate: Date? { get set } + var tokenURI: String { get } +} + +@available(*, deprecated, renamed: "FernoDefaultConfiguration") +public struct FernoConfiguration: FernoConfigurationProvider { + public let logger: Logger + public let basePath: String + public let email: String + public let privateKey: String + public var accessToken: String? + public var tokenExpirationDate: Date? + public let tokenURI: String = "https://oauth2.googleapis.com/token" + public init( + basePath: String, + email: String, + privateKey: String, + accessToken: String? = nil, + tokenExpriationDate: Date? = nil, + logger: Logger = .init(label: "codes.vapor.ferno") + ) { + self.basePath = basePath + self.email = email + self.privateKey = privateKey + self.accessToken = accessToken + self.tokenExpirationDate = tokenExpriationDate + self.logger = logger + } +} + +public struct FernoDefaultConfiguration: FernoConfigurationProvider { + public let logger: Logger + public let basePath: String + public let email: String + public let privateKey: String + public var accessToken: String? + public var tokenExpirationDate: Date? + public let tokenURI: String = "https://oauth2.googleapis.com/token" + + public init( + basePath: String, + email: String, + privateKey: String, + accessToken: String? = nil, + tokenExpirationDate: Date? = nil, + logger: Logger = .init(label: "codes.vapor.ferno") + ) { + self.basePath = basePath + self.email = email + self.privateKey = privateKey + self.accessToken = accessToken + self.tokenExpirationDate = tokenExpirationDate + self.logger = logger + } +} diff --git a/Sources/Ferno/FernoDriver.swift b/Sources/Ferno/FernoDriver.swift index 2a95871..9e96646 100644 --- a/Sources/Ferno/FernoDriver.swift +++ b/Sources/Ferno/FernoDriver.swift @@ -10,14 +10,20 @@ import Vapor /// A new driver for Ferno public protocol FernoDriver { /// Makes the ferno client - func makeClient(with config: FernoConfiguration) -> FernoClient - + func makeClient(with config: FernoConfigurationProvider) -> FernoClient + /// Shuts down the driver func shutdown() } -struct DefaultFernoDriver: FernoDriver { +struct FernoDefaultDriver: FernoDriver { + var client: Client + func makeClient(with config: FernoConfigurationProvider) -> FernoClient { FernoAPIClient(configuration: config, client: client) } + func shutdown() {} +} + +struct FernoServiceAccountKeyDriver: FernoDriver { var client: Client - func makeClient(with config: FernoConfiguration) -> FernoClient { FernoAPIClient(configuration: config, client: client) } + func makeClient(with config: FernoConfigurationProvider) -> FernoClient { FernoAPIClient(configuration: config, client: client) } func shutdown() {} } diff --git a/Sources/Ferno/FernoPath.swift b/Sources/Ferno/FernoPath.swift index 1088e81..5ce6216 100644 --- a/Sources/Ferno/FernoPath.swift +++ b/Sources/Ferno/FernoPath.swift @@ -7,7 +7,7 @@ import Foundation -//Firebase Path Enum +// Firebase Path Enum public enum FernoPath { case child(String) case json diff --git a/Sources/Ferno/FernoQuery.swift b/Sources/Ferno/FernoQuery.swift index 904c831..82368ae 100644 --- a/Sources/Ferno/FernoQuery.swift +++ b/Sources/Ferno/FernoQuery.swift @@ -7,7 +7,7 @@ import Foundation -//FernoQuery Enum +// FernoQuery Enum public enum FernoQuery { case shallow(Bool) diff --git a/Sources/Ferno/FernoServiceJsonConfiguration.swift b/Sources/Ferno/FernoServiceJsonConfiguration.swift new file mode 100644 index 0000000..9edf3a0 --- /dev/null +++ b/Sources/Ferno/FernoServiceJsonConfiguration.swift @@ -0,0 +1,129 @@ +import Vapor + +public struct FernoServiceJsonConfiguration: FernoConfigurationProvider, Decodable, Sendable { + public let type: String + public let projectId: String + public let email: String + public let clientId: String + public let basePath: String + public var accessToken: String? + public var tokenExpirationDate: Date? + public var logger: Logger = .init(label: "codes.vapor.ferno") + public let privateKey: String + public let tokenURI: String + internal let privateKeyId: String + internal let authURI: String + internal let authProviderX509CertURL: String + internal let clientX509CertURL: String + internal let universeDomain: String + + enum CodingKeys: String, CodingKey { + case type + case projectId = "project_id" + case privateKeyId = "private_key_id" + case privateKey = "private_key" + case email = "client_email" + case clientId = "client_id" + case authURI = "auth_uri" + case tokenURI = "token_uri" + case authProviderX509CertURL = "auth_provider_x509_cert_url" + case clientX509CertURL = "client_x509_cert_url" + case universeDomain = "universe_domain" + } + + public init(type: String, + projectId: String, + privateKeyId: String, + privateKey: String, + clientEmail: String, + clientId: String, + authUri: String, + tokenUri: String, + authProviderX509CertUrl: String, + clientX509CertUrl: String, + universeDomain: String, + logger: Logger = .init(label: "codes.vapor.ferno")) { + self.type = type + self.projectId = projectId + self.privateKeyId = privateKeyId + self.privateKey = privateKey + self.email = clientEmail + self.clientId = clientId + self.authURI = authUri + self.tokenURI = tokenUri + self.authProviderX509CertURL = authProviderX509CertUrl + self.clientX509CertURL = clientX509CertUrl + self.universeDomain = universeDomain + self.logger = logger + self.basePath = "https://\(projectId).firebaseio.com" + } + + public init(json: Data, logger: Logger = .init(label: "codes.vapor.ferno")) throws { + let configuration = try JSONDecoder().decode(FernoServiceJsonConfiguration.self, from: json) + self.type = configuration.type + self.projectId = configuration.projectId + self.privateKeyId = configuration.privateKeyId + self.privateKey = configuration.privateKey + self.email = configuration.email + self.clientId = configuration.clientId + self.authURI = configuration.authURI + self.tokenURI = configuration.tokenURI + self.authProviderX509CertURL = configuration.authProviderX509CertURL + self.clientX509CertURL = configuration.clientX509CertURL + self.universeDomain = configuration.universeDomain + self.logger = logger + self.basePath = "https://\(projectId).firebaseio.com" + } + + public init(json: ByteBuffer, logger: Logger = .init(label: "codes.vapor.ferno")) throws { + let configuration = try JSONDecoder().decode(FernoServiceJsonConfiguration.self, from: json) + self.type = configuration.type + self.projectId = configuration.projectId + self.privateKeyId = configuration.privateKeyId + self.privateKey = configuration.privateKey + self.email = configuration.email + self.clientId = configuration.clientId + self.authURI = configuration.authURI + self.tokenURI = configuration.tokenURI + self.authProviderX509CertURL = configuration.authProviderX509CertURL + self.clientX509CertURL = configuration.clientX509CertURL + self.universeDomain = configuration.universeDomain + self.logger = logger + self.basePath = "https://\(projectId).firebaseio.com" + } + + /// The `ServiceAccountKey.json` path in URL format + public init(path: URL, logger: Logger = .init(label: "codes.vapor.ferno")) throws { + let data = try Data(contentsOf: path) + let configuration = try JSONDecoder().decode(FernoServiceJsonConfiguration.self, from: data) + self.type = configuration.type + self.projectId = configuration.projectId + self.privateKeyId = configuration.privateKeyId + self.privateKey = configuration.privateKey + self.email = configuration.email + self.clientId = configuration.clientId + self.authURI = configuration.authURI + self.tokenURI = configuration.tokenURI + self.authProviderX509CertURL = configuration.authProviderX509CertURL + self.clientX509CertURL = configuration.clientX509CertURL + self.universeDomain = configuration.universeDomain + self.logger = logger + self.basePath = "https://\(projectId).firebaseio.com" + } + + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: FernoServiceJsonConfiguration.CodingKeys.self) + self.type = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.type) + self.projectId = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.projectId) + self.privateKeyId = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.privateKeyId) + self.privateKey = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.privateKey) + self.email = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.email) + self.clientId = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.clientId) + self.authURI = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.authURI) + self.tokenURI = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.tokenURI) + self.authProviderX509CertURL = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.authProviderX509CertURL) + self.clientX509CertURL = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.clientX509CertURL) + self.universeDomain = try container.decode(String.self, forKey: FernoServiceJsonConfiguration.CodingKeys.universeDomain) + self.basePath = "https://\(projectId).firebaseio.com" + } +} diff --git a/Sources/Ferno/JWTPayload.swift b/Sources/Ferno/JWTPayload.swift index ebcf1b8..697719a 100644 --- a/Sources/Ferno/JWTPayload.swift +++ b/Sources/Ferno/JWTPayload.swift @@ -9,14 +9,13 @@ import Vapor import JWT struct Payload: JWTPayload { - - func verify(using signer: JWTSigner) throws { - try exp.verifyNotExpired() - } - var iss: IssuerClaim var scope: String var aud: String var exp: ExpirationClaim var iat: IssuedAtClaim + + func verify(using signer: JWTSigner) throws { + try exp.verifyNotExpired() + } } diff --git a/Tests/FernoTests/Application+Testing.swift b/Tests/FernoTests/Application+Testing.swift index 8f56516..ae261c5 100644 --- a/Tests/FernoTests/Application+Testing.swift +++ b/Tests/FernoTests/Application+Testing.swift @@ -8,7 +8,7 @@ import Ferno import XCTVapor -func launch(_ test: (Application) async throws -> Void) async throws { +func defaultLaunch(_ test: (Application) async throws -> Void) async throws { let app = Application(.testing) defer { app.shutdown() } app.ferno.use( @@ -22,3 +22,15 @@ func launch(_ test: (Application) async throws -> Void) async throws { ) try await test(app) } + +func serviceJsonLaunch(_ test: (Application) async throws -> Void) async throws { + let app = Application(.testing) + guard let json = FirebaseAccountKey.json.data(using: .utf8) else { return } + + let configuration = try FernoServiceJsonConfiguration(json: json) + defer { app.shutdown() } + app.ferno.use( + .serviceAccountKey(configuration) + ) + try await test(app) +} diff --git a/Tests/FernoTests/FernoTests.swift b/Tests/FernoTests/FernoDefaultTests.swift similarity index 66% rename from Tests/FernoTests/FernoTests.swift rename to Tests/FernoTests/FernoDefaultTests.swift index b0f57fc..13cb880 100644 --- a/Tests/FernoTests/FernoTests.swift +++ b/Tests/FernoTests/FernoDefaultTests.swift @@ -8,32 +8,14 @@ import XCTVapor import Ferno -struct Teacher: Content { - var name: String - var teachesGrade: String - var age: Int -} - -struct UpdateStudentInfo: Content { - var major: String -} - -struct Student: Content { - var name: String - var major: String - var school: String - var age: Int - var willGraduate: Bool -} +final class FernoDefaultTests: XCTestCase { -final class FirebaseTests: XCTestCase { - - //GET a student + // GET a student func testGetStudent() async throws { - try await launch { app in - - //Create 3 new students - let austin = Student(name: "Austin", major: "Computer Science", school: "Cornell University", age: 21, willGraduate: true) + try await defaultLaunch { app in + // Create 3 new students + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell University", age: 21, willGraduate: true) let child = try await app.ferno.create(["Student-get"], body: austin) let student: Student = try await app.ferno.retrieve(["Student-get", child.name], queryItems: []) @@ -47,26 +29,30 @@ final class FirebaseTests: XCTestCase { } } - //GET students + // GET students func testGetStudents() async throws { - try await launch { app in - - //Create 3 new students - let austin = Student(name: "Austin", major: "Computer Science", school: "Cornell University", age: 21, willGraduate: true) - let ashley = Student(name: "Ashley", major: "Biology", school: "Siena & Cornell University", age: 20, willGraduate: true) - let billy = Student(name: "Billy", major: "Business", school: "Mira Costa Community", age: 22, willGraduate: false) - - let _ = try await app.ferno.create(["Students-get"], body: austin) - let _ = try await app.ferno.create(["Students-get"], body: ashley) - let _ = try await app.ferno.create(["Students-get"], body: billy) - + try await defaultLaunch { app in + // Create 3 new students + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell University", age: 21, willGraduate: true) + let ashley = Student(name: "Ashley", major: "Biology", + school: "Siena & Cornell University", age: 20, willGraduate: true) + let billy = Student(name: "Billy", major: "Business", + school: "Mira Costa Community", age: 22, willGraduate: false) + + _ = try await app.ferno.create(["Students-get"], body: austin) + _ = try await app.ferno.create(["Students-get"], body: ashley) + _ = try await app.ferno.create(["Students-get"], body: billy) let names = ["Austin", "Ashley", "Billy"] let ages = ["Austin": 21, "Ashley": 20, "Billy": 22] let majors = ["Austin": "Computer Science", "Ashley": "Biology", "Billy": "Business"] - let schools = ["Austin": "Cornell University", "Ashley": "Siena & Cornell University", "Billy": "Mira Costa Community"] - let willGradaute = ["Austin": true, "Ashley": true, "Billy": false] - + let schools = ["Austin": "Cornell University", + "Ashley": "Siena & Cornell University", + "Billy": "Mira Costa Community"] + let willGradaute = ["Austin": true, + "Ashley": true, + "Billy": false] let students: [Student] = try await app.ferno.retrieveMany("Students-get", queryItems: []).map { $0.value } @@ -88,10 +74,11 @@ final class FirebaseTests: XCTestCase { } } - //POST Student + // POST Student func testCreateStudent() async throws { - try await launch { app in - let student = Student(name: "Matt", major: "Computer Science", school: "Cornell University", age: 20, willGraduate: true) + try await defaultLaunch { app in + let student = Student(name: "Matt", major: "Computer Science", + school: "Cornell University", age: 20, willGraduate: true) let child = try await app.ferno.create(body: student) XCTAssertNotNil(child.name) @@ -101,10 +88,11 @@ final class FirebaseTests: XCTestCase { } } - //DELETE student + // DELETE student func testDeleteStudent() async throws { - try await launch { app in - let timothy = Student(name: "Timothy", major: "Agriculture", school: "Mira Costa Community", age: 24, willGraduate: false) + try await defaultLaunch { app in + let timothy = Student(name: "Timothy", major: "Agriculture", + school: "Mira Costa Community", age: 24, willGraduate: false) let child = try await app.ferno.create("Students-delete", body: timothy) @@ -113,10 +101,11 @@ final class FirebaseTests: XCTestCase { } } - //PATCH update student + // PATCH update student func testUpdateStudent() async throws { - try await launch { app in - let austin = Student(name: "Austin", major: "Computer Science", school: "Cornell Univeristy", age: 21, willGraduate: true) + try await defaultLaunch { app in + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell Univeristy", age: 21, willGraduate: true) let child = try await app.ferno.create(["Students-patch"], body: austin) let updateStudentInfo = UpdateStudentInfo(major: "Cooking") @@ -129,10 +118,11 @@ final class FirebaseTests: XCTestCase { } } - //PUT overwrite student + // PUT overwrite student func testOverwriteStudent() async throws { - try await launch { app in - let austin = Student(name: "Austin", major: "Computer Science", school: "Cornell Univeristy", age: 21, willGraduate: true) + try await defaultLaunch { app in + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell Univeristy", age: 21, willGraduate: true) let child = try await app.ferno.create(["Students-put"], body: austin) let teacher = Teacher(name: "Ms. Jennifer", teachesGrade: "12th", age: 29) diff --git a/Tests/FernoTests/FernoServiceJsonTests.swift b/Tests/FernoTests/FernoServiceJsonTests.swift new file mode 100644 index 0000000..e47adae --- /dev/null +++ b/Tests/FernoTests/FernoServiceJsonTests.swift @@ -0,0 +1,129 @@ +import XCTVapor +import Ferno + +class FernoServiceJsonTests: XCTestCase { + // GET a student + func testGetStudent() async throws { + try await serviceJsonLaunch { app in + // Create 3 new students + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell University", age: 21, willGraduate: true) + let child = try await app.ferno.create(["Student-get"], body: austin) + + let student: Student = try await app.ferno.retrieve(["Student-get", child.name], queryItems: []) + + XCTAssert(student.name == "Austin") + XCTAssert(student.major == "Computer Science") + + let success = try await app.ferno.delete(["Student-get", child.name]) + + XCTAssertTrue(success) + } + } + + // GET students + func testGetStudents() async throws { + try await serviceJsonLaunch { app in + // Create 3 new students + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell University", age: 21, willGraduate: true) + let ashley = Student(name: "Ashley", major: "Biology", + school: "Siena & Cornell University", age: 20, willGraduate: true) + let billy = Student(name: "Billy", major: "Business", + school: "Mira Costa Community", age: 22, willGraduate: false) + + _ = try await app.ferno.create(["Students-get"], body: austin) + _ = try await app.ferno.create(["Students-get"], body: ashley) + _ = try await app.ferno.create(["Students-get"], body: billy) + + let names = ["Austin", "Ashley", "Billy"] + let ages = ["Austin": 21, "Ashley": 20, "Billy": 22] + let majors = ["Austin": "Computer Science", "Ashley": "Biology", "Billy": "Business"] + let schools = ["Austin": "Cornell University", + "Ashley": "Siena & Cornell University", + "Billy": "Mira Costa Community"] + let willGradaute = ["Austin": true, + "Ashley": true, + "Billy": false] + + let students: [Student] = try await app.ferno.retrieveMany("Students-get", queryItems: []).map { $0.value } + + XCTAssertNotNil(students) + + XCTAssert(students.count == 3, "Making sure all 3 students are returned") + students.forEach { student in + XCTAssert(names.contains(student.name), "Checking name for \(student.name)") + XCTAssert(ages[student.name] == student.age, "Checking age for \(student.name)") + XCTAssert(majors[student.name] == student.major, "Checking major for \(student.name)") + XCTAssert(schools[student.name] == student.school, "Checking school for \(student.name)") + XCTAssert(willGradaute[student.name] == student.willGraduate, "Checking willGraduate for \(student.name)") + } + + let success = try await app.ferno.delete("Students-get") + + XCTAssertTrue(success) + + } + } + + // POST Student + func testCreateStudent() async throws { + try await serviceJsonLaunch { app in + let student = Student(name: "Matt", major: "Computer Science", + school: "Cornell University", age: 20, willGraduate: true) + let child = try await app.ferno.create(body: student) + XCTAssertNotNil(child.name) + + let success = try await app.ferno.delete(child.name) + + XCTAssertTrue(success) + } + } + + // DELETE student + func testDeleteStudent() async throws { + try await serviceJsonLaunch { app in + let timothy = Student(name: "Timothy", major: "Agriculture", + school: "Mira Costa Community", age: 24, willGraduate: false) + + let child = try await app.ferno.create("Students-delete", body: timothy) + + let success = try await app.ferno.delete(["Students-delete", child.name]) + XCTAssertTrue(success, "did delete child") + } + } + + // PATCH update student + func testUpdateStudent() async throws { + try await serviceJsonLaunch { app in + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell Univeristy", age: 21, willGraduate: true) + let child = try await app.ferno.create(["Students-patch"], body: austin) + + let updateStudentInfo = UpdateStudentInfo(major: "Cooking") + let response = try await app.ferno.update(["Students-patch", child.name], body: updateStudentInfo) + XCTAssertTrue(response.major == updateStudentInfo.major) + + let success = try await app.ferno.delete(["Students-patch", child.name]) + + XCTAssertTrue(success) + } + } + + // PUT overwrite student + func testOverwriteStudent() async throws { + try await serviceJsonLaunch { app in + let austin = Student(name: "Austin", major: "Computer Science", + school: "Cornell Univeristy", age: 21, willGraduate: true) + let child = try await app.ferno.create(["Students-put"], body: austin) + + let teacher = Teacher(name: "Ms. Jennifer", teachesGrade: "12th", age: 29) + let response: Teacher = try await app.ferno.overwrite(["Students-put", child.name], body: teacher) + XCTAssertTrue(response.name == teacher.name) + + let success = try await app.ferno.delete(["Students-put", child.name]) + XCTAssertTrue(success) + } + } + +} diff --git a/Tests/FernoTests/Models/FirebaseAccountKey.swift b/Tests/FernoTests/Models/FirebaseAccountKey.swift new file mode 100644 index 0000000..d563c6b --- /dev/null +++ b/Tests/FernoTests/Models/FirebaseAccountKey.swift @@ -0,0 +1,10 @@ +import Foundation + +/// AccountServiceMock, fill it out with the need it information to test. +/// +/// private_key may containt `"\n"`. Replace the `"\n"`=> `"\\n"` manually if needed +struct FirebaseAccountKey { + static let json: String = """ + {\"type\": \"\", \"project_id\": \"\", \"private_key_id\": \"\", \"private_key\": \"\",\"client_email\": \"\", \"client_id\": \"\",\"auth_uri\": \"\",\"token_uri\": \"\",\"auth_provider_x509_cert_url\": \"\", \"client_x509_cert_url\": \"\", \"universe_domain\": \"\"} +""" +} diff --git a/Tests/FernoTests/Models/Student.swift b/Tests/FernoTests/Models/Student.swift new file mode 100644 index 0000000..5263f21 --- /dev/null +++ b/Tests/FernoTests/Models/Student.swift @@ -0,0 +1,9 @@ +import XCTVapor + +struct Student: Content { + var name: String + var major: String + var school: String + var age: Int + var willGraduate: Bool +} diff --git a/Tests/FernoTests/Models/Teacher.swift b/Tests/FernoTests/Models/Teacher.swift new file mode 100644 index 0000000..7299971 --- /dev/null +++ b/Tests/FernoTests/Models/Teacher.swift @@ -0,0 +1,7 @@ +import XCTVapor + +struct Teacher: Content { + var name: String + var teachesGrade: String + var age: Int +} diff --git a/Tests/FernoTests/Models/UpdateStudentInfo.swift b/Tests/FernoTests/Models/UpdateStudentInfo.swift new file mode 100644 index 0000000..536ea3d --- /dev/null +++ b/Tests/FernoTests/Models/UpdateStudentInfo.swift @@ -0,0 +1,5 @@ +import XCTVapor + +struct UpdateStudentInfo: Content { + var major: String +}