Compare commits

...

4 Commits

Author SHA1 Message Date
Flávio Caetano e6dd6889bb Stop running UI tests in CI 2019-04-12 19:54:32 -03:00
Flávio Caetano 891d499ee4 Fix improve resource loading verification 2019-04-12 19:41:30 -03:00
Flávio Caetano cc03104a72 Revert "Add verification for ReCaptcha config"
This reverts commit 4e6c022a23.
2019-04-12 17:08:01 -03:00
Flávio Caetano 4e6c022a23 Add verification for ReCaptcha config
This will verify if ReCaptchaKey and ReCaptchaDomain are correctly configured and the JS lib was able to load correctly
2019-04-10 19:26:53 -03:00
11 changed files with 241 additions and 94 deletions

View File

@ -1,7 +1,7 @@
PODS:
- AppSwizzle (1.3.1)
- ReCaptcha/Core (1.4.1)
- ReCaptcha/RxSwift (1.4.1):
- ReCaptcha/Core (1.4.2)
- ReCaptcha/RxSwift (1.4.2):
- ReCaptcha/Core
- RxSwift (~> 4.3)
- RxAtomic (4.4.0)
@ -36,7 +36,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppSwizzle: db36e436f56110d93e5ae0147683435df593cabc
ReCaptcha: 520a707a38dfbb1e5de812aa3c041df60bd31827
ReCaptcha: 9a0e1c02a9db9dface31cca63515e28fc3ed6ba8
RxAtomic: eacf60db868c96bfd63320e28619fe29c179656f
RxBlocking: 138ad53217434444d6eeeb4fb406a45431d92e31
RxCocoa: df63ebf7b9a70d6b4eeea407ed5dd4efc8979749

View File

@ -54,7 +54,7 @@
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F28FAC9B200E425600E14987"

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0910"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F28FAC9B200E425600E14987"
BuildableName = "ReCaptcha_UITests.xctest"
BlueprintName = "ReCaptcha_UITests"
ReferencedContainer = "container:ReCaptcha.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F28FAC9B200E425600E14987"
BuildableName = "ReCaptcha_UITests.xctest"
BlueprintName = "ReCaptcha_UITests"
ReferencedContainer = "container:ReCaptcha.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F28FAC9B200E425600E14987"
BuildableName = "ReCaptcha_UITests.xctest"
BlueprintName = "ReCaptcha_UITests"
ReferencedContainer = "container:ReCaptcha.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "F28FAC9B200E425600E14987"
BuildableName = "ReCaptcha_UITests.xctest"
BlueprintName = "ReCaptcha_UITests"
ReferencedContainer = "container:ReCaptcha.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -164,4 +164,23 @@ class ReCaptchaDecoder__Tests: XCTestCase {
// Check
XCTAssertEqual(result, .didLoad)
}
func test__Decode__Error_Setup_Failed() {
let exp = expectation(description: "send error")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let message = MockMessage(message: ["error": 27])
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertEqual(result, .error(.failedSetup))
}
}

View File

@ -15,7 +15,8 @@ extension ReCaptchaError: Equatable {
case (.htmlLoadError, .htmlLoadError),
(.apiKeyNotFound, .apiKeyNotFound),
(.baseURLNotFound, .baseURLNotFound),
(.wrongMessageFormat, .wrongMessageFormat):
(.wrongMessageFormat, .wrongMessageFormat),
(.failedSetup, .failedSetup):
return true
case (.unexpected(let lhe as NSError), .unexpected(let rhe as NSError)):
return lhe == rhe
@ -25,11 +26,12 @@ extension ReCaptchaError: Equatable {
}
static func random() -> ReCaptchaError {
switch arc4random_uniform(4) {
switch arc4random_uniform(5) {
case 0: return .htmlLoadError
case 1: return .apiKeyNotFound
case 2: return .baseURLNotFound
case 3: return .wrongMessageFormat
case 4: return .failedSetup
default: return .unexpected(NSError())
}
}

View File

@ -6,11 +6,11 @@
var endpoint = "${endpoint}";
var shouldFail = ${shouldFail};
var post = function(value) {
var post = (value) => {
window.webkit.messageHandlers.recaptcha.postMessage(value);
};
var execute = function() {
var execute = () => {
if (shouldFail) {
post("error");
}
@ -19,10 +19,12 @@
}
};
var reset = function() {
var reset = () => {
shouldFail = false;
post({action: "didLoad"});
post({ action: "didLoad" });
};
post({ action: "didLoad" });
</script>
</head>
<body>

View File

@ -2,61 +2,81 @@
<head>
<meta name="viewport" content="width=device-width" />
<script type="text/javascript">
var post = function(value) {
window.webkit.messageHandlers.recaptcha.postMessage(value);
const post = (value) => window.webkit.messageHandlers.recaptcha.postMessage(value);
console.log = (message) => post({ log: message });
let observers = []
const observeDOM = (element, completion) => {
const obs = new MutationObserver(completion);
obs.observe(element, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['style'],
});
observers.push(obs);
};
console.log = function(message) {
post({log: message});
const clearObservers = () => {
observers.forEach(o => o.disconnect());
observers = [];
};
var showReCaptcha = function() {
console.log("showReCaptcha");
post({action: "showReCaptcha"});
};
var observeDOM = function(element, completion) {
new MutationObserver(function(mutations) {
mutations.forEach(function(mutationRecord) {
completion();
});
})
.observe(element, {attributes: true, attributeFilter: ['style']})
};
var execute = function() {
console.log("executing");
const execute = () => {
console.log('executing');
// Removes ReCaptcha dismissal when clicking outside div area
try {
document.getElementsByTagName("div")[4].outerHTML = ""
document.getElementsByTagName('div')[4].outerHTML = ''
}
catch(e) {
}
// Listens to changes on the div element that presents the ReCaptcha challenge
observeDOM(document.getElementsByTagName("div")[3], showReCaptcha);
try {
// Listens to changes on the div element that presents the ReCaptcha challenge
observeDOM(document.getElementsByTagName('div')[3], () => {
post({ action: 'showReCaptcha' });
});
} catch(e) {
post({ error: 27 })
}
grecaptcha.execute();
};
var onSubmit = function(token) {
console.log(token);
post({token: token});
};
const reset = () => {
console.log('resetting');
grecaptcha.reset();
grecaptcha.ready(() => post({ action: 'didLoad' }));
};
var onloadCallback = function() {
console.log("did load");
var onloadCallback = () => {
grecaptcha.render('submit', {
'sitekey' : '${apiKey}',
'callback' : onSubmit,
'size': 'invisible'
sitekey: '${apiKey}',
callback: (token) => {
console.log(token);
post({ token });
clearObservers();
},
size: 'invisible'
});
};
var reset = function() {
console.log("resetting");
grecaptcha.reset();
grecaptcha.ready(() => {
observeDOM(document.getElementById('body'), mut => {
const success = !!mut.find(
({ addedNodes }) =>
Array.from(addedNodes.values())
.find(({ nodeName, name }) =>
nodeName === 'IFRAME' && !!name
)
);
if (success) {
post({ action: 'didLoad' });
}
});
});
};
</script>
</head>

View File

@ -89,6 +89,12 @@ fileprivate extension ReCaptchaDecoder.Result {
if let token = response["token"] as? String {
return .token(token)
}
else if let message = response["log"] as? String {
return .log(message)
}
else if let message = response["error"] as? Int {
return .error(.failedSetup)
}
if let action = response["action"] as? String {
switch action {

View File

@ -25,6 +25,8 @@ public enum ReCaptchaError: Error, CustomStringConvertible {
/// Received an unexpected message from javascript
case wrongMessageFormat
/// ReCaptcha setup failed
case failedSetup
/// A human-readable description for each error
public var description: String {
@ -43,6 +45,14 @@ public enum ReCaptchaError: Error, CustomStringConvertible {
case .wrongMessageFormat:
return "Unexpected message from javascript"
case .failedSetup:
// swiftlint:disable line_length
return """
WARNING! ReCaptcha wasn't successfully configured. Please double check your ReCaptchaKey and ReCaptchaDomain.
Also check that you're using ReCaptcha's **SITE KEY** for client side integration.
"""
// swiftlint:enable line_length
}
}
}

View File

@ -13,6 +13,10 @@ import WebKit
/** Handles comunications with the webview containing the ReCaptcha challenge.
*/
internal class ReCaptchaWebViewManager {
enum JSCommand: String {
case execute = "execute();"
case reset = "reset();"
}
fileprivate struct Constants {
static let ExecuteJSCommand = "execute();"
@ -53,24 +57,11 @@ internal class ReCaptchaWebViewManager {
fileprivate var decoder: ReCaptchaDecoder!
/// Indicates if the script has already been loaded by the `webView`
fileprivate var didFinishLoading = false { // webView.isLoading does not work for WKWebview.loadHTMLString
didSet {
if didFinishLoading && completion != nil {
// User has requested for validation
// A small delay is necessary to allow JS to wrap its operations and avoid errors.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
self?.execute()
}
}
}
}
fileprivate var didFinishLoading = false
/// The observer for `.UIWindowDidBecomeVisible`
fileprivate var observer: NSObjectProtocol?
/// The observer for `\WKWebView.estimatedProgress`
fileprivate var loadingObservation: NSKeyValueObservation?
/// The endpoint url being used
fileprivate var endpoint: String
@ -83,9 +74,6 @@ internal class ReCaptchaWebViewManager {
webview.accessibilityIdentifier = "webview"
webview.accessibilityTraits = UIAccessibilityTraits.link
webview.isHidden = true
self.loadingObservation = webview.observe(\.estimatedProgress, options: [.new]) { [weak self] _, change in
self?.didFinishLoading = change.newValue == 1
}
return webview
}()
@ -135,7 +123,7 @@ internal class ReCaptchaWebViewManager {
webView.isHidden = false
view.addSubview(webView)
execute()
executeJS(command: .execute)
}
@ -150,14 +138,9 @@ internal class ReCaptchaWebViewManager {
The reset is achieved by calling `grecaptcha.reset()` on the JS API.
*/
func reset() {
didFinishLoading = false
configureWebViewDispatchToken = UUID()
webView.evaluateJavaScript(Constants.ResetCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
executeJS(command: .reset)
didFinishLoading = false
}
}
@ -166,22 +149,6 @@ internal class ReCaptchaWebViewManager {
/** Private methods for ReCaptchaWebViewManager
*/
fileprivate extension ReCaptchaWebViewManager {
/** Executes the JS command that loads the ReCaptcha challenge.
This method has no effect if the webview hasn't finished loading.
*/
func execute() {
guard didFinishLoading else {
// Hasn't finished loading the HTML yet
return
}
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
/**
- returns: An instance of `WKWebViewConfiguration`
@ -223,12 +190,14 @@ fileprivate extension ReCaptchaWebViewManager {
}
case .didLoad:
// For testing purposes
didFinishLoading = true
if completion != nil {
executeJS(command: .execute)
}
case .log(let message):
#if DEBUG
print("[JS LOG]:", message)
print("[JS LOG]:", message)
#endif
}
}
@ -249,4 +218,24 @@ fileprivate extension ReCaptchaWebViewManager {
NotificationCenter.default.removeObserver(observer)
}
}
/**
- parameters:
- command: The JavaScript command to be executed
Executes the JS command that loads the ReCaptcha challenge. This method has no effect if the webview hasn't
finished loading.
*/
func executeJS(command: JSCommand) {
guard didFinishLoading else {
// Hasn't finished loading all the resources
return
}
webView.evaluateJavaScript(command.rawValue) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
}

View File

@ -10,10 +10,10 @@ default_platform :ios
platform :ios do
skip_docs
devices = ["iPhone XR (~> 12)"]
devices << "iPhone X (~> 11)" if !Helper.is_ci?
devices << "iPhone 7 (~> 10)" if !Helper.is_ci?
devices << "iPhone 6s (~> 9)" if !Helper.is_ci?
devices = ["iPhone X (~> 12)"]
# devices << "iPhone X (~> 11)" if !Helper.is_ci?
# devices << "iPhone 7 (~> 10)" if !Helper.is_ci?
# devices << "iPhone 6s (~> 9)" if !Helper.is_ci?
desc "Runs the following lanes:\n- test\n- pod_lint\n- carthage_lint"
lane :ci do
@ -57,12 +57,21 @@ platform :ios do
code_coverage: true,
)
if is_ci
if Helper.is_ci?
codecov(
project_name: 'ReCaptcha',
use_xcodeplist: true,
)
else
puts "Running UI Tests"
scan(
test_without_building: true,
devices: self.select_similar_simulator(devices),
scheme: "ReCaptcha_UITests",
workspace: "Example/ReCaptcha.xcworkspace",
code_coverage: true,
)
puts "Not CI: Skipping coverage files upload"
end
end