Skip to content

Commit

Permalink
Adding api key header to v3 profile request (#257)
Browse files Browse the repository at this point in the history
* Ignoring api-key file

* Adding ApiKey config to ProfileService headers

* Adding ApiKey to demo app

* Improve script to create secrets file

* Generate secrets file from make command

* Try build demo in CI

* Using `Configuration.shared.apiKey` as default on URLRequest.authorised

* Update Makefile

* Making Configuration an actor

* Update openapi specs

* Adding profile service tests
  • Loading branch information
etoledom authored May 29, 2024
1 parent 2696fd3 commit c8a7c73
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 374 deletions.
3 changes: 3 additions & 0 deletions .buildkite/commands/build-demos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
echo "--- :rubygems: Setting up Gems"
install_gems

echo "--- Generate Secrets.swift source file"
make secrets

echo "--- 🛠 Building Demo (Swift)"
bundle exec fastlane build_demo scheme:Gravatar-Demo

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ Carthage/Build
fastlane/README.md
fastlane/report.xml
fastlane/test_output

# Other
openapi-generator/
Demo/Demo/Gravatar-Demo/Secrets.swift
19 changes: 6 additions & 13 deletions Demo/Demo/Gravatar-Demo/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
//
// AppDelegate.swift
// Gravatar-Demo
//
// Created by Andrew Montgomery on 1/19/24.
//

import UIKit
import Gravatar

@main
class AppDelegate: UIResponder, UIApplicationDelegate {



func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if let apiKey = apiKey {
Task {
await Configuration.shared.configure(with: apiKey)
}
}
return true
}

Expand All @@ -30,7 +26,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}


}

3 changes: 2 additions & 1 deletion Demo/Demo/Gravatar-Demo/DemoFetchProfileViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ class DemoFetchProfileViewController: UIViewController {
profileTextView.text = """
Profile URL: \(profile.profileUrl)
Display name: \(profile.displayName)
Name: \(profile.displayName)
Preferred User Name: \(profile.displayName)
Thumbnail URL: \(profile.avatarUrl)
Wallets: \(String(describing: profile.payments?.cryptoWallets))
Last edit: \(String(describing: profile.lastProfileEdit))
Registration date: \(String(describing: profile.registrationDate))
"""
}

Expand Down
6 changes: 6 additions & 0 deletions Demo/Gravatar-Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
1E0087932B63CFFE0012ECEA /* DemoFetchProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0087922B63CFFE0012ECEA /* DemoFetchProfileViewController.swift */; };
1E0087952B63DBCB0012ECEA /* DemoUploadImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0087942B63DBCB0012ECEA /* DemoUploadImageViewController.swift */; };
1ECAB5072BC984440043A331 /* DemoProfileConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ECAB5062BC984440043A331 /* DemoProfileConfigurationViewController.swift */; };
1ED769E72C048D9C00680D78 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ED769E62C048D9C00680D78 /* Secrets.swift */; };
4948C4EC2B61C41100AC4875 /* Gravatar in Frameworks */ = {isa = PBXBuildFile; productRef = 4948C4EB2B61C41100AC4875 /* Gravatar */; };
4948C4EE2B61C41800AC4875 /* Gravatar in Frameworks */ = {isa = PBXBuildFile; productRef = 4948C4ED2B61C41800AC4875 /* Gravatar */; };
495775E22B5B34970082812A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 495775E12B5B34970082812A /* AppDelegate.swift */; };
Expand Down Expand Up @@ -37,6 +38,7 @@
1E0087922B63CFFE0012ECEA /* DemoFetchProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoFetchProfileViewController.swift; sourceTree = "<group>"; };
1E0087942B63DBCB0012ECEA /* DemoUploadImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoUploadImageViewController.swift; sourceTree = "<group>"; };
1ECAB5062BC984440043A331 /* DemoProfileConfigurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoProfileConfigurationViewController.swift; sourceTree = "<group>"; };
1ED769E62C048D9C00680D78 /* Secrets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = "<group>"; };
4948C4E92B61C3FD00AC4875 /* Gravatar-SDK-iOS */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "Gravatar-SDK-iOS"; path = ..; sourceTree = "<group>"; };
495775DF2B5B34970082812A /* Gravatar-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gravatar-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
495775E12B5B34970082812A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -105,6 +107,7 @@
children = (
91956A502B67939F00BF3CF0 /* Common */,
495775E12B5B34970082812A /* AppDelegate.swift */,
1ED769E62C048D9C00680D78 /* Secrets.swift */,
91F0B3E12B6281A60025C4F8 /* Main.storyboard */,
495775E32B5B34970082812A /* SceneDelegate.swift */,
914AC0172BD7FF08005DA4A5 /* DemoBaseProfileViewController.swift */,
Expand Down Expand Up @@ -288,6 +291,7 @@
914AC01A2BD7FF08005DA4A5 /* DemoProfilePresentationStylesViewController.swift in Sources */,
91F0B3DE2B62815F0025C4F8 /* DemoAvatarDownloadViewController.swift in Sources */,
91956A542B67943A00BF3CF0 /* DemoUIImageViewExtensionViewController.swift in Sources */,
1ED769E72C048D9C00680D78 /* Secrets.swift in Sources */,
914AC0202BDAAC3C005DA4A5 /* DemoRemoteSVGViewController.swift in Sources */,
495775E42B5B34970082812A /* SceneDelegate.swift in Sources */,
1ECAB5072BC984440043A331 /* DemoProfileConfigurationViewController.swift in Sources */,
Expand Down Expand Up @@ -335,6 +339,7 @@
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = PZYM8XX95Q;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Demo/Gravatar-Demo/Info.plist";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand Down Expand Up @@ -365,6 +370,7 @@
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = PZYM8XX95Q;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Demo/Gravatar-Demo/Info.plist";
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand Down
16 changes: 12 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ OPENAPI_GENERATOR_CLONE_DIR ?= $(CURRENT_MAKEFILE_DIR)/openapi-generator
OPENAPI_YAML_PATH ?= $(CURRENT_MAKEFILE_DIR)/openapi/spec.yaml
MODEL_TEMPLATE_PATH ?= $(CURRENT_MAKEFILE_DIR)/openapi
OUTPUT_DIRECTORY ?= $(CURRENT_MAKEFILE_DIR)/Sources/Gravatar/OpenApi/Generated
SECRETS_PATH=$(CURRENT_MAKEFILE_DIR)/Demo/Demo/Gravatar-Demo/Secrets.swift

# Derived values (don't change these).
CURRENT_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
CURRENT_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(CURRENT_MAKEFILE_PATH)))


# If no target is specified, display help
.DEFAULT_GOAL := help

Expand All @@ -27,17 +29,17 @@ help: # Display this help.
@-+echo
@-+grep -Eh "^[a-z-]+:.*#" $(CURRENT_MAKEFILE_PATH) | sed -E 's/^(.*:)(.*#+)(.*)/ \1 @@@ \3 /' | column -t -s "@@@"

dev: # Open the package in xcode
dev: secrets # Open the package in xcode
xed .

dev-demo: # Open an xcode project with the package and a demo project
dev-demo: secrets # Open an xcode project with the package and a demo project
xed Demo/

test: bundle-install
bundle exec fastlane test

build-demo: build-demo-swift build-demo-swiftui
build-demo: secrets build-demo-swift build-demo-swiftui

build-demo-swift: bundle-install
bundle exec fastlane build_demo scheme:Gravatar-Demo

Expand Down Expand Up @@ -76,6 +78,12 @@ update-example-snapshots:
cd ./Sources/GravatarUI/GravatarUI.docc/Resources/ProfileExamples && \
for filePath in *; do name=$${filePath%.*}; mv $$filePath $${name//-dark/~dark}@2x$${filePath#$$name}; done

secrets: # Creates the Secrets file in the Demo app.
if [ ! -f $(SECRETS_PATH) ]; then \
touch $(SECRETS_PATH); \
echo "let apiKey: String? = nil" > $(SECRETS_PATH); \
fi

install-and-generate: $(OPENAPI_GENERATOR_CLONE_DIR) # Clones and setup the openapi-generator.
"$(OPENAPI_GENERATOR_CLONE_DIR)"/run-in-docker.sh mvn package
make generate
Expand Down
12 changes: 12 additions & 0 deletions Sources/Gravatar/Configuration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Foundation

public actor Configuration {
private(set) var apiKey: String?
public static let shared = Configuration()

private init() {}

public func configure(with apiKey: String?) {
self.apiKey = apiKey
}
}
21 changes: 18 additions & 3 deletions Sources/Gravatar/Network/Services/ProfileService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ public struct ProfileService: ProfileFetching, Sendable {

public func fetch(with profileID: ProfileIdentifier) async throws -> Profile {
let url = baseURL.appending(pathComponent: profileID.id)
let request = URLRequest(url: url)
// TODO: Add API key to headers
let request = await URLRequest(url: url).authorized()
return try await fetch(with: request)
}
}
Expand All @@ -62,7 +61,9 @@ extension ProfileService {

private func map(_ data: Data, _: HTTPURLResponse) -> Result<Profile, ProfileServiceError> {
do {
let profile = try JSONDecoder().decode(Profile.self, from: data)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let profile = try decoder.decode(Profile.self, from: data)
return .success(profile)
} catch let error as HTTPClientError {
return .failure(.responseError(reason: error.map()))
Expand All @@ -75,3 +76,17 @@ extension ProfileService {
}
}
}

extension URLRequest {
private enum HeaderField: String {
case authorization = "Authorization"
}

fileprivate func authorized() async -> URLRequest {
guard let key = await Configuration.shared.apiKey else { return self }
let bearerKey = "Bearer \(key)"
var copy = self
copy.setValue(bearerKey, forHTTPHeaderField: HeaderField.authorization.rawValue)
return copy
}
}
54 changes: 54 additions & 0 deletions Tests/GravatarTests/ProfileServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@testable import Gravatar
import XCTest

final class ProfileServiceTests: XCTestCase {
override func tearDown() async throws {
await Configuration.shared.configure(with: nil)
}

func testProfileRequest() async {
guard let data = Bundle.fullProfileJsonData else {
return XCTFail("Could not create data")
}
let session = URLSessionMock(returnData: data, response: .successResponse())
let service = ProfileService(client: HTTPClientMock(session: session))

do {
_ = try await service.fetch(with: .hashID(""))
XCTAssertNil(session.request?.value(forHTTPHeaderField: "Authorization"))
} catch {
XCTFail(error.localizedDescription)
}
}

func testProfileRequestWithApiKey() async {
guard let data = Bundle.fullProfileJsonData else {
return XCTFail("Could not create data")
}

await Configuration.shared.configure(with: "somekey")

let session = URLSessionMock(returnData: data, response: .successResponse())
let service = ProfileService(client: HTTPClientMock(session: session))

do {
_ = try await service.fetch(with: .hashID(""))
XCTAssertNotNil(session.request?.value(forHTTPHeaderField: "Authorization"))
} catch {
XCTFail(error.localizedDescription)
}
}
}

extension Bundle {
func jsonData(forResource resource: String) -> Data? {
guard let url = Bundle.testsBundle.url(forResource: resource, withExtension: "json") else {
return nil
}
return try? Data(contentsOf: url)
}

static var fullProfileJsonData: Data? {
testsBundle.jsonData(forResource: "fullProfile")
}
}
Loading

0 comments on commit c8a7c73

Please sign in to comment.