diff --git a/.prettierignore b/.prettierignore index 4617a743e28..db543108bee 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,4 @@ # This file is pre-built and need not be formatted -packages/auth/src/auth.js +packages/auth packages/firebase/firebase* dist diff --git a/packages/auth/.gitignore b/packages/auth/.gitignore new file mode 100644 index 00000000000..f71b748c809 --- /dev/null +++ b/packages/auth/.gitignore @@ -0,0 +1,17 @@ +# Node modules. +node_modules/ + +# Generated directories. +generated/ +out/ +dist/ + +# Generated files. +*.log +*.pyc +.DS_Store +*~ +*.swp + +#Local config. +**/.firebaserc diff --git a/packages/auth/.npmignore b/packages/auth/.npmignore deleted file mode 100644 index 5729078ddb8..00000000000 --- a/packages/auth/.npmignore +++ /dev/null @@ -1,5 +0,0 @@ -# Directories not needed by end users -/src - -# Files not needed by end users -gulpfile.js diff --git a/packages/auth/CONTRIBUTING.md b/packages/auth/CONTRIBUTING.md new file mode 100644 index 00000000000..3fa84d6782a --- /dev/null +++ b/packages/auth/CONTRIBUTING.md @@ -0,0 +1,36 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +Before we can use your code, you must sign the [Google Individual Contributor +License Agreement](https://cla.developers.google.com/about/google-individual) +(CLA), which you can do online. The CLA is necessary mainly because you own the +copyright to your changes, even after your contribution becomes part of our +codebase, so we need your permission to use and distribute your code. We also +need to be sure of various other things—for instance that you'll tell us if you +know that your code infringes on other people's patents. You don't have to sign +the CLA until after you've submitted your code for review and a member has +approved it, but you must do it before we can put your code into our codebase. + +### Adding new features + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +If this has been discussed in an issue, make sure to mention the issue number. +If not, go file an issue about this to make sure this is a desirable change. + +### Code reviews + +All submissions, including submissions by project members, require review. We +use Github pull requests for this purpose. Please refer to the +[Style Guide](STYLEGUIDE.md) and ensure you respect it before submitting a PR. + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the [Software Grant and Corporate Contributor License +Agreement](https://cla.developers.google.com/about/google-corporate). diff --git a/packages/auth/LICENSE b/packages/auth/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/packages/auth/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/auth/README.md b/packages/auth/README.md index b3b1de7cd83..1a67aaeb8bc 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -5,40 +5,81 @@ dependency on the [`@firebase/app`](https://npm.im/@firebase/app) package on NPM is included by default in the [`firebase`](https://npm.im/firebase) wrapper package. -## Installation +## Table of Contents -You can install this package by running the following in your project: +1. [Developer Setup](#developer-setup) + +## Developer Setup + +### Dependencies + +To set up a development environment to build Firebase-auth from source, you must +have the following installed: +- Node.js (>= 6.0.0) +- npm (should be included with Node.js) +- Java Runtime Environment + +In order to run the tests, you must also have: +- Python (2.7) + +Download the Firebase source and its dependencies with: ```bash -$ npm install @firebase/auth +git clone https://github.com/firebase/firebase-js-sdk.git +cd firebase-js-sdk +yarn install ``` -## Usage +### Building Firebase-auth + +To build the library, run: +```bash +cd packages/auth +yarn build +``` + +This will create output files in the `dist/` folder. + +### Running unit tests. -You can then use the firebase namespace exposed by this package as illustrated -below: +All unit tests can be run on the command line (via Chrome and Firefox) with: -**ES Modules** +```bash +yarn test +``` -```javascript -import firebase from '@firebase/app'; -import '@firebase/auth' +Alternatively, the unit tests can be run manually by running -// Do stuff w/ `firebase` and `firebase.auth` +```bash +yarn run serve ``` -**CommonJS Modules** +Then, all unit tests can be run at: http://localhost:4000/buildtools/all_tests.html +You can also run tests individually by accessing each HTML file under +`generated/tests`, for example: http://localhost:4000/generated/tests/test/auth_test.html + +### Run tests using SauceLabs + +*You need a [SauceLabs](https://saucelabs.com/) account to run tests on +SauceLabs.* -```javascript -const firebase = require('@firebase/app').default; -require('@firebase/auth'); +Go to your SauceLab account, under "My Account", and copy paste the access key. +Now export the following variables, *in two Terminal windows*: -// Do stuff with `firebase` and `firebase.auth` +```bash +export SAUCE_USERNAME= +export SAUCE_ACCESS_KEY= ``` -## Documentation + Then, in one Terminal window, start SauceConnect: + + ```bash +./buildtools/sauce_connect.sh +``` -For comprehensive documentation please see the [Firebase Reference -Docs][reference-docs]. +Take note of the "Tunnel Identifier" value logged in the terminal,at the top. In +the other terminal that has the exported variables, run the tests: -[reference-docs]: https://firebase.google.com/docs/reference/js/ +```bash +yarn test -- --saucelabs --tunnelIdentifier= +``` diff --git a/packages/auth/STYLEGUIDE.md b/packages/auth/STYLEGUIDE.md new file mode 100644 index 00000000000..4fd244b6c19 --- /dev/null +++ b/packages/auth/STYLEGUIDE.md @@ -0,0 +1,34 @@ +# Firebase-auth Web Style Guide + +Here are the style rules to follow for Firebase-auth: + +## #1 Be consistent with the rest of the codebase + +This is the number one rule and should help determine what to do in most cases. + +## #2 Respect Google JavaScript style guide + +The style guide accessible +[here](https://google.github.io/styleguide/javascriptguide.xml) has to be fully +respected. + +## #3 Follow these grammar rules + +- Functions descriptions have to start with a verb using the third person of the +singular. + - *Ex: `/** Tests the validity of the input. */`* +- Inline comments within procedures should always use the imperative. + - *Ex: `// Check whether the value is true.`* +- Acronyms have to be uppercased in comments. + - *Ex: `// IP, DOM, CORS, URL...`* + - *Exception: Identity Provider = IdP* +- Acronyms have to be capitalized (but not uppercased) in variable names. + - *Ex: `redirectUrl()`, `signInIdp()`* +- Never use login/log in in comments. Use “sign-in” if it’s a noun, “sign in” if +it’s a verb. The same goes for the variable name. Never use `login`; always use +`signIn`. + - *Ex: `// The sign-in method.`* + - *Ex: `// Signs in the user.`* +- Always start an inline comment with a capital (unless referring to the name of +a variable/function), and end it with a period. + - *Ex: `// This is a valid inline comment.`* diff --git a/packages/auth/buildtools/all_tests.html b/packages/auth/buildtools/all_tests.html new file mode 100644 index 00000000000..83de0a0e247 --- /dev/null +++ b/packages/auth/buildtools/all_tests.html @@ -0,0 +1,24 @@ + + + + + FirebaseUI Auth Tests + + + + + + +

Firebase Auth Tests

+
+ + + diff --git a/packages/auth/buildtools/common.py b/packages/auth/buildtools/common.py new file mode 100644 index 00000000000..c5a473ba4e7 --- /dev/null +++ b/packages/auth/buildtools/common.py @@ -0,0 +1,44 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Common methods and constants for generating test files.""" +import os + + +# The directory in which test HTML files are generated. +TESTS_BASE_PATH = "./generated/tests/" + + +def cd_to_firebaseauth_root(): + """Changes the current directory to the firebase-auth root directory. + This method assumes that this script is in the buildtools/ directory, which is + a direct child of the root directory. + This allows us to avoid writing to the wrong files. + """ + root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + os.chdir(root_dir) + + +def get_files_with_suffix(root, suffix): + """Yields file names under a directory with a given suffix. + Args: + root: The path to the directory where we wish to search. + suffix: The suffix we wish to search for. + Yields: + The paths to files under the directory that have the given suffix. + """ + for root, _, files in os.walk(root): + for file_name in files: + if file_name.endswith(suffix): + yield os.path.join(root, file_name) diff --git a/packages/auth/buildtools/gen_all_tests_js.py b/packages/auth/buildtools/gen_all_tests_js.py new file mode 100644 index 00000000000..ced11b5d9de --- /dev/null +++ b/packages/auth/buildtools/gen_all_tests_js.py @@ -0,0 +1,45 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generates the all_tests.js file. +all_tests.js tells all_tests.html the paths to the files to test. +Usage: +$ python ./buildtools/gen_all_tests_js.py > generated/all_tests.js +""" + +import common + + +def main(): + common.cd_to_firebaseauth_root() + print "var allTests = [" + _print_test_files_under_root(common.TESTS_BASE_PATH) + print "];" + # The following is required in the context of protractor. + print "if (typeof module !== 'undefined' && module.exports) {" + print " module.exports = allTests;" + print "}" + + +def _print_test_files_under_root(root): + """Prints all test HTML files found under a given directory (recursively). + Args: + root: The path to the directory. + """ + for file_name in common.get_files_with_suffix(root, "_test.html"): + print " '%s'," % file_name[2:] # Ignore the beginning './'. + + +if __name__ == "__main__": + main() diff --git a/packages/auth/buildtools/gen_test_html.py b/packages/auth/buildtools/gen_test_html.py new file mode 100644 index 00000000000..90a2362c268 --- /dev/null +++ b/packages/auth/buildtools/gen_test_html.py @@ -0,0 +1,122 @@ +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generates *_test.html files from *_test.js files. +This modifies files in place and will overwrite existing *_test.html files. +Usage: +$ python ./buildtools/gen_test_html.py +""" + +from collections import namedtuple +import os +import re +from string import Template +import common + + +# Stores the paths of files related to a test file +# (e.g. *_test.html, *_test_dom.html) +RelatedPaths = namedtuple("RelatedPaths", ["html", "dom"]) + + +# The root-level directories containing JS tests. +DIRECTORIES_WITH_TESTS = ["test"] + + +def main(): + common.cd_to_firebaseauth_root() + template_data = _read_file("./buildtools/test_template.html") + template = Template(template_data) + for directory in DIRECTORIES_WITH_TESTS: + for js_path in common.get_files_with_suffix(directory, "_test.js"): + _gen_html(js_path, template) + + +def _gen_html(js_path, template): + """Generates a Closure test HTML wrapper file and saves it to the filesystem. + Args: + js_path: The path to the JS test (*_test.js) file. + template: The template for the HTML wrapper. + """ + try: + related_paths = _get_related_paths_from_js_path(js_path) + js_data = _read_file(js_path) + dom = (_read_file(related_paths.dom) + if os.path.isfile(related_paths.dom) else "") + package = _extract_closure_package(js_data) + generated_html = template.substitute(package=package, dom=dom) + + _write_file(related_paths.html, generated_html) + + except: # pylint: disable=bare-except + print "HTML generation failed for: %s" % js_path + + +def _get_related_paths_from_js_path(js_path): + """Converts the JS test file path to paths of related files. + For example, ./path/to/foo_test.js becomes + ./generated/tests/path/to/foo_test.html + ./path/to/foo_test_dom.html + Args: + js_path: The path to the JS test (*_test.js) file. + Returns: + The paths to the related files, as a RelatedPaths. + """ + base_name = os.path.splitext(js_path)[0] + return RelatedPaths(common.TESTS_BASE_PATH + base_name + ".html", + base_name + "_dom.html") + + +def _extract_closure_package(js_data): + """Extracts the package name that is goog.provide()d in the JS file. + Args: + js_data: The contents of a JS test (*_test.js) file. + Returns: + The closure package goog.provide()d by the file. + Raises: + ValueError: The JS does not contain a goog.provide(). + """ + matches = re.search(r"goog\.provide\('(.+)'\);", js_data) + if matches is None: + raise ValueError("goog.provide() not found in file") + return matches.group(1) + + +def _read_file(path): + """Reads a file into a string. + Args: + path: The path to a file. + Returns: + The contents of the file. + """ + with open(path) as f: + return f.read() + + +def _write_file(path, contents): + """Writes a string to file, overwriting existing content. + Intermediate directories are created if not present. + Args: + path: The path to a file. + contents: The string to write to the file. + """ + dir_name = os.path.dirname(path) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + with open(path, "w") as f: + f.write(contents) + + +if __name__ == "__main__": + main() diff --git a/packages/auth/buildtools/generate_test_files.sh b/packages/auth/buildtools/generate_test_files.sh new file mode 100755 index 00000000000..cd5e56f151d --- /dev/null +++ b/packages/auth/buildtools/generate_test_files.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generates the temporary files needed for tests to run, putting them in the +# generated/ directory. +# +# Usage: +# $ buildtools/generate_test_files.sh + +# CD to the root firebase-auth directory, which should be the parent directory of +# buildtools/. +cd "$(dirname $(dirname "$0"))" +mkdir -p generated + +echo "Generating dependency file..." +python ../../node_modules/google-closure-library/closure/bin/build/depswriter.py \ + --root_with_prefix="test ../../../../test" \ + --root_with_prefix="src ../../../../src" \ + > generated/deps.js + +echo "Generating test HTML files..." +python ./buildtools/gen_test_html.py +python ./buildtools/gen_all_tests_js.py > generated/all_tests.js + +echo "Done." diff --git a/packages/auth/buildtools/run_tests.sh b/packages/auth/buildtools/run_tests.sh new file mode 100755 index 00000000000..86544abba61 --- /dev/null +++ b/packages/auth/buildtools/run_tests.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Prepares the setup for running unit tests. It starts a Selenium Webdriver. +# creates a local webserver to serve test files, and run protractor. +# +# Usage: +# +# ./buildtools/run_tests.sh [--saucelabs [--tunnelIdentifier=]] +# +# Can take up to two arguments: +# --saucelabs: Use SauceLabs instead of local Chrome and Firefox. +# --tunnelIdentifier=: when using SauceLabs, specify the tunnel +# identifier. Otherwise, uses the environment variable TRAVIS_JOB_NUMBER. +# +# Prefer to use the `npm test` command as explained below. +# +# Run locally with Chrome & Firefox: +# $ npm test +# It will start a local Selenium Webdriver server as well as the HTTP server +# that serves test files. +# +# Run locally using SauceLabs: +# Go to your SauceLab account, under "My Account", and copy paste the +# access key. Now export the following variables: +# $ export SAUCE_USERNAME= +# $ export SAUCE_ACCESS_KEY= +# Then, start SauceConnect: +# $ ./buildtools/sauce_connect.sh +# Take note of the "Tunnel Identifier" value logged in the terminal. +# Run the tests: +# $ npm run -- --saucelabs --tunnelIdentifier= +# This will start the HTTP Server locally, and connect through SauceConnect +# to SauceLabs remote browsers instances. +# +# Travis will run `npm test -- --saucelabs`. + +cd "$(dirname $(dirname "$0"))" + +function killServer () { + if [ "$seleniumStarted" = true ]; then + echo "Stopping Selenium..." + ./node_modules/.bin/webdriver-manager shutdown + ./node_modules/.bin/webdriver-manager clean + # Selenium is not getting shutdown. Send a kill signal. + lsof -t -i :4444 | xargs kill + fi + echo "Killing HTTP Server..." + kill $serverPid +} + +# Start the local webserver. +./node_modules/.bin/gulp serve & +serverPid=$! +echo "Local HTTP Server started with PID $serverPid." + +trap killServer EXIT + +# If --saucelabs option is passed, forward it to the protractor command adding +# the second argument that is required for local SauceLabs test run. +if [[ $1 = "--saucelabs" ]]; then + seleniumStarted=false + sleep 2 + echo "Using SauceLabs." + # $2 contains the tunnelIdentifier argument if specified, otherwise is empty. + ./node_modules/.bin/protractor protractor.conf.js --saucelabs $2 +else + echo "Using Chrome and Firefox." + ./node_modules/.bin/webdriver-manager clean + # Updates Selenium Webdriver. + ./node_modules/.bin/webdriver-manager update + # Start Selenium Webdriver. + ./node_modules/.bin/webdriver-manager start &>/dev/null & + seleniumStarted=true + echo "Selenium Server started." + # Wait for servers to come up. + sleep 10 + ./node_modules/.bin/protractor protractor.conf.js +fi diff --git a/packages/auth/buildtools/sauce_connect.sh b/packages/auth/buildtools/sauce_connect.sh new file mode 100755 index 00000000000..a75d2671af9 --- /dev/null +++ b/packages/auth/buildtools/sauce_connect.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Copyright 2016 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Download and install SauceConnect under Linux 64-bit. To be used when testing +# with SauceLabs locally. See the instructions in protractor.conf.js file. +# +# It should not be used on Travis. Travis already handles SauceConnect. +# +# Script copied from the Closure Library repository: +# https://github.com/google/closure-library/blob/master/scripts/ci/sauce_connect.sh +# + +# Setup and start Sauce Connect locally. +CONNECT_URL="https://saucelabs.com/downloads/sc-4.4.1-linux.tar.gz" +CONNECT_DIR="/tmp/sauce-connect-$RANDOM" +CONNECT_DOWNLOAD="sc-latest-linux.tar.gz" + +BROWSER_PROVIDER_READY_FILE="/tmp/sauce-connect-ready" + +# Get Connect and start it. +mkdir -p $CONNECT_DIR +cd $CONNECT_DIR +curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null +mkdir sauce-connect +tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 \ + --directory=sauce-connect > /dev/null +rm $CONNECT_DOWNLOAD + +function removeFiles() { + echo "Removing SauceConnect files..." + rm -rf $CONNECT_DIR +} + +trap removeFiles EXIT + +# This will be used by Protractor to connect to SauceConnect. +TUNNEL_IDENTIFIER="tunnelId-$RANDOM" +echo "" +echo "=========================================================================" +echo " Tunnel Identifier to pass to Protractor:" +echo " $TUNNEL_IDENTIFIER" +echo "=========================================================================" +echo "" +echo "" + +echo "Starting Sauce Connect..." + +# Start SauceConnect. +sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY \ + -i $TUNNEL_IDENTIFIER diff --git a/packages/auth/buildtools/test_template.html b/packages/auth/buildtools/test_template.html new file mode 100644 index 00000000000..096a3f18aa1 --- /dev/null +++ b/packages/auth/buildtools/test_template.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + $dom + + diff --git a/packages/auth/externs/externs.js b/packages/auth/externs/externs.js new file mode 100644 index 00000000000..faefe2524ae --- /dev/null +++ b/packages/auth/externs/externs.js @@ -0,0 +1,41 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Firebase Auth-specific externs. + */ + + +/** + * A verifier that asserts that the user calling an API is a real user. + * @interface + */ +firebase.auth.ApplicationVerifier = function() {}; + + +/** + * The type of the ApplicationVerifier assertion, e.g. "recaptcha". + * @type {string} + */ +firebase.auth.ApplicationVerifier.prototype.type; + + +/** + * Returns a promise for the assertion to verify the app identity, e.g. the + * g-recaptcha-response in reCAPTCHA. + * @return {!firebase.Promise} + */ +firebase.auth.ApplicationVerifier.prototype.verify = function() {}; diff --git a/packages/auth/externs/gapi.iframes.js b/packages/auth/externs/gapi.iframes.js new file mode 100644 index 00000000000..614a5945a16 --- /dev/null +++ b/packages/auth/externs/gapi.iframes.js @@ -0,0 +1,502 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provide gapi.iframes public api. + * + * @externs + */ + + +var gapi = {}; + +/** + * Namespace associated with gapi iframes API. + * @const + */ +gapi.iframes = {}; + +/** + * Type for options bag for create and open functions. + * Please use gapix.iframes.Options to construct the options. + * (See javascript/abc/iframes/api/options.js) + * @typedef {Object} + **/ +gapi.iframes.OptionsBag; + +/** + * Type of iframes filter function. + * @typedef {function(gapi.iframes.Iframe):boolean} + **/ +gapi.iframes.IframesFilter; + +/** + * Message handlers type. The iframe the message came from is passed in as + * 'this'. The handler can return any value or a Promise for an async response. + * @typedef {function(this:gapi.iframes.Iframe, *, + * !gapi.iframes.Iframe): (*|Thenable)} + **/ +gapi.iframes.MessageHandler; + +/** + * Sent message callback function type. + * @typedef {function(Array<*>)} + **/ +gapi.iframes.SendCallback; + +/** + * Style function which processes an open request parameter set. + * It can create the new iframe container and style it, + * and update the open request parameters accordingly. + * It can add message handlers to support style specific behavior. + * @typedef {function(gapi.iframes.OptionsBag)} + */ +gapi.iframes.StyleHandler; + +/** + * Message filter handler type. + * @typedef {function(this:gapi.iframes.Iframe, *): + * (boolean|IThenable)} + **/ +gapi.iframes.RpcFilter; + +/** + * Create a new iframe, pass abc context. + * @param {string} url the url for the opened iframe. + * @param {Element} whereToPut the location to put the new iframe. + * @param {gapi.iframes.OptionsBag=} opt_options extra options for the iframe. + * @return {Element} the new iframe dom element. + */ +gapi.iframes.create = function(url, whereToPut, opt_options) {}; + +/** + * Class to handle the iframes context. + * This contains info about the current iframe - parent, pub/sub etc. + * In most cases there will be one object for this (selfContext), + * but for controller iframe, separate object will be created for + * each controlled iframe. + * @param {gapi.iframes.OptionsBag=} opt_options Context override options. + * @constructor + */ +gapi.iframes.Context = function(opt_options) {}; + +/** + * Get the default context for current frame. + * @return {gapi.iframes.Context} The current context. + */ +gapi.iframes.getContext = function() {}; + +/** + * Implement an iframes filter to check same origin connection. + * @param {gapi.iframes.Iframe} iframe - The iframe to check for same + * origin as current context. + * @return {boolean} true if the iframe has same domain has the context. + */ +gapi.iframes.SAME_ORIGIN_IFRAMES_FILTER = function(iframe) {}; + +/** + * Implement a filter that accept any iframe. + * This should be used only if the message handler sanitize the data, + * and the code that use it must go through security review. + * @param {gapi.iframes.Iframe} iframe The iframe to check. + * @return {boolean} always true. + */ +gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER = function(iframe) {}; + +/** + * Create an iframes filter that allow iframes from a list of origins. + * @param {Array} origins List of allowed origins. + * @return {gapi.iframes.IframesFilter} New iframes filter that allow, + * iframes only from the provided origins. + */ +gapi.iframes.makeWhiteListIframesFilter = function(origins) {}; + +/** + * Check if the context was disposed. + * @return {boolean} True if the context was disposed. + */ +gapi.iframes.Context.prototype.isDisposed = function() {}; + +/** + * @return {string} Iframe current page frame name. + */ +gapi.iframes.Context.prototype.getFrameName = function() {}; + +/** + * Get the context window object. + * @return {Window} The window object. + */ +gapi.iframes.Context.prototype.getWindow = function() {}; + +/** + * Get context global parameters. + * @param {string} key Parameter name. + * @return {*} Parameter value. + */ +gapi.iframes.Context.prototype.getGlobalParam = function(key) {}; + +/** + * Set context global parameters. + * @param {string} key Parameter name. + * @param {*} value Parameter value. + */ +gapi.iframes.Context.prototype.setGlobalParam = function(key, value) {}; + +/** + * Register a new style. + * @param {string} style The new style name. + * @param {gapi.iframes.StyleHandler} func The style handler. + */ +gapi.iframes.registerStyle = function(style, func) {}; + +/** + * Register a new style to handle options before relaying request. + * @param {string} style The new style name. + * @param {gapi.iframes.StyleHandler} func The style handler. + */ +gapi.iframes.registerBeforeOpenStyle = function(style, func) {}; + +/** + * Get style hanlder. + * @param {string} style The new style name. + * @return {gapi.iframes.StyleHandler} The style handler. + */ +gapi.iframes.getStyle = function(style) {}; + +/** + * Get a style hanlder for open options before relaying request. + * @param {string} style The new style name. + * @return {gapi.iframes.StyleHandler} The style handler. + */ +gapi.iframes.getBeforeOpenStyle = function(style) {}; + +/** + * Open a new child iframe and attach rpc to it. + * @param {!gapi.iframes.OptionsBag} options Open parameters. + * @return {!gapi.iframes.Iframe} The new Iframe object. + */ +gapi.iframes.Context.prototype.openChild = function(options) {}; + +/** + * Open a new iframe, support relay open to parent or other iframe. + * @param {!gapi.iframes.OptionsBag} options Open parameters. + * @param {function(gapi.iframes.Iframe)=} opt_callback Callback to be called. + * with the created iframe. + * @return {!IThenable} The created iframe. + */ +gapi.iframes.Context.prototype.open = function(options, opt_callback) {}; + +/** + * Get the context parent Iframe if available. + * (Available if current iframe has an id). + * @return {gapi.iframes.Iframe} Parent iframe. + */ +gapi.iframes.Context.prototype.getParentIframe = function() {}; + +/** + * An Iframe object to represent an iframe that can be communicated with. + * Use send to send a message to the iframe, and register to set a handler + * for a message from the iframe. + * @param {gapi.iframes.Context} context New iframe context. + * @param {string} rpcAddr rpc routing to the iframe. + * @param {string} frameName The frame-name the rpc messages are identified by. + * @param {gapi.iframes.OptionsBag} options Iframe options. + * @constructor + */ +gapi.iframes.Iframe = function(context, rpcAddr, frameName, options) {}; + +/** + * Check if the iframe was disposed. + * @return {boolean} True if the iframe was disposed. + */ +gapi.iframes.Iframe.prototype.isDisposed = function() {}; + +/** + * Get the Iframe context. + * @return {gapi.iframes.Context} Iframe context. + */ +gapi.iframes.Iframe.prototype.getContext = function() {}; + +/** + * Get the Iframe name. + * @return {string} Iframe frame-name. + */ +gapi.iframes.Iframe.prototype.getFrameName = function() {}; + +/** + * @return {string} Iframe id. + */ +gapi.iframes.Iframe.prototype.getId = function() {}; + +/** + * Get Iframe parameters. + * @param {string} key Parameter name. + * @return {*} Parameter value. + */ +gapi.iframes.Iframe.prototype.getParam = function(key) {}; + +/** + * Get Iframe parameters. + * @param {string} key Parameter name. + * @param {*} value Parameter value. + */ +gapi.iframes.Iframe.prototype.setParam = function(key, value) {}; + +/** + * Register a message handler. + * The handler should have two parameters: the Iframe object and message data. + * @param {string} message The message to register for. + * @param {gapi.iframes.MessageHandler} func Message handler. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overridden. + */ +gapi.iframes.Iframe.prototype.register = function(message, func, opt_filter) {}; + +/** + * Un-register a message handler. + * @param {string} message Message to unregister from. + * @param {gapi.iframes.MessageHandler=} opt_func Optional message handler, + * if specified only that handler is unregistered, + * otherwise all handlers for the message are unregistered. + */ +gapi.iframes.Iframe.prototype.unregister = function(message, opt_func) {}; + +/** + * Send a message to the Iframe. + * If there is no handler for the message, it will be queued, + * and the callback will be called only when an handler is registered. + * @param {string} message Message name. + * @param {*=} opt_data The data to send to the iframe. + * @param {gapi.iframes.SendCallback=} opt_callback Callback function to call + * with return values of handler for the message (list). + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overridden. + * @return {!IThenable} Array of return values of all handlers. + */ +gapi.iframes.Iframe.prototype.send = + function(message, opt_data, opt_callback, opt_filter) {}; + +/** +`* Send a ping to the iframe whcih echo back the optional data. + * Useful to check if the iframe is responsive/correct. + * @param {!gapi.iframes.SendCallback} callback Callback function to call + * with return values (array of first element echo of opt_data). + * @param {*=} opt_data The data to send to the iframe. + * @return {!IThenable} Array of return values of all handlers. + */ +gapi.iframes.Iframe.prototype.ping = function(callback, opt_data) {}; + +/** + * Add iframes api registry. + * @param {string} apiName The api name. + * @param {Object} registry + * Map of handlers. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overridden. + */ +gapi.iframes.registerIframesApi = function(apiName, registry, opt_filter) {}; + +/** + * Utility function to build api by adding handler one by one. + * Should be used on initialization time only + * (is not applied on already opened iframes). + * @param {string} apiName The api name. + * @param {string} message The message name to register an handler for. + * @param {gapi.iframes.MessageHandler} handler The handler to register. + */ +gapi.iframes.registerIframesApiHandler = function(apiName, message, handler) {}; + +/** + * Apply an iframes api on the iframe. + * @param {string} api Name of the api. + */ +gapi.iframes.Iframe.prototype.applyIframesApi = function(api) {}; + +/** + * Get the dom node for the iframe. + * Return null if the iframe is not a direct child iframe. + * @return {?Element} the iframe dom node. + */ +gapi.iframes.Iframe.prototype.getIframeEl = function() {}; + +/** + * Get the iframe container dom node. + * The site element can be override by the style when + * the container of the iframe is more then simple parent. + * The site element is used as reference when positioning + * other iframes relative the an iframe. + * @return {?Element} The iframe container dom node. + */ +gapi.iframes.Iframe.prototype.getSiteEl = function() {}; + +/** + * Set the iframe container dom node. + * Can be used by style code to indicate a more complex dom + * to contain the iframe. + * @param {!Element} el The iframe container dom node. + */ +gapi.iframes.Iframe.prototype.setSiteEl = function(el) {}; + +/** + * Get the Window object of the remote iframe. + * It is only supported for same origin iframes, otherwise return null. + * @return {?Window} The window object for the iframe or null. + */ +gapi.iframes.Iframe.prototype.getWindow = function() {}; + +/** + * Get the iframe url origin. + * @return {string} Iframe url origin. + */ +gapi.iframes.Iframe.prototype.getOrigin = function() {}; + +/** + * Send a request to close the iframe. + * @param {*=} opt_params Optional parameters. + * @param {gapi.iframes.SendCallback=} opt_callback + * Optional callback to indicate close was done or canceled. + * @return {!IThenable} Array of return values of all handlers. + */ +gapi.iframes.Iframe.prototype.close = function(opt_params, opt_callback) {}; + +/** + * Send a request to change the iframe style. + * @param {*} styleData Restyle parameters. + * @param {gapi.iframes.SendCallback=} opt_callback + * Optional callback to indicate restyle was done or canceled. + * @return {!IThenable} Array of return values of all handlers. + */ +gapi.iframes.Iframe.prototype.restyle = function(styleData, opt_callback) {}; + +/** + * Register a handler on relayed open iframe to get restyle status. + * Called in the context of the opener, to handle style changes events. + * @param {gapi.iframes.MessageHandler} handler Message handler. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * default is same origin filter, which is not used if overridden. + */ +gapi.iframes.Iframe.prototype.registerWasRestyled = + function(handler, opt_filter) {}; + +/** + * Register a callback to be notified on iframe closed. + * Called in the context of the opener, to handle close event. + * @param {gapi.iframes.MessageHandler} handler Message handler. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overrided. + */ +gapi.iframes.Iframe.prototype.registerWasClosed = function( + handler, opt_filter) {}; + +/** + * Close current iframe (send request to parent). + * @param {*=} opt_params Optional parameters. + * @param {function(this:gapi.iframes.Iframe, boolean)=} opt_callback + * Optional callback to indicate close was done or canceled. + * @return {!IThenable} True if close request was issued, + * false if close was denied. + */ +gapi.iframes.Context.prototype.closeSelf = + function(opt_params, opt_callback) {}; + +/** + * Restyle current iframe (send request to parent). + * @param {*} styleData Restyle parameters. + * @param {function(this:gapi.iframes.Iframe, boolean)=} opt_callback + * Optional callback to indicate restyle was done or canceled. + * @return {!IThenable} True if restyle request was issued, + * false if restyle was denied. + */ +gapi.iframes.Context.prototype.restyleSelf = + function(styleData, opt_callback) {}; + +/** + * Style helper function to indicate current iframe is ready. + * It send ready both to parent and opener, and register methods on + * the opener. + * Adds calculated height of current page if height not provided. + * @param {Object=} opt_params Ready message data. + * @param {Object=} opt_methods + * Map of message handler to register on the opener. + * @param {gapi.iframes.SendCallback=} opt_callback Ready message callback. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * used for sending ready and register provided methods to opener. + */ +gapi.iframes.Context.prototype.ready = + function(opt_params, opt_methods, opt_callback, opt_filter) {}; + +/** + * Provide a filter for close request on current iframe. + * @param {gapi.iframes.RpcFilter} filter Filter function. + * @return {undefined} + */ +gapi.iframes.Context.prototype.setCloseSelfFilter = function(filter) {}; + +/** + * Provide a filter for restyle request on current iframe. + * @param {gapi.iframes.RpcFilter} filter Filter function. + * @return {undefined} + */ +gapi.iframes.Context.prototype.setRestyleSelfFilter = function(filter) {}; + + +/** + * Connect between two iframes, provide the subject each side + * is subscribed to. + * @param {gapi.iframes.OptionsBag} iframe1Data - one side of connection. + * @param {gapi.iframes.OptionsBag=} opt_iframe2Data - other side of connection. + * Supported options: + * iframe: the iframe to connect to. + * role: the role to send to that iframe (optional). + * data: the data to send to that iframe (optional). + * isReady: indicate that we do not need for ready from the other side. + */ +gapi.iframes.Context.prototype.connectIframes = + function(iframe1Data, opt_iframe2Data) {}; + +/** + * Configure how to handle new connection by role. + * Provide a filter, list of apis, and callback to be notified on connection. + * @param {string|gapi.iframes.OptionsBag} optionsOrRole Connection options + * object (see gapix.iframes.OnConnectOptions}, or connect role. + * Options are preferred, and if provided the next optional params will + * be ignored. + * @param {function(!gapi.iframes.Iframe, Object<*>=)=} opt_handler Callback + * for the new connection. + * @param {Array=} opt_apis List of iframes apis to apply to + * connected iframes. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overridden. + */ +gapi.iframes.Context.prototype.addOnConnectHandler = + function(optionsOrRole, opt_handler, opt_apis, opt_filter) {}; + +/** + * Remove all on connect handlers for a role. + * @param {string} role Connection role. + */ +gapi.iframes.Context.prototype.removeOnConnectHandler = function(role) {}; + +/** + * Helper function to set handler for the opener connection. + * @param {function(gapi.iframes.Iframe)} handler Callback for new connection. + * @param {Array=} opt_apis List of iframes apis to apply to + * connected iframes. + * @param {gapi.iframes.IframesFilter=} opt_filter Optional iframe filter, + * Default is same origin filter, which is not used if overridden. + */ +gapi.iframes.Context.prototype.addOnOpenerHandler = + function(handler, opt_apis, opt_filter) {}; diff --git a/packages/auth/externs/grecaptcha.js b/packages/auth/externs/grecaptcha.js new file mode 100644 index 00000000000..6327d34aae4 --- /dev/null +++ b/packages/auth/externs/grecaptcha.js @@ -0,0 +1,67 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Public externs for the recaptcha javascript API. + * @externs + */ + + +/** + * The namespace for reCaptcha V2. + * + * https://developers.google.com/recaptcha/docs/display#js_api + */ +var grecaptcha = {}; + + +/** + * Creates a new instance of the recaptcha client. + * + * @param {(!Element|string)} elementOrId Element or element id for the + * placeholder to render the recaptcha client. + * @param {!Object} params Parameters for the recaptcha client. + * @return {number} The client id. + */ +grecaptcha.render = function(elementOrId, params) {}; + + +/** + * Resets a client with the given id. If an id is not provided, resets the + * default client. + * + * @param {number=} opt_id The id of the recaptcha client. + * @param {?Object=} opt_params Parameters for the recaptcha client. + */ +grecaptcha.reset = function(opt_id, opt_params) {}; + + +/** + * Gets the response for the client with the given id. If an id is not + * provided, gets the response for the default client. + * + * @param {number=} opt_id The id of the recaptcha client. + * @return {string} + */ +grecaptcha.getResponse = function(opt_id) {}; + + +/** + * Programmatically triggers the invisible reCAPTCHA. + * + * @param {number=} opt_id The id of the recaptcha client. + */ +grecaptcha.execute = function(opt_id) {}; diff --git a/packages/auth/gulpfile.js b/packages/auth/gulpfile.js index ce5c0cb6c60..30ee7f0c4ab 100644 --- a/packages/auth/gulpfile.js +++ b/packages/auth/gulpfile.js @@ -15,27 +15,69 @@ */ const gulp = require('gulp'); -const through2 = require('through2'); - -function buildModule() { - return gulp - .src('src/auth.js') - .pipe( - through2.obj(function(file, encoding, callback) { - file.contents = Buffer.concat([ - new Buffer( - `var firebase = require('@firebase/app').default; (function(){` - ), - file.contents, - new Buffer( - `}).call(typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {});` - ) - ]); - - return callback(null, file); - }) - ) - .pipe(gulp.dest('dist')); -} - -gulp.task('build', buildModule); +const closureCompiler = require('gulp-closure-compiler'); +const del = require('del'); +const express = require('express'); +const path = require('path'); + +// The optimization level for the JS compiler. +// Valid levels: WHITESPACE_ONLY, SIMPLE_OPTIMIZATIONS, ADVANCED_OPTIMIZATIONS. +// TODO: Add ability to pass this in as a flag. +const OPTIMIZATION_LEVEL = 'ADVANCED_OPTIMIZATIONS'; + +// For minified builds, wrap the output so we avoid leaking global variables. +const OUTPUT_WRAPPER = `(function() { + var firebase = require('@firebase/app').default; + %output% +}).call(typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : {});`; + +// The path to Closure Compiler. +const COMPILER_PATH = `${path.dirname(require.resolve('google-closure-compiler/package.json'))}/compiler.jar`; + +const closureLibRoot = path.dirname(require.resolve('google-closure-library/package.json')); +// Builds the core Firebase-auth JS. +gulp.task('build-firebase-auth-js', () => + gulp + .src([ + `${closureLibRoot}/closure/goog/**/*.js`, + `${closureLibRoot}/third_party/closure/goog/**/*.js`, + 'src/**/*.js' + ]) + .pipe(closureCompiler({ + compilerPath: COMPILER_PATH, + fileName: 'auth.js', + compilerFlags: { + closure_entry_point: 'fireauth.exports', + compilation_level: OPTIMIZATION_LEVEL, + externs: [ + 'externs/externs.js', + 'externs/grecaptcha.js', + 'externs/gapi.iframes.js', + path.resolve(__dirname, '../firebase/externs/firebase-app-externs.js'), + path.resolve(__dirname, '../firebase/externs/firebase-error-externs.js'), + path.resolve(__dirname, '../firebase/externs/firebase-app-internal-externs.js') + ], + language_out: 'ES5', + only_closure_dependencies: true, + output_wrapper: OUTPUT_WRAPPER + } + })) + .pipe(gulp.dest('dist'))); + +// Deletes intermediate files. +gulp.task('clean', () => del(['dist/*', 'dist'])); + +// Creates a webserver that serves all files from the root of the package. +gulp.task('serve', () => { + const app = express(); + + app.use('/node_modules', express.static(path.resolve(__dirname, '../../node_modules'))); + app.use(express.static(__dirname)); + + app.listen(4000); +}); + + +gulp.task( + 'default', + ['build-firebase-auth-js']); diff --git a/packages/auth/package.json b/packages/auth/package.json index 1875c8a52b2..eb1ea1a6d00 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -2,24 +2,33 @@ "name": "@firebase/auth", "version": "0.2.0", "main": "dist/auth.js", + "description": "Javascript library for Firebase Auth SDK", + "files": [ + "dist/**", + "LICENSE", + "README.md", + "package.json" + ], "scripts": { - "dev": "echo 'Skipping @firebase/auth dev step'", - "test": "echo 'Skipping @firebase/auth test step'", - "prepare": "gulp build" + "build": "gulp", + "test": "npm run build && npm run generate-test-files && ./buildtools/run_tests.sh", + "serve": "npm run build && npm run generate-test-files && gulp serve", + "generate-test-files": "./buildtools/generate_test_files.sh", + "prepare": "npm run build" }, + "author": "Google", "license": "Apache-2.0", - "peerDependencies": { - "@firebase/app": "^0.1.0" - }, "devDependencies": { - "gulp": "gulpjs/gulp#4.0", - "through2": "^2.0.3" + "closure-builder": "^2.0.17", + "del": "^2.2.2", + "express": "^4.16.2", + "google-closure-compiler": "^20170910.0.0", + "google-closure-library": "^20170910.0.0", + "gulp": "^3.9.1", + "gulp-closure-compiler": "^0.4.0", + "protractor": "^5.1.2" }, - "repository": { - "type": "git", - "url": "https://github.com/firebase/firebase-js-sdk/tree/master/packages/auth" - }, - "bugs": { - "url": "https://github.com/firebase/firebase-js-sdk/issues" + "peerDependencies": { + "@firebase/app": "^0.1.0" } } diff --git a/packages/auth/protractor.conf.js b/packages/auth/protractor.conf.js new file mode 100644 index 00000000000..57a71ef2ddb --- /dev/null +++ b/packages/auth/protractor.conf.js @@ -0,0 +1,114 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Stores the configuration of Protractor. It is loaded by protractor to run + * tests. + * + * Usage: + * + * Run locally: + * $ npm test + * It will start a local Selenium Webdriver server as well as the HTTP server + * that serves test files. + * + * Run locally using SauceLabs: + * Go to your SauceLab account, under "My Account", and copy paste the + * access key. Now export the following variables: + * $ export SAUCE_USERNAME= + * $ export SAUCE_ACCESS_KEY= + * Then, start SauceConnect: + * $ ./buildtools/sauce_connect.sh + * Take note of the "Tunnel Identifier" value logged in the terminal. + * Run the tests: + * $ npm test -- --saucelabs --tunnelIdentifier= + * This will start the HTTP Server locally, and connect through SauceConnect + * to SauceLabs remote browsers instances. + * + * Travis will run `npm test -- --saucelabs`. + */ + +// Common configuration. +config = { + // Using jasmine to wrap Closure JSUnit tests. + framework: 'jasmine', + // The jasmine specs to run. + specs: ['protractor_spec.js'], + // Jasmine options. Increase the timeout to 5min instead of the default 30s. + jasmineNodeOpts: { + // Default time to wait in ms before a test fails. + defaultTimeoutInterval: 5 * 60 * 1000 + } +}; + +// Read arguments to the protractor command. +// The first 3 arguments are something similar to: +// [ '.../bin/node', +// '.../node_modules/.bin/protractor', +// 'protractor.conf.js' ] +var arguments = process.argv.slice(3); + +// Default options: run tests locally (saucelabs false) and use the env variable +// TRAVIS_JOB_NUMBER to get the tunnel identifier, when using saucelabs. +var options = { + saucelabs: false, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER +}; + +for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + if (arg == '--saucelabs') { + options.saucelabs = true; + } else if (arg.indexOf('--tunnelIdentifier') == 0) { + options.tunnelIdentifier = arg.split('=')[1]; + } +} + +if (options.saucelabs) { + if (!options.tunnelIdentifier) { + throw 'No tunnel identifier given! Either the TRAVIS_JOB_NUMBER is not ' + + 'set, or you haven\'t passed the --tunnelIdentifier=xxx argument.'; + } + // SauceLabs configuration. + config.sauceUser = process.env.SAUCE_USERNAME; + config.sauceKey = process.env.SAUCE_ACCESS_KEY; + if (!config.sauceKey || !config.sauceUser) { + throw 'The SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables have '+ + ' to be set.'; + } + // Avoid going over the SauceLabs concurrency limit (5). + config.maxSessions = 5; + // List of browsers configurations tested. + var sauceBrowsers = require('./sauce_browsers.json'); + // Configuration for SauceLabs browsers. + config.multiCapabilities = sauceBrowsers.map(function(browser) { + browser['tunnel-identifier'] = options.tunnelIdentifier; + return browser; + }); +} else { + // Configuration for local Chrome and Firefox. + config.seleniumAddress = 'http://localhost:4444/wd/hub'; + config.multiCapabilities = [ + { + 'browserName': 'chrome' + }, + { + 'browserName': 'firefox' + } + ]; +} + +exports.config = config; diff --git a/packages/auth/protractor_spec.js b/packages/auth/protractor_spec.js new file mode 100644 index 00000000000..66036e37e3e --- /dev/null +++ b/packages/auth/protractor_spec.js @@ -0,0 +1,137 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var allTests = require('./generated/all_tests'); + +var TEST_SERVER = 'http://localhost:4000'; + +var FLAKY_TEST_RETRIAL = 3; + +describe('Run all Closure unit tests', function() { + /** + * Waits for current tests to be executed. + * @param {!Object} done The function called when the test is finished. + * @param {!Error} fail The function called when an unrecoverable error + * happened during the test. + * @param {?number=} tries The number of trials so far for the current test. + * This is used to retry flaky tests. + */ + var waitForTest = function(done, fail, tries) { + // The default retrial policy. + if (typeof tries === 'undefined') { + tries = FLAKY_TEST_RETRIAL; + } + // executeScript runs the passed method in the "window" context of + // the current test. JSUnit exposes hooks into the test's status through + // the "G_testRunner" global object. + browser.executeScript(function() { + if (window['G_testRunner'] && window['G_testRunner']['isFinished']()) { + return { + isFinished: true, + isSuccess: window['G_testRunner']['isSuccess'](), + report: window['G_testRunner']['getReport']() + }; + } else { + return {'isFinished': false}; + } + }).then(function(status) { + // Tests completed on the page but something failed. Retry a certain + // number of times in case of flakiness. + if (status && status.isFinished && !status.isSuccess && tries > 1) { + // Try again in a few ms. + setTimeout(waitForTest.bind(undefined, done, fail, tries - 1), 300); + } else if (status && status.isFinished) { + done(status); + } else { + // Try again in a few ms. + setTimeout(waitForTest.bind(undefined, done, fail, tries), 300); + } + }, function(err) { + // This can happen if the webdriver had an issue executing the script. + fail(err); + }); + }; + + /** + * Executes the test cases for the file at the given testPath. + * @param {!string} testPath The path of the current test suite to execute. + */ + var executeTest = function(testPath) { + it('runs ' + testPath + ' with success', function(done) { + /** + * Runs the test routines for a given test path and retries up to a + * certain number of times on timeout. + * @param {number} tries The number of times to retry on timeout. + * @param {function()} done The function to run on completion. + */ + var runRoutine = function(tries, done) { + browser.navigate() + .to(TEST_SERVER + '/' + testPath) + .then(function() { + waitForTest(function(status) { + expect(status).toBeSuccess(); + done(); + }, function(err) { + // If browser test execution times out try up to trial times. + if (err.message && + err.message.indexOf('ETIMEDOUT') != -1 && + tries > 0) { + runRoutine(tries - 1, done); + } else { + done.fail(err); + } + }); + }, function(err) { + // If browser test execution times out try up to trial times. + if (err.message && + err.message.indexOf('ETIMEOUT') != -1 && + trial > 0) { + runRoutine(tries - 1, done); + } else { + done.fail(err); + } + }); + }; + // Run test routine. Set timeout retrial to 2 times, eg. test will try + // 2 more times before giving up. + runRoutine(2, done); + }); + }; + + beforeEach(function() { + jasmine.addMatchers({ + // This custom matcher allows for cleaner reports. + toBeSuccess: function() { + return { + // Checks that the status report is successful, otherwise displays + // the report as error message. + compare: function(status) { + return { + pass: status.isSuccess, + message: 'Some test cases failed!\n\n' + status.report + }; + } + }; + } + }); + }); + + // Run all tests. + for (var i = 0; i < allTests.length; i++) { + var testPath = allTests[i]; + executeTest(testPath); + } +}); diff --git a/packages/auth/sauce_browsers.json b/packages/auth/sauce_browsers.json new file mode 100644 index 00000000000..c72162d3e94 --- /dev/null +++ b/packages/auth/sauce_browsers.json @@ -0,0 +1,43 @@ +[ + { + "browserName" : "firefox", + "platform" : "OS X 10.9", + "version": "49.0", + "name": "firefox-latest-mac" + }, + { + "browserName" : "chrome", + "platform" : "OS X 10.9", + "timeZone": "Pacific", + "version" : "54.0", + "name": "chrome-latest-mac" + }, + { + "browserName" : "internet explorer", + "version" : "11.0", + "platform" : "Windows 7", + "timeZone": "Pacific", + "name": "ie-11-windows" + }, + { + "browserName" : "internet explorer", + "version" : "10.0", + "timeZone": "Pacific", + "platform" : "Windows 7", + "name": "ie-10-windows" + }, + { + "browserName" : "MicrosoftEdge", + "platform" : "Windows 10", + "timeZone": "Pacific", + "version" : "14.14393", + "name": "edge-14-windows" + }, + { + "browserName" : "safari", + "version" : "10.0", + "platform" : "OS X 10.11", + "timeZone": "Pacific", + "name": "safari-10-mac" + } +] diff --git a/packages/auth/src/actioncodeinfo.js b/packages/auth/src/actioncodeinfo.js new file mode 100644 index 00000000000..58529cd6f6c --- /dev/null +++ b/packages/auth/src/actioncodeinfo.js @@ -0,0 +1,93 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the firebase.auth.ActionCodeInfo class that is returned + * when calling checkActionCode API and is populated from the server response + * directly. + */ + +goog.provide('fireauth.ActionCodeInfo'); + +goog.require('fireauth.object'); + + +/** + * Constructs the action code info object which provides metadata corresponding + * to action codes. This includes the type of operation (RESET_PASSWORD, + * VERIFY_EMAIL and RECOVER_EMAIL), the email corresponding to the operation + * and in case of the recover email flow, the old and new email. + * @param {!Object} response The server response for checkActionCode. + * @constructor + */ +fireauth.ActionCodeInfo = function(response) { + var data = {}; + // Original email for email change revocation. + var email = response[fireauth.ActionCodeInfo.ServerFieldName.EMAIL]; + // The new email. + var newEmail = response[fireauth.ActionCodeInfo.ServerFieldName.NEW_EMAIL]; + var operation = + response[fireauth.ActionCodeInfo.ServerFieldName.REQUEST_TYPE]; + if (!email || !operation) { + // This is internal only. + throw new Error('Invalid provider user info!'); + } + data[fireauth.ActionCodeInfo.DataField.FROM_EMAIL] = newEmail || null; + data[fireauth.ActionCodeInfo.DataField.EMAIL] = email; + fireauth.object.setReadonlyProperty( + this, + fireauth.ActionCodeInfo.PropertyName.OPERATION, + operation); + fireauth.object.setReadonlyProperty( + this, + fireauth.ActionCodeInfo.PropertyName.DATA, + fireauth.object.unsafeCreateReadOnlyCopy(data)); +}; + + +/** + * The checkActionCode endpoint server response field names. + * @enum {string} + */ +fireauth.ActionCodeInfo.ServerFieldName = { + // This is the current email of the account and in email recovery, the email + // to revert to. + EMAIL: 'email', + // For email recovery, this is the new email. + NEW_EMAIL: 'newEmail', + // The action code request type. + REQUEST_TYPE: 'requestType' +}; + + +/** + * The ActionCodeInfo data object field names. + * @enum {string} + */ +fireauth.ActionCodeInfo.DataField = { + EMAIL: 'email', + FROM_EMAIL: 'fromEmail' +}; + + +/** + * The ActionCodeInfo main property names + * @enum {string} + */ +fireauth.ActionCodeInfo.PropertyName = { + DATA: 'data', + OPERATION: 'operation' +}; diff --git a/packages/auth/src/actioncodesettings.js b/packages/auth/src/actioncodesettings.js new file mode 100644 index 00000000000..7099273ba16 --- /dev/null +++ b/packages/auth/src/actioncodesettings.js @@ -0,0 +1,229 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility for firebase.auth.ActionCodeSettings and its helper + * functions. + */ + +goog.provide('fireauth.ActionCodeSettings'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); + + +/** + * Defines the action code settings structure used to specify how email action + * links are handled. + * @param {!Object} settingsObj The action code settings object used to + * construct the action code link. + * @constructor @struct @final + */ +fireauth.ActionCodeSettings = function(settingsObj) { + // Validate the settings object passed. + this.initialize_(settingsObj); +}; + + +/** + * Validate the action code settings object. + * @param {!Object} settingsObj The action code settings object to validate. + * @private + */ +fireauth.ActionCodeSettings.prototype.initialize_ = function(settingsObj) { + // URL should be required. + var continueUrl = settingsObj[fireauth.ActionCodeSettings.RawField.URL]; + if (typeof continueUrl === 'undefined') { + throw new fireauth.AuthError(fireauth.authenum.Error.MISSING_CONTINUE_URI); + } else if (typeof continueUrl !== 'string' || + (typeof continueUrl === 'string' && !continueUrl.length)) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_CONTINUE_URI); + } + /** @private {string} The continue URL. */ + this.continueUrl_ = /** @type {string} */ (continueUrl); + + // Validate Android parameters. + /** @private {?string} The Android package name. */ + this.apn_ = null; + /** @private {?string} The Android minimum version. */ + this.amv_ = null; + /** @private {boolean} Whether to install the Android app. */ + this.installApp_ = false; + var androidSettings = + settingsObj[fireauth.ActionCodeSettings.RawField.ANDROID]; + if (androidSettings && typeof androidSettings === 'object') { + var apn = androidSettings[ + fireauth.ActionCodeSettings.AndroidRawField.PACKAGE_NAME]; + var installApp = androidSettings[ + fireauth.ActionCodeSettings.AndroidRawField.INSTALL_APP]; + var amv = androidSettings[ + fireauth.ActionCodeSettings.AndroidRawField.MINIMUM_VERSION]; + if (typeof apn === 'string' && apn.length) { + this.apn_ = /** @type {string} */ (apn); + if (typeof installApp !== 'undefined' && + typeof installApp !== 'boolean') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.AndroidRawField.INSTALL_APP + + ' property must be a boolean when specified.'); + } + this.installApp_ = !!installApp; + if (typeof amv !== 'undefined' && + (typeof amv !== 'string' || + (typeof amv === 'string' && !amv.length))) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.AndroidRawField.MINIMUM_VERSION + + ' property must be a non empty string when specified.'); + } + this.amv_ = /** @type {?string}*/ (amv || null); + } else if (typeof apn !== 'undefined') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.AndroidRawField.PACKAGE_NAME + + ' property must be a non empty string when specified.'); + } else if (typeof installApp !== 'undefined' || + typeof amv !== 'undefined') { + // If installApp or amv specified with no valid APN, fail quickly. + throw new fireauth.AuthError( + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME); + } + } else if (typeof androidSettings !== 'undefined') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.RawField.ANDROID + + ' property must be a non null object when specified.'); + } + + // Validate iOS parameters. + /** @private {?string} The iOS bundle ID. */ + this.ibi_ = null; + var iosSettings = settingsObj[fireauth.ActionCodeSettings.RawField.IOS]; + if (iosSettings && typeof iosSettings === 'object') { + var ibi = iosSettings[ + fireauth.ActionCodeSettings.IosRawField.BUNDLE_ID]; + if (typeof ibi === 'string' && ibi.length) { + this.ibi_ = /** @type {string}*/ (ibi); + } else if (typeof ibi !== 'undefined') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.IosRawField.BUNDLE_ID + + ' property must be a non empty string when specified.'); + } + } else if (typeof iosSettings !== 'undefined') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.RawField.IOS + + ' property must be a non null object when specified.'); + } + + // Validate canHandleCodeInApp. + var canHandleCodeInApp = + settingsObj[fireauth.ActionCodeSettings.RawField.HANDLE_CODE_IN_APP]; + if (typeof canHandleCodeInApp !== 'undefined' && + typeof canHandleCodeInApp !== 'boolean') { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.RawField.HANDLE_CODE_IN_APP + + ' property must be a boolean when specified.'); + } + /** @private {boolean} Whether the code can be handled in app. */ + this.canHandleCodeInApp_ = !!canHandleCodeInApp; + // canHandleCodeInApp can't be true when no mobile application is provided. + if (this.canHandleCodeInApp_ && !this.ibi_ && !this.apn_) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + fireauth.ActionCodeSettings.RawField.HANDLE_CODE_IN_APP + + ' property can\'t be true when no mobile application is provided.'); + } +}; + + +/** + * Action code settings backend request field names. + * @enum {string} + */ +fireauth.ActionCodeSettings.RequestField = { + ANDROID_INSTALL_APP: 'androidInstallApp', + ANDROID_MINIMUM_VERSION: 'androidMinimumVersion', + ANDROID_PACKAGE_NAME: 'androidPackageName', + CAN_HANDLE_CODE_IN_APP: 'canHandleCodeInApp', + CONTINUE_URL: 'continueUrl', + IOS_BUNDLE_ID: 'iOSBundleId' +}; + + +/** + * Action code settings raw field names. + * @enum {string} + */ +fireauth.ActionCodeSettings.RawField = { + ANDROID: 'android', + HANDLE_CODE_IN_APP: 'handleCodeInApp', + IOS: 'iOS', + URL: 'url' +}; + + +/** + * Action code settings raw Android raw field names. + * @enum {string} + */ +fireauth.ActionCodeSettings.AndroidRawField = { + INSTALL_APP: 'installApp', + MINIMUM_VERSION: 'minimumVersion', + PACKAGE_NAME: 'packageName' +}; + + +/** + * Action code settings raw iOS raw field names. + * @enum {string} + */ +fireauth.ActionCodeSettings.IosRawField = { + BUNDLE_ID: 'bundleId' +}; + + +/** + * Builds and returns the backend request for the passed action code settings. + * @return {!Object} The constructed backend request populated with the action + * code settings parameters. + */ +fireauth.ActionCodeSettings.prototype.buildRequest = function() { + // Construct backend request. + var request = {}; + request[fireauth.ActionCodeSettings.RequestField.CONTINUE_URL] = + this.continueUrl_; + request[fireauth.ActionCodeSettings.RequestField.CAN_HANDLE_CODE_IN_APP] = + this.canHandleCodeInApp_; + request[fireauth.ActionCodeSettings.RequestField.ANDROID_PACKAGE_NAME] = + this.apn_; + if (this.apn_) { + request[fireauth.ActionCodeSettings.RequestField.ANDROID_MINIMUM_VERSION] = + this.amv_; + request[fireauth.ActionCodeSettings.RequestField.ANDROID_INSTALL_APP] = + this.installApp_; + } + request[fireauth.ActionCodeSettings.RequestField.IOS_BUNDLE_ID] = this.ibi_; + // Remove null fields. + for (var key in request) { + if (request[key] === null) { + delete request[key]; + } + } + return request; +}; diff --git a/packages/auth/src/additionaluserinfo.js b/packages/auth/src/additionaluserinfo.js new file mode 100644 index 00000000000..23c7a17508f --- /dev/null +++ b/packages/auth/src/additionaluserinfo.js @@ -0,0 +1,249 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines all the fireauth additional user info interfaces, + * implementations and subclasses. + */ + +goog.provide('fireauth.AdditionalUserInfo'); +goog.provide('fireauth.FacebookAdditionalUserInfo'); +goog.provide('fireauth.FederatedAdditionalUserInfo'); +goog.provide('fireauth.GenericAdditionalUserInfo'); +goog.provide('fireauth.GithubAdditionalUserInfo'); +goog.provide('fireauth.GoogleAdditionalUserInfo'); +goog.provide('fireauth.TwitterAdditionalUserInfo'); + +goog.require('fireauth.IdToken'); +goog.require('fireauth.idp'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); + + +/** + * The interface that represents additional user info. + * @interface + */ +fireauth.AdditionalUserInfo = function() {}; + + +/** + * Constructs the corresponding additional user info for the backend + * verifyAssertion response. + * @param {?Object|undefined} resp The backend verifyAssertion, + * verifyPhoneNumber or verifyPassword/setAccountInfo response. + * @return {?fireauth.AdditionalUserInfo} The fireauth.AdditionalUserInfo + * instance. + */ +fireauth.AdditionalUserInfo.fromPlainObject = function(resp) { + var factory = {}; + factory[fireauth.idp.ProviderId.FACEBOOK] = + fireauth.FacebookAdditionalUserInfo; + factory[fireauth.idp.ProviderId.GOOGLE] = + fireauth.GoogleAdditionalUserInfo; + factory[fireauth.idp.ProviderId.GITHUB] = + fireauth.GithubAdditionalUserInfo; + factory[fireauth.idp.ProviderId.TWITTER] = + fireauth.TwitterAdditionalUserInfo; + // Provider ID and UID are required. + var providerId = + resp && + resp[fireauth.AdditionalUserInfo.VerifyAssertionField.PROVIDER_ID]; + try { + // Provider ID already present. + if (providerId) { + if (factory[providerId]) { + // 1st class supported federated providers. + return new factory[providerId](resp); + } else { + // Generic federated providers. + return new fireauth.FederatedAdditionalUserInfo( + /** @type {!Object} */ (resp)); + } + } else if (typeof resp[fireauth.AdditionalUserInfo.VerifyAssertionField + .ID_TOKEN] !== 'undefined') { + // For all other ID token responses with no providerId, get the required + // providerId from the ID token itself. + return new fireauth.GenericAdditionalUserInfo( + /** @type {!Object} */ (resp)); + } + } catch (e) { + // Do nothing, null will be returned. + } + return null; +}; + + + +/** + * verifyAssertion response additional user info fields. + * @enum {string} + */ +fireauth.AdditionalUserInfo.VerifyAssertionField = { + ID_TOKEN: 'idToken', + IS_NEW_USER: 'isNewUser', + PROVIDER_ID: 'providerId', + RAW_USER_INFO: 'rawUserInfo', + SCREEN_NAME: 'screenName' +}; + + +/** + * Constructs a generic additional user info object from the backend + * verifyPhoneNumber and verifyPassword provider response. + * @param {!Object} info The verifyPhoneNumber/verifyPassword/setAccountInfo + * response data object. + * @constructor + * @implements {fireauth.AdditionalUserInfo} + */ +fireauth.GenericAdditionalUserInfo = function(info) { + // Federated provider profile data. + var providerId = + info[fireauth.AdditionalUserInfo.VerifyAssertionField.PROVIDER_ID]; + // Try to get providerId from the ID token if available. + if (!providerId && + info[fireauth.AdditionalUserInfo.VerifyAssertionField.ID_TOKEN]) { + // verifyPassword/setAccountInfo and verifyPhoneNumber return an ID token + // but no providerId. Get providerId from the token itself. + // isNewUser will be returned for verifyPhoneNumber. + var idToken = fireauth.IdToken.parse( + info[fireauth.AdditionalUserInfo.VerifyAssertionField.ID_TOKEN]); + if (idToken && idToken.getProviderId()) { + providerId = idToken.getProviderId(); + } + } + if (!providerId) { + // This is internal only. + throw new Error('Invalid additional user info!'); + } + // Check whether user is new. If not provided, default to false. + var isNewUser = + !!info[fireauth.AdditionalUserInfo.VerifyAssertionField.IS_NEW_USER]; + // Set required providerId. + fireauth.object.setReadonlyProperty(this, 'providerId', providerId); + // Set read-only isNewUser property. + fireauth.object.setReadonlyProperty(this, 'isNewUser', isNewUser); +}; + + +/** + * Constructs a federated additional user info object from the backend + * verifyAssertion federated provider response. + * @param {!Object} info The verifyAssertion response data object. + * @constructor + * @extends {fireauth.GenericAdditionalUserInfo} + */ +fireauth.FederatedAdditionalUserInfo = function(info) { + fireauth.FederatedAdditionalUserInfo.base(this, 'constructor', info); + // Federated provider profile data. + // This structure will also be used for generic IdPs. + var profile = fireauth.util.parseJSON( + info[fireauth.AdditionalUserInfo.VerifyAssertionField.RAW_USER_INFO] || + '{}'); + // Set read-only profile property. + fireauth.object.setReadonlyProperty( + this, + 'profile', + fireauth.object.unsafeCreateReadOnlyCopy(profile || {})); +}; +goog.inherits( + fireauth.FederatedAdditionalUserInfo, fireauth.GenericAdditionalUserInfo); + + +/** + * Constructs a Facebook additional user info object from the backend + * verifyAssertion Facebook provider response. + * @param {!Object} info The verifyAssertion response data object. + * @constructor + * @extends {fireauth.FederatedAdditionalUserInfo} + */ +fireauth.FacebookAdditionalUserInfo = function(info) { + fireauth.FacebookAdditionalUserInfo.base(this, 'constructor', info); + // This should not happen as this object is initialized via fromPlainObject. + if (this['providerId'] != fireauth.idp.ProviderId.FACEBOOK) { + throw new Error('Invalid provider ID!'); + } +}; +goog.inherits( + fireauth.FacebookAdditionalUserInfo, fireauth.FederatedAdditionalUserInfo); + + + +/** + * Constructs a GitHub additional user info object from the backend + * verifyAssertion GitHub provider response. + * @param {!Object} info The verifyAssertion response data object. + * @constructor + * @extends {fireauth.FederatedAdditionalUserInfo} + */ +fireauth.GithubAdditionalUserInfo = function(info) { + fireauth.GithubAdditionalUserInfo.base(this, 'constructor', info); + // This should not happen as this object is initialized via fromPlainObject. + if (this['providerId'] != fireauth.idp.ProviderId.GITHUB) { + throw new Error('Invalid provider ID!'); + } + // GitHub username. + fireauth.object.setReadonlyProperty( + this, + 'username', + (this['profile'] && this['profile']['login']) || null); +}; +goog.inherits( + fireauth.GithubAdditionalUserInfo, fireauth.FederatedAdditionalUserInfo); + + + +/** + * Constructs a Google additional user info object from the backend + * verifyAssertion Google provider response. + * @param {!Object} info The verifyAssertion response data object. + * @constructor + * @extends {fireauth.FederatedAdditionalUserInfo} + */ +fireauth.GoogleAdditionalUserInfo = function(info) { + fireauth.GoogleAdditionalUserInfo.base(this, 'constructor', info); + // This should not happen as this object is initialized via fromPlainObject. + if (this['providerId'] != fireauth.idp.ProviderId.GOOGLE) { + throw new Error('Invalid provider ID!'); + } +}; +goog.inherits( + fireauth.GoogleAdditionalUserInfo, fireauth.FederatedAdditionalUserInfo); + + + +/** + * Constructs a Twitter additional user info object from the backend + * verifyAssertion Twitter provider response. + * @param {!Object} info The verifyAssertion response data object. + * @constructor + * @extends {fireauth.FederatedAdditionalUserInfo} + */ +fireauth.TwitterAdditionalUserInfo = function(info) { + fireauth.TwitterAdditionalUserInfo.base(this, 'constructor', info); + // This should not happen as this object is initialized via fromPlainObject. + if (this['providerId'] != fireauth.idp.ProviderId.TWITTER) { + throw new Error('Invalid provider ID!'); + } + // Twitter user name. + fireauth.object.setReadonlyProperty( + this, + 'username', + info[fireauth.AdditionalUserInfo.VerifyAssertionField.SCREEN_NAME] || + null); +}; +goog.inherits( + fireauth.TwitterAdditionalUserInfo, fireauth.FederatedAdditionalUserInfo); diff --git a/packages/auth/src/args.js b/packages/auth/src/args.js new file mode 100644 index 00000000000..8c9e51b428b --- /dev/null +++ b/packages/auth/src/args.js @@ -0,0 +1,498 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provides function argument validation for third-party calls + * that cannot be validated with Closure compiler. + */ + +goog.provide('fireauth.args'); +goog.provide('fireauth.args.Argument'); + +goog.require('fireauth.Auth'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); + + +/** + * Represents an argument to a function. Fields: + *
    + *
  • name: A label for the argument. For example, the names of the arguments + * to a signIn() function might be "email" and "password". + *
  • typeLabel: A label for the expected type of the argument, starting with + * an article, for example, "an object" or "a valid credential". + *
  • optional: Whether or not this argument is optional. Optional arguments + * cannot come after non-optional arguments in the input to validate(). + *
  • validator: A function that takes the passed value of this argument + * and returns whether the value is valid or not. + *
+ * @typedef {{ + * name: string, + * typeLabel: string, + * optional: boolean, + * validator: function (*) : boolean, + * }} + */ +fireauth.args.Argument; + + +/** + * Validates the arguments to a method call and throws an error if invalid. This + * can be used to validate external calls where the Closure compiler cannot + * detect errors. + * + * Example usage: + * function greet(recipient, opt_useFormalLanguage) { + * fireauth.args.validate('greet', [ + * fireauth.args.string('recipient'), + * fireauth.args.bool('opt_useFormalLanguage', true) + * ], arguments); + * if (opt_useFormalLanguage) { + * console.log('Good day, ' + recipient + '.'); + * } else { + * console.log('Wassup, ' + recipient + '?'); + * } + * } + * greet('Mr. Manager', true); // Prints 'Good day, Mr. Manager.' + * greet('Billy Bob'); // Prints 'Wassup, Billy Bob?' + * greet(133); // Throws 'greet failed: First argument "recipient" must be a + * // valid string.' + * greet(); // Throws 'greet failed: Expected 1-2 arguments but got 0.' + * greet('Mr. Manager', true, 'ohno'); // Throws 'greet failed: Expected 1-2 + * // arguments but got 3.' + * + * This can also be used to validate setters by passing an additional true + * argument to fireauth.args.validate. This modifies the error message to be + * relevant for that setter. + * + * @param {string} apiName The name of the method being called, to display in + * the error message for debugging purposes. + * @param {!Array} expected The expected arguments. + * @param {!IArrayLike} actual The arguments object of the function whose + * parameters we want to validate. + * @param {boolean=} opt_isSetter Whether the function is a setter which takes + * a single argument. + */ +fireauth.args.validate = function(apiName, expected, actual, opt_isSetter) { + // Convert the arguments object into a real array. + var actualAsArray = Array.prototype.slice.call(actual); + var errorMessage = fireauth.args.validateAndGetMessage_( + expected, actualAsArray, opt_isSetter); + if (errorMessage) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + apiName + ' failed: ' + errorMessage); + } +}; + + +/** + * @param {!Array} expected + * @param {!Array<*>} actual + * @param {boolean=} opt_isSetter Whether the function is a setter which takes + * a single argument. + * @return {?string} The error message if there is an error, or otherwise + * null. + * @private + */ +fireauth.args.validateAndGetMessage_ = + function(expected, actual, opt_isSetter) { + var minNumArgs = fireauth.args.calcNumRequiredArgs_(expected); + var maxNumArgs = expected.length; + if (actual.length < minNumArgs || maxNumArgs < actual.length) { + return fireauth.args.makeLengthError_(minNumArgs, maxNumArgs, + actual.length); + } + + for (var i = 0; i < actual.length; i++) { + // Argument is optional and undefined is explicitly passed. + var optionalUndefined = expected[i].optional && actual[i] === undefined; + // Check if invalid argument and the argument is not optional with undefined + // passed. + if (!expected[i].validator(actual[i]) && !optionalUndefined) { + return fireauth.args.makeErrorAtPosition_(i, expected[i], opt_isSetter); + } + } + + return null; +}; + + +/** + * @param {!Array} expected + * @return {number} The number of required arguments. + * @private + */ +fireauth.args.calcNumRequiredArgs_ = function(expected) { + var numRequiredArgs = 0; + var isOptionalSection = false; + for (var i = 0; i < expected.length; i++) { + if (expected[i].optional) { + isOptionalSection = true; + } else { + if (isOptionalSection) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'Argument validator encountered a required argument after an ' + + 'optional argument.'); + } + numRequiredArgs++; + } + } + return numRequiredArgs; +}; + + +/** + * @param {number} min The minimum number of arguments to the function, + * inclusive. + * @param {number} max The maximum number of arguments to the function, + * inclusive. + * @param {number} actual The actual number of arguments received. + * @return {string} The error message. + * @private + */ +fireauth.args.makeLengthError_ = function(min, max, actual) { + var numExpectedString; + if (min == max) { + if (min == 1) { + numExpectedString = '1 argument'; + } else { + numExpectedString = min + ' arguments'; + } + } else { + numExpectedString = min + '-' + max + ' arguments'; + } + return 'Expected ' + numExpectedString + ' but got ' + actual + '.'; +}; + + +/** + * @param {number} position The position at which there was an error. + * @param {!fireauth.args.Argument} expectedType The expected type of the + * argument, which was violated. + * @param {boolean=} opt_isSetter Whether the function is a setter which takes + * a single argument. + * @return {string} The error message. + * @private + */ +fireauth.args.makeErrorAtPosition_ = + function(position, expectedType, opt_isSetter) { + var ordinal = fireauth.args.makeOrdinal_(position); + var argName = expectedType.name ? + fireauth.args.quoteString_(expectedType.name) + ' ' : ''; + // Add support to setters for readable/writable properties which take a + // required single argument. + var errorPrefix = !!opt_isSetter ? '' : ordinal + ' argument '; + return errorPrefix + argName + 'must be ' + + expectedType.typeLabel + '.'; +}; + + +/** @private {!Array} The first few ordinal numbers. */ +fireauth.args.ORDINAL_NUMBERS_ = ['First', 'Second', 'Third', 'Fourth', + 'Fifth', 'Sixth', 'Seventh', 'Eighth', 'Ninth']; + + +/** + * @param {number} cardinal An integer. + * @return {string} The integer converted to an ordinal number, starting at + * "First". That is, makeOrdinal_(0) returns "First" and makeOrdinal_(1) + * returns "Second", etc. + * @private + */ +fireauth.args.makeOrdinal_ = function(cardinal) { + // We only support the first few ordinal numbers. We could provide a more + // robust solution, but it is unlikely that a function would need more than + // nine arguments. + if (cardinal < 0 || cardinal >= fireauth.args.ORDINAL_NUMBERS_.length) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'Argument validator received an unsupported number of arguments.'); + } + return fireauth.args.ORDINAL_NUMBERS_[cardinal]; +}; + + +/** + * Specifies a string argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.string = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'a valid string', + optional: !!opt_optional, + validator: goog.isString + }; +}; + + +/** + * Specifies a boolean argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.bool = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'a boolean', + optional: !!opt_optional, + validator: goog.isBoolean + }; +}; + + +/** + * Specifies a number argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.number = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'a valid number', + optional: !!opt_optional, + validator: goog.isNumber + }; +}; + + +/** + * Specifies an object argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.object = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'a valid object', + optional: !!opt_optional, + validator: goog.isObject + }; +}; + + +/** + * Specifies a function argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.func = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'a function', + optional: !!opt_optional, + validator: goog.isFunction + }; +}; + + +/** + * Specifies a null argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.null = function(opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: 'null', + optional: !!opt_optional, + validator: goog.isNull + }; +}; + + +/** + * Specifies an HTML element argument. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.element = function(opt_name, opt_optional) { + return /** @type {!fireauth.args.Argument} */ ({ + name: opt_name || '', + typeLabel: 'an HTML element', + optional: !!opt_optional, + validator: /** @type {function(!Element) : boolean} */ ( + function(element) { + return !!(element && element instanceof Element); + }) + }); +}; + + +/** + * Specifies an instance of Firebase Auth. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.firebaseAuth = function(opt_optional) { + return /** @type {!fireauth.args.Argument} */ ({ + name: 'auth', + typeLabel: 'an instance of Firebase Auth', + optional: !!opt_optional, + validator: /** @type {function(!fireauth.Auth) : boolean} */ ( + function(auth) { + return !!(auth && auth instanceof fireauth.Auth); + }) + }); +}; + + +/** + * Specifies an instance of Firebase App. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.firebaseApp = function(opt_optional) { + return /** @type {!fireauth.args.Argument} */ ({ + name: 'app', + typeLabel: 'an instance of Firebase App', + optional: !!opt_optional, + validator: /** @type {function(!firebase.app.App) : boolean} */ ( + function(app) { + return !!(app && app instanceof firebase.app.App); + }) + }); +}; + + +/** + * Specifies an argument that implements the fireauth.AuthCredential interface. + * @param {?fireauth.idp.ProviderId=} opt_requiredProviderId The required type + * of provider. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.authCredential = + function(opt_requiredProviderId, opt_name, opt_optional) { + var name = opt_name || + (opt_requiredProviderId ? + opt_requiredProviderId + 'Credential' : + 'credential'); + var typeLabel = opt_requiredProviderId ? + 'a valid ' + opt_requiredProviderId + ' credential' : + 'a valid credential'; + return /** @type {!fireauth.args.Argument} */ ({ + name: name, + typeLabel: typeLabel, + optional: !!opt_optional, + validator: /** @type {function(!fireauth.AuthCredential) : boolean} */ ( + function(credential) { + if (!credential) { + return false; + } + // If opt_requiredProviderId is set, make sure it matches the + // credential's providerId. + var matchesRequiredProvider = !opt_requiredProviderId || + (credential['providerId'] === opt_requiredProviderId); + return !!(credential.getIdTokenProvider && matchesRequiredProvider); + }) + }); +}; + + +/** + * Specifies an argument that implements the fireauth.AuthProvider interface. + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.authProvider = function(opt_name, opt_optional) { + return /** @type {!fireauth.args.Argument} */ ({ + name: opt_name || 'authProvider', + typeLabel: 'a valid Auth provider', + optional: !!opt_optional, + validator: /** @type {function(!fireauth.AuthProvider) : boolean} */ ( + function(provider) { + return !!(provider && + provider['providerId'] && + provider.hasOwnProperty && + provider.hasOwnProperty('isOAuthProvider')); + }) + }); +}; + + +/** + * Specifies an argument that implements the firebase.auth.ApplicationVerifier + * interface. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.applicationVerifier = function(opt_optional) { + return /** @type {!fireauth.args.Argument} */ ({ + name: 'applicationVerifier', + typeLabel: 'an implementation of firebase.auth.ApplicationVerifier', + optional: !!opt_optional, + validator: + /** @type {function(!firebase.auth.ApplicationVerifier) : boolean} */ ( + function(applicationVerifier) { + return !!(applicationVerifier && + goog.isString(applicationVerifier.type) && + goog.isFunction(applicationVerifier.verify)); + }) + }); +}; + + +/** + * Specifies an argument that can be either of two argument types. + * @param {!fireauth.args.Argument} optionA + * @param {!fireauth.args.Argument} optionB + * @param {?string=} opt_name The name of the argument. + * @param {?boolean=} opt_optional Whether or not this argument is optional. + * Defaults to false. + * @return {!fireauth.args.Argument} + */ +fireauth.args.or = function(optionA, optionB, opt_name, opt_optional) { + return { + name: opt_name || '', + typeLabel: optionA.typeLabel + ' or ' + optionB.typeLabel, + optional: !!opt_optional, + validator: function(value) { + return optionA.validator(value) || optionB.validator(value); + } + }; +}; + + +/** + * @param {string} str + * @return {string} The string surrounded with quotes. + * @private + */ +fireauth.args.quoteString_ = function(str) { + return '"' + str + '"'; +}; diff --git a/packages/auth/src/auth.js b/packages/auth/src/auth.js index 94e59bcb8b7..8634104658c 100644 --- a/packages/auth/src/auth.js +++ b/packages/auth/src/auth.js @@ -14,304 +14,1847 @@ * limitations under the License. */ -(function(){var h,aa=aa||{},k=this,ba=function(a){return void 0!==a},m=function(a){return"string"==typeof a},ca=function(a){return"boolean"==typeof a},da=function(){},ea=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array"; -if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a.call)return"object";return b},fa=function(a){return null===a},ha=function(a){return"array"==ea(a)},ia=function(a){var b=ea(a);return"array"==b||"object"==b&&"number"==typeof a.length},p=function(a){return"function"==ea(a)},q=function(a){var b=typeof a;return"object"==b&&null!=a||"function"== -b},ja=function(a,b,c){return a.call.apply(a.bind,arguments)},ka=function(a,b,c){if(!a)throw Error();if(2")&&(a=a.replace(sa,">"));-1!=a.indexOf('"')&&(a=a.replace(ta,"""));-1!=a.indexOf("'")&& -(a=a.replace(ua,"'"));-1!=a.indexOf("\x00")&&(a=a.replace(va,"�"));return a},qa=/&/g,ra=//g,ta=/"/g,ua=/'/g,va=/\x00/g,pa=/[\x00&<>"']/,v=function(a,b){return-1!=a.indexOf(b)},xa=function(a,b){return ab?1:0};var ya=function(a,b){b.unshift(a);u.call(this,na.apply(null,b));b.shift()};t(ya,u);ya.prototype.name="AssertionError"; -var za=function(a,b,c,d){var e="Assertion failed";if(c){e+=": "+c;var f=d}else a&&(e+=": "+a,f=b);throw new ya(""+e,f||[]);},w=function(a,b,c){a||za("",null,b,Array.prototype.slice.call(arguments,2));return a},Aa=function(a,b){throw new ya("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));},Ba=function(a,b,c){"number"==typeof a||za("Expected number but got %s: %s.",[ea(a),a],b,Array.prototype.slice.call(arguments,2));return a},Ca=function(a,b,c){m(a)||za("Expected string but got %s: %s.", -[ea(a),a],b,Array.prototype.slice.call(arguments,2))},Da=function(a,b,c){p(a)||za("Expected function but got %s: %s.",[ea(a),a],b,Array.prototype.slice.call(arguments,2))};var Fa=function(){this.Tc="";this.kf=Ea};Fa.prototype.qb=!0;Fa.prototype.ob=function(){return this.Tc};Fa.prototype.toString=function(){return"Const{"+this.Tc+"}"};var Ga=function(a){if(a instanceof Fa&&a.constructor===Fa&&a.kf===Ea)return a.Tc;Aa("expected object of type Const, got '"+a+"'");return"type_error:Const"},Ea={},Ha=function(a){var b=new Fa;b.Tc=a;return b};Ha("");var Ja=function(){this.Lc="";this.lf=Ia};Ja.prototype.qb=!0;Ja.prototype.ob=function(){return this.Lc};Ja.prototype.toString=function(){return"TrustedResourceUrl{"+this.Lc+"}"}; -var Ka=function(a){if(a instanceof Ja&&a.constructor===Ja&&a.lf===Ia)return a.Lc;Aa("expected object of type TrustedResourceUrl, got '"+a+"' of type "+ea(a));return"type_error:TrustedResourceUrl"},Oa=function(a,b){var c=Ga(a);if(!La.test(c))throw Error("Invalid TrustedResourceUrl format: "+c);a=c.replace(Ma,function(a,e){if(!Object.prototype.hasOwnProperty.call(b,e))throw Error('Found marker, "'+e+'", in format string, "'+c+'", but no valid label mapping found in args: '+JSON.stringify(b));a=b[e]; -return a instanceof Fa?Ga(a):encodeURIComponent(String(a))});return Na(a)},Ma=/%{(\w+)}/g,La=/^(?:https:)?\/\/[0-9a-z.:[\]-]+\/|^\/[^\/\\]|^about:blank(#|$)/i,Ia={},Na=function(a){var b=new Ja;b.Lc=a;return b};var Pa=Array.prototype.indexOf?function(a,b,c){w(null!=a.length);return Array.prototype.indexOf.call(a,b,c)}:function(a,b,c){c=null==c?0:0>c?Math.max(0,a.length+c):c;if(m(a))return m(b)&&1==b.length?a.indexOf(b,c):-1;for(;cb?null:m(a)?a.charAt(b):a[b]},Va=function(a,b){return 0<=Pa(a,b)},Xa=function(a,b){b=Pa(a,b);var c;(c=0<=b)&&Wa(a,b);return c},Wa=function(a,b){w(null!=a.length);return 1==Array.prototype.splice.call(a,b,1).length},Ya=function(a,b){var c=0;Qa(a,function(d,e){b.call(void 0,d,e,a)&&Wa(a,e)&&c++})},Za=function(a){return Array.prototype.concat.apply([],arguments)}, -$a=function(a){var b=a.length;if(0parseFloat(xb)){wb=String(Ab);break a}}wb=xb} -var Bb=wb,ob={},A=function(a){return pb(a,function(){for(var b=0,c=oa(String(Bb)).split("."),d=oa(String(a)).split("."),e=Math.max(c.length,d.length),f=0;0==b&&f>4);64!=g&&(b(f<<4&240|g>>2),64!=l&&b(g<<6&192|l))}},Ib=function(){if(!Eb){Eb={};Fb={};for(var a=0;65>a;a++)Eb[a]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(a), -Fb[Eb[a]]=a,62<=a&&(Fb["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.".charAt(a)]=a)}};var Jb=function(){this.Da=-1};var Mb=function(a,b){this.Da=-1;this.Da=64;this.qc=k.Uint8Array?new Uint8Array(this.Da):Array(this.Da);this.Xc=this.rb=0;this.l=[];this.bg=a;this.Fe=b;this.Cg=k.Int32Array?new Int32Array(64):Array(64);ba(Kb)||(Kb=k.Int32Array?new Int32Array(Lb):Lb);this.reset()},Kb;t(Mb,Jb);for(var Nb=[],Ob=0;63>Ob;Ob++)Nb[Ob]=0;var Pb=Za(128,Nb);Mb.prototype.reset=function(){this.Xc=this.rb=0;this.l=k.Int32Array?new Int32Array(this.Fe):$a(this.Fe)}; -var Qb=function(a){var b=a.qc;w(b.length==a.Da);for(var c=a.Cg,d=0,e=0;eb;b++){e=c[b-15]|0;d=c[b-2]|0;var f=(c[b-16]|0)+((e>>>7|e<<25)^(e>>>18|e<<14)^e>>>3)|0,g=(c[b-7]|0)+((d>>>17|d<<15)^(d>>>19|d<<13)^d>>>10)|0;c[b]=f+g|0}d=a.l[0]|0;e=a.l[1]|0;var l=a.l[2]|0,n=a.l[3]|0,F=a.l[4]|0,zb=a.l[5]|0,ic=a.l[6]|0;f=a.l[7]|0;for(b=0;64>b;b++){var yi=((d>>>2|d<<30)^(d>>>13|d<<19)^(d>>>22|d<<10))+(d&e^d&l^e&l)|0;g=F&zb^~F⁣f=f+((F>>> -6|F<<26)^(F>>>11|F<<21)^(F>>>25|F<<7))|0;g=g+(Kb[b]|0)|0;g=f+(g+(c[b]|0)|0)|0;f=ic;ic=zb;zb=F;F=n+g|0;n=l;l=e;e=d;d=g+yi|0}a.l[0]=a.l[0]+d|0;a.l[1]=a.l[1]+e|0;a.l[2]=a.l[2]+l|0;a.l[3]=a.l[3]+n|0;a.l[4]=a.l[4]+F|0;a.l[5]=a.l[5]+zb|0;a.l[6]=a.l[6]+ic|0;a.l[7]=a.l[7]+f|0}; -Mb.prototype.update=function(a,b){ba(b)||(b=a.length);var c=0,d=this.rb;if(m(a))for(;c=e&&e==(e|0)))throw Error("message must be a byte array");this.qc[d++]=e;d==this.Da&&(Qb(this),d=0)}else throw Error("message must be string or array");this.rb=d;this.Xc+=b}; -Mb.prototype.digest=function(){var a=[],b=8*this.Xc;56>this.rb?this.update(Pb,56-this.rb):this.update(Pb,this.Da-(this.rb-56));for(var c=63;56<=c;c--)this.qc[c]=b&255,b/=256;Qb(this);for(c=b=0;c>d&255;return a}; -var Lb=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804, -4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];var Sb=function(){Mb.call(this,8,Rb)};t(Sb,Mb);var Rb=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225];var Tb=Object.freeze||function(a){return a};var Ub=function(){this.Ma=this.Ma;this.Ic=this.Ic};Ub.prototype.Ma=!1;Ub.prototype.isDisposed=function(){return this.Ma};Ub.prototype.lb=function(){if(this.Ic)for(;this.Ic.length;)this.Ic.shift()()};var Vb=!z||9<=Number(Cb),Wb=z&&!A("9");!ub||A("528");tb&&A("1.9b")||z&&A("8")||qb&&A("9.5")||ub&&A("528");tb&&!A("8")||z&&A("9");var Xb=function(){if(!k.addEventListener||!Object.defineProperty)return!1;var a=!1,b=Object.defineProperty({},"passive",{get:function(){a=!0}});k.addEventListener("test",da,b);k.removeEventListener("test",da,b);return a}();var B=function(a,b){this.type=a;this.currentTarget=this.target=b;this.defaultPrevented=this.Wa=!1;this.Ue=!0};B.prototype.stopPropagation=function(){this.Wa=!0};B.prototype.preventDefault=function(){this.defaultPrevented=!0;this.Ue=!1};var Yb=function(a,b){B.call(this,a?a.type:"");this.relatedTarget=this.currentTarget=this.target=null;this.button=this.screenY=this.screenX=this.clientY=this.clientX=this.offsetY=this.offsetX=0;this.key="";this.charCode=this.keyCode=0;this.metaKey=this.shiftKey=this.altKey=this.ctrlKey=!1;this.state=null;this.pointerId=0;this.pointerType="";this.R=null;a&&this.init(a,b)};t(Yb,B);var Zb=Tb({2:"touch",3:"pen",4:"mouse"}); -Yb.prototype.init=function(a,b){var c=this.type=a.type,d=a.changedTouches?a.changedTouches[0]:null;this.target=a.target||a.srcElement;this.currentTarget=b;if(b=a.relatedTarget){if(tb){a:{try{nb(b.nodeName);var e=!0;break a}catch(f){}e=!1}e||(b=null)}}else"mouseover"==c?b=a.fromElement:"mouseout"==c&&(b=a.toElement);this.relatedTarget=b;null===d?(this.offsetX=ub||void 0!==a.offsetX?a.offsetX:a.layerX,this.offsetY=ub||void 0!==a.offsetY?a.offsetY:a.layerY,this.clientX=void 0!==a.clientX?a.clientX:a.pageX, -this.clientY=void 0!==a.clientY?a.clientY:a.pageY,this.screenX=a.screenX||0,this.screenY=a.screenY||0):(this.clientX=void 0!==d.clientX?d.clientX:d.pageX,this.clientY=void 0!==d.clientY?d.clientY:d.pageY,this.screenX=d.screenX||0,this.screenY=d.screenY||0);this.button=a.button;this.keyCode=a.keyCode||0;this.key=a.key||"";this.charCode=a.charCode||("keypress"==c?a.keyCode:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=a.metaKey;this.pointerId=a.pointerId||0;this.pointerType= -m(a.pointerType)?a.pointerType:Zb[a.pointerType]||"";this.state=a.state;this.R=a;a.defaultPrevented&&this.preventDefault()};Yb.prototype.stopPropagation=function(){Yb.Uc.stopPropagation.call(this);this.R.stopPropagation?this.R.stopPropagation():this.R.cancelBubble=!0};Yb.prototype.preventDefault=function(){Yb.Uc.preventDefault.call(this);var a=this.R;if(a.preventDefault)a.preventDefault();else if(a.returnValue=!1,Wb)try{if(a.ctrlKey||112<=a.keyCode&&123>=a.keyCode)a.keyCode=-1}catch(b){}}; -Yb.prototype.Hf=function(){return this.R};var $b="closure_listenable_"+(1E6*Math.random()|0),ac=0;var bc=function(a,b,c,d,e){this.listener=a;this.Mc=null;this.src=b;this.type=c;this.capture=!!d;this.vc=e;this.key=++ac;this.Bb=this.pc=!1},cc=function(a){a.Bb=!0;a.listener=null;a.Mc=null;a.src=null;a.vc=null};var dc=function(a){this.src=a;this.I={};this.jc=0};dc.prototype.add=function(a,b,c,d,e){var f=a.toString();a=this.I[f];a||(a=this.I[f]=[],this.jc++);var g=ec(a,b,d,e);-1d.keyCode||void 0!=d.returnValue)){a:{var e=!1;if(0==d.keyCode)try{d.keyCode=-1;break a}catch(g){e=!0}if(e||void 0==d.returnValue)d.returnValue= -!0}d=[];for(e=b.currentTarget;e;e=e.parentNode)d.push(e);a=a.type;for(e=d.length-1;!b.Wa&&0<=e;e--){b.currentTarget=d[e];var f=wc(d[e],a,!0,b);c=c&&f}for(e=0;!b.Wa&&e>>0),mc=function(a){w(a,"Listener can not be null.");if(p(a))return a;w(a.handleEvent,"An object listener must have handleEvent method."); -a[xc]||(a[xc]=function(b){return a.handleEvent(b)});return a[xc]};var yc=/^[+a-zA-Z0-9_.!#$%&'*\/=?^`{|}~-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9]{2,63}$/;var Ac=function(){this.xa="";this.jf=zc};Ac.prototype.qb=!0;Ac.prototype.ob=function(){return this.xa};Ac.prototype.toString=function(){return"SafeUrl{"+this.xa+"}"}; -var Bc=function(a){if(a instanceof Ac&&a.constructor===Ac&&a.jf===zc)return a.xa;Aa("expected object of type SafeUrl, got '"+a+"' of type "+ea(a));return"type_error:SafeUrl"},Cc=/^(?:(?:https?|mailto|ftp):|[^:/?#]*(?:[/?#]|$))/i,Ec=function(a){if(a instanceof Ac)return a;a=a.qb?a.ob():String(a);Cc.test(a)||(a="about:invalid#zClosurez");return Dc(a)},zc={},Dc=function(a){var b=new Ac;b.xa=a;return b};Dc("about:blank");var Hc=function(a){var b=[];Fc(new Gc,a,b);return b.join("")},Gc=function(){this.Nc=void 0},Fc=function(a,b,c){if(null==b)c.push("null");else{if("object"==typeof b){if(ha(b)){var d=b;b=d.length;c.push("[");for(var e="",f=0;f");f=f.join("")}f=e.createElement(f);g&&(m(g)?f.className=g:ha(g)?f.className=g.join(" "):nd(f,g));2=b.gd&&b.cancel())}this.Ne?this.Ne.call(this.re,this):this.be=!0;this.nb||Od(this,new Pd)}};Nd.prototype.oe=function(a,b){this.fd=!1;Qd(this,a,b)}; -var Qd=function(a,b,c){a.nb=!0;a.za=c;a.Pb=!b;Rd(a)},Td=function(a){if(a.nb){if(!a.be)throw new Sd;a.be=!1}};Nd.prototype.callback=function(a){Td(this);Ud(a);Qd(this,!0,a)};var Od=function(a,b){Td(a);Ud(b);Qd(a,!1,b)},Ud=function(a){w(!(a instanceof Nd),"An execution sequence may not be initiated with a blocking Deferred.")},Wd=function(a,b){Vd(a,null,b,void 0)},Vd=function(a,b,c,d){w(!a.he,"Blocking Deferreds can not be re-used");a.Qc.push([b,c,d]);a.nb&&Rd(a)}; -Nd.prototype.then=function(a,b,c){var d,e,f=new C(function(a,b){d=a;e=b});Vd(this,d,function(a){a instanceof Pd?f.cancel():e(a)});return f.then(a,b,c)};rd(Nd); -var Xd=function(a){return Sa(a.Qc,function(a){return p(a[1])})},Rd=function(a){if(a.Yc&&a.nb&&Xd(a)){var b=a.Yc,c=Yd[b];c&&(k.clearTimeout(c.Qb),delete Yd[b]);a.Yc=0}a.w&&(a.w.gd--,delete a.w);b=a.za;for(var d=c=!1;a.Qc.length&&!a.fd;){var e=a.Qc.shift(),f=e[0],g=e[1];e=e[2];if(f=a.Pb?g:f)try{var l=f.call(e||a.re,b);ba(l)&&(a.Pb=a.Pb&&(l==b||l instanceof Error),a.za=b=l);if(sd(b)||"function"===typeof k.Promise&&b instanceof k.Promise)d=!0,a.fd=!0}catch(n){b=n,a.Pb=!0,Xd(a)||(c=!0)}}a.za=b;d&&(l=r(a.oe, -a,!0),d=r(a.oe,a,!1),b instanceof Nd?(Vd(b,l,d),b.he=!0):b.then(l,d));c&&(b=new Zd(b),Yd[b.Qb]=b,a.Yc=b.Qb)},Sd=function(){u.call(this)};t(Sd,u);Sd.prototype.message="Deferred has already fired";Sd.prototype.name="AlreadyCalledError";var Pd=function(){u.call(this)};t(Pd,u);Pd.prototype.message="Deferred was canceled";Pd.prototype.name="CanceledError";var Zd=function(a){this.Qb=k.setTimeout(r(this.yg,this),0);this.ba=a}; -Zd.prototype.yg=function(){w(Yd[this.Qb],"Cannot throw an error that is not scheduled.");delete Yd[this.Qb];throw this.ba;};var Yd={};var de=function(a){var b={},c=b.document||document,d=Ka(a),e=document.createElement("SCRIPT"),f={We:e,ic:void 0},g=new Nd($d,f),l=null,n=null!=b.timeout?b.timeout:5E3;0=me(this).value)for(p(b)&&(b=b()),a=new ee(a,String(b),this.Le),c&&(a.te=c),c=this;c;){var d=c,e=a;if(d.Ae)for(var f=0;b=d.Ae[f];f++)b(e);c=c.getParent()}};ge.prototype.info=function(a,b){this.log(je,a,b)};ge.prototype.config=function(a,b){this.log(ke,a,b)}; -var ne={},oe=null,pe=function(a){oe||(oe=new ge(""),ne[""]=oe,oe.Xe(ke));var b;if(!(b=ne[a])){b=new ge(a);var c=a.lastIndexOf("."),d=a.substr(c+1);c=pe(a.substr(0,c));c.kd||(c.kd={});c.kd[d]=b;b.w=c;ne[a]=b}return b};var G=function(){Ub.call(this);this.ga=new dc(this);this.nf=this;this.Kd=null};t(G,Ub);G.prototype[$b]=!0;h=G.prototype;h.addEventListener=function(a,b,c,d){lc(this,a,b,c,d)};h.removeEventListener=function(a,b,c,d){tc(this,a,b,c,d)}; -h.dispatchEvent=function(a){qe(this);var b=this.Kd;if(b){var c=[];for(var d=1;b;b=b.Kd)c.push(b),w(1E3>++d,"infinite loop")}b=this.nf;d=a.type||a;if(m(a))a=new B(a,b);else if(a instanceof B)a.target=a.target||b;else{var e=a;a=new B(d,b);mb(a,e)}e=!0;if(c)for(var f=c.length-1;!a.Wa&&0<=f;f--){var g=a.currentTarget=c[f];e=re(g,d,!0,a)&&e}a.Wa||(g=a.currentTarget=b,e=re(g,d,!0,a)&&e,a.Wa||(e=re(g,d,!1,a)&&e));if(c)for(f=0;!a.Wa&&f2*this.u&&ve(this),!0):!1};var ve=function(a){if(a.u!=a.C.length){for(var b=0,c=0;b=d.C.length)throw se;var e=d.C[b++];return a?e:d.ja[e]};return e};var we=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)};var xe=function(a){if(a.ha&&"function"==typeof a.ha)return a.ha();if(m(a))return a.split("");if(ia(a)){for(var b=[],c=a.length,d=0;db)throw Error("Bad port number "+b);a.vb=b}else a.vb=null},We=function(a,b,c){J(a);a.Ga=c?Ye(b,!0):b},Xe=function(a,b,c){J(a);b instanceof Ze?(a.ea=b,a.ea.$d(a.da)):(c||(b=$e(b,ef)),a.ea=new Ze(b,0,a.da))},K=function(a,b,c){J(a);a.ea.set(b,c)},ff=function(a,b){return a.ea.get(b)}; -Se.prototype.removeParameter=function(a){J(this);this.ea.remove(a);return this};var J=function(a){if(a.Xf)throw Error("Tried to modify a read-only Uri");};Se.prototype.$d=function(a){this.da=a;this.ea&&this.ea.$d(a);return this}; -var gf=function(a){return a instanceof Se?a.clone():new Se(a,void 0)},hf=function(a,b){var c=new Se(null,void 0);Te(c,"https");a&&Ue(c,a);b&&We(c,b);return c},Ye=function(a,b){return a?b?decodeURI(a.replace(/%25/g,"%2525")):decodeURIComponent(a):""},$e=function(a,b,c){return m(a)?(a=encodeURI(a).replace(b,jf),c&&(a=a.replace(/%25([0-9a-fA-F]{2})/g,"%$1")),a):null},jf=function(a){a=a.charCodeAt(0);return"%"+(a>>4&15).toString(16)+(a&15).toString(16)},af=/[#\/\?@]/g,cf=/[#\?:]/g,bf=/[#\?]/g,ef=/[#\?@]/g, -df=/#/g,Ze=function(a,b,c){this.u=this.s=null;this.X=a||null;this.da=!!c},kf=function(a){a.s||(a.s=new ue,a.u=0,a.X&&De(a.X,function(b,c){a.add(decodeURIComponent(b.replace(/\+/g," ")),c)}))},mf=function(a){var b=ye(a);if("undefined"==typeof b)throw Error("Keys are undefined");var c=new Ze(null,0,void 0);a=xe(a);for(var d=0;da?!1:!z||!Cb||9',Ca(Ga(a),"must provide justification"),w(!/^[\s\xa0]*$/.test(Ga(a)),"must provide non-empty justification"),g.document.write(jd((new id).Tf(d))),g.document.close())):(g=d.open(Bc(b),c,g))&&a.noopener&&(g.opener=null);if(g)try{g.focus()}catch(l){}return g},zf=function(a){return new C(function(b){var c= -function(){Be(2E3).then(function(){if(!a||a.closed)b();else return c()})};return c()})},Af=/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,Bf=function(){var a=null;return(new C(function(b){"complete"==k.document.readyState?b():(a=function(){b()},kc(window,"load",a))})).g(function(b){tc(window,"load",a);throw b;})},Df=function(){return Cf(void 0)?Bf().then(function(){return new C(function(a,b){var c=k.document,d=setTimeout(function(){b(Error("Cordova framework is not ready."))},1E3);c.addEventListener("deviceready", -function(){clearTimeout(d);a()},!1)})}):E(Error("Cordova must run in an Android or iOS file scheme."))},Cf=function(a){a=a||L();return!("file:"!==Ef()||!a.toLowerCase().match(/iphone|ipad|ipod|android/))},Ff=function(){var a=k.window;try{return!(!a||a==a.top)}catch(b){return!1}},Gf=function(){return firebase.INTERNAL.hasOwnProperty("reactNative")?"ReactNative":firebase.INTERNAL.hasOwnProperty("node")?"Node":"Browser"},Hf=function(){var a=Gf();return"ReactNative"===a||"Node"===a},uf=function(a){var b= -a.toLowerCase();if(v(b,"opera/")||v(b,"opr/")||v(b,"opios/"))return"Opera";if(v(b,"iemobile"))return"IEMobile";if(v(b,"msie")||v(b,"trident/"))return"IE";if(v(b,"edge/"))return"Edge";if(v(b,"firefox/"))return"Firefox";if(v(b,"silk/"))return"Silk";if(v(b,"blackberry"))return"Blackberry";if(v(b,"webos"))return"Webos";if(!v(b,"safari/")||v(b,"chrome/")||v(b,"crios/")||v(b,"android"))if(!v(b,"chrome/")&&!v(b,"crios/")||v(b,"edge/")){if(v(b,"android"))return"Android";if((a=a.match(/([a-zA-Z\d\.]+)\/[a-zA-Z\d\.]*$/))&& -2==a.length)return a[1]}else return"Chrome";else return"Safari";return"Other"},If={Fg:"FirebaseCore-web",Hg:"FirebaseUI-web"},Jf=function(a,b){b=b||[];var c=[],d={},e;for(e in If)d[If[e]]=!0;for(e=0;eb)throw Error("Short delay should be less than long delay!");this.ug=a;this.ag=b;a=c||L();d=d||Gf();this.Wf=wf(a)||"ReactNative"===d};Vf.prototype.get=function(){return this.Wf?this.ag:this.ug}; -var Wf=function(){var a=k.document;return a&&"undefined"!==typeof a.visibilityState?"visible"==a.visibilityState:!0},Xf=function(){var a=k.document,b=null;return Wf()||!a?D():(new C(function(c){b=function(){Wf()&&(a.removeEventListener("visibilitychange",b,!1),c())};a.addEventListener("visibilitychange",b,!1)})).g(function(c){a.removeEventListener("visibilitychange",b,!1);throw c;})},Yf=function(a){try{var b=new Date(parseInt(a,10));if(!isNaN(b.getTime())&&!/[^0-9]/.test(a))return b.toUTCString()}catch(c){}return null};var Zf={};var $f;try{var ag={};Object.defineProperty(ag,"abcd",{configurable:!0,enumerable:!0,value:1});Object.defineProperty(ag,"abcd",{configurable:!0,enumerable:!0,value:2});$f=2==ag.abcd}catch(a){$f=!1} -var N=function(a,b,c){$f?Object.defineProperty(a,b,{configurable:!0,enumerable:!0,value:c}):a[b]=c},bg=function(a,b){if(b)for(var c in b)b.hasOwnProperty(c)&&N(a,c,b[c])},cg=function(a){var b={};bg(b,a);return b},dg=function(a){var b={},c;for(c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b},eg=function(a,b){if(!b||!b.length)return!0;if(!a)return!1;for(var c=0;c Auth section -> Sign in method tab.",a):"http"==d||"https"==d?c=na("This domain (%s) is not authorized to run this operation. Add it to the OAuth redirect domains list in the Firebase console -> Auth section -> Sign in method tab.",a):b= -"operation-not-supported-in-this-environment";O.call(this,b,c)};t(og,O);var pg=function(a){this.Zf=a.sub;ma();this.Mb=a.email||null;this.Rd=a.provider_id||a.firebase&&a.firebase.sign_in_provider||null;this.qf=!!a.is_anonymous||"anonymous"==this.Rd};pg.prototype.getEmail=function(){return this.Mb};pg.prototype.isAnonymous=function(){return this.qf};var qg=function(a){a=a.split(".");if(3!=a.length)return null;a=a[1];for(var b=(4-a.length%4)%4,c=0;ca.fe&&(a.Sa=a.fe);return b},Mi=function(a,b){a.stop();a.ub=Be(Ni(a,b)).then(function(){return a.ng?D():Xf()}).then(function(){return a.fg()}).then(function(){Mi(a,!0)}).g(function(b){a.mg(b)&&Mi(a,!1)})};Li.prototype.stop=function(){this.ub&&(this.ub.cancel(),this.ub=null)};var Ui=function(a){var b={};b["facebook.com"]=Oi;b["google.com"]=Pi;b["github.com"]=Qi;b["twitter.com"]=Ri;var c=a&&a.providerId;try{if(c)return b[c]?new b[c](a):new Si(a);if("undefined"!==typeof a.idToken)return new Ti(a)}catch(d){}return null},Ti=function(a){var b=a.providerId;if(!b&&a.idToken){var c=qg(a.idToken);c&&c.Rd&&(b=c.Rd)}if(!b)throw Error("Invalid additional user info!");a=!!a.isNewUser;N(this,"providerId",b);N(this,"isNewUser",a)},Si=function(a){Ti.call(this,a);a=Rf(a.rawUserInfo||"{}"); -N(this,"profile",fg(a||{}))};t(Si,Ti);var Oi=function(a){Si.call(this,a);if("facebook.com"!=this.providerId)throw Error("Invalid provider id!");};t(Oi,Si);var Qi=function(a){Si.call(this,a);if("github.com"!=this.providerId)throw Error("Invalid provider id!");N(this,"username",this.profile&&this.profile.login||null)};t(Qi,Si);var Pi=function(a){Si.call(this,a);if("google.com"!=this.providerId)throw Error("Invalid provider id!");};t(Pi,Si); -var Ri=function(a){Si.call(this,a);if("twitter.com"!=this.providerId)throw Error("Invalid provider id!");N(this,"username",a.screenName||null)};t(Ri,Si);var Vi={LOCAL:"local",NONE:"none",SESSION:"session"},Wi=function(a){var b=new O("invalid-persistence-type"),c=new O("unsupported-persistence-type");a:{for(d in Vi)if(Vi[d]==a){var d=!0;break a}d=!1}if(!d||"string"!==typeof a)throw b;switch(Gf()){case "ReactNative":if("session"===a)throw c;break;case "Node":if("none"!==a)throw c;break;default:if(!Lf()&&"none"!==a)throw c;}},Xi=function(a,b,c,d){this.Me=a;this.Xd=b;this.og=c;this.Ve=d;this.S={};Fi||(Fi=new Ei);a=Fi;try{if(qf()){ui||(ui=new ti("firebaseLocalStorageDb", -"firebaseLocalStorage","fbase_key","value",1));var e=ui}else e=new a.se.D;this.Se=e}catch(f){this.Se=new ni,this.Ve=!0}try{this.af=new a.se.ee}catch(f){this.af=new ni}this.Qf=new ni;this.de=r(this.Ze,this);this.N={}},Yi,Zi=function(){Yi||(Yi=new Xi("firebase",":",!Sf(L())&&Ff()?!0:!1,Of()));return Yi},$i=function(a,b){switch(b){case "session":return a.af;case "none":return a.Qf;default:return a.Se}};h=Xi.prototype;h.ca=function(a,b){return this.Me+this.Xd+a.name+(b?this.Xd+b:"")}; -h.get=function(a,b){return $i(this,a.D).get(this.ca(a,b))};h.remove=function(a,b){b=this.ca(a,b);"local"==a.D&&(this.N[b]=null);return $i(this,a.D).remove(b)};h.set=function(a,b,c){var d=this.ca(a,c),e=this,f=$i(this,a.D);return f.set(d,b).then(function(){return f.get(d)}).then(function(b){"local"==a.D&&(e.N[d]=b)})}; -h.addListener=function(a,b,c){a=this.ca(a,b);"undefined"!==typeof k.localStorage&&"function"===typeof k.localStorage.getItem&&(this.N[a]=k.localStorage.getItem(a));ib(this.S)&&this.ce();this.S[a]||(this.S[a]=[]);this.S[a].push(c)};h.removeListener=function(a,b,c){a=this.ca(a,b);this.S[a]&&(Ya(this.S[a],function(a){return a==c}),0==this.S[a].length&&delete this.S[a]);ib(this.S)&&this.Sc()};h.ce=function(){$i(this,"local").jb(this.de);this.Ve||qf()||aj(this)}; -var aj=function(a){bj(a);a.Fd=setInterval(function(){for(var b in a.S){var c=k.localStorage.getItem(b),d=a.N[b];c!=d&&(a.N[b]=c,c=new Yb({type:"storage",key:b,target:window,oldValue:d,newValue:c,Md:!0}),a.Ze(c))}},1E3)},bj=function(a){a.Fd&&(clearInterval(a.Fd),a.Fd=null)};Xi.prototype.Sc=function(){$i(this,"local").$a(this.de);bj(this)}; -Xi.prototype.Ze=function(a){if(a&&a.Hf){var b=a.R.key;if(null==b)for(var c in this.S){var d=this.N[c];"undefined"===typeof d&&(d=null);var e=k.localStorage.getItem(c);e!==d&&(this.N[c]=e,this.hd(c))}else if(0==b.indexOf(this.Me+this.Xd)&&this.S[b]){"undefined"!==typeof a.R.Md?$i(this,"local").$a(this.de):bj(this);if(this.og)if(c=k.localStorage.getItem(b),d=a.R.newValue,d!==c)null!==d?k.localStorage.setItem(b,d):k.localStorage.removeItem(b);else if(this.N[b]===d&&"undefined"===typeof a.R.Md)return; -var f=this;c=function(){if("undefined"!==typeof a.R.Md||f.N[b]!==k.localStorage.getItem(b))f.N[b]=k.localStorage.getItem(b),f.hd(b)};z&&Cb&&10==Cb&&k.localStorage.getItem(b)!==a.R.newValue&&a.R.newValue!==a.R.oldValue?setTimeout(c,10):c()}}else x(a,r(this.hd,this))};Xi.prototype.hd=function(a){this.S[a]&&x(this.S[a],function(a){a()})};var cj=function(a,b){this.j=a;this.h=b||Zi()},dj={name:"authEvent",D:"local"},ej=function(a){return a.h.get(dj,a.j).then(function(a){return ng(a)})};cj.prototype.ib=function(a){this.h.addListener(dj,this.j,a)};cj.prototype.dc=function(a){this.h.removeListener(dj,this.j,a)};var fj=function(a){this.h=a||Zi()},gj={name:"sessionId",D:"session"};fj.prototype.tc=function(a){return this.h.get(gj,a)};var hj=function(a,b,c,d,e,f,g){this.B=a;this.m=b;this.o=c;this.La=d||null;this.P=g||null;this.$e=b+":"+c;this.pg=new fj;this.xe=new cj(this.$e);this.Ad=null;this.ta=[];this.Vf=e||500;this.ig=f||2E3;this.Rb=this.Jc=null},ij=function(a){return new O("invalid-cordova-configuration",a)}; -hj.prototype.Ra=function(){return this.Tb?this.Tb:this.Tb=Df().then(function(){if("function"!==typeof M("universalLinks.subscribe",k))throw ij("cordova-universal-links-plugin is not installed");if("undefined"===typeof M("BuildInfo.packageName",k))throw ij("cordova-plugin-buildinfo is not installed");if("function"!==typeof M("cordova.plugins.browsertab.openUrl",k))throw ij("cordova-plugin-browsertab is not installed");if("function"!==typeof M("cordova.InAppBrowser.open",k))throw ij("cordova-plugin-inappbrowser is not installed"); -},function(){throw new O("cordova-not-ready");})};var jj=function(){for(var a=20,b=[];0this.Na-3E4?this.fa?Mj(this,{grant_type:"refresh_token",refresh_token:this.fa}):D(null):D({accessToken:this.Ja,expirationTime:this.Na,refreshToken:this.fa})};var Nj=function(a,b){this.pe=a||null;this.Je=b||null;bg(this,{lastSignInTime:Yf(b||null),creationTime:Yf(a||null)})};Nj.prototype.clone=function(){return new Nj(this.pe,this.Je)};Nj.prototype.A=function(){return{lastLoginAt:this.Je,createdAt:this.pe}};var Oj=function(a,b,c,d,e,f){bg(this,{uid:a,displayName:d||null,photoURL:e||null,email:c||null,phoneNumber:f||null,providerId:b})},Pj=function(a,b){B.call(this,a);for(var c in b)this[c]=b[c]};t(Pj,B); -var S=function(a,b,c){this.J=[];this.m=a.apiKey;this.o=a.appName;this.B=a.authDomain||null;a=firebase.SDK_VERSION?Jf(firebase.SDK_VERSION):null;this.f=new R(this.m,of(pf),a);this.ra=new Jj(this.f);Qj(this,b.idToken);Lj(this.ra,b);N(this,"refreshToken",this.ra.fa);Rj(this,c||{});G.call(this);this.Kc=!1;this.B&&Nf()&&(this.v=Ej(this.B,this.m,this.o));this.Rc=[];this.sa=null;this.wb=Sj(this);this.Gb=r(this.wd,this);var d=this;this.ia=null;this.Pe=function(a){d.Cb(a.languageCode)};this.Dd=null;this.M= -[];this.Oe=function(a){Tj(d,a.Ff)};this.sd=null};t(S,G);S.prototype.Cb=function(a){this.ia=a;ah(this.f,a)};var Uj=function(a,b){a.Dd&&tc(a.Dd,"languageCodeChanged",a.Pe);(a.Dd=b)&&lc(b,"languageCodeChanged",a.Pe)},Tj=function(a,b){a.M=b;bh(a.f,firebase.SDK_VERSION?Jf(firebase.SDK_VERSION,a.M):null)},Vj=function(a,b){a.sd&&tc(a.sd,"frameworkChanged",a.Oe);(a.sd=b)&&lc(b,"frameworkChanged",a.Oe)};S.prototype.wd=function(){this.wb.ub&&(this.wb.stop(),this.wb.start())}; -var Wj=function(a){try{return firebase.app(a.o).auth()}catch(b){throw new O("internal-error","No firebase.auth.Auth instance is available for the Firebase App '"+a.o+"'!");}},Sj=function(a){return new Li(function(){return a.getIdToken(!0)},function(a){return a&&"auth/network-request-failed"==a.code?!0:!1},function(){var b=a.ra.Na-ma()-3E5;return 0this.Oa&&(this.Oa=0);0==this.Oa&&U(this)&&Yj(U(this));this.removeAuthTokenListener(a)};h.addAuthTokenListener=function(a){var b=this;this.Ka.push(a);this.c(this.ya.then(function(){b.Ea||Va(b.Ka,a)&&a(Uk(b))}))};h.removeAuthTokenListener=function(a){Ya(this.Ka,function(b){return b==a})};var Tk=function(a,b){a.Fb.push(b);a.c(a.ya.then(function(){!a.Ea&&Va(a.Fb,b)&&a.kc!==a.getUid()&&(a.kc=a.getUid(),b(Uk(a)))}))};h=T.prototype; -h["delete"]=function(){this.Ea=!0;for(var a=0;ae||e>=Vk.length)throw new O("internal-error", -"Argument validator received an unsupported number of arguments.");c=Vk[e];d=(d?"":c+" argument ")+(b.name?'"'+b.name+'" ':"")+"must be "+b.V+".";break a}d=null}}if(d)throw new O("argument-error",a+" failed: "+d);},Vk="First Second Third Fourth Fifth Sixth Seventh Eighth Ninth".split(" "),V=function(a,b){return{name:a||"",V:"a valid string",optional:!!b,W:m}},Xk=function(){return{name:"opt_forceRefresh",V:"a boolean",optional:!0,W:ca}},W=function(a,b){return{name:a||"",V:"a valid object",optional:!!b, -W:q}},Yk=function(a,b){return{name:a||"",V:"a function",optional:!!b,W:p}},Zk=function(a,b){return{name:a||"",V:"null",optional:!!b,W:fa}},$k=function(){return{name:"",V:"an HTML element",optional:!1,W:function(a){return!!(a&&a instanceof Element)}}},al=function(){return{name:"auth",V:"an instance of Firebase Auth",optional:!0,W:function(a){return!!(a&&a instanceof T)}}},bl=function(){return{name:"app",V:"an instance of Firebase App",optional:!0,W:function(a){return!!(a&&a instanceof firebase.app.App)}}}, -cl=function(a){return{name:a?a+"Credential":"credential",V:a?"a valid "+a+" credential":"a valid credential",optional:!1,W:function(b){if(!b)return!1;var c=!a||b.providerId===a;return!(!b.Ob||!c)}}},dl=function(){return{name:"authProvider",V:"a valid Auth provider",optional:!1,W:function(a){return!!(a&&a.providerId&&a.hasOwnProperty&&a.hasOwnProperty("isOAuthProvider"))}}},el=function(){return{name:"applicationVerifier",V:"an implementation of firebase.auth.ApplicationVerifier",optional:!1,W:function(a){return!!(a&& -m(a.type)&&p(a.verify))}}},X=function(a,b,c,d){return{name:c||"",V:a.V+" or "+b.V,optional:!!d,W:function(c){return a.W(c)||b.W(c)}}};var Y=function(a,b){for(var c in b){var d=b[c].name;a[d]=fl(d,a[c],b[c].a)}},Z=function(a,b,c,d){a[b]=fl(b,c,d)},fl=function(a,b,c){if(!c)return b;var d=gl(a);a=function(){var a=Array.prototype.slice.call(arguments);Wk(d,c,a);return b.apply(this,a)};for(var e in b)a[e]=b[e];for(e in b.prototype)a.prototype[e]=b.prototype[e];return a},gl=function(a){a=a.split(".");return a[a.length-1]};Y(T.prototype,{applyActionCode:{name:"applyActionCode",a:[V("code")]},checkActionCode:{name:"checkActionCode",a:[V("code")]},confirmPasswordReset:{name:"confirmPasswordReset",a:[V("code"),V("newPassword")]},createUserWithEmailAndPassword:{name:"createUserWithEmailAndPassword",a:[V("email"),V("password")]},fetchProvidersForEmail:{name:"fetchProvidersForEmail",a:[V("email")]},getRedirectResult:{name:"getRedirectResult",a:[]},onAuthStateChanged:{name:"onAuthStateChanged",a:[X(W(),Yk(),"nextOrObserver"), -Yk("opt_error",!0),Yk("opt_completed",!0)]},onIdTokenChanged:{name:"onIdTokenChanged",a:[X(W(),Yk(),"nextOrObserver"),Yk("opt_error",!0),Yk("opt_completed",!0)]},sendPasswordResetEmail:{name:"sendPasswordResetEmail",a:[V("email"),X(W("opt_actionCodeSettings",!0),Zk(null,!0),"opt_actionCodeSettings",!0)]},setPersistence:{name:"setPersistence",a:[V("persistence")]},signInAndRetrieveDataWithCredential:{name:"signInAndRetrieveDataWithCredential",a:[cl()]},signInAnonymously:{name:"signInAnonymously",a:[]}, -signInWithCredential:{name:"signInWithCredential",a:[cl()]},signInWithCustomToken:{name:"signInWithCustomToken",a:[V("token")]},signInWithEmailAndPassword:{name:"signInWithEmailAndPassword",a:[V("email"),V("password")]},signInWithPhoneNumber:{name:"signInWithPhoneNumber",a:[V("phoneNumber"),el()]},signInWithPopup:{name:"signInWithPopup",a:[dl()]},signInWithRedirect:{name:"signInWithRedirect",a:[dl()]},signOut:{name:"signOut",a:[]},toJSON:{name:"toJSON",a:[V(null,!0)]},useDeviceLanguage:{name:"useDeviceLanguage", -a:[]},verifyPasswordResetCode:{name:"verifyPasswordResetCode",a:[V("code")]}});(function(a,b){for(var c in b){var d=b[c].name;if(d!==c){var e=b[c].rf;Object.defineProperty(a,d,{get:function(){return this[c]},set:function(a){Wk(d,[e],[a],!0);this[c]=a},enumerable:!0})}}})(T.prototype,{lc:{name:"languageCode",rf:X(V(),Zk(),"languageCode")}});T.Persistence=Vi;T.Persistence.LOCAL="local";T.Persistence.SESSION="session";T.Persistence.NONE="none"; -Y(S.prototype,{"delete":{name:"delete",a:[]},getIdToken:{name:"getIdToken",a:[Xk()]},getToken:{name:"getToken",a:[Xk()]},linkAndRetrieveDataWithCredential:{name:"linkAndRetrieveDataWithCredential",a:[cl()]},linkWithCredential:{name:"linkWithCredential",a:[cl()]},linkWithPhoneNumber:{name:"linkWithPhoneNumber",a:[V("phoneNumber"),el()]},linkWithPopup:{name:"linkWithPopup",a:[dl()]},linkWithRedirect:{name:"linkWithRedirect",a:[dl()]},reauthenticateAndRetrieveDataWithCredential:{name:"reauthenticateAndRetrieveDataWithCredential", -a:[cl()]},reauthenticateWithCredential:{name:"reauthenticateWithCredential",a:[cl()]},reauthenticateWithPhoneNumber:{name:"reauthenticateWithPhoneNumber",a:[V("phoneNumber"),el()]},reauthenticateWithPopup:{name:"reauthenticateWithPopup",a:[dl()]},reauthenticateWithRedirect:{name:"reauthenticateWithRedirect",a:[dl()]},reload:{name:"reload",a:[]},sendEmailVerification:{name:"sendEmailVerification",a:[X(W("opt_actionCodeSettings",!0),Zk(null,!0),"opt_actionCodeSettings",!0)]},toJSON:{name:"toJSON",a:[V(null, -!0)]},unlink:{name:"unlink",a:[V("provider")]},updateEmail:{name:"updateEmail",a:[V("email")]},updatePassword:{name:"updatePassword",a:[V("password")]},updatePhoneNumber:{name:"updatePhoneNumber",a:[cl("phone")]},updateProfile:{name:"updateProfile",a:[W("profile")]}});Y(C.prototype,{g:{name:"catch"},then:{name:"then"}});Y(Ii.prototype,{confirm:{name:"confirm",a:[V("verificationCode")]}});Z(Jg,"credential",function(a,b){return new Gg(a,b)},[V("email"),V("password")]); -Y(yg.prototype,{addScope:{name:"addScope",a:[V("scope")]},setCustomParameters:{name:"setCustomParameters",a:[W("customOAuthParameters")]}});Z(yg,"credential",zg,[X(V(),W(),"token")]);Y(Ag.prototype,{addScope:{name:"addScope",a:[V("scope")]},setCustomParameters:{name:"setCustomParameters",a:[W("customOAuthParameters")]}});Z(Ag,"credential",Bg,[X(V(),W(),"token")]);Y(Cg.prototype,{addScope:{name:"addScope",a:[V("scope")]},setCustomParameters:{name:"setCustomParameters",a:[W("customOAuthParameters")]}}); -Z(Cg,"credential",Dg,[X(V(),X(W(),Zk()),"idToken"),X(V(),Zk(),"accessToken",!0)]);Y(Eg.prototype,{setCustomParameters:{name:"setCustomParameters",a:[W("customOAuthParameters")]}});Z(Eg,"credential",Fg,[X(V(),W(),"token"),V("secret",!0)]);Y(P.prototype,{addScope:{name:"addScope",a:[V("scope")]},credential:{name:"credential",a:[X(V(),Zk(),"idToken",!0),X(V(),Zk(),"accessToken",!0)]},setCustomParameters:{name:"setCustomParameters",a:[W("customOAuthParameters")]}}); -Z(Og,"credential",Qg,[V("verificationId"),V("verificationCode")]);Y(Og.prototype,{verifyPhoneNumber:{name:"verifyPhoneNumber",a:[V("phoneNumber"),el()]}});Y(O.prototype,{toJSON:{name:"toJSON",a:[V(null,!0)]}});Y(Tg.prototype,{toJSON:{name:"toJSON",a:[V(null,!0)]}});Y(og.prototype,{toJSON:{name:"toJSON",a:[V(null,!0)]}});Y(li.prototype,{clear:{name:"clear",a:[]},render:{name:"render",a:[]},verify:{name:"verify",a:[]}}); -(function(){if("undefined"!==typeof firebase&&firebase.INTERNAL&&firebase.INTERNAL.registerService){var a={Auth:T,Error:O};Z(a,"EmailAuthProvider",Jg,[]);Z(a,"FacebookAuthProvider",yg,[]);Z(a,"GithubAuthProvider",Ag,[]);Z(a,"GoogleAuthProvider",Cg,[]);Z(a,"TwitterAuthProvider",Eg,[]);Z(a,"OAuthProvider",P,[V("providerId")]);Z(a,"PhoneAuthProvider",Og,[al()]);Z(a,"RecaptchaVerifier",li,[X(V(),$k(),"recaptchaContainer"),W("recaptchaParameters",!0),bl()]);firebase.INTERNAL.registerService("auth",function(a, -c){a=new T(a);c({INTERNAL:{getUid:r(a.getUid,a),getToken:r(a.If,a),addAuthTokenListener:r(a.pf,a),removeAuthTokenListener:r(a.jg,a)}});return a},a,function(a,c){if("create"===a)try{c.auth()}catch(d){}});firebase.INTERNAL.extendNamespace({User:S})}else throw Error("Cannot find the firebase namespace; be sure to include firebase-app.js before this library.");})();}).call(this); +/** + * @fileoverview The headless Auth class used for authenticating Firebase users. + */ + +goog.provide('fireauth.Auth'); + +goog.require('fireauth.ActionCodeInfo'); +goog.require('fireauth.ActionCodeSettings'); +goog.require('fireauth.AdditionalUserInfo'); +goog.require('fireauth.AuthCredential'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthEventHandler'); +goog.require('fireauth.AuthEventManager'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.AuthUser'); +goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.UserEventType'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.idp'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.object'); +goog.require('fireauth.storage.RedirectUserManager'); +goog.require('fireauth.storage.UserManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.events.Event'); +goog.require('goog.events.EventTarget'); +goog.require('goog.object'); + + + +/** + * Creates the Firebase Auth corresponding for the App provided. + * + * @param {!firebase.app.App} app The corresponding Firebase App. + * @constructor + * @implements {fireauth.AuthEventHandler} + * @implements {firebase.Service} + * @extends {goog.events.EventTarget} + */ +fireauth.Auth = function(app) { + /** @private {boolean} Whether this instance is deleted. */ + this.deleted_ = false; + /** Auth's corresponding App. */ + fireauth.object.setReadonlyProperty(this, 'app', app); + // Initialize RPC handler. + // API key is required for web client RPC calls. + if (this.app_().options && this.app_().options['apiKey']) { + var clientFullVersion = firebase.SDK_VERSION ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION) : + null; + this.rpcHandler_ = new fireauth.RpcHandler( + this.app_().options && this.app_().options['apiKey'], + // Get the client Auth endpoint used. + fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint), + clientFullVersion); + } else { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY); + } + /** @private {!Array|!goog.Promise>} List of + * pending promises. */ + this.pendingPromises_ = []; + /** @private {!Array} Auth token listeners. */ + this.authListeners_ = []; + /** @private {!Array} User change listeners. */ + this.userChangeListeners_ = []; + /** + * @private {!firebase.Subscribe} The subscribe function to the Auth ID token + * change observer. This will trigger on ID token changes, including + * token refresh on the same user. + */ + this.onIdTokenChanged_ = firebase.INTERNAL.createSubscribe( + goog.bind(this.initIdTokenChangeObserver_, this)); + /** + * @private {?string|undefined} The UID of the user that last triggered the + * user state change listener. + */ + this.userStateChangeUid_ = undefined; + /** + * @private {!firebase.Subscribe} The subscribe function to the user state + * change observer. + */ + this.onUserStateChanged_ = firebase.INTERNAL.createSubscribe( + goog.bind(this.initUserStateObserver_, this)); + // Set currentUser to null. + this.setCurrentUser_(null); + /** + * @private {!fireauth.storage.UserManager} The Auth user storage + * manager instance. + */ + this.userStorageManager_ = + new fireauth.storage.UserManager(this.getStorageKey()); + /** + * @private {!fireauth.storage.RedirectUserManager} The redirect user + * storagemanager instance. + */ + this.redirectUserStorageManager_ = + new fireauth.storage.RedirectUserManager(this.getStorageKey()); + /** + * @private {!goog.Promise} Promise that resolves when initial + * state is loaded from storage. + */ + this.authStateLoaded_ = this.registerPendingPromise_(this.initAuthState_()); + /** + * @private {!goog.Promise} Promise that resolves when state and + * redirect result is ready, after which sign in and sign out operations + * are safe to execute. + */ + this.redirectStateIsReady_ = this.registerPendingPromise_( + this.initAuthRedirectState_()); + /** @private {boolean} Whether initial state has already been resolved. */ + this.isStateResolved_ = false; + /** + * @private {!function()} The syncAuthChanges function with context set to + * auth instance. + */ + this.getSyncAuthUserChanges_ = goog.bind(this.syncAuthUserChanges_, this); + /** @private {!function(!fireauth.AuthUser):!goog.Promise} The handler for + * user state changes. */ + this.userStateChangeListener_ = + goog.bind(this.handleUserStateChange_, this); + /** @private {!function(!Object)} The handler for user token changes. */ + this.userTokenChangeListener_ = + goog.bind(this.handleUserTokenChange_, this); + /** @private {!function(!Object)} The handler for user deletion. */ + this.userDeleteListener_ = goog.bind(this.handleUserDelete_, this); + /** @private {!function(!Object)} The handler for user invalidation. */ + this.userInvalidatedListener_ = goog.bind(this.handleUserInvalidated_, this); + // TODO: find better way to enable or disable auth event manager. + if (fireauth.AuthEventManager.ENABLED) { + // Initialize Auth event manager to handle popup and redirect operations. + this.initAuthEventManager_(); + } + + // Export INTERNAL namespace. + this.INTERNAL = {}; + this.INTERNAL['delete'] = goog.bind(this.delete, this); + this.INTERNAL['logFramework'] = goog.bind(this.logFramework, this); + /** + * @private {number} The number of Firebase services subscribed to Auth + * changes. + */ + this.firebaseServices_ = 0; + // Add call to superclass constructor. + fireauth.Auth.base(this, 'constructor'); + // Initialize readable/writable Auth properties. + this.initializeReadableWritableProps_(); + /** + * @private {!Array} List of Firebase frameworks/libraries used. This + * is currently only used to log FirebaseUI. + */ + this.frameworks_ = []; +}; +goog.inherits(fireauth.Auth, goog.events.EventTarget); + + +/** + * Language code change custom event. + * @param {?string} languageCode The new language code. + * @constructor + * @extends {goog.events.Event} + */ +fireauth.Auth.LanguageCodeChangeEvent = function(languageCode) { + goog.events.Event.call( + this, fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED); + this.languageCode = languageCode; +}; +goog.inherits(fireauth.Auth.LanguageCodeChangeEvent, goog.events.Event); + + +/** + * Framework change custom event. + * @param {!Array} frameworks The new frameworks array. + * @constructor + * @extends {goog.events.Event} + */ +fireauth.Auth.FrameworkChangeEvent = function(frameworks) { + goog.events.Event.call( + this, fireauth.constants.AuthEventType.FRAMEWORK_CHANGED); + this.frameworks = frameworks; +}; +goog.inherits(fireauth.Auth.FrameworkChangeEvent, goog.events.Event); + + +/** + * Changes the Auth state persistence to the specified one. + * @param {!fireauth.authStorage.Persistence} persistence The Auth state + * persistence mechanism. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.setPersistence = function(persistence) { + // TODO: fix auth.delete() behavior and how this affects persistence + // change after deletion. + // Throw an error if already destroyed. + // Set current persistence. + var p = this.userStorageManager_.setPersistence(persistence); + return /** @type {!goog.Promise} */ (this.registerPendingPromise_(p)); +}; + + +/** + * Get rid of Closure warning - the property is adding in the constructor. + * @type {!firebase.app.App} + */ +fireauth.Auth.prototype.app; + + +/** + * Sets the language code. + * @param {?string} languageCode + */ +fireauth.Auth.prototype.setLanguageCode = function(languageCode) { + // Don't do anything if no change detected. + if (this.languageCode_ !== languageCode && !this.deleted_) { + this.languageCode_ = languageCode; + // Update custom Firebase locale field. + this.rpcHandler_.updateCustomLocaleHeader(this.languageCode_); + // Notify external language code change listeners. + this.notifyLanguageCodeListeners_(); + } +}; + + +/** + * Returns the current auth instance's language code if available. + * @return {?string} + */ +fireauth.Auth.prototype.getLanguageCode = function() { + return this.languageCode_; +}; + + +/** + * Sets the current language to the default device/browser preference. + */ +fireauth.Auth.prototype.useDeviceLanguage = function() { + this.setLanguageCode(fireauth.util.getUserLanguage()); +}; + + +/** + * @param {string} frameworkId The framework identifier. + */ +fireauth.Auth.prototype.logFramework = function(frameworkId) { + // Theoretically multiple frameworks could be used + // (angularfire and FirebaseUI). Once a framework is used, it is not going + // to be unused, so no point adding a method to remove the framework ID. + this.frameworks_.push(frameworkId); + // Update the client version in RPC handler with the new frameworks. + this.rpcHandler_.updateClientVersion(firebase.SDK_VERSION ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + this.frameworks_) : + null); + this.dispatchEvent(new fireauth.Auth.FrameworkChangeEvent( + this.frameworks_)); +}; + + +/** @return {!Array} The current Firebase frameworks. */ +fireauth.Auth.prototype.getFramework = function() { + return goog.array.clone(this.frameworks_); +}; + + +/** + * Updates the framework list on the provided user and configures the user to + * listen to the Auth instance for any framework ID changes. + * @param {!fireauth.AuthUser} user The user to whose framework list needs to be + * updated. + * @private + */ +fireauth.Auth.prototype.setUserFramework_ = function(user) { + // Sets the framework ID on the user. + user.setFramework(this.frameworks_); + // Sets current Auth instance as framework list change dispatcher on the user. + user.setFrameworkChangeDispatcher(this); +}; + + +/** + * Initializes readable/writable properties on Auth. + * @suppress {invalidCasts} + * @private + */ +fireauth.Auth.prototype.initializeReadableWritableProps_ = function() { + Object.defineProperty(/** @type {!Object} */ (this), 'lc', { + /** + * @this {!Object} + * @return {?string} The current language code. + */ + get: function() { + return this.getLanguageCode(); + }, + /** + * @this {!Object} + * @param {string} value The new language code. + */ + set: function(value) { + this.setLanguageCode(value); + }, + enumerable: false + }); + // Initialize to null. + /** @private {?string} The current Auth instance's language code. */ + this.languageCode_ = null; +}; + + +/** + * Notifies all external listeners of the language code change. + * @private + */ +fireauth.Auth.prototype.notifyLanguageCodeListeners_ = function() { + // Notify external listeners on the language code change. + this.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent( + this.getLanguageCode())); +}; + + + + + +/** + * @return {!Object} The object representation of the Auth instance. + * @override + */ +fireauth.Auth.prototype.toJSON = function() { + // Return the plain object representation in case JSON.stringify is called on + // an Auth instance. + return { + 'apiKey': this.app_().options['apiKey'], + 'authDomain': this.app_().options['authDomain'], + 'appName': this.app_().name, + 'currentUser': this.currentUser_() && this.currentUser_().toPlainObject() + }; +}; + + +/** + * Returns the Auth event manager promise. + * @return {!goog.Promise} + * @private + */ +fireauth.Auth.prototype.getAuthEventManager_ = function() { + // Either return cached Auth event manager promise provider if available or a + // promise that rejects with missing Auth domain error. + return this.eventManagerProviderPromise_ || + goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN)); +}; + + +/** + * Initializes the Auth event manager when state is ready. + * @private + */ +fireauth.Auth.prototype.initAuthEventManager_ = function() { + // Initialize Auth event manager on initState. + var self = this; + var authDomain = this.app_().options['authDomain']; + var apiKey = this.app_().options['apiKey']; + // Make sure environment also supports popup and redirect. + if (authDomain && fireauth.util.isPopupRedirectSupported()) { + // Auth domain is required for Auth event manager to resolve. + // Auth state has to be loaded first. One reason is to process link events. + this.eventManagerProviderPromise_ = this.authStateLoaded_.then(function() { + if (self.deleted_) { + return; + } + // By this time currentUser should be ready if available and will be able + // to resolve linkWithRedirect if detected. + self.authEventManager_ = fireauth.AuthEventManager.getManager( + authDomain, apiKey, self.app_().name); + // Subscribe Auth instance. + self.authEventManager_.subscribe(self); + // Subscribe current user by enabling popup and redirect on that user. + if (self.currentUser_()) { + self.currentUser_().enablePopupRedirect(); + } + // If a redirect user is present, subscribe to popup and redirect events. + // In case current user was not available and the developer called link + // with redirect on a signed out user, this will work and the linked + // logged out user will be returned in getRedirectResult. + // current user and redirect user are the same (was already logged in), + // currentUser will have priority as it is subscribed before redirect + // user. This change will also allow further popup and redirect events on + // the redirect user going forward. + if (self.redirectUser_) { + self.redirectUser_.enablePopupRedirect(); + // Set the user language for the redirect user. + self.setUserLanguage_( + /** @type {!fireauth.AuthUser} */ (self.redirectUser_)); + // Set the user Firebase frameworks for the redirect user. + self.setUserFramework_( + /** @type {!fireauth.AuthUser} */ (self.redirectUser_)); + // Reference to redirect user no longer needed. + self.redirectUser_ = null; + } + return self.authEventManager_; + }); + } +}; + + +/** + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?string=} opt_eventId The event ID. + * @return {boolean} Whether the auth event handler can handler the provided + * event. + * @override + */ +fireauth.Auth.prototype.canHandleAuthEvent = function(mode, opt_eventId) { + // Only sign in events are handled. + switch (mode) { + // Accept all general sign in with redirect and unknowns. + // Migrating redirect events to use session storage will prevent this event + // from leaking to other tabs. + case fireauth.AuthEvent.Type.UNKNOWN: + case fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT: + return true; + case fireauth.AuthEvent.Type. SIGN_IN_VIA_POPUP: + // Pending sign in with popup event must match the stored popup event ID. + return this.popupEventId_ == opt_eventId && + !!this.pendingPopupResolvePromise_; + default: + return false; + } +}; + + +/** + * Completes the pending popup operation. If error is not null, rejects with the + * error. Otherwise, it resolves with the popup redirect result. + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?fireauth.AuthEventManager.Result} popupRedirectResult The result + * to resolve with when no error supplied. + * @param {?fireauth.AuthError} error When supplied, the promise will reject. + * @param {?string=} opt_eventId The event ID. + * @override + */ +fireauth.Auth.prototype.resolvePendingPopupEvent = + function(mode, popupRedirectResult, error, opt_eventId) { + // Only handles popup events of type sign in and which match popup event ID. + if (mode != fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP || + this.popupEventId_ != opt_eventId) { + return; + } + if (error && this.pendingPopupRejectPromise_) { + // Reject with error for supplied mode. + this.pendingPopupRejectPromise_(error); + } else if (popupRedirectResult && + !error && + this.pendingPopupResolvePromise_) { + // Resolve with result for supplied mode. + this.pendingPopupResolvePromise_(popupRedirectResult); + } + // Now that event is resolved, delete popup timeout promise. + if (this.popupTimeoutPromise_) { + this.popupTimeoutPromise_.cancel(); + this.popupTimeoutPromise_ = null; + } + // Delete pending promises. + delete this.pendingPopupResolvePromise_; + delete this.pendingPopupRejectPromise_; +}; + + +/** + * Returns the handler's appropriate popup and redirect sign in operation + * finisher. + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?string=} opt_eventId The optional event ID. + * @return {?function(string, + * string):!goog.Promise} + * @override + */ +fireauth.Auth.prototype.getAuthEventHandlerFinisher = + function(mode, opt_eventId) { + // Sign in events will be completed by finishPopupAndRedirectSignIn. + if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT) { + return goog.bind(this.finishPopupAndRedirectSignIn, this); + } else if (mode == fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP && + this.popupEventId_ == opt_eventId && + this.pendingPopupResolvePromise_) { + return goog.bind(this.finishPopupAndRedirectSignIn, this); + } + return null; +}; + + +/** + * Finishes the popup and redirect sign in operations. + * @param {string} requestUri The callback url with the oauth response. + * @param {string} sessionId The session id used to generate the authUri. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.finishPopupAndRedirectSignIn = + function(requestUri, sessionId) { + var self = this; + // Verify assertion request. + var request = { + 'requestUri': requestUri, + 'sessionId': sessionId + }; + // Now that popup has responded, delete popup timeout promise. + if (this.popupTimeoutPromise_) { + this.popupTimeoutPromise_.cancel(); + this.popupTimeoutPromise_ = null; + } + // This routine could be run before init state, make sure it waits for that to + // complete. + var credential = null; + var additionalUserInfo = null; + var idTokenResolver = self.rpcHandler_.verifyAssertion(request) + .then(function(response) { + // Get Auth credential from verify assert request and save it. + credential = fireauth.AuthProvider.getCredentialFromResponse(response); + // Get additional IdP data if available in the response. + additionalUserInfo = fireauth.AdditionalUserInfo.fromPlainObject( + response); + return response; + }); + // When state is ready, run verify assertion request. + // This will only run either after initial and redirect state is ready for + // popups or after initial state is ready for redirect resolution. + var p = self.authStateLoaded_.then(function() { + return idTokenResolver; + }).then(function(idTokenResponse) { + // Use ID token response to sign in the Auth user. + return self.signInWithIdTokenResponse(idTokenResponse); + }).then(function() { + // On sign in success, construct redirect and popup result and return a + // readonly copy of it. + return fireauth.object.makeReadonlyCopy({ + 'user': self.currentUser_(), + 'credential': credential, + 'additionalUserInfo': additionalUserInfo, + // Sign in operation type. + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * @return {string} The generated event ID used to identify a popup event. + * @private + */ +fireauth.Auth.prototype.generateEventId_ = function() { + return fireauth.util.generateEventId(); +}; + + +/** + * Signs in to Auth provider via popup. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithPopup = function(provider) { + // Check if popup and redirect are supported in this environment. + if (!fireauth.util.isPopupRedirectSupported()) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP; + var self = this; + // Popup the window immediately to make sure the browser associates the + // popup with the click that triggered it. + + // Get provider settings. + var settings = fireauth.idp.getIdpSettings(provider['providerId']); + // There could multiple sign in with popup events in different windows. + // We need to match the correct popup to the correct pending promise. + var eventId = this.generateEventId_(); + // If incapable of redirecting popup from opener, popup destination URL + // directly. This could also happen in a sandboxed iframe. + var oauthHelperWidgetUrl = null; + if ((!fireauth.util.runsInBackground() || fireauth.util.isIframe()) && + this.app_().options['authDomain'] && + provider['isOAuthProvider']) { + oauthHelperWidgetUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + this.app_().options['authDomain'], + this.app_().options['apiKey'], + this.app_().name, + mode, + provider, + null, + eventId, + firebase.SDK_VERSION || null); + } + // The popup must have a name, otherwise when successive popups are triggered + // they will all render in the same instance and none will succeed since the + // popup cancel of first window will close the shared popup window instance. + var popupWin = + fireauth.util.popup( + oauthHelperWidgetUrl, + fireauth.util.generateRandomString(), + settings && settings.popupWidth, + settings && settings.popupHeight); + // Auth event manager must be available for popup sign in to be possible. + var p = this.getAuthEventManager_().then(function(manager) { + // Process popup request tagging it with newly created event ID. + return manager.processPopup( + popupWin, mode, provider, eventId, !!oauthHelperWidgetUrl); + }).then(function() { + return new goog.Promise(function(resolve, reject) { + // Expire other pending promises if still available.. + self.resolvePendingPopupEvent( + mode, + null, + new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST), + // Existing pending popup event ID. + self.popupEventId_); + // Save current pending promises. + self.pendingPopupResolvePromise_ = resolve; + self.pendingPopupRejectPromise_ = reject; + // Overwrite popup event ID with new one corresponding to popup. + self.popupEventId_ = eventId; + // Keep track of timeout promise to cancel it on promise resolution before + // it times out. + self.popupTimeoutPromise_ = + self.authEventManager_.startPopupTimeout( + self, mode, /** @type {!Window} */ (popupWin), eventId); + }); + }).then(function(result) { + // On resolution, close popup if still opened and pass result through. + if (popupWin) { + fireauth.util.closeWindow(popupWin); + } + if (result) { + return fireauth.object.makeReadonlyCopy(result); + } + return null; + }).thenCatch(function(error) { + if (popupWin) { + fireauth.util.closeWindow(popupWin); + } + throw error; + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * Signs in to Auth provider via redirect. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithRedirect = function(provider) { + // Check if popup and redirect are supported in this environment. + if (!fireauth.util.isPopupRedirectSupported()) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + var self = this; + var mode = fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT; + // Auth event manager must be available for sign in via redirect to be + // possible. + var p = this.getAuthEventManager_().then(function(manager) { + // Remember current persistence to apply it on the next page. + // This is the only time the state is passed to the next page (when user is + // not already logged in). + // This is not needed for link and reauthenticate as the user is already + // stored with specified persistence. + return self.userStorageManager_.savePersistenceForRedirect(); + }).then(function() { + // Process redirect operation. + return self.authEventManager_.processRedirect(mode, provider); + }); + return /** @type {!goog.Promise} */ (this.registerPendingPromise_(p)); +}; + + +/** + * Returns the redirect result. If coming back from a successful redirect sign + * in, will resolve to the signed in user. If coming back from an unsuccessful + * redirect sign, will reject with the proper error. If no redirect operation + * called, resolves with null. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.getRedirectResult = function() { + // Check if popup and redirect are supported in this environment. + if (!fireauth.util.isPopupRedirectSupported()) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + var self = this; + // Auth event manager must be available for get redirect result to be + // possible. + var p = this.getAuthEventManager_().then(function(manager) { + // Return redirect result when resolved. + return self.authEventManager_.getRedirectResult(); + }).then(function(result) { + if (result) { + return fireauth.object.makeReadonlyCopy(result); + } + return null; + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * Completes the headless sign in with the server response containing the STS + * access and refresh tokens, and sets the Auth user as current user while + * setting all listeners to it and saving it to storage. + * @param {!Object} idTokenResponse The ID token response from + * the server. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithIdTokenResponse = + function(idTokenResponse) { + var self = this; + var options = {}; + options['apiKey'] = self.app_().options['apiKey']; + options['authDomain'] = self.app_().options['authDomain']; + options['appName'] = self.app_().name; + // Wait for state to be ready. + // This is used internally and is also used for redirect sign in so there is + // no need for waiting for redirect result to resolve since redirect result + // depends on it. + return this.authStateLoaded_.then(function() { + // Initialize an Auth user using the provided ID token response. + return fireauth.AuthUser.initializeFromIdTokenResponse( + options, + idTokenResponse, + /** @type {!fireauth.storage.RedirectUserManager} */ ( + self.redirectUserStorageManager_), + // Pass frameworks so they are logged in getAccountInfo while populating + // the user info. + self.getFramework()); + }).then(function(user) { + // Check if the same user is already signed in. + if (self.currentUser_() && + user['uid'] == self.currentUser_()['uid']) { + // Same user signed in. Update user data and notify Auth listeners. + // No need to resubscribe to user events. + self.currentUser_().copy(user); + return self.handleUserStateChange_(user); + } + // New user. + // Set current user and attach all listeners to it. + self.setCurrentUser_(user); + // Enable popup and redirect events. + user.enablePopupRedirect(); + // Save user changes. + return self.handleUserStateChange_(user); + }).then(function() { + // Notify external Auth listeners only when state is ready. + self.notifyAuthListeners_(); + }); +}; + + +/** + * Updates the current auth user and attaches event listeners to changes on it. + * Also removes all event listeners from previously signed in user. + * @param {?fireauth.AuthUser} user The current user instance. + * @private + */ +fireauth.Auth.prototype.setCurrentUser_ = function(user) { + // Must be called first before updating currentUser reference. + this.attachEventListeners_(user); + // Update currentUser property. + fireauth.object.setReadonlyProperty(this, 'currentUser', user); + if (user) { + // If a user is available, set the language code on it and set current Auth + // instance as language code change dispatcher. + this.setUserLanguage_(user); + // Set the current frameworks used on the user and set current Auth instance + // as the framework change dispatcher. + this.setUserFramework_(user); + } +}; + + +/** + * Signs out the current user while deleting the Auth user from storage and + * removing all listeners from it. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signOut = function() { + var self = this; + // Wait for final state to be ready first, otherwise a signed out user could + // come back to life. + var p = this.redirectStateIsReady_.then(function() { + // Ignore if already signed out. + if (!self.currentUser_()) { + return goog.Promise.resolve(); + } + // Detach all event listeners. + // Set current user to null. + self.setCurrentUser_(null); + // Remove current user from storage + return /** @type {!fireauth.storage.UserManager} */ ( + self.userStorageManager_).removeCurrentUser() + .then(function() { + // Notify external Auth listeners of this Auth change event. + self.notifyAuthListeners_(); + }); + }); + return /** @type {!goog.Promise} */ (this.registerPendingPromise_(p)); +}; + + +/** + * @return {!goog.Promise} A promise that resolved when any stored redirect user + * is loaded and removed from session storage and then stored locally. + * @private + */ +fireauth.Auth.prototype.initRedirectUser_ = function() { + var self = this; + var authDomain = this.app_().options['authDomain']; + // Get any saved redirect user and delete from session storage. + // Override user's authDomain with app's authDomain if there is a mismatch. + var p = /** @type {!fireauth.storage.RedirectUserManager} */ ( + this.redirectUserStorageManager_).getRedirectUser(authDomain) + .then(function(user) { + // Save redirect user. + self.redirectUser_ = user; + if (user) { + // Set redirect storage manager on user. + user.setRedirectStorageManager( + /** @type {!fireauth.storage.RedirectUserManager} */ ( + self.redirectUserStorageManager_)); + } + // Delete redirect user. + return /** @type {!fireauth.storage.RedirectUserManager} */ ( + self.redirectUserStorageManager_).removeRedirectUser(); + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * Loads the initial Auth state for current application from web storage and + * initializes Auth user accordingly to reflect that state. This routine does + * not wait for any pending redirect result to be resolved. + * @return {!goog.Promise} Promise that resolves when state is ready, + * loaded from storage. + * @private + */ +fireauth.Auth.prototype.initAuthState_ = function() { + // Load current user from storage. + var self = this; + var authDomain = this.app_().options['authDomain']; + // Get any saved redirected user first. + var p = this.initRedirectUser_().then(function() { + // Override user's authDomain with app's authDomain if there is a mismatch. + return /** @type {!fireauth.storage.UserManager} */ ( + self.userStorageManager_).getCurrentUser(authDomain); + }).then(function(user) { + // Logged in user. + if (user) { + // Set redirect storage manager on user. + user.setRedirectStorageManager( + /** @type {!fireauth.storage.RedirectUserManager} */ ( + self.redirectUserStorageManager_)); + // If the current user is undergoing a redirect operation, do not reload + // as that could could potentially delete the user if the token is + // expired. Instead any token problems will be detected via the + // verifyAssertion flow or the remaining flow. This is critical for + // reauthenticateWithRedirect as this flow is potentially used to recover + // from a token expiration error. + if (self.redirectUser_ && + self.redirectUser_.getRedirectEventId() == + user.getRedirectEventId()) { + return user; + } + // Confirm user valid first before setting listeners. + return user.reload().then(function() { + // Force user saving after reload as state change listeners not + // subscribed yet below via setCurrentUser_. Changes may have happened + // externally such as email actions or changes on another device. + return self.userStorageManager_.setCurrentUser(user).then(function() { + return user; + }); + }).thenCatch(function(error) { + if (error['code'] == 'auth/network-request-failed') { + // Do not delete the user from storage if connection is lost or app is + // offline. + return user; + } + // Invalid user, could be deleted, remove from storage and resolve with + // null. + return /** @type {!fireauth.storage.UserManager} */( + self.userStorageManager_).removeCurrentUser(); + }); + } + // No logged in user, resolve with null; + return null; + }).then(function(user) { + // Even though state not ready yet pending any redirect result. + // Current user needs to be available for link with redirect to complete. + // This will also set listener on the user changes in case state changes + // occur they would get updated in storage too. + self.setCurrentUser_(user || null); + }); + // In case the app is deleted before it is initialized with state from + // storage. + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * After initial Auth state is loaded, waits for any pending redirect result, + * resolves it and then adds the external Auth state change listeners and + * triggers first state of all observers. + * @return {!goog.Promise} Promise that resolves when state is ready + * taking into account any pending redirect result. + * @private + */ +fireauth.Auth.prototype.initAuthRedirectState_ = function() { + var self = this; + // Wait first for state to be loaded from storage. + return this.authStateLoaded_.then(function() { + // Resolve any pending redirect result. + return self.getRedirectResult(); + }).thenCatch(function(error) { + // Ignore any error in the process. Redirect could be not supported. + return; + }).then(function() { + // Make sure instance was not deleted before proceeding. + if (self.deleted_) { + return; + } + // Between init Auth state and get redirect result resolution there + // could have been a sign in attempt in another window. + // Force sync and then add listener to run sync on change below. + return self.getSyncAuthUserChanges_(); + }).thenCatch(function(error) { + // Ignore any error in the process. + return; + }).then(function() { + // Now that final state is ready, make sure instance was not deleted before + // proceeding. + if (self.deleted_) { + return; + } + // Initial state has been resolved. + self.isStateResolved_ = true; + // Add user state change listener so changes are synchronized with + // other windows and tabs. + /** @type {!fireauth.storage.UserManager} */ (self.userStorageManager_ + ).addCurrentUserChangeListener(self.getSyncAuthUserChanges_); + }); +}; + + +/** + * Synchronizes current Auth to stored auth state, used when external state + * changes occur. + * @return {!goog.Promise} + * @private + */ +fireauth.Auth.prototype.syncAuthUserChanges_ = function() { + // Get Auth user state from storage and compare to current state. + // Safe to run when no external change is detected. + var self = this; + var authDomain = this.app_().options['authDomain']; + // Override user's authDomain with app's authDomain if there is a mismatch. + return /** @type {!fireauth.storage.UserManager} */ ( + this.userStorageManager_).getCurrentUser(authDomain) + .then(function(user) { + // In case this was deleted. + if (self.deleted_) { + return; + } + // Since the authDomain could be modified here, saving to storage here + // could trigger an infinite loop of changes between this tab and + // another tab using different Auth domain but since sync Auth user + // changes does not save changes to storage, except for getToken below + // if the token needs refreshing but will stop triggering the first time + // the token is refreshed on one of the first tab that refreshes it. + // The latter should not happen anyway since getToken should be valid + // at all times since anything that triggers the storage change should + // have communicated with the backend and that requires a valid token. + // In addition, authDomain difference is an edge case to begin with. + + // If the same user is to be synchronized. + if (self.currentUser_() && + user && + self.currentUser_().hasSameUserIdAs(user)) { + // Data update, simply copy data changes. + self.currentUser_().copy(user); + // If tokens changed from previous user tokens, this will trigger + // notifyAuthListeners_. + return self.currentUser_().getIdToken(); + } else if (!self.currentUser_() && !user) { + // No change, do nothing (was signed out and remained signed out). + return; + } else { + // Update current Auth state. Either a new login or logout. + self.setCurrentUser_(user); + // If a new user is signed in, enabled popup and redirect on that + // user. + if (user) { + user.enablePopupRedirect(); + // Set redirect storage manager on user. + user.setRedirectStorageManager( + /** @type {!fireauth.storage.RedirectUserManager} */ ( + self.redirectUserStorageManager_)); + } + if (self.authEventManager_) { + self.authEventManager_.subscribe(self); + } + // Notify external Auth changes of Auth change event. + self.notifyAuthListeners_(); + } + }); +}; + + +/** + * Updates the language code on the provided user and configures the user to + * listen to the Auth instance for any language code changes. + * @param {!fireauth.AuthUser} user The user to whose language needs to be set. + * @private + */ +fireauth.Auth.prototype.setUserLanguage_ = function(user) { + // Sets the current language code on the user. + user.setLanguageCode(this.getLanguageCode()); + // Sets current Auth instance as language code change dispatcher on the user. + user.setLanguageCodeChangeDispatcher(this); +}; + + +/** + * Handles user state changes. + * @param {!fireauth.AuthUser} user The user which triggered the state changes. + * @return {!goog.Promise} The promise that resolves when state changes are + * handled. + * @private + */ +fireauth.Auth.prototype.handleUserStateChange_ = function(user) { + // Save Auth user state changes. + return /** @type {!fireauth.storage.UserManager} */ ( + this.userStorageManager_).setCurrentUser(user); +}; + + +/** + * Handles user token changes. + * @param {!Object} event The token change event. + * @private + */ +fireauth.Auth.prototype.handleUserTokenChange_ = function(event) { + // This is only called when user is ready and Auth state has been resolved. + // Notify external Auth change listeners. + this.notifyAuthListeners_(); + // Save user token changes. + this.handleUserStateChange_(/** @type {!fireauth.AuthUser} */ ( + this.currentUser_())); +}; + + +/** + * Handles user deletion events. + * @param {!Object} event The user delete event. + * @private + */ +fireauth.Auth.prototype.handleUserDelete_ = function(event) { + // A deleted user will be treated like a sign out event. + this.signOut(); +}; + + +/** + * Handles user invalidation events. + * @param {!Object} event The user invalidation event. + * @private + */ +fireauth.Auth.prototype.handleUserInvalidated_ = function(event) { + // An invalidated user will be treated like a sign out event. + this.signOut(); +}; + + +/** + * Detaches all previous listeners on current user and reattach new listeners to + * provided user if not null. + * @param {?fireauth.AuthUser} user The user to attach event listeners to. + * @private + */ +fireauth.Auth.prototype.attachEventListeners_ = function(user) { + // Remove existing event listeners from previous current user if available. + if (this.currentUser_()) { + this.currentUser_().removeStateChangeListener( + this.userStateChangeListener_); + goog.events.unlisten( + this.currentUser_(), + fireauth.UserEventType.TOKEN_CHANGED, + this.userTokenChangeListener_); + goog.events.unlisten( + this.currentUser_(), + fireauth.UserEventType.USER_DELETED, + this.userDeleteListener_); + goog.events.unlisten( + this.currentUser_(), + fireauth.UserEventType.USER_INVALIDATED, + this.userInvalidatedListener_); + // Stop proactive token refresh on the current user. + this.currentUser_().stopProactiveRefresh(); + } + // If a new user is provided, attach event listeners to state, token, user + // invalidation and delete events. + if (user) { + user.addStateChangeListener(this.userStateChangeListener_); + goog.events.listen( + user, + fireauth.UserEventType.TOKEN_CHANGED, + this.userTokenChangeListener_); + goog.events.listen( + user, + fireauth.UserEventType.USER_DELETED, + this.userDeleteListener_); + goog.events.listen( + user, + fireauth.UserEventType.USER_INVALIDATED, + this.userInvalidatedListener_); + // Start proactive token refresh on new user if there is at least one + // Firebase service subscribed to Auth changes. + if (this.firebaseServices_ > 0) { + user.startProactiveRefresh(); + } + } +}; + + +/** + * Signs in with ID token promise provider. + * @param {!goog.Promise} idTokenPromise + * The rpc handler method that returns a promise which resolves with an ID + * token. + * @return {!goog.Promise} + * @private + */ +fireauth.Auth.prototype.signInWithIdTokenProvider_ = function(idTokenPromise) { + var self = this; + var credential = null; + var additionalUserInfo = null; + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_( + idTokenPromise + .then(function(idTokenResponse) { + // Get credential if available in the response. + credential = fireauth.AuthProvider.getCredentialFromResponse( + idTokenResponse); + // Get additional IdP data if available in the response. + additionalUserInfo = fireauth.AdditionalUserInfo.fromPlainObject( + idTokenResponse); + // When custom token is exchanged for idToken, continue sign in with + // ID token and return firebase Auth user. + return self.signInWithIdTokenResponse(idTokenResponse); + }) + .then(function() { + // Resolve promise with a readonly user credential object. + return fireauth.object.makeReadonlyCopy({ + // Return the current user reference. + 'user': self.currentUser_(), + // Return any credential passed from the backend. + 'credential': credential, + // Return any additional IdP data passed from the backend. + 'additionalUserInfo': additionalUserInfo, + // Sign in operation type. + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + }))); +}; + + +/** + * Initializes the Auth state change observer returned by the + * firebase.INTERNAL.createSubscribe helper. + * @param {!firebase.Observer} observer The Auth state change observer. + * @private + */ +fireauth.Auth.prototype.initIdTokenChangeObserver_ = function(observer) { + var self = this; + // Adds a listener that will transmit the event everytime it's called. + this.addAuthTokenListener(function(accessToken) { + observer.next(self.currentUser_()); + }); +}; + + +/** + * Initializes the user state change observer returned by the + * firebase.INTERNAL.createSubscribe helper. + * @param {!firebase.Observer} observer The user state change observer. + * @private + */ +fireauth.Auth.prototype.initUserStateObserver_ = function(observer) { + var self = this; + // Adds a listener that will transmit the event everytime it's called. + this.addUserChangeListener_(function(accessToken) { + observer.next(self.currentUser_()); + }); +}; + + +/** + * Adds an observer for Auth state changes, we need to wrap the call as + * the args checking code needs a method defined on the prototype this way, + * not within the constructor, and we also have to implement the behavior + * that will trigger an observer right away if state is ready. + * @param {!firebase.Observer|function(?fireauth.AuthUser)} + * nextOrObserver An observer object or a function triggered on change. + * @param {function(!fireauth.AuthError)=} opt_error Optional A function + * triggered on Auth error. + * @param {function()=} opt_completed Optional A function triggered when the + * observer is removed. + * @return {!function()} The unsubscribe function for the observer. + */ +fireauth.Auth.prototype.onIdTokenChanged = function( + nextOrObserver, opt_error, opt_completed) { + var self = this; + // State already determined. Trigger immediately, otherwise initState will + // take care of notifying all pending listeners on initialization. + // In this case we do not trigger synchronously and trigger via a resolved + // promise as required by specs. + if (this.isStateResolved_) { + // The observer cannot be called synchronously. We're using the + // firebase.Promise implementation as otherwise it creates weird behavior + // where the order of promises resolution would not be as expected. + // It is due to the fact fireauth and firebase.app use their own + // and different promises library and this leads to calls resolutions order + // being different from the promises registration order. + firebase.Promise.resolve().then(function() { + if (goog.isFunction(nextOrObserver)) { + nextOrObserver(self.currentUser_()); + } else if (goog.isFunction(nextOrObserver['next'])) { + nextOrObserver['next'](self.currentUser_()); + } + }); + } + return this.onIdTokenChanged_( + /** @type {!firebase.Observer|function(*)|undefined} */ (nextOrObserver), + /** @type {function(!Error)|undefined} */ (opt_error), + opt_completed); +}; + + +/** + * Adds an observer for user state changes, we need to wrap the call as + * the args checking code needs a method defined on the prototype this way, + * not within the constructor, and we also have to implement the behavior + * that will trigger an observer right away if state is ready. + * @param {!firebase.Observer|function(?fireauth.AuthUser)} + * nextOrObserver An observer object or a function triggered on change. + * @param {function(!fireauth.AuthError)=} opt_error Optional A function + * triggered on Auth error. + * @param {function()=} opt_completed Optional A function triggered when the + * observer is removed. + * @return {!function()} The unsubscribe function for the observer. + */ +fireauth.Auth.prototype.onAuthStateChanged = function( + nextOrObserver, opt_error, opt_completed) { + var self = this; + // State already determined. Trigger immediately, otherwise initState will + // take care of notifying all pending listeners on initialization. + // In this case we do not trigger synchronously and trigger via a resolved + // promise as required by specs. + if (this.isStateResolved_) { + // The observer cannot be called synchronously. We're using the + // firebase.Promise implementation as otherwise it creates weird behavior + // where the order of promises resolution would not be as expected. + // It is due to the fact fireauth and firebase.app use their own + // and different promises library and this leads to calls resolutions order + // being different from the promises registration order. + firebase.Promise.resolve().then(function() { + // This ensures that the first time notifyAuthListeners_ is triggered, + // it has the correct UID before triggering the user state change + // listeners. + self.userStateChangeUid_ = self.getUid(); + if (goog.isFunction(nextOrObserver)) { + nextOrObserver(self.currentUser_()); + } else if (goog.isFunction(nextOrObserver['next'])) { + nextOrObserver['next'](self.currentUser_()); + } + }); + } + return this.onUserStateChanged_( + /** @type {!firebase.Observer|function(*)|undefined} */ (nextOrObserver), + /** @type {function(!Error)|undefined} */ (opt_error), + opt_completed); +}; + + +/** + * Returns an STS token. If the cached one is unexpired it is directly returned. + * Otherwise the existing ID token or refresh token is exchanged for a new one. + * If there is no user signed in, returns null. + * + * This method is called getIdTokenInternal as the symbol getIdToken is not + * obfuscated, which could lead to developers incorrectly calling + * firebase.auth().getIdToken(). + * + * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.getIdTokenInternal = function(opt_forceRefresh) { + var self = this; + // Wait for state to be ready. + var p = this.redirectStateIsReady_.then(function() { + // Call user's underlying getIdToken method. + if (self.currentUser_()) { + return self.currentUser_().getIdToken(opt_forceRefresh) + .then(function(stsAccessToken) { + // This is used internally by other services which expect the access + // token to be returned in an object. + return { + 'accessToken': stsAccessToken + }; + }); + } + // No logged in user, return null token. + return null; + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * Sign in using a custom token (Bring Your Own Auth). + * @param {string} token The custom token to sign in with. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithCustomToken = function(token) { + var self = this; + // Wait for the redirect state to be determined before proceeding. If critical + // errors like web storage unsupported are detected, fail before RPC, instead + // of after. + return this.redirectStateIsReady_.then(function() { + return self.signInWithIdTokenProvider_( + self.getRpcHandler().verifyCustomToken(token)); + }).then(function(result) { + var user = result['user']; + // Manually sets the isAnonymous flag to false as the GetAccountInfo + // response will look like an anonymous user (no credentials visible). + user.updateProperty('isAnonymous', false); + // Save isAnonymous flag changes to current user in storage. + return self.handleUserStateChange_(user); + }).then(function() { + return self.currentUser_(); + }); +}; + + +/** + * Sign in using an email and password. + * @param {string} email The email to sign in with. + * @param {string} password The password to sign in with. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithEmailAndPassword = + function(email, password) { + var self = this; + // Wait for the redirect state to be determined before proceeding. If critical + // errors like web storage unsupported are detected, fail before RPC, instead + // of after. + return this.redirectStateIsReady_.then(function() { + return self.signInWithIdTokenProvider_( + self.getRpcHandler().verifyPassword(email, password)); + }).then(function(result) { + return result['user']; + }); +}; + + +/** + * Create a new email and password account. + * @param {string} email The email to sign up with. + * @param {string} password The password to sign up with. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.createUserWithEmailAndPassword = + function(email, password) { + var self = this; + // Wait for the redirect state to be determined before proceeding. If critical + // errors like web storage unsupported are detected, fail before RPC, instead + // of after. + return this.redirectStateIsReady_.then(function() { + return self.signInWithIdTokenProvider_( + self.getRpcHandler().createAccount(email, password)); + }).then(function(result) { + return result['user']; + }); +}; + + +/** + * Logs into Firebase with the given 3rd party credentials. + * @param {!fireauth.AuthCredential} credential The auth credential. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithCredential = function(credential) { + // Get signInAndRetrieveDataWithCredential result and return the user only. + return this.signInAndRetrieveDataWithCredential(credential) + .then(function(result) { + return result['user']; + }); +}; + + +/** + * Logs into Firebase with the given 3rd party credentials and returns any + * additional user info data or credentials returned form the backend. + * @param {!fireauth.AuthCredential} credential The Auth credential. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInAndRetrieveDataWithCredential = + function(credential) { + // Credential could be extended in the future, so leave it to credential to + // decide how to retrieve ID token. + var self = this; + // Wait for the redirect state to be determined before proceeding. If critical + // errors like web storage unsupported are detected, fail before RPC, instead + // of after. + return this.redirectStateIsReady_.then(function() { + // Return the full response object and not just the user. + return self.signInWithIdTokenProvider_( + credential.getIdTokenProvider(self.getRpcHandler())); + }); +}; + + +/** + * Sign in using anonymously a user. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInAnonymously = function() { + var self = this; + // Wait for the redirect state to be determined before proceeding. If critical + // errors like web storage unsupported are detected, fail before RPC, instead + // of after. + return this.redirectStateIsReady_.then(function() { + var user = self.currentUser_(); + // If an anonymous user is already signed in, no need to sign him again. + if (user && user['isAnonymous']) { + return user; + } else { + // No anonymous user currently signed in. + return self.signInWithIdTokenProvider_( + self.getRpcHandler().signInAnonymously()) + .then(function(result) { + var user = result['user']; + // Manually sets the isAnonymous flag to true as + // initializeFromIdTokenResponse uses the default value of false and + // even though getAccountInfo sets that to true, it will be reverted + // to false in reloadWithoutSaving. + // TODO: consider optimizing this and cleaning these manual + // overwrites. + user.updateProperty('isAnonymous', true); + // Save isAnonymous flag changes to current user in storage. + return self.handleUserStateChange_(user); + }).then(function() { + return self.currentUser_(); + }); + } + }); +}; + + +/** + * @return {string} The key used for storing Auth state. + */ +fireauth.Auth.prototype.getStorageKey = function() { + return fireauth.util.createStorageKey( + this.app_().options['apiKey'], + this.app_().name); +}; + + +/** + * @return {!firebase.app.App} The Firebase App this auth object is connected + * to. + * @private + */ +fireauth.Auth.prototype.app_ = function() { + return this['app']; +}; + + +/** + * @return {!fireauth.RpcHandler} The RPC handler. + */ +fireauth.Auth.prototype.getRpcHandler = function() { + return this.rpcHandler_; +}; + + +/** + * @return {?fireauth.AuthUser} The currently logged in user. + * @private + */ +fireauth.Auth.prototype.currentUser_ = function() { + return this['currentUser']; +}; + + +/** @return {?string} The current user UID if available, null if not. */ +fireauth.Auth.prototype.getUid = function() { + return (this.currentUser_() && this.currentUser_()['uid']) || null; +}; + + +/** + * @return {?string} The last cached access token. + * @private + */ +fireauth.Auth.prototype.getLastAccessToken_ = function() { + return (this.currentUser_() && this.currentUser_()['_lat']) || null; +}; + + + +/** + * Called internally on Auth state change to notify listeners. + * @private + */ +fireauth.Auth.prototype.notifyAuthListeners_ = function() { + // Only run when state is resolved. After state is resolved, the Auth listener + // will always trigger. + if (this.isStateResolved_) { + for (var i = 0; i < this.authListeners_.length; i++) { + if (this.authListeners_[i]) { + this.authListeners_[i](this.getLastAccessToken_()); + } + } + // Trigger user change only if UID changed. + if (this.userStateChangeUid_ !== this.getUid() && + this.userChangeListeners_.length) { + // Update user state change UID. + this.userStateChangeUid_ = this.getUid(); + // Trigger all subscribed user state change listeners. + for (var i = 0; i < this.userChangeListeners_.length; i++) { + if (this.userChangeListeners_[i]) { + this.userChangeListeners_[i](this.getLastAccessToken_()); + } + } + } + } +}; + + +/** + * Attaches a listener function to Auth state change. + * This is used only by internal Firebase services. + * @param {!function(?string)} listener The auth state change listener. + */ +fireauth.Auth.prototype.addAuthTokenListenerInternal = function(listener) { + this.addAuthTokenListener(listener); + // This is not exact science but should be good enough to detect Firebase + // services subscribing to Auth token changes. + // This is needed to start proactive token refresh on a user. + this.firebaseServices_++; + if (this.firebaseServices_ > 0 && this.currentUser_()) { + // Start proactive token refresh on the current user. + this.currentUser_().startProactiveRefresh(); + } +}; + + +/** + * Detaches the provided listener from Auth state change event. + * This is used only by internal Firebase services. + * @param {!function(?string)} listener The Auth state change listener. + */ +fireauth.Auth.prototype.removeAuthTokenListenerInternal = function(listener) { + // This is unlikely to be called by Firebase services. Services are unlikely + // to remove Auth token listeners. + // Make sure listener is still subscribed before decrementing. + var self = this; + goog.array.forEach(this.authListeners_, function(ele) { + // This covers the case where the same listener is subscribed more than + // once. + if (ele == listener) { + self.firebaseServices_--; + } + }); + if (this.firebaseServices_ < 0) { + this.firebaseServices_ = 0; + } + if (this.firebaseServices_ == 0 && this.currentUser_()) { + // Stop proactive token refresh on the current user. + this.currentUser_().stopProactiveRefresh(); + } + this.removeAuthTokenListener(listener); +}; + + +/** + * Attaches a listener function to Auth state change. + * @param {!function(?string)} listener The Auth state change listener. + */ +fireauth.Auth.prototype.addAuthTokenListener = function(listener) { + var self = this; + // Save listener. + this.authListeners_.push(listener); + // Make sure redirect state is ready and then trigger listener. + this.registerPendingPromise_(this.redirectStateIsReady_.then(function() { + // Do nothing if deleted. + if (self.deleted_) { + return; + } + // Make sure listener is still subscribed. + if (goog.array.contains(self.authListeners_, listener)) { + // Trigger the first call for this now that redirect state is resolved. + listener(self.getLastAccessToken_()); + } + })); +}; + + +/** + * Detaches the provided listener from Auth state change event. + * @param {!function(?string)} listener The Auth state change listener. + */ +fireauth.Auth.prototype.removeAuthTokenListener = function(listener) { + // Remove from Auth listeners. + goog.array.removeAllIf(this.authListeners_, function(ele) { + return ele == listener; + }); +}; + + +/** + * Attaches a listener function to user state change. + * @param {!function(?string)} listener The user state change listener. + * @private + */ +fireauth.Auth.prototype.addUserChangeListener_ = function(listener) { + var self = this; + // Save listener. + this.userChangeListeners_.push(listener); + // Make sure redirect state is ready and then trigger listener. + this.registerPendingPromise_(this.redirectStateIsReady_.then(function() { + // Do nothing if deleted. + if (self.deleted_) { + return; + } + // Make sure listener is still subscribed. + if (goog.array.contains(self.userChangeListeners_, listener)) { + // Confirm UID change before triggering. + if (self.userStateChangeUid_ !== self.getUid()) { + self.userStateChangeUid_ = self.getUid(); + // Trigger the first call for this now that redirect state is resolved. + listener(self.getLastAccessToken_()); + } + } + })); +}; + + +/** + * Deletes the Auth instance, handling cancellation of all pending async Auth + * operations. + * @return {!firebase.Promise} + */ +fireauth.Auth.prototype.delete = function() { + this.deleted_ = true; + // Cancel all pending promises. + for (var i = 0; i < this.pendingPromises_.length; i++) { + this.pendingPromises_[i].cancel(fireauth.authenum.Error.MODULE_DESTROYED); + } + + // Empty pending promises array. + this.pendingPromises_ = []; + // Remove current user change listener. + if (this.userStorageManager_) { + this.userStorageManager_.removeCurrentUserChangeListener( + this.getSyncAuthUserChanges_); + } + // Unsubscribe from Auth event handling. + if (this.authEventManager_) { + this.authEventManager_.unsubscribe(this); + } + return firebase.Promise.resolve(); +}; + + +/** @return {boolean} Whether Auth instance has pending promises. */ +fireauth.Auth.prototype.hasPendingPromises = function() { + return this.pendingPromises_.length != 0; +}; + + +/** + * Takes in a pending promise, saves it and adds a clean up callback which + * forgets the pending promise after it is fulfilled and echoes the promise + * back. + * @param {!goog.Promise<*, *>|!goog.Promise} p The pending promise. + * @return {!goog.Promise<*, *>|!goog.Promise} + * @private + */ +fireauth.Auth.prototype.registerPendingPromise_ = function(p) { + var self = this; + // Save created promise in pending list. + this.pendingPromises_.push(p); + p.thenAlways(function() { + // When fulfilled, remove from pending list. + goog.array.remove(self.pendingPromises_, p); + }); + // Return the promise. + return p; +}; + + +/** + * Gets the list of IDPs that can be used to log in for the given email address. + * @param {string} email The email address. + * @return {!goog.Promise>} + */ +fireauth.Auth.prototype.fetchProvidersForEmail = function(email) { + return /** @type {!goog.Promise>} */ ( + this.registerPendingPromise_( + this.getRpcHandler().fetchProvidersForIdentifier(email) + )); +}; + + +/** + * Verifies an email action code for password reset and returns a promise + * that resolves with the associated email if verified. + * @param {string} code The email action code to verify for password reset. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.verifyPasswordResetCode = function(code) { + return this.checkActionCode(code).then(function(info) { + return info['data']['email']; + }); +}; + + +/** + * Requests resetPassword endpoint for password reset, verifies the action code + * and updates the new password, returns an empty promise. + * @param {string} code The email action code to confirm for password reset. + * @param {string} newPassword The new password. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.confirmPasswordReset = function(code, newPassword) { + return this.registerPendingPromise_( + this.getRpcHandler().confirmPasswordReset(code, newPassword) + .then(function(email) { + // Do not return the email. + })); +}; + + +/** + * Verifies an email action code and returns an empty promise if verified. + * @param {string} code The email action code to verify for password reset. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.checkActionCode = function(code) { + return this.registerPendingPromise_( + this.getRpcHandler().checkActionCode(code) + .then(function(response) { + return new fireauth.ActionCodeInfo(response); + })); +}; + + +/** + * Applies an out-of-band email action code, such as an email verification code. + * @param {string} code The email action code. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.applyActionCode = function(code) { + return this.registerPendingPromise_( + this.getRpcHandler().applyActionCode(code) + .then(function(email) { + // Returns nothing. + })); +}; + + +/** + * Sends the password reset email for the email account provided. + * @param {string} email The email account with the password to be reset. + * @param {?Object=} opt_actionCodeSettings The optional action code settings + * object. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.sendPasswordResetEmail = + function(email, opt_actionCodeSettings) { + var self = this; + return this.registerPendingPromise_( + // Wrap in promise as ActionCodeSettings constructor could throw a + // synchronous error if invalid arguments are specified. + goog.Promise.resolve().then(function() { + if (typeof opt_actionCodeSettings !== 'undefined' && + // Ignore empty objects. + !goog.object.isEmpty(opt_actionCodeSettings)) { + return new fireauth.ActionCodeSettings( + /** @type {!Object} */ (opt_actionCodeSettings)).buildRequest(); + } + return {}; + }) + .then(function(additionalRequestData) { + return self.getRpcHandler().sendPasswordResetEmail( + email, additionalRequestData); + }).then(function(email) { + // Do not return the email. + })); +}; + + +/** + * Signs in with a phone number using the app verifier instance and returns a + * promise that resolves with the confirmation result which on confirmation + * will resolve with the UserCredential object. + * @param {string} phoneNumber The phone number to authenticate with. + * @param {!firebase.auth.ApplicationVerifier} appVerifier The application + * verifier. + * @return {!goog.Promise} + */ +fireauth.Auth.prototype.signInWithPhoneNumber = + function(phoneNumber, appVerifier) { + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(fireauth.ConfirmationResult.initialize( + this, + phoneNumber, + appVerifier, + // This will wait for redirectStateIsReady to resolve first. + goog.bind(this.signInAndRetrieveDataWithCredential, this)))); +}; diff --git a/packages/auth/src/authcredential.js b/packages/auth/src/authcredential.js new file mode 100644 index 00000000000..38fb56cc3d3 --- /dev/null +++ b/packages/auth/src/authcredential.js @@ -0,0 +1,1026 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines Auth credentials used for signInWithCredential. + */ + +goog.provide('fireauth.AuthCredential'); +goog.provide('fireauth.AuthProvider'); +goog.provide('fireauth.EmailAuthCredential'); +goog.provide('fireauth.EmailAuthProvider'); +goog.provide('fireauth.FacebookAuthProvider'); +goog.provide('fireauth.FederatedProvider'); +goog.provide('fireauth.GithubAuthProvider'); +goog.provide('fireauth.GoogleAuthProvider'); +goog.provide('fireauth.OAuthCredential'); +goog.provide('fireauth.OAuthProvider'); +goog.provide('fireauth.OAuthResponse'); +goog.provide('fireauth.PhoneAuthCredential'); +goog.provide('fireauth.PhoneAuthProvider'); +goog.provide('fireauth.TwitterAuthProvider'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.IdToken'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.idp'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.array'); +goog.require('goog.object'); + +goog.forwardDeclare('fireauth.RpcHandler'); + + + +/** + * The interface that represents Auth credential. It provides the underlying + * implementation for retrieving the ID token depending on the type of + * credential. + * @interface + */ +fireauth.AuthCredential = function() {}; + + +/** + * Returns a promise to retrieve ID token using the underlying RPC handler API + * for the current credential. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @return {!goog.Promise} + * idTokenPromise The RPC handler method that returns a promise which + * resolves with an ID token. + */ +fireauth.AuthCredential.prototype.getIdTokenProvider; + + +/** + * Links the credential to an existing account, identified by an ID token. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} idToken The ID token of the existing account. + * @return {!goog.Promise} A Promise that resolves when the accounts + * are linked. + */ +fireauth.AuthCredential.prototype.linkToIdToken; + + +/** + * Tries to match the credential's idToken with the provided UID. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} uid The UID of the user to reauthenticate. + * @return {!goog.Promise} A Promise that resolves when + * idToken UID match succeeds and returns the server response. + */ +fireauth.AuthCredential.prototype.matchIdTokenWithUid; + + +/** @return {!Object} The plain object representation of an Auth credential. */ +fireauth.AuthCredential.prototype.toPlainObject; + + +/** + * @param {!goog.Promise} idTokenResolver A promise that resolves with + * the ID token response. + * @param {string} uid The UID to match in the token response. + * @return {!goog.Promise} A promise that resolves with the same + * response if the UID matches. + */ +fireauth.AuthCredential.verifyTokenResponseUid = + function(idTokenResolver, uid) { + return idTokenResolver.then(function(response) { + // This should not happen as rpcHandler verifyAssertion and verifyPassword + // always guarantee an ID token is available. + if (response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { + // Parse the token object. + var parsedIdToken = fireauth.IdToken.parse( + response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]); + // Confirm token localId matches the provided UID. If not, throw the user + // mismatch error. + if (!parsedIdToken || uid != parsedIdToken.getLocalId()) { + throw new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH); + } + return response; + } + throw new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH); + }) + .thenCatch(function(error) { + // Translate auth/user-not-found error directly to auth/user-mismatch. + throw fireauth.AuthError.translateError( + error, + fireauth.authenum.Error.USER_DELETED, + fireauth.authenum.Error.USER_MISMATCH); + }); +}; + + + +/** + * The interface that represents the Auth provider. + * @interface + */ +fireauth.AuthProvider = function() {}; + + +/** + * @param {...*} var_args The credential data. + * @return {!fireauth.AuthCredential} The Auth provider credential. + */ +fireauth.AuthProvider.credential; + + +/** + * @typedef {{ + * idToken: (?string|undefined), + * accessToken: (?string|undefined), + * oauthToken: (?string|undefined), + * oauthTokenSecret: (?string|undefined) + * }} + */ +fireauth.OAuthResponse; + + + +/** + * The OAuth credential class. + * @param {!fireauth.idp.ProviderId} providerId The provider ID. + * @param {!fireauth.OAuthResponse} oauthResponse The OAuth + * response object containing token information. + * @constructor + * @implements {fireauth.AuthCredential} + */ +fireauth.OAuthCredential = function(providerId, oauthResponse) { + if (oauthResponse['idToken'] || oauthResponse['accessToken']) { + // OAuth 2 and either ID token or access token. + if (oauthResponse['idToken']) { + fireauth.object.setReadonlyProperty( + this, 'idToken', oauthResponse['idToken']); + } + if (oauthResponse['accessToken']) { + fireauth.object.setReadonlyProperty( + this, 'accessToken', oauthResponse['accessToken']); + } + } else if (oauthResponse['oauthToken'] && + oauthResponse['oauthTokenSecret']) { + // OAuth 1 and OAuth token with OAuth token secret. + fireauth.object.setReadonlyProperty( + this, 'accessToken', oauthResponse['oauthToken']); + fireauth.object.setReadonlyProperty( + this, 'secret', oauthResponse['oauthTokenSecret']); + } else { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'failed to construct a credential'); + } + + fireauth.object.setReadonlyProperty(this, 'providerId', providerId); +}; + + +/** + * Returns a promise to retrieve ID token using the underlying RPC handler API + * for the current credential. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @return {!goog.Promise} + * idTokenPromise The RPC handler method that returns a promise which + * resolves with an ID token. + * @override + */ +fireauth.OAuthCredential.prototype.getIdTokenProvider = function(rpcHandler) { + return rpcHandler.verifyAssertion( + /** @type {!fireauth.RpcHandler.VerifyAssertionData} */ ( + this.makeVerifyAssertionRequest_())); +}; + + +/** + * Links the credential to an existing account, identified by an ID token. + * @param {!fireauth.RpcHandler} rpcHandler The rpc handler. + * @param {string} idToken The ID token of the existing account. + * @return {!goog.Promise} A Promise that resolves when the accounts + * are linked, returning the backend response. + * @override + */ +fireauth.OAuthCredential.prototype.linkToIdToken = + function(rpcHandler, idToken) { + var request = this.makeVerifyAssertionRequest_(); + request['idToken'] = idToken; + return rpcHandler.verifyAssertionForLinking( + /** @type {!fireauth.RpcHandler.VerifyAssertionData} */ (request)); +}; + + +/** + * Tries to match the credential's idToken with the provided UID. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} uid The UID of the user to reauthenticate. + * @return {!goog.Promise} A Promise that resolves when + * idToken UID match succeeds and returns the server response. + * @override + */ +fireauth.OAuthCredential.prototype.matchIdTokenWithUid = + function(rpcHandler, uid) { + var request = this.makeVerifyAssertionRequest_(); + // Do not create a new account if the user doesn't exist. + return fireauth.AuthCredential.verifyTokenResponseUid( + rpcHandler.verifyAssertionForExisting( + /** @type {!fireauth.RpcHandler.VerifyAssertionData} */ (request)), + uid); +}; + + +/** + * @return {!Object} A request to the VerifyAssertion endpoint, populated with + * the OAuth data from this credential. + * @private + */ +fireauth.OAuthCredential.prototype.makeVerifyAssertionRequest_ = function() { + var postBody = {}; + if (this['idToken']) { + postBody['id_token'] = this['idToken']; + } + if (this['accessToken']) { + postBody['access_token'] = this['accessToken']; + } + if (this['secret']) { + postBody['oauth_token_secret'] = this['secret']; + } + postBody['providerId'] = this['providerId']; + return { + 'postBody': goog.Uri.QueryData.createFromMap(postBody).toString(), + // Always use http://localhost. + 'requestUri': 'http://localhost' + }; +}; + + +/** + * @return {!Object} The plain object representation of an Auth credential. + * @override + */ +fireauth.OAuthCredential.prototype.toPlainObject = function() { + var obj = { + 'providerId': this['providerId'] + }; + if (this['idToken']) { + obj['oauthIdToken'] = this['idToken']; + } + if (this['accessToken']) { + obj['oauthAccessToken'] = this['accessToken']; + } + if (this['secret']) { + obj['oauthTokenSecret'] = this['secret']; + } + return obj; +}; + + +/** + * A generic OAuth provider (OAuth1 or OAuth2). + * @param {string} providerId The IdP provider ID (e.g. google.com, + * facebook.com) registered with the backend. + * @param {?Array=} opt_reservedParams The backlist of parameters that + * cannot be set through setCustomParameters. + * @constructor + */ +fireauth.FederatedProvider = function(providerId, opt_reservedParams) { + /** @private {!Array} */ + this.reservedParams_ = opt_reservedParams || []; + + // Set read only instance providerId property. + // Set read only instance isOAuthProvider property. + fireauth.object.setReadonlyProperties(this, { + 'providerId': providerId, + 'isOAuthProvider': true + }); + + /** @private {!Object} The OAuth custom parameters for current provider. */ + this.customParameters_ = {}; + /** @protected {?string} The custom OAuth language parameter. */ + this.languageParameter = + (fireauth.idp.getIdpSettings(/** @type {!fireauth.idp.ProviderId} */ ( + providerId)) || {}).languageParam || null; + /** @protected {?string} The default language. */ + this.defaultLanguageCode = null; +}; + +/** + * @param {!Object} customParameters The custom OAuth parameters to pass + * in OAuth request. + * @return {!fireauth.FederatedProvider} The FederatedProvider instance, for + * chaining method calls. + */ +fireauth.FederatedProvider.prototype.setCustomParameters = + function(customParameters) { + this.customParameters_ = goog.object.clone(customParameters); + return this; +}; + + +/** + * Set the default language code on the provider instance. + * @param {?string} languageCode The default language code to set if not already + * provided in the custom parameters. + */ +fireauth.FederatedProvider.prototype.setDefaultLanguage = + function(languageCode) { + this.defaultLanguageCode = languageCode; +}; + + +/** + * @return {!Object} The custom OAuth parameters to pass in OAuth request. + */ +fireauth.FederatedProvider.prototype.getCustomParameters = function() { + // The backend already checks for these values and makes sure no reserved + // fields like client ID, redirect URI, state are overwritten by these + // fields. + var params = + fireauth.util.copyWithoutNullsOrUndefined(this.customParameters_); + // Convert to strings. + for (var key in params) { + params[key] = params[key].toString(); + } + // Remove blacklisted OAuth custom parameters. + var customParams = + fireauth.util.removeEntriesWithKeys(params, this.reservedParams_); + // If language param supported and not already provided, use default language. + if (this.languageParameter && + this.defaultLanguageCode && + !customParams[this.languageParameter]) { + customParams[this.languageParameter] = this.defaultLanguageCode; + } + return customParams; +}; + + +/** + * Generic OAuth2 Auth provider. + * @param {string} providerId The IdP provider ID (e.g. google.com, + * facebook.com) registered with the backend. + * @constructor + * @extends {fireauth.FederatedProvider} + * @implements {fireauth.AuthProvider} + */ +fireauth.OAuthProvider = function(providerId) { + fireauth.OAuthProvider.base(this, 'constructor', providerId, + fireauth.idp.RESERVED_OAUTH2_PARAMS); + + /** @private {!Array} The list of OAuth2 scopes to request. */ + this.scopes_ = []; +}; +goog.inherits(fireauth.OAuthProvider, fireauth.FederatedProvider); + + +/** + * @param {string} scope The OAuth scope to request. + * @return {!fireauth.OAuthProvider} The OAuthProvider instance, for chaining + * method calls. + */ +fireauth.OAuthProvider.prototype.addScope = function(scope) { + // If not already added, add scope to list. + if (!goog.array.contains(this.scopes_, scope)) { + this.scopes_.push(scope); + } + return this; +}; + + +/** @return {!Array} The Auth provider's list of scopes. */ +fireauth.OAuthProvider.prototype.getScopes = function() { + return goog.array.clone(this.scopes_); +}; + + +/** + * Initializes an OAuth AuthCredential. At least one of ID token or access token + * must be defined. + * @param {?string=} opt_idToken The optional OAuth ID token. + * @param {?string=} opt_accessToken The optional OAuth access token. + * @return {!fireauth.AuthCredential} The Auth credential object. + */ +fireauth.OAuthProvider.prototype.credential = function(opt_idToken, + opt_accessToken) { + if (!opt_idToken && !opt_accessToken) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: must provide the ID token and/or the access ' + + 'token.'); + } + var oauthResponse = { + 'idToken': opt_idToken || null, + 'accessToken': opt_accessToken || null + }; + return new fireauth.OAuthCredential(this['providerId'], oauthResponse); +}; + + +/** + * Facebook Auth provider. + * @constructor + * @extends {fireauth.OAuthProvider} + * @implements {fireauth.AuthProvider} + */ +fireauth.FacebookAuthProvider = function() { + fireauth.FacebookAuthProvider.base(this, 'constructor', + fireauth.idp.ProviderId.FACEBOOK); +}; +goog.inherits(fireauth.FacebookAuthProvider, fireauth.OAuthProvider); + +fireauth.object.setReadonlyProperty(fireauth.FacebookAuthProvider, + 'PROVIDER_ID', fireauth.idp.ProviderId.FACEBOOK); + + +/** + * Initializes a Facebook AuthCredential. + * @param {string} accessTokenOrObject The Facebook access token, or object + * containing the token for FirebaseUI backwards compatibility. + * @return {!fireauth.AuthCredential} The Auth credential object. + * @override + */ +fireauth.FacebookAuthProvider.credential = function(accessTokenOrObject) { + if (!accessTokenOrObject) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: expected 1 argument (the OAuth access token).'); + } + var accessToken = accessTokenOrObject; + if (goog.isObject(accessTokenOrObject)) { + accessToken = accessTokenOrObject['accessToken']; + } + return new fireauth.FacebookAuthProvider().credential(null, + /** @type {string} */ (accessToken)); +}; + + +/** + * GitHub Auth provider. + * @constructor + * @extends {fireauth.OAuthProvider} + * @implements {fireauth.AuthProvider} + */ +fireauth.GithubAuthProvider = function() { + fireauth.GithubAuthProvider.base(this, 'constructor', + fireauth.idp.ProviderId.GITHUB); +}; +goog.inherits(fireauth.GithubAuthProvider, fireauth.OAuthProvider); + +fireauth.object.setReadonlyProperty(fireauth.GithubAuthProvider, + 'PROVIDER_ID', fireauth.idp.ProviderId.GITHUB); + + +/** + * Initializes a GitHub AuthCredential. + * @param {string} accessTokenOrObject The GitHub access token, or object + * containing the token for FirebaseUI backwards compatibility. + * @return {!fireauth.AuthCredential} The Auth credential object. + * @override + */ +fireauth.GithubAuthProvider.credential = function(accessTokenOrObject) { + if (!accessTokenOrObject) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: expected 1 argument (the OAuth access token).'); + } + var accessToken = accessTokenOrObject; + if (goog.isObject(accessTokenOrObject)) { + accessToken = accessTokenOrObject['accessToken']; + } + return new fireauth.GithubAuthProvider().credential(null, + /** @type {string} */ (accessToken)); +}; + + +/** + * Google Auth provider. + * @constructor + * @extends {fireauth.OAuthProvider} + * @implements {fireauth.AuthProvider} + */ +fireauth.GoogleAuthProvider = function() { + fireauth.GoogleAuthProvider.base(this, 'constructor', + fireauth.idp.ProviderId.GOOGLE); + + // Add profile scope to Google Auth provider as default scope. + // This is to ensure profile info is populated in current user. + this.addScope('profile'); +}; +goog.inherits(fireauth.GoogleAuthProvider, fireauth.OAuthProvider); + +fireauth.object.setReadonlyProperty(fireauth.GoogleAuthProvider, + 'PROVIDER_ID', fireauth.idp.ProviderId.GOOGLE); + + +/** + * Initializes a Google AuthCredential. + * @param {?string=} idTokenOrObject The Google ID token. If null or undefined, + * we expect the access token to be passed. It can also be an object + * containing the tokens for FirebaseUI backwards compatibility. + * @param {?string=} accessToken The Google access token. If null or + * undefined, we expect the ID token to have been passed. + * @return {!fireauth.AuthCredential} The Auth credential object. + * @override + */ +fireauth.GoogleAuthProvider.credential = + function(idTokenOrObject, accessToken) { + var idToken = idTokenOrObject; + if (goog.isObject(idTokenOrObject)) { + idToken = idTokenOrObject['idToken']; + accessToken = idTokenOrObject['accessToken']; + } + return new fireauth.GoogleAuthProvider().credential( + /** @type {string} */ (idToken), /** @type {string} */ (accessToken)); +}; + + +/** + * Twitter Auth provider. + * @constructor + * @extends {fireauth.FederatedProvider} + * @implements {fireauth.AuthProvider} + */ +fireauth.TwitterAuthProvider = function() { + fireauth.TwitterAuthProvider.base(this, 'constructor', + fireauth.idp.ProviderId.TWITTER, + fireauth.idp.RESERVED_OAUTH1_PARAMS); +}; +goog.inherits(fireauth.TwitterAuthProvider, fireauth.FederatedProvider); + +fireauth.object.setReadonlyProperty(fireauth.TwitterAuthProvider, + 'PROVIDER_ID', fireauth.idp.ProviderId.TWITTER); + + +/** + * Initializes a Twitter AuthCredential. + * @param {string} tokenOrObject The Twitter access token, or object + * containing the token for FirebaseUI backwards compatibility. + * @param {string} secret The Twitter secret. + * @return {!fireauth.AuthCredential} The Auth credential object. + * @override + */ +fireauth.TwitterAuthProvider.credential = function(tokenOrObject, secret) { + var tokenObject = tokenOrObject; + if (!goog.isObject(tokenObject)) { + tokenObject = { + 'oauthToken': tokenOrObject, + 'oauthTokenSecret': secret + }; + } + + if (!tokenObject['oauthToken'] || !tokenObject['oauthTokenSecret']) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: expected 2 arguments (the OAuth access token ' + + 'and secret).'); + } + + return new fireauth.OAuthCredential(fireauth.idp.ProviderId.TWITTER, + /** @type {!fireauth.OAuthResponse} */ (tokenObject)); +}; + + +/** + * The email and password credential class. + * @param {string} email The credential email. + * @param {string} password The credential password. + * @constructor + * @implements {fireauth.AuthCredential} + */ +fireauth.EmailAuthCredential = function(email, password) { + this.email_ = email; + this.password_ = password; + fireauth.object.setReadonlyProperty(this, 'providerId', + fireauth.idp.ProviderId.PASSWORD); +}; + + +/** + * Returns a promise to retrieve ID token using the underlying RPC handler API + * for the current credential. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @return {!goog.Promise} + * idTokenPromise The RPC handler method that returns a promise which + * resolves with an ID token. + * @override + */ +fireauth.EmailAuthCredential.prototype.getIdTokenProvider = + function(rpcHandler) { + return rpcHandler.verifyPassword(this.email_, this.password_); +}; + + +/** + * Adds an email and password account to an existing account, identified by an + * ID token. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} idToken The ID token of the existing account. + * @return {!goog.Promise} A Promise that resolves when the accounts + * are linked, returning the backend response. + * @override + */ +fireauth.EmailAuthCredential.prototype.linkToIdToken = + function(rpcHandler, idToken) { + return rpcHandler.updateEmailAndPassword( + idToken, this.email_, this.password_); +}; + + +/** + * Tries to match the credential's idToken with the provided UID. + * @param {!fireauth.RpcHandler} rpcHandler The rpc handler. + * @param {string} uid The UID of the user to reauthenticate. + * @return {!goog.Promise} A Promise that resolves when + * reauthentication succeeds. + * @override + */ +fireauth.EmailAuthCredential.prototype.matchIdTokenWithUid = + function(rpcHandler, uid) { + // Do not create a new account if the user doesn't exist. + return fireauth.AuthCredential.verifyTokenResponseUid( + // This shouldn't create a new email/password account. + this.getIdTokenProvider(rpcHandler), + uid); +}; + + +/** + * @return {!Object} The plain object representation of an Auth credential. + * @override + */ +fireauth.EmailAuthCredential.prototype.toPlainObject = function() { + return { + 'email': this.email_, + 'password': this.password_ + }; +}; + + + +/** + * Email password Auth provider implementation. + * @constructor + * @implements {fireauth.AuthProvider} + */ +fireauth.EmailAuthProvider = function() { + // Set read-only instance providerId and isOAuthProvider property. + fireauth.object.setReadonlyProperties(this, { + 'providerId': fireauth.idp.ProviderId.PASSWORD, + 'isOAuthProvider': false + }); +}; + + +/** + * Initializes an instance of an email/password Auth credential. + * @param {string} email The credential email. + * @param {string} password The credential password. + * @return {!fireauth.EmailAuthCredential} The Auth credential object. + * @override + */ +fireauth.EmailAuthProvider.credential = function(email, password) { + return new fireauth.EmailAuthCredential(email, password); +}; + + +// Set read only PROVIDER_ID property. +fireauth.object.setReadonlyProperties(fireauth.EmailAuthProvider, { + 'PROVIDER_ID': fireauth.idp.ProviderId.PASSWORD +}); + + +/** + * A credential for phone number sign-in. + * @param {!fireauth.PhoneAuthCredential.Parameters_} params The credential + * parameters that prove the user owns the claimed phone number. + * @constructor + * @implements {fireauth.AuthCredential} + */ +fireauth.PhoneAuthCredential = function(params) { + // Either verification ID and code, or phone number temporary proof must be + // provided. + if (!(params.verificationId && params.verificationCode) && + !(params.temporaryProof && params.phoneNumber)) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + + /** + * The phone Auth parameters that prove ownership of a phone number, either + * through completion of a phone verification flow, or by referencing a + * previously completed verification flow ("temporaryProof"). + * @private {!fireauth.PhoneAuthCredential.Parameters_} + */ + this.params_ = params; + + fireauth.object.setReadonlyProperty(this, 'providerId', + fireauth.idp.ProviderId.PHONE); +}; + + +/** + * Parameters that prove ownership of a phone number via a ID "verificationId" + * of a request to send a code to the phone number, with the code + * "verificationCode" that the user received on their phone. + * @private + * @typedef {{ + * verificationId: string, + * verificationCode: string + * }} + */ +fireauth.PhoneAuthCredential.VerificationParameters_; + + +/** + * Parameters that prove ownership of a phone number by referencing a previously + * completed phone Auth flow. + * @private + * @typedef {{ + * temporaryProof: string, + * phoneNumber: string + * }} + */ +fireauth.PhoneAuthCredential.TemporaryProofParameters_; + + +/** + * @private + * @typedef { + * fireauth.PhoneAuthCredential.VerificationParameters_| + * fireauth.PhoneAuthCredential.TemporaryProofParameters_ + * } + */ +fireauth.PhoneAuthCredential.Parameters_; + + +/** + * Retrieves an ID token from the backend given the current credential. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @return {!goog.Promise} A Promise that resolves with the + * backend response. + * @override + */ +fireauth.PhoneAuthCredential.prototype.getIdTokenProvider = + function(rpcHandler) { + return rpcHandler.verifyPhoneNumber(this.makeVerifyPhoneNumberRequest_()); +}; + + +/** + * Adds a phone credential to an existing account identified by an ID token. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} idToken The ID token of the existing account. + * @return {!goog.Promise} A Promise that resolves when the accounts + * are linked, returning the backend response. + * @override + */ +fireauth.PhoneAuthCredential.prototype.linkToIdToken = + function(rpcHandler, idToken) { + var request = this.makeVerifyPhoneNumberRequest_(); + request['idToken'] = idToken; + return rpcHandler.verifyPhoneNumberForLinking(request); +}; + + +/** + * Tries to match the credential's idToken with the provided UID. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler. + * @param {string} uid The UID of the user to reauthenticate. + * @return {!goog.Promise} A Promise that resolves when + * reauthentication succeeds. + * @override + */ +fireauth.PhoneAuthCredential.prototype.matchIdTokenWithUid = + function(rpcHandler, uid) { + var request = this.makeVerifyPhoneNumberRequest_(); + return fireauth.AuthCredential.verifyTokenResponseUid( + rpcHandler.verifyPhoneNumberForExisting(request), + uid); +}; + + +/** + * Converts a PhoneAuthCredential to a plain object. + * @return {!Object} + * @override + */ +fireauth.PhoneAuthCredential.prototype.toPlainObject = function() { + var obj = { + 'providerId': fireauth.idp.ProviderId.PHONE + }; + if (this.params_.verificationId) { + obj['verificationId'] = this.params_.verificationId; + } + if (this.params_.verificationCode) { + obj['verificationCode'] = this.params_.verificationCode; + } + if (this.params_.temporaryProof) { + obj['temporaryProof'] = this.params_.temporaryProof; + } + if (this.params_.phoneNumber) { + obj['phoneNumber'] = this.params_.phoneNumber; + } + return obj; +}; + + +/** + * @return {!Object} A request to the verifyPhoneNumber endpoint based on the + * current state of the object. + * @private + */ +fireauth.PhoneAuthCredential.prototype.makeVerifyPhoneNumberRequest_ = + function() { + if (this.params_.temporaryProof && this.params_.phoneNumber) { + return { + 'temporaryProof': this.params_.temporaryProof, + 'phoneNumber': this.params_.phoneNumber + }; + } + + return { + 'sessionInfo': this.params_.verificationId, + 'code': this.params_.verificationCode + }; +}; + + +/** + * Phone Auth provider implementation. + * @param {?fireauth.Auth=} opt_auth The Firebase Auth instance. + * @constructor + * @implements {fireauth.AuthProvider} + */ +fireauth.PhoneAuthProvider = function(opt_auth) { + try { + /** @private {!fireauth.Auth} */ + this.auth_ = opt_auth || firebase['auth'](); + } catch (e) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'Either an instance of firebase.auth.Auth must be passed as an ' + + 'argument to the firebase.auth.PhoneAuthProvider constructor, or the ' + + 'default firebase App instance must be initialized via ' + + 'firebase.initializeApp().'); + } + fireauth.object.setReadonlyProperties(this, { + 'providerId': fireauth.idp.ProviderId.PHONE, + 'isOAuthProvider': false + }); +}; + + +/** + * Initiates a phone number confirmation flow. + * @param {string} phoneNumber The user's phone number. + * @param {!firebase.auth.ApplicationVerifier} applicationVerifier The + * application verifier for anti-abuse purposes. + * @return {!goog.Promise} A Promise that resolves with the + * verificationId of the phone number confirmation flow. + */ +fireauth.PhoneAuthProvider.prototype.verifyPhoneNumber = + function(phoneNumber, applicationVerifier) { + var rpcHandler = this.auth_.getRpcHandler(); + + // Convert the promise into a goog.Promise. If the applicationVerifier throws + // an error, just propagate it to the client. Reset the reCAPTCHA widget every + // time after sending the token to the server. + return goog.Promise.resolve(applicationVerifier['verify']()) + .then(function(assertion) { + if (!goog.isString(assertion)) { + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'An implementation of firebase.auth.ApplicationVerifier' + + '.prototype.verify() must return a firebase.Promise ' + + 'that resolves with a string.'); + } + + switch (applicationVerifier['type']) { + case 'recaptcha': + return rpcHandler + .sendVerificationCode( + {'phoneNumber': phoneNumber, 'recaptchaToken': assertion}) + .then( + function(verificationId) { + if (typeof applicationVerifier.reset === 'function') { + applicationVerifier.reset(); + } + return verificationId; + }, + function(error) { + if (typeof applicationVerifier.reset === 'function') { + applicationVerifier.reset(); + } + throw error; + }); + default: + throw new fireauth.AuthError(fireauth.authenum.Error.ARGUMENT_ERROR, + 'Only firebase.auth.ApplicationVerifiers with ' + + 'type="recaptcha" are currently supported.'); + } + }); +}; + + +/** + * Creates a PhoneAuthCredential. + * @param {string} verificationId The ID of the phone number flow, to correlate + * this request with a previous call to + * PhoneAuthProvider.prototype.verifyPhoneNumber. + * @param {string} verificationCode The verification code that was sent to the + * user's phone. + * @return {!fireauth.PhoneAuthCredential} + * @override + */ +fireauth.PhoneAuthProvider.credential = + function(verificationId, verificationCode) { + if (!verificationId) { + throw new fireauth.AuthError(fireauth.authenum.Error.MISSING_SESSION_INFO); + } + if (!verificationCode) { + throw new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE); + } + return new fireauth.PhoneAuthCredential({ + verificationId: verificationId, + verificationCode: verificationCode + }); +}; + + +// Set read only PROVIDER_ID property. +fireauth.object.setReadonlyProperties(fireauth.PhoneAuthProvider, { + 'PROVIDER_ID': fireauth.idp.ProviderId.PHONE +}); + + +/** + * Constructs an Auth credential from a backend response. + * @param {?Object} response The backend response to build a credential from. + * @return {?fireauth.AuthCredential} The corresponding AuthCredential. + */ +fireauth.AuthProvider.getCredentialFromResponse = function(response) { + // Handle phone Auth credential responses, as they have a different format + // from other backend responses (i.e. no providerId). + if (response['temporaryProof'] && response['phoneNumber']) { + return new fireauth.PhoneAuthCredential({ + temporaryProof: response['temporaryProof'], + phoneNumber: response['phoneNumber'] + }); + } + + // Get all OAuth response parameters from response. + var providerId = response && response['providerId']; + + // Email and password is not supported as there is no situation where the + // server would return the password to the client. + if (!providerId || providerId === fireauth.idp.ProviderId.PASSWORD) { + return null; + } + + var accessToken = response && response['oauthAccessToken']; + var accessTokenSecret = response && response['oauthTokenSecret']; + // Google Id Token returned when no additional scopes provided. + var idToken = response && response['oauthIdToken']; + try { + switch (providerId) { + case fireauth.idp.ProviderId.GOOGLE: + return fireauth.GoogleAuthProvider.credential( + idToken, accessToken); + + case fireauth.idp.ProviderId.FACEBOOK: + return fireauth.FacebookAuthProvider.credential( + accessToken); + + case fireauth.idp.ProviderId.GITHUB: + return fireauth.GithubAuthProvider.credential( + accessToken); + + case fireauth.idp.ProviderId.TWITTER: + return fireauth.TwitterAuthProvider.credential( + accessToken, accessTokenSecret); + + default: + return new fireauth.OAuthProvider(providerId).credential( + idToken, accessToken); + } + } catch (e) { + return null; + } +}; + + +/** + * Checks if OAuth is supported by provider, if not throws an error. + * @param {!fireauth.AuthProvider} provider The provider to check. + */ +fireauth.AuthProvider.checkIfOAuthSupported = + function(provider) { + if (!provider['isOAuthProvider']) { + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + } +}; diff --git a/packages/auth/src/authevent.js b/packages/auth/src/authevent.js new file mode 100644 index 00000000000..0b4945d963d --- /dev/null +++ b/packages/auth/src/authevent.js @@ -0,0 +1,161 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the Auth event object. + */ + +goog.provide('fireauth.AuthEvent'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); + + +/** + * Defines the authentication event. + * @param {!fireauth.AuthEvent.Type} type The Auth event type. + * @param {?string=} opt_eventId The event identifier. + * @param {?string=} opt_urlResponse The URL with IdP response. + * @param {?string=} opt_sessionId The session ID used to prevent session + * fixation attacks. + * @param {?fireauth.AuthError=} opt_error The optional error encountered. + * @constructor + */ +fireauth.AuthEvent = function( + type, opt_eventId, opt_urlResponse, opt_sessionId, opt_error) { + this.type_ = type; + this.eventId_ = opt_eventId || null; + this.urlResponse_ = opt_urlResponse || null; + this.sessionId_ = opt_sessionId || null; + this.error_ = opt_error || null; + if (!this.urlResponse_ && !this.error_) { + // Either URL or error is required. They can't be both null. + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT); + } else if (this.urlResponse_ && this.error_) { + // An error must not be provided when a URL is available. + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT); + } else if (this.urlResponse_ && !this.sessionId_) { + // A session ID must accompany a URL response. + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT); + } +}; + + +/** + * Auth event operation types. + * All Auth event types that are used for popup operations should be suffixed + * with `Popup`, whereas those used for redirect operations should be suffixed + * with `Redirect`. + * TODO: consider changing the type from a string to an object with ID + * and some metadata for determining mode: redirect, popup or none. + * @enum {string} + */ +fireauth.AuthEvent.Type = { + LINK_VIA_POPUP: 'linkViaPopup', + LINK_VIA_REDIRECT: 'linkViaRedirect', + REAUTH_VIA_POPUP: 'reauthViaPopup', + REAUTH_VIA_REDIRECT: 'reauthViaRedirect', + SIGN_IN_VIA_POPUP: 'signInViaPopup', + SIGN_IN_VIA_REDIRECT: 'signInViaRedirect', + UNKNOWN: 'unknown', + VERIFY_APP: 'verifyApp' +}; + + +/** + * @param {!fireauth.AuthEvent} event The Auth event. + * @return {boolean} Whether the event is a redirect type. + */ +fireauth.AuthEvent.isRedirect = function(event) { + return !!event.getType().match(/Redirect$/); +}; + + +/** + * @param {!fireauth.AuthEvent} event The Auth event. + * @return {boolean} Whether the event is a popup type. + */ +fireauth.AuthEvent.isPopup = function(event) { + return !!event.getType().match(/Popup$/); +}; + + +/** @return {!fireauth.AuthEvent.Type} The type of Auth event. */ +fireauth.AuthEvent.prototype.getType = function() { + return this.type_; +}; + + +/** @return {?string} The Auth event identifier. */ +fireauth.AuthEvent.prototype.getEventId = function() { + return this.eventId_; +}; + + +/** @return {?string} The url response of Auth event. */ +fireauth.AuthEvent.prototype.getUrlResponse = function() { + return this.urlResponse_; +}; + + +/** @return {?string} The session ID Auth event. */ +fireauth.AuthEvent.prototype.getSessionId = function() { + return this.sessionId_; +}; + + +/** @return {?fireauth.AuthError} The error of Auth event. */ +fireauth.AuthEvent.prototype.getError = function() { + return this.error_; +}; + + +/** @return {boolean} Whether Auth event has an error. */ +fireauth.AuthEvent.prototype.hasError = function() { + return !!this.error_; +}; + + +/** @return {!Object} The plain object representation of event. */ +fireauth.AuthEvent.prototype.toPlainObject = function() { + return { + 'type': this.type_, + 'eventId': this.eventId_, + 'urlResponse': this.urlResponse_, + 'sessionId': this.sessionId_, + 'error': this.error_ && this.error_.toPlainObject() + }; +}; + + +/** + * @param {?Object} rawResponse The plain object representation of Auth event. + * @return {?fireauth.AuthEvent} The Auth event representation of plain object. + */ +fireauth.AuthEvent.fromPlainObject = function(rawResponse) { + var response = rawResponse || {}; + if (response['type']) { + return new fireauth.AuthEvent( + response['type'], + response['eventId'], + response['urlResponse'], + response['sessionId'], + response['error'] && + fireauth.AuthError.fromPlainObject(response['error']) + ); + } + return null; +}; diff --git a/packages/auth/src/autheventmanager.js b/packages/auth/src/autheventmanager.js new file mode 100644 index 00000000000..3db856b5f7c --- /dev/null +++ b/packages/auth/src/autheventmanager.js @@ -0,0 +1,1067 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the Auth event manager instance. + */ + +goog.provide('fireauth.AuthEventHandler'); +goog.provide('fireauth.AuthEventManager'); +goog.provide('fireauth.AuthEventManager.Result'); +goog.provide('fireauth.PopupAuthEventProcessor'); +goog.provide('fireauth.RedirectAuthEventProcessor'); + +goog.require('fireauth.AuthCredential'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.CordovaHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.storage.PendingRedirectManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.array'); + + +/** + * Initializes the Auth event manager which provides the mechanism to connect + * external Auth events to their corresponding listeners. + * @param {string} authDomain The Firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The App ID for the Auth instance that triggered this + * request. + * @constructor + */ +fireauth.AuthEventManager = function(authDomain, apiKey, appName) { + /** @private {string} The Auth domain. */ + this.authDomain_ = authDomain; + /** @private {string} The browser API key. */ + this.apiKey_ = apiKey; + /** @private {string} The App name. */ + this.appName_ = appName; + /** + * @private {!Array} List of subscribed handlers. + */ + this.subscribedHandlers_ = []; + /** + * @private {boolean} Whether the Auth event manager instance is initialized. + */ + this.initialized_ = false; + /** @private {!function(?fireauth.AuthEvent)} The Auth event handler. */ + this.authEventHandler_ = goog.bind(this.handleAuthEvent_, this); + /** @private {!fireauth.RedirectAuthEventProcessor} The redirect event + * processor. */ + this.redirectAuthEventProcessor_ = + new fireauth.RedirectAuthEventProcessor(this); + /** @private {!fireauth.PopupAuthEventProcessor} The popup event processor. */ + this.popupAuthEventProcessor_ = new fireauth.PopupAuthEventProcessor(this); + /** + * @private {!fireauth.storage.PendingRedirectManager} The pending redirect + * storage manager instance. + */ + this.pendingRedirectStorageManager_ = + new fireauth.storage.PendingRedirectManager( + fireauth.AuthEventManager.getKey_(this.apiKey_, this.appName_)); + + /** + * @private {!Object.} + * Map containing Firebase event processor instances keyed by event type. + */ + this.typeToManager_ = {}; + this.typeToManager_[fireauth.AuthEvent.Type.UNKNOWN] = + this.redirectAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT] = + this.redirectAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.LINK_VIA_REDIRECT] = + this.redirectAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT] = + this.redirectAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP] = + this.popupAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.LINK_VIA_POPUP] = + this.popupAuthEventProcessor_; + this.typeToManager_[fireauth.AuthEvent.Type.REAUTH_VIA_POPUP] = + this.popupAuthEventProcessor_; + /** + * @private {!fireauth.OAuthSignInHandler} The OAuth sign in handler depending + * on the current environment. + */ + this.oauthSignInHandler_ = + fireauth.AuthEventManager.instantiateOAuthSignInHandler( + this.authDomain_, this.apiKey_, this.appName_, + firebase.SDK_VERSION || null, + fireauth.constants.clientEndpoint); +}; + + +/** + * Instantiates an OAuth sign-in handler depending on the current environment + * and returns it. + * @param {string} authDomain The Firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The App ID for the Auth instance that triggered this + * request. + * @param {?string} version The SDK client version. + * @param {?string=} opt_endpointId The endpoint ID (staging, test Gaia, etc). + * @return {!fireauth.OAuthSignInHandler} The OAuth sign in handler depending + * on the current environment. + */ +fireauth.AuthEventManager.instantiateOAuthSignInHandler = + function(authDomain, apiKey, appName, version, opt_endpointId) { + // This assumes that android/iOS file environment must be a Cordova + // environment which is not true. This is the best way currently available + // to instantiate this synchronously without waiting for checkIfCordova to + // resolve. If it is determined that the Cordova was falsely detected, it will + // be caught via actionable public popup and redirect methods. + return fireauth.util.isAndroidOrIosFileEnvironment() ? + new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, undefined, undefined, + opt_endpointId) : + new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version, opt_endpointId); +}; + + +/** Reset iframe. This will require reinitializing it.*/ +fireauth.AuthEventManager.prototype.reset = function() { + // Reset initialized status. This will force a popup request to re-initialize + // the iframe. + this.initialized_ = false; + // Remove any previous existing Auth event listener. + this.oauthSignInHandler_.removeAuthEventListener(this.authEventHandler_); + // Construct a new instance of OAuth sign in handler. + + this.oauthSignInHandler_ = + fireauth.AuthEventManager.instantiateOAuthSignInHandler( + this.authDomain_, this.apiKey_, this.appName_, + firebase.SDK_VERSION || null); +}; + + +/** + * @typedef {{ + * user: (?fireauth.AuthUser|undefined), + * credential: (?fireauth.AuthCredential|undefined), + * operationType: (?string|undefined), + * additionalUserInfo: (?fireauth.AdditionalUserInfo|undefined) + * }} + */ +fireauth.AuthEventManager.Result; + + +/** + * Whether to enable Auth event manager subscription. + * @const {boolean} + */ +fireauth.AuthEventManager.ENABLED = true; + + +/** + * Initializes the ifchandler and add Auth event listener on it. + * @return {!goog.Promise} The promise that resolves when the iframe is ready. + */ +fireauth.AuthEventManager.prototype.initialize = function() { + var self = this; + // Initialize once. + if (!this.initialized_) { + this.initialized_ = true; + // Listen to Auth events on iframe. + this.oauthSignInHandler_.addAuthEventListener(this.authEventHandler_); + } + var previousOauthSignInHandler = this.oauthSignInHandler_; + // This should initialize ifchandler underneath. + // Return on OAuth handler ready promise. + // Check for error in ifcHandler used to embed the iframe. + return this.oauthSignInHandler_.initializeAndWait() + .thenCatch(function(error) { + // Force ifchandler to reinitialize on retrial. + if (self.oauthSignInHandler_ == previousOauthSignInHandler) { + // If a new OAuth sign in handler was already created, do not reset. + self.reset(); + } + throw error; + }); +}; + + +/** + * Called after it is determined that there is no pending redirect result. + * Will populate the redirect result if it is guaranteed to be null and will + * force an early initialization of the OAuth sign in handler if the + * environment requires it. + * @private + */ +fireauth.AuthEventManager.prototype.initializeWithNoPendingRedirectResult_ = + function() { + var self = this; + // Check if the OAuth sign in handler should be initialized early in all + // cases. + if (this.oauthSignInHandler_.shouldBeInitializedEarly()) { + this.initialize().thenCatch(function(error) { + // Current environment was falsely detected as Cordova, trigger a fake + // Auth event to notify getRedirectResult that operation is not supported. + var notSupportedEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + if (fireauth.AuthEventManager.isCordovaFalsePositive_( + /** @type {?fireauth.AuthError} */ (error))) { + self.handleAuthEvent_(notSupportedEvent); + } + }); + } + // For environments where storage is volatile, we can't determine that + // there is no pending redirect response. This is true in Cordova + // where an activity would be destroyed in some cases and the + // sessionStorage is lost. + if (!this.oauthSignInHandler_.hasVolatileStorage()) { + // Since there is no redirect result, it is safe to default to empty + // redirect result instead of blocking on this. + // The downside here is that on iOS devices, calling signInWithPopup + // after getRedirectResult resolves and the iframe does not finish + // loading, the popup event propagating to the iframe would not be + // detected. This is because in iOS devices, storage events only trigger + // in iframes but are not actually saved in web storage. The iframe must + // be embedded and ready before the storage event propagates. Otherwise + // it won't be detected. + this.redirectAuthEventProcessor_.defaultToEmptyResponse(); + } +}; + + +/** + * Subscribes an Auth event handler to list of handlers. + * @param {!fireauth.AuthEventHandler} handler The instance to subscribe. + */ +fireauth.AuthEventManager.prototype.subscribe = function(handler) { + if (!goog.array.contains(this.subscribedHandlers_, handler)) { + this.subscribedHandlers_.push(handler); + } + if (this.initialized_) { + return; + } + var self = this; + // Check pending redirect status. + this.pendingRedirectStorageManager_.getPendingStatus() + .then(function(status) { + // Pending redirect detected. + if (status) { + // Remove pending status and initialize. + self.pendingRedirectStorageManager_.removePendingStatus() + .then(function() { + self.initialize().thenCatch(function(error) { + // Current environment was falsely detected as Cordova, trigger a + // fake Auth event to notify getRedirectResult that operation is + // not supported. + var notSupportedEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + if (fireauth.AuthEventManager.isCordovaFalsePositive_( + /** @type {?fireauth.AuthError} */ (error))) { + self.handleAuthEvent_(notSupportedEvent); + } + }); + }); + } else { + // No previous redirect, default to empty response. + self.initializeWithNoPendingRedirectResult_(); + } + }).thenCatch(function(error) { + // Error checking pending status, default to empty response. + self.initializeWithNoPendingRedirectResult_(); + }); +}; + + +/** + * @param {!fireauth.AuthEventHandler} handler The possible subscriber. + * @return {boolean} Whether the handle is subscribed. + */ +fireauth.AuthEventManager.prototype.isSubscribed = function(handler) { + return goog.array.contains(this.subscribedHandlers_, handler); +}; + + +/** + * Unsubscribes an Auth event handler to list of handlers. + * @param {!fireauth.AuthEventHandler} handler The instance to unsubscribe. + */ +fireauth.AuthEventManager.prototype.unsubscribe = function(handler) { + goog.array.removeAllIf(this.subscribedHandlers_, function(ele) { + return ele == handler; + }); +}; + + +/** + * Handles external Auth event detected by the OAuth sign-in handler. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @return {boolean} Whether the event found an appropriate owner that can + * handle it. This signals to the OAuth helper iframe that the event is safe + * to delete. + * @private + */ +fireauth.AuthEventManager.prototype.handleAuthEvent_ = function(authEvent) { + // This should not happen as fireauth.iframe.AuthRelay will not send null + // events. + if (!authEvent) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT); + } + // Initialize event processed status to false. When set to false, the event is + // not clear to delete in the OAuth helper iframe as the owner of this event + // could be a user in another tab. + var processed = false; + // Lookup a potential handler for this event. + for (var i = 0; i < this.subscribedHandlers_.length; i++) { + var potentialHandler = this.subscribedHandlers_[i]; + if (potentialHandler.canHandleAuthEvent( + authEvent.getType(), authEvent.getEventId())) { + var eventManager = this.typeToManager_[authEvent.getType()]; + if (eventManager) { + eventManager.processAuthEvent(authEvent, potentialHandler); + } + // Event has been processed, free to clear in OAuth helper. + processed = true; + break; + } + } + // If no redirect response ready yet, default to an empty response. + this.redirectAuthEventProcessor_.defaultToEmptyResponse(); + // Notify iframe of processed status. + return processed; +}; + + +/** + * The popup promise timeout delay with units in ms between the time the iframe + * is ready (successfully embedded on the page) and the time the popup Auth + * event is detected in the parent container. + * @const {!fireauth.util.Delay} + * @private + */ +fireauth.AuthEventManager.POPUP_TIMEOUT_MS_ = + new fireauth.util.Delay(2000, 10000); + + +/** + * The redirect promise timeout delay with units in ms. Unlike the popup + * timeout, this covers the entire duration from start to getRedirectResult + * resolution. + * @const {!fireauth.util.Delay} + * @private + */ +fireauth.AuthEventManager.REDIRECT_TIMEOUT_MS_ = + new fireauth.util.Delay(30000, 60000); + + +/** + * Returns the redirect result. If coming back from a successful redirect sign + * in, will resolve to the signed in user. If coming back from an unsuccessful + * redirect sign, will reject with the proper error. If no redirect operation + * called, resolves with null. + * @return {!goog.Promise} + */ +fireauth.AuthEventManager.prototype.getRedirectResult = function() { + return this.redirectAuthEventProcessor_.getRedirectResult(); +}; + + +/** + * Processes the popup request. The popup instance must be provided externally + * and on error, the requestor must close the window. + * @param {?Window} popupWin The popup window reference. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {string=} opt_eventId The optional event ID. + * @param {boolean=} opt_alreadyRedirected Whether popup is already redirected + * to final destination. + * @return {!goog.Promise} The popup window promise. + */ +fireauth.AuthEventManager.prototype.processPopup = + function(popupWin, mode, provider, opt_eventId, opt_alreadyRedirected) { + var self = this; + return this.oauthSignInHandler_.processPopup( + popupWin, + mode, + provider, + // On initialization, add Auth event listener if not already added. + function() { + if (!self.initialized_) { + self.initialized_ = true; + // Listen to Auth events on iframe. + self.oauthSignInHandler_.addAuthEventListener(self.authEventHandler_); + } + }, + // On error, reset to force re-initialization on retrial. + function(error) { + self.reset(); + }, + opt_eventId, + opt_alreadyRedirected); +}; + + +/** + * @param {?fireauth.AuthError} error The error to check for Cordova false + * positive. + * @return {boolean} Whether the current environment was falsely identified as + * Cordova. + * @private + */ +fireauth.AuthEventManager.isCordovaFalsePositive_ = function(error) { + if (error && error['code'] == 'auth/cordova-not-ready') { + return true; + } + return false; +}; + + +/** + * Processes the redirect request. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {string=} opt_eventId The optional event ID. + * @return {!goog.Promise} + */ +fireauth.AuthEventManager.prototype.processRedirect = + function(mode, provider, opt_eventId) { + var self = this; + var error; + // Save pending status first. + return this.pendingRedirectStorageManager_.setPendingStatus() + .then(function() { + // Try to redirect. + return self.oauthSignInHandler_.processRedirect( + mode, provider, opt_eventId) + .thenCatch(function(e) { + if (fireauth.AuthEventManager.isCordovaFalsePositive_( + /** @type {?fireauth.AuthError} */ (e))) { + throw new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + } + // On failure, remove pending status and rethrow the error. + error = e; + return self.pendingRedirectStorageManager_.removePendingStatus() + .then(function() { + throw error; + }); + }) + .then(function() { + // Resolve, if the OAuth handler unloads the page on redirect. + if (!self.oauthSignInHandler_.unloadsOnRedirect()) { + // Relevant to Cordova case, will not matter in web case where + // browser redirects. + // In Cordova, the activity could still be running in the background + // so we need to wait for getRedirectResult to resolve before + // resolving this current promise. + // Otherwise, if the activity is destroyed, getRedirectResult would + // be used. + // At this point, authEvent should have been triggered. + // When this promise resolves, the developer should be able to + // call getRedirectResult to get the result of this operation. + // Remove pending status as result should be resolved. + return self.pendingRedirectStorageManager_.removePendingStatus() + .then(function() { + // Ensure redirect result ready before resolving. + return self.getRedirectResult(); + }).then(function(result) { + // Do nothing. Developer expected to call getRedirectResult to + // get result. + }).thenCatch(function(error) { + // Do nothing. Developer expected to call getRedirectResult to + // get result. + }); + } else { + // For environments that will unload the page on redirect, keep + // the promise pending on success. This makes it easier to reuse + // the same code for Cordova environment and browser environment. + // The developer can always add getRedirectResult on promise + // resolution and expect that when it runs, the redirect operation + // was completed. + return new goog.Promise(function(resolve, reject) { + // Keep this pending. + }); + } + }); + }); +}; + + +/** + * Waits for popup window to close. When closed start timeout listener for popup + * pending promise. If in the process, it was detected that the iframe does not + * support web storage, the popup is closed and the web storage unsupported + * error is thrown. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!Window} popupWin The popup window. + * @param {?string=} opt_eventId The event ID. + * @return {!goog.Promise} + */ +fireauth.AuthEventManager.prototype.startPopupTimeout = + function(owner, mode, popupWin, opt_eventId) { + return this.oauthSignInHandler_.startPopupTimeout( + popupWin, + // On popup error such as popup closed by user or web storage not + // supported. + function(error) { + // Notify owner of the error. + owner.resolvePendingPopupEvent(mode, null, error, opt_eventId); + }, + fireauth.AuthEventManager.POPUP_TIMEOUT_MS_.get()); +}; + + + +/** + * @private {!Object.} Map containing + * Firebase event manager instances keyed by Auth event manager ID. + */ +fireauth.AuthEventManager.manager_ = {}; + + +/** + * The separator for manager keys to concatenate app name and apiKey. + * @const {string} + * @private + */ +fireauth.AuthEventManager.KEY_SEPARATOR_ = ':'; + + +/** + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The Auth instance that initiated the Auth event. + * @return {string} The key identifying the Auth event manager instance. + * @private + */ +fireauth.AuthEventManager.getKey_ = function(apiKey, appName) { + return apiKey + fireauth.AuthEventManager.KEY_SEPARATOR_ + appName; +}; + + +/** + * @param {string} authDomain The Firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The Auth instance that initiated the Auth event + * manager. + * @return {!fireauth.AuthEventManager} the requested manager instance. + */ +fireauth.AuthEventManager.getManager = function(authDomain, apiKey, appName) { + // Construct storage key. + var key = fireauth.AuthEventManager.getKey_(apiKey, appName); + if (!fireauth.AuthEventManager.manager_[key]) { + fireauth.AuthEventManager.manager_[key] = + new fireauth.AuthEventManager(authDomain, apiKey, appName); + } + return fireauth.AuthEventManager.manager_[key]; +}; + + + +/** + * The interface that represents a specific type of Auth event processor. + * @interface + */ +fireauth.AuthEventProcessor = function() {}; + + +/** + * Completes the processing of an external Auth event detected by the embedded + * iframe. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + */ +fireauth.AuthEventProcessor.prototype.processAuthEvent = + function(authEvent, owner) {}; + + + +/** + * Redirect Auth event manager. + * @param {!fireauth.AuthEventManager} manager The parent Auth event manager. + * @constructor + * @implements {fireauth.AuthEventProcessor} + */ +fireauth.RedirectAuthEventProcessor = function(manager) { + this.manager_ = manager; + // Only one redirect result can be tracked on first load. + /** + * @private {?function():!goog.Promise} + * Redirect result resolver. This will be used to resolve the + * getRedirectResult promise. When the redirect result is obtained, this + * field will be set. + */ + this.redirectedUserPromise_ = null; + /** + * @private {!Array} Pending + * promise redirect resolver. When the redirect result is obtained and the + * user is detected, this will be called. + */ + this.redirectResolve_ = []; + /** + * @private {!Array} Pending Promise redirect rejecter. When the + * redirect result is obtained and an error is detected, this will be + * called. + */ + this.redirectReject_ = []; + /** @private {?goog.Promise} Pending timeout promise for redirect. */ + this.redirectTimeoutPromise_ = null; + /** @private {boolean} Whether redirect result is resolved. This is true + when a valid Auth event has been triggered. */ + this.redirectResultResolved_ = false; +}; + + +/** Reset any previous redirect result. */ +fireauth.RedirectAuthEventProcessor.prototype.reset = function() { + // Reset to allow override getRedirectResult. This is relevant for Cordova + // environment where redirect events do not necessarily unload the current + // page. + this.redirectedUserPromise_ = null; + if (this.redirectTimeoutPromise_) { + this.redirectTimeoutPromise_.cancel(); + this.redirectTimeoutPromise_ = null; + } +}; + + +/** + * Completes the processing of an external Auth event detected by the embedded + * iframe. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @override + */ +fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent = + function(authEvent, owner) { + // This should not happen as fireauth.iframe.AuthRelay will not send null + // events. + if (!authEvent) { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT)); + } + // Reset any pending redirect result. This event will overwrite it. + this.reset(); + this.redirectResultResolved_ = true; + var mode = authEvent.getType(); + var eventId = authEvent.getEventId(); + // Check if web storage is not supported in the iframe. + var isWebStorageNotSupported = + authEvent.getError() && + authEvent.getError()['code'] == 'auth/web-storage-unsupported'; + /// Check if operation is supported in this environment. + var isOperationNotSupported = + authEvent.getError() && + authEvent.getError()['code'] == 'auth/operation-not-supported-in-this-' + + 'environment'; + // UNKNOWN mode is always triggered on load by iframe when no popup/redirect + // data is available. If web storage unsupported error is thrown, process as + // error and not as unknown event. If the operation is not supported in this + // environment, also treat as an error and not as an unknown event. + if (mode == fireauth.AuthEvent.Type.UNKNOWN && + !isWebStorageNotSupported && + !isOperationNotSupported) { + return this.processUnknownEvent_(); + } else if (authEvent.hasError()) { + return this.processErrorEvent_(authEvent, owner); + } else if (owner.getAuthEventHandlerFinisher(mode, eventId)) { + return this.processSuccessEvent_(authEvent, owner); + } else { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT)); + } +}; + + +/** + * Sets an empty redirect result response when no redirect result is available. + */ +fireauth.RedirectAuthEventProcessor.prototype.defaultToEmptyResponse = + function() { + // If the first event does not resolve redirectResult and no subscriber can + // handle it, set redirect result to null. + // An example of this scenario would be a link via redirect that was triggered + // by a user that was not logged in. canHandleAuthEvent will be false for all + // subscribers. So make sure getRedirectResult when called will resolve to a + // null user. + if (!this.redirectResultResolved_) { + this.redirectResultResolved_ = true; + // No Auth event available, getRedirectResult should resolve with null. + this.setRedirectResult_(false, null, null); + } +}; + + +/** + * Processes the unknown event. + * @return {!goog.Promise} + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.processUnknownEvent_ = + function() { + // No Auth event available, getRedirectResult should resolve with null. + this.setRedirectResult_(false, null, null); + return goog.Promise.resolve(); +}; + + +/** + * Processes an error event. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.processErrorEvent_ = + function(authEvent, owner) { + // Set redirect result to resolve with null if event is not a redirect or + // reject with error if event is an error. + this.setRedirectResult_(true, null, authEvent.getError()); + return goog.Promise.resolve(); +}; + + +/** + * Processes a successful event. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.processSuccessEvent_ = + function(authEvent, owner) { + var self = this; + var eventId = authEvent.getEventId(); + var mode = authEvent.getType(); + var handler = owner.getAuthEventHandlerFinisher(mode, eventId); + var requestUri = /** @type {string} */ (authEvent.getUrlResponse()); + var sessionId = /** @type {string} */ (authEvent.getSessionId()); + var isRedirect = fireauth.AuthEvent.isRedirect(authEvent); + // Complete sign in or link account operation and then pass result to + // relevant pending popup promise. + return handler(requestUri, sessionId).then(function(popupRedirectResponse) { + // Flow completed. + // For a redirect operation resolve with the popupRedirectResponse, + // otherwise resolve with null. + self.setRedirectResult_(isRedirect, popupRedirectResponse, null); + }).thenCatch(function(error) { + // Flow not completed due to error. + // For a redirect operation reject with the error, otherwise resolve + // with null. + self.setRedirectResult_( + isRedirect, null, /** @type {!fireauth.AuthError} */ (error)); + // Always resolve. + return; + }); +}; + + +/** + * Sets redirect error result. + * @param {!fireauth.AuthError} error The redirect operation error. + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.setRedirectReject_ = + function(error) { + // If a redirect error detected, reject getRedirectResult with that error. + this.redirectedUserPromise_ = function() { + return goog.Promise.reject(error); + }; + // Reject all pending getRedirectResult promises. + if (this.redirectReject_.length) { + for (var i = 0; i < this.redirectReject_.length; i++) { + this.redirectReject_[i](error); + } + } +}; + + +/** + * Sets redirect success result. + * @param {!fireauth.AuthEventManager.Result} popupRedirectResult The + * resolved user for a successful or null user redirect. + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.setRedirectResolve_ = + function(popupRedirectResult) { + // If a redirect user detected, resolve getRedirectResult with the + // popupRedirectResult. + // Result should not be null in this case. + this.redirectedUserPromise_ = function() { + return goog.Promise.resolve( + /** @type {!fireauth.AuthEventManager.Result} */ (popupRedirectResult)); + }; + // Resolve all pending getRedirectResult promises. + if (this.redirectResolve_.length) { + for (var i = 0; i < this.redirectResolve_.length; i++) { + this.redirectResolve_[i]( + /** @type {!fireauth.AuthEventManager.Result} */ ( + popupRedirectResult)); + } + } +}; + + +/** + * @param {boolean} isRedirect Whether Auth event is a redirect event. + * @param {?fireauth.AuthEventManager.Result} popupRedirectResult The + * resolved user for a successful redirect. This user is null if no redirect + * operation run. + * @param {?fireauth.AuthError} error The redirect operation error. + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.setRedirectResult_ = + function(isRedirect, popupRedirectResult, error) { + if (isRedirect) { + // This is a redirect operation, either resolves with user or error. + if (error) { + // If a redirect error detected, reject getRedirectResult with that error. + this.setRedirectReject_(error); + } else { + // If a redirect user detected, resolve getRedirectResult with the + // popupRedirectResult. + // Result should not be null in this case. + this.setRedirectResolve_( + /** @type {!fireauth.AuthEventManager.Result} */ ( + popupRedirectResult)); + } + } else { + // Not a redirect, set redirectUser_ to return null. + this.setRedirectResolve_({ + 'user': null + }); + } + // Reset all pending promises. + this.redirectResolve_ = []; + this.redirectReject_ = []; +}; + + +/** + * Returns the redirect result. If coming back from a successful redirect sign + * in, will resolve to the signed in user. If coming back from an unsuccessful + * redirect sign, will reject with the proper error. If no redirect operation + * called, resolves with null. + * @return {!goog.Promise} + */ +fireauth.RedirectAuthEventProcessor.prototype.getRedirectResult = function() { + var self = this; + // Initial result could be overridden in the case of Cordova. + // Auth domain must be included for this to resolve. + // If still pending just return the pending promise. + var p = new goog.Promise(function(resolve, reject) { + // The following logic works if this method was called before Auth event + // is triggered. + if (!self.redirectedUserPromise_) { + // Save resolves and rejects of pending promise for redirect operation. + self.redirectResolve_.push(resolve); + self.redirectReject_.push(reject); + // Start timeout listener to getRedirectResult pending promise. + // Call this only when redirectedUserPromise_ is not determined. + self.startRedirectTimeout_(); + } else { + // Called after Auth event is triggered. + self.redirectedUserPromise_().then(resolve, reject); + } + }); + return /** @type {!goog.Promise} */ (p); +}; + + +/** + * Starts timeout listener for getRedirectResult pending promise. This method + * should not be called again after getRedirectResult's redirectedUserPromise_ + * is determined. + * @private + */ +fireauth.RedirectAuthEventProcessor.prototype.startRedirectTimeout_ = + function() { + // Expire pending timeout promise for popup operation. + var self = this; + var error = new fireauth.AuthError( + fireauth.authenum.Error.TIMEOUT); + if (this.redirectTimeoutPromise_) { + this.redirectTimeoutPromise_.cancel(); + } + // For redirect mode. + this.redirectTimeoutPromise_ = + goog.Timer.promise(fireauth.AuthEventManager.REDIRECT_TIMEOUT_MS_.get()) + .then(function() { + // If not resolved yet, reject with timeout error. + if (!self.redirectedUserPromise_) { + self.setRedirectResult_(true, null, error); + } + }); + +}; + + + +/** + * Popup Auth event manager. + * @param {!fireauth.AuthEventManager} manager The parent Auth event manager. + * @constructor + * @implements {fireauth.AuthEventProcessor} + */ +fireauth.PopupAuthEventProcessor = function(manager) { + this.manager_ = manager; +}; + + +/** + * Completes the processing of an external Auth event detected by the embedded + * iframe. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @override + */ +fireauth.PopupAuthEventProcessor.prototype.processAuthEvent = + function(authEvent, owner) { + // This should not happen as fireauth.iframe.AuthRelay will not send null + // events. + if (!authEvent) { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT)); + } + var mode = authEvent.getType(); + var eventId = authEvent.getEventId(); + if (authEvent.hasError()) { + return this.processErrorEvent_(authEvent, owner); + } else if (owner.getAuthEventHandlerFinisher(mode, eventId)) { + return this.processSuccessEvent_(authEvent, owner); + } else { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT)); + } +}; + + +/** + * Processes an error event. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @private + */ +fireauth.PopupAuthEventProcessor.prototype.processErrorEvent_ = + function(authEvent, owner) { + var eventId = authEvent.getEventId(); + var mode = authEvent.getType(); + // For pending popup promises trigger rejects with the error. + owner.resolvePendingPopupEvent(mode, null, authEvent.getError(), eventId); + return goog.Promise.resolve(); +}; + + +/** + * Processes a successful event. + * @param {?fireauth.AuthEvent} authEvent External Auth event detected by + * iframe. + * @param {!fireauth.AuthEventHandler} owner The owner of the event. + * @return {!goog.Promise} + * @private + */ +fireauth.PopupAuthEventProcessor.prototype.processSuccessEvent_ = + function(authEvent, owner) { + var eventId = authEvent.getEventId(); + var mode = authEvent.getType(); + var handler = owner.getAuthEventHandlerFinisher(mode, eventId); + // Successful operation, complete the exchange for an ID token. + var requestUri = /** @type {string} */ (authEvent.getUrlResponse()); + var sessionId = /** @type {string} */ (authEvent.getSessionId()); + // Complete sign in or link account operation and then pass result to + // relevant pending popup promise. + return handler(requestUri, sessionId).then(function(popupRedirectResponse) { + // Flow completed. + // Resolve pending popup promise if it exists. + owner.resolvePendingPopupEvent(mode, popupRedirectResponse, null, eventId); + }).thenCatch(function(error) { + // Flow not completed due to error. + // Resolve pending promise if it exists. + owner.resolvePendingPopupEvent( + mode, null, /** @type {!fireauth.AuthError} */ (error), eventId); + // Always resolve. + return; + }); +}; + + + +/** + * The interface that represents an Auth event handler. It provides the + * ability for the Auth event manager to determine the owner of an Auth event, + * the ability to resolve a pending popup event and the appropriate handler for + * an event. + * @interface + */ +fireauth.AuthEventHandler = function() {}; + + +/** + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?string=} opt_eventId The event ID. + * @return {boolean} Whether the Auth event handler can handler the provided + * event. + */ +fireauth.AuthEventHandler.prototype.canHandleAuthEvent = + function(mode, opt_eventId) {}; + + +/** + * Completes the pending popup operation. If error is not null, rejects with the + * error. Otherwise, it resolves with the popup redirect result. + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?fireauth.AuthEventManager.Result} popupRedirectResult The result + * to resolve with when no error supplied. + * @param {?fireauth.AuthError} error When supplied, the promise will reject. + * @param {?string=} opt_eventId The event ID. + */ +fireauth.AuthEventHandler.prototype.resolvePendingPopupEvent = + function(mode, popupRedirectResult, error, opt_eventId) {}; + + +/** + * Returns the handler's appropriate popup and redirect sign in operation + * finisher. + * @param {!fireauth.AuthEvent.Type} mode The Auth type mode. + * @param {?string=} opt_eventId The optional event ID. + * @return {?function(string, + * string):!goog.Promise} + */ +fireauth.AuthEventHandler.prototype.getAuthEventHandlerFinisher = + function(mode, opt_eventId) {}; diff --git a/packages/auth/src/authstorage.js b/packages/auth/src/authstorage.js new file mode 100644 index 00000000000..a0d95871fc3 --- /dev/null +++ b/packages/auth/src/authstorage.js @@ -0,0 +1,584 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines utilities for session management. + */ + +goog.provide('fireauth.authStorage'); +goog.provide('fireauth.authStorage.Key'); +goog.provide('fireauth.authStorage.Manager'); +goog.provide('fireauth.authStorage.Persistence'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.Factory'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.object'); + + + +/** + * The namespace for Firebase Auth storage. + * @private @const {string} + */ +fireauth.authStorage.NAMESPACE_ = 'firebase'; + + +/** + * The separator for Firebase Auth storage with App ID key. + * @private @const {string} + */ +fireauth.authStorage.SEPARATOR_ = ':'; + + +/** + * @const {number} The IE 10 localStorage cross tab synchronization delay in + * milliseconds. + */ +fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY = 10; + + +/** + * Enums for Auth state persistence. + * @enum {string} + */ +fireauth.authStorage.Persistence = { + // State will persist even when the browser window is closed or the activity + // is destroyed in react-native. + LOCAL: 'local', + // State is only stored in memory and will be cleared when the window or + // activity is refreshed. + NONE: 'none', + // State will only persist in current session/tab, relevant to web only, and + // will be cleared when the tab is closed. + SESSION: 'session' +}; + + +/** + * Validates that an argument is a valid persistence value. If an invalid type + * is specified, an error is thrown synchronously. + * @param {*} arg The argument to validate. + */ +fireauth.authStorage.validatePersistenceArgument = + function(arg) { + // Invalid type error. + var invalidTypeError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_PERSISTENCE); + // Unsupported type error. + var unsupportedTypeError = new fireauth.AuthError( + fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE); + // Check if the persistence type is a valid one. + // Throw invalid type error if not valid. + if (!goog.object.containsValue(fireauth.authStorage.Persistence, arg) || + // goog.object.containsValue(fireauth.authStorage.Persistence, ['none']) + // returns true. + typeof arg !== 'string') { + throw invalidTypeError; + } + // Validate if the specified type is supported in the current environment. + switch (fireauth.util.getEnvironment()) { + case fireauth.util.Env.REACT_NATIVE: + // This is only supported in a browser. + if (arg === fireauth.authStorage.Persistence.SESSION) { + throw unsupportedTypeError; + } + break; + case fireauth.util.Env.NODE: + // Only none is supported in Node.js. + if (arg !== fireauth.authStorage.Persistence.NONE) { + throw unsupportedTypeError; + } + break; + case fireauth.util.Env.BROWSER: + default: + // This is restricted by what the browser supports. + if (!fireauth.util.isWebStorageSupported() && + arg !== fireauth.authStorage.Persistence.NONE) { + throw unsupportedTypeError; + } + break; + } +}; + + +/** + * Storage key metadata. + * @typedef {{name: string, persistent: !fireauth.authStorage.Persistence}} + */ +fireauth.authStorage.Key; + + +/** + * Storage manager. + * @param {string} namespace The optional namespace. + * @param {string} separator The optional separator. + * @param {boolean} safariLocalStorageNotSynced Whether browser has Safari + * iframe restriction with storage event triggering but storage not updated. + * @param {boolean} runsInBackground Whether browser can detect storage event + * when it had already been pushed to the background. This may happen in + * some mobile browsers. A localStorage change in the foreground window + * will not be detected in the background window via the storage event. + * This was detected in iOS 7.x mobile browsers. + * @constructor @struct @final + */ +fireauth.authStorage.Manager = function( + namespace, + separator, + safariLocalStorageNotSynced, + runsInBackground) { + /** @const @private {string} Storage namespace. */ + this.namespace_ = namespace; + /** @const @private {string} Storage namespace key separator. */ + this.separator_ = separator; + /** + * @const @private {boolean} Whether browser has Safari iframe restriction + * with storage event triggering but storage not updated. + */ + this.safariLocalStorageNotSynced_ = safariLocalStorageNotSynced; + /** + * @private {boolean} Whether browser can detect storage event when it + * had already been pushed to the background. This may happen in some + * mobile browsers. + */ + this.runsInBackground_ = runsInBackground; + + /** + * @const @private {!Object.>} The storage event + * key to listeners map. + */ + this.listeners_ = {}; + + var storageFactory = fireauth.storage.Factory.getInstance(); + try { + /** + * @private {!fireauth.storage.Storage} Persistence storage. + */ + this.persistentStorage_ = storageFactory.makePersistentStorage(); + } catch (e) { + // Default to in memory storage if the preferred persistent storage is not + // supported. + this.persistentStorage_ = storageFactory.makeInMemoryStorage(); + // Do not use indexedDB fallback. + this.localStorageNotSynchronized_ = false; + // Do not set polling functions on window.localStorage. + this.runsInBackground_ = true; + } + try { + /** + * @private {!fireauth.storage.Storage} Temporary session storage. + */ + this.temporaryStorage_ = storageFactory.makeTemporaryStorage(); + } catch (e) { + // Default to in memory storage if the preferred temporary storage is not + // supported. This should be a different in memory instance as the + // persistent storage, since the same key could be available for both types + // of storage. + this.temporaryStorage_ = storageFactory.makeInMemoryStorage(); + } + /** + * @private {!fireauth.storage.Storage} In memory storage. + */ + this.inMemoryStorage_ = storageFactory.makeInMemoryStorage(); + + /** + * @const @private {function(!goog.events.BrowserEvent)| + * function(!Array)} Storage change handler. + */ + this.storageChangeEventHandler_ = goog.bind(this.storageChangeEvent_, this); + /** @private {!Object.} Local map for localStorage. */ + this.localMap_ = {}; +}; + + +/** + * @return {!fireauth.authStorage.Manager} The default Auth storage manager + * instance. + */ +fireauth.authStorage.Manager.getInstance = function() { + // Creates the default instance for Auth storage maanger. + if (!fireauth.authStorage.Manager.instance_) { + /** + * @private {?fireauth.authStorage.Manager} The default storage manager + * instance. + */ + fireauth.authStorage.Manager.instance_ = new fireauth.authStorage.Manager( + fireauth.authStorage.NAMESPACE_, + fireauth.authStorage.SEPARATOR_, + fireauth.util.isSafariLocalStorageNotSynced(), + fireauth.util.runsInBackground()); + } + return fireauth.authStorage.Manager.instance_; +}; + + +/** + * Returns the storage corresponding to the specified persistence. + * @param {!fireauth.authStorage.Persistence} persistent The type of storage + * persistence. + * @return {!fireauth.storage.Storage} The corresponding storage instance. + * @private + */ +fireauth.authStorage.Manager.prototype.getStorage_ = function(persistent) { + switch (persistent) { + case fireauth.authStorage.Persistence.SESSION: + return this.temporaryStorage_; + case fireauth.authStorage.Persistence.NONE: + return this.inMemoryStorage_; + case fireauth.authStorage.Persistence.LOCAL: + default: + return this.persistentStorage_; + } +}; + + +/** + * Constructs the corresponding storage key name. + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {?string=} opt_id This ID associates storage values with specific + * apps. + * @return {string} The corresponding key name with namespace prefixed. + * @private + */ +fireauth.authStorage.Manager.prototype.getKeyName_ = function(dataKey, opt_id) { + return this.namespace_ + this.separator_ + dataKey.name + + (opt_id ? this.separator_ + opt_id : ''); +}; + + +/** + * Gets the stored value from the corresponding storage. + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {?string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + * @return {!goog.Promise} A Promise that resolves with the stored value. + */ +fireauth.authStorage.Manager.prototype.get = function(dataKey, opt_id) { + var keyName = this.getKeyName_(dataKey, opt_id); + return this.getStorage_(dataKey.persistent).get(keyName); +}; + + +/** + * Removes the stored value from the corresponding storage. + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {?string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + * @return {!goog.Promise} A Promise that resolves when the operation is + * completed. + */ +fireauth.authStorage.Manager.prototype.remove = function(dataKey, opt_id) { + var keyName = this.getKeyName_(dataKey, opt_id); + // Keep local map up to date for requested key if persistent storage is used. + if (dataKey.persistent == fireauth.authStorage.Persistence.LOCAL) { + this.localMap_[keyName] = null; + } + return this.getStorage_(dataKey.persistent).remove(keyName); +}; + + +/** + * Stores the value in the corresponding storage. + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {*} value The value to be stored. + * @param {?string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + * @return {!goog.Promise} A Promise that resolves when the operation is + * completed. + */ +fireauth.authStorage.Manager.prototype.set = function(dataKey, value, opt_id) { + var keyName = this.getKeyName_(dataKey, opt_id); + var self = this; + var storage = this.getStorage_(dataKey.persistent); + return storage.set(keyName, value) + .then(function() { + return storage.get(keyName); + }) + .then(function(serializedValue) { + // Keep local map up to date for requested key if persistent storage is + // used. + if (dataKey.persistent == fireauth.authStorage.Persistence.LOCAL) { + self.localMap_[keyName] = serializedValue; + } + }); +}; + + +/** + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {?string} id When operating in multiple app mode, this ID associates + * storage values with specific apps. + * @param {function()} listener The callback listener to run on storage event + * related to key. + */ +fireauth.authStorage.Manager.prototype.addListener = + function(dataKey, id, listener) { + var key = this.getKeyName_(dataKey, id); + // Initialize local map for current key if web storage is supported. + if (typeof goog.global['localStorage'] !== 'undefined' && + typeof goog.global['localStorage']['getItem'] === 'function') { + this.localMap_[key] = goog.global['localStorage']['getItem'](key); + } + if (goog.object.isEmpty(this.listeners_)) { + // Start listeners. + this.startListeners_(); + } + if (!this.listeners_[key]) { + this.listeners_[key] = []; + } + this.listeners_[key].push(listener); +}; + + +/** + * @param {fireauth.authStorage.Key} dataKey The key under which the value is + * stored. + * @param {?string} id When operating in multiple app mode, this ID associates + * storage values with specific apps. + * @param {function()} listener The listener to remove. + */ +fireauth.authStorage.Manager.prototype.removeListener = + function(dataKey, id, listener) { + var key = this.getKeyName_(dataKey, id); + if (this.listeners_[key]) { + goog.array.removeAllIf( + this.listeners_[key], + function(ele) { + return ele == listener; + }); + if (this.listeners_[key].length == 0) { + delete this.listeners_[key]; + } + } + if (goog.object.isEmpty(this.listeners_)) { + // Stop listeners. + this.stopListeners_(); + } +}; + + +/** + * The delay to wait between continuous checks of localStorage on browsers where + * tabs do not run in the background. After each interval wait, we check for + * external changes in localStorage that were not detected in the current tab. + * @const {number} + * @private + */ +fireauth.authStorage.Manager.LOCAL_STORAGE_POLLING_TIMER_ = 1000; + + +/** + * Starts all storage event listeners. + * @private + */ +fireauth.authStorage.Manager.prototype.startListeners_ = function() { + this.getStorage_(fireauth.authStorage.Persistence.LOCAL) + .addStorageListener(this.storageChangeEventHandler_); + // TODO: refactor this implementation to be handled by the underlying + // storage mechanism. + if (!this.runsInBackground_ && + // Add an exception for IE11 and Edge browsers, we should stick to + // indexedDB in that case. + !fireauth.util.isLocalStorageNotSynchronized()) { + this.startManualListeners_(); + } +}; + +/** + * Starts manual polling function to detect storage event changes. + * @private + */ +fireauth.authStorage.Manager.prototype.startManualListeners_ = function() { + var self = this; + this.stopManualListeners_(); + /** @private {?number} The interval timer for manual storage checking. */ + this.manualListenerTimer_ = setInterval(function() { + // Check all keys with listeners on them. + for (var key in self.listeners_) { + // Get value from localStorage. + var currentValue = goog.global['localStorage']['getItem'](key); + var oldValue = self.localMap_[key]; + // If local map value does not match, trigger listener with storage event. + if (currentValue != oldValue) { + self.localMap_[key] = currentValue; + var event = new goog.events.BrowserEvent(/** @type {!Event} */ ({ + type: 'storage', + key: key, + target: window, + oldValue: oldValue, + newValue: currentValue, + // Differentiate this simulated event from the real storage event. + poll: true + })); + self.storageChangeEvent_(event); + } + } + }, fireauth.authStorage.Manager.LOCAL_STORAGE_POLLING_TIMER_); +}; + + +/** + * Stops manual polling function to detect storage event changes. + * @private + */ +fireauth.authStorage.Manager.prototype.stopManualListeners_ = function() { + if (this.manualListenerTimer_) { + clearInterval(this.manualListenerTimer_); + this.manualListenerTimer_ = null; + } +}; + + +/** + * Stops all storage event listeners. + * @private + */ +fireauth.authStorage.Manager.prototype.stopListeners_ = function() { + this.getStorage_(fireauth.authStorage.Persistence.LOCAL) + .removeStorageListener(this.storageChangeEventHandler_); + this.stopManualListeners_(); +}; + + +/** + * @param {!goog.events.BrowserEvent|!Array} data The storage event + * triggered or the array of keys modified. + * @private + */ +fireauth.authStorage.Manager.prototype.storageChangeEvent_ = function(data) { + if (data && data.getBrowserEvent) { + var event = /** @type {!goog.events.BrowserEvent} */ (data); + var key = event.getBrowserEvent().key; + // Key would be null in some situations, like when localStorage is cleared + // from the browser developer tools. + if (key == null) { + // For all keys of interest. + for (var keyName in this.listeners_) { + // Check if something changed in this key's real value. + var storedValue = this.localMap_[keyName]; + // localStorage returns null when a field is not found. + if (typeof storedValue === 'undefined') { + storedValue = null; + } + var realValue = goog.global['localStorage']['getItem'](keyName); + if (realValue !== storedValue) { + // Update local map with real value. + this.localMap_[keyName] = realValue; + // Trigger that key's listener. + this.callListeners_(keyName); + } + } + return; + } + // Check if the key is Firebase Auth related, otherwise ignore. + if (key.indexOf(this.namespace_ + this.separator_) != 0 || + // Ignore keys that have no listeners. + !this.listeners_[key]) { + return; + } + // Check the mechanism how this event was detected. + // The first event will dictate the mechanism to be used. + // Do not use hasOwnProperty('poll') as poll gets obfuscated. + if (typeof event.getBrowserEvent().poll !== 'undefined') { + // Environment detects storage changes via polling. + // Remove storage event listener to prevent possible event duplication. + this.getStorage_(fireauth.authStorage.Persistence.LOCAL) + .removeStorageListener(this.storageChangeEventHandler_); + } else { + // Environment detects storage changes via storage event listener. + // Remove polling listener to prevent possible event duplication. + this.stopManualListeners_(); + } + // Safari embedded iframe. Storage event will trigger with the delta changes + // but no changes will be applied to the iframe localStorage. + if (this.safariLocalStorageNotSynced_) { + // Get current iframe page value, old value and new value. + var currentValue = goog.global['localStorage']['getItem'](key); + var newValue = event.getBrowserEvent().newValue; + // Value not synchronized, synchronize manually. + if (newValue !== currentValue) { + if (newValue !== null) { + // Value changed from current value. + goog.global['localStorage']['setItem'](key, newValue); + } else { + // Current value deleted. + goog.global['localStorage']['removeItem'](key); + } + } else { + // Already detected and processed, do not trigger listeners again. + if (this.localMap_[key] === newValue && + // Real storage event. + typeof event.getBrowserEvent().poll === 'undefined') { + return; + } + } + } + var self = this; + var triggerListeners = function() { + // Keep local map up to date in case storage event is triggered before + // poll. + if (typeof event.getBrowserEvent().poll === 'undefined' && + self.localMap_[key] === goog.global['localStorage']['getItem'](key)) { + // Real storage event which has already been detected, do nothing. + // This seems to trigger in some IE browsers for some reason. + return; + } + self.localMap_[key] = goog.global['localStorage']['getItem'](key); + self.callListeners_(key); + }; + if (fireauth.util.isIe10() && + goog.global['localStorage']['getItem'](key) !== + event.getBrowserEvent().newValue && + event.getBrowserEvent().newValue !== event.getBrowserEvent().oldValue) { + // IE 10 has this weird bug where a storage event would trigger with the + // correct key, oldValue and newValue but localStorage.getItem(key) does + // not yield the updated value until a few milliseconds. This ensures this + // recovers from that situation. + setTimeout( + triggerListeners, fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY); + } else { + triggerListeners(); + } + } else { + var keys = /** @type {!Array} */ (data); + goog.array.forEach(keys, goog.bind(this.callListeners_, this)); + } +}; + + +/** + * Calls all listeners for specified storage event key. + * @param {string} key The storage event key whose listeners are to be run. + * @private + */ +fireauth.authStorage.Manager.prototype.callListeners_ = function(key) { + if (this.listeners_[key]) { + goog.array.forEach( + this.listeners_[key], + function(listener) { + listener(); + }); + } +}; diff --git a/packages/auth/src/authuser.js b/packages/auth/src/authuser.js new file mode 100644 index 00000000000..e4b200226df --- /dev/null +++ b/packages/auth/src/authuser.js @@ -0,0 +1,2313 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the user info pertaining to an identity provider and + * the Firebase user object. + */ + +goog.provide('fireauth.AuthUser'); +goog.provide('fireauth.AuthUser.AccountInfo'); +goog.provide('fireauth.AuthUserInfo'); +goog.provide('fireauth.TokenRefreshTime'); +goog.provide('fireauth.UserEvent'); +goog.provide('fireauth.UserEventType'); +goog.provide('fireauth.UserMetadata'); + +goog.require('fireauth.ActionCodeSettings'); +goog.require('fireauth.AdditionalUserInfo'); +goog.require('fireauth.AuthCredential'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthEventHandler'); +goog.require('fireauth.AuthEventManager'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.ProactiveRefresh'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.StsTokenManager'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.constants.AuthEventType'); +goog.require('fireauth.deprecation'); +goog.require('fireauth.idp'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.events.Event'); +goog.require('goog.events.EventTarget'); +goog.require('goog.object'); + + + +/** + * Initializes an instance of a user metadata object. + * @param {?string=} opt_createdAt The optional creation date UTC timestamp. + * @param {?string=} opt_lastLoginAt The optional last login date UTC timestamp. + * @constructor + */ +fireauth.UserMetadata = function(opt_createdAt, opt_lastLoginAt) { + /** @private {?string} The created at UTC timestamp. */ + this.createdAt_ = opt_createdAt || null; + /** @private {?string} The last login at UTC timestamp. */ + this.lastLoginAt_ = opt_lastLoginAt || null; + fireauth.object.setReadonlyProperties(this, { + 'lastSignInTime': fireauth.util.utcTimestampToDateString( + opt_lastLoginAt || null), + 'creationTime': fireauth.util.utcTimestampToDateString( + opt_createdAt || null), + }); +}; + + +/** + * @return {!fireauth.UserMetadata} A clone of the current user metadata object. + */ +fireauth.UserMetadata.prototype.clone = function() { + return new fireauth.UserMetadata(this.createdAt_, this.lastLoginAt_); +}; + + +/** + * @return {!Object} The object representation of the user metadata instance. + */ +fireauth.UserMetadata.prototype.toPlainObject = function() { + return { + 'lastLoginAt': this.lastLoginAt_, + 'createdAt': this.createdAt_ + }; +}; + + +/** + * Initializes an instance of the user info for an identity provider. + * @param {string} uid The user ID. + * @param {!fireauth.idp.ProviderId} providerId The provider ID. + * @param {?string=} opt_email The optional user email. + * @param {?string=} opt_displayName The optional display name. + * @param {?string=} opt_photoURL The optional photo URL. + * @param {?string=} opt_phoneNumber The optional phone number. + * @constructor + */ +fireauth.AuthUserInfo = function( + uid, + providerId, + opt_email, + opt_displayName, + opt_photoURL, + opt_phoneNumber) { + fireauth.object.setReadonlyProperties(this, { + 'uid': uid, + 'displayName': opt_displayName || null, + 'photoURL': opt_photoURL || null, + 'email': opt_email || null, + 'phoneNumber': opt_phoneNumber || null, + 'providerId': providerId + }); +}; + + + +/** + * User custom event. + * @param {string} type The event type. + * @param {?Object=} opt_properties The optional properties to set to the custom + * event using same keys as object provided. + * @constructor + * @extends {goog.events.Event} + */ +fireauth.UserEvent = function(type, opt_properties) { + goog.events.Event.call(this, type); + // If optional properties provided. + // Add each property to custom event. + for (var key in opt_properties) { + this[key] = opt_properties[key]; + } +}; +goog.inherits(fireauth.UserEvent, goog.events.Event); + + +/** + * Events dispatched by pages on containers. + * @enum {string} + */ +fireauth.UserEventType = { + /** Dispatched when token is changed due to Auth event. */ + TOKEN_CHANGED: 'tokenChanged', + + /** Dispatched when user is deleted. */ + USER_DELETED: 'userDeleted', + + /** + * Dispatched when user session is invalidated. This could happen when the + * following errors occur: user-disabled or user-token-expired. + */ + USER_INVALIDATED: 'userInvalidated' +}; + + +/** + * Defines the proactive token refresh time constraints in milliseconds. + * @enum {number} + */ +fireauth.TokenRefreshTime = { + /** + * The offset time before token natural expiration to run the refresh. + * This is currently 5 minutes. + */ + OFFSET_DURATION: 5 * 60 * 1000, + /** + * This is the first retrial wait after an error. This is currently + * 30 seconds. + */ + RETRIAL_MIN_WAIT: 30 * 1000, + /** + * This is the maximum retrial wait, currently 16 minutes. + */ + RETRIAL_MAX_WAIT: 16 * 60 * 1000 +}; + + + +/** + * The Firebase user. + * @param {!Object} appOptions The application options. + * @param {!Object} stsTokenResponse The server STS token response. + * @param {?Object=} opt_accountInfo The optional user account info. + * @constructor + * @extends {goog.events.EventTarget} + * @implements {fireauth.AuthEventHandler} + */ +fireauth.AuthUser = + function(appOptions, stsTokenResponse, opt_accountInfo) { + /** @private {!Array|!goog.Promise>} List of pending + * promises. */ + this.pendingPromises_ = []; + // User is only created via Auth so API key should always be available. + /** @private {string} The API key. */ + this.apiKey_ = /** @type {string} */ (appOptions['apiKey']); + // This is needed to associate a user to the corresponding Auth instance. + /** @private {string} The App name. */ + this.appName_ = /** @type {string} */ (appOptions['appName']); + /** @private {?string} The Auth domain. */ + this.authDomain_ = appOptions['authDomain'] || null; + var clientFullVersion = firebase.SDK_VERSION ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION) : + null; + /** @private {!fireauth.RpcHandler} The RPC handler instance. */ + this.rpcHandler_ = new fireauth.RpcHandler( + this.apiKey_, + // Get the client Auth endpoint used. + fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint), + clientFullVersion); + // TODO: Consider having AuthUser take a fireauth.StsTokenManager + // instance instead of a token response but make sure lastAccessToken_ also + // initialized at the right time. In this case initializeFromIdTokenResponse + // will take in a token response object and convert it to an instance of + // fireauth.StsTokenManager to properly initialize user. + /** @private {!fireauth.StsTokenManager} The STS token manager instance. */ + this.stsTokenManager_ = new fireauth.StsTokenManager(this.rpcHandler_); + + this.setLastAccessToken_( + stsTokenResponse[fireauth.RpcHandler.AuthServerField.ID_TOKEN]); + // STS token manager will always be populated using server response. + this.stsTokenManager_.parseServerResponse(stsTokenResponse); + fireauth.object.setReadonlyProperty( + this, 'refreshToken', this.stsTokenManager_.getRefreshToken()); + this.setAccountInfo(/** @type {!fireauth.AuthUser.AccountInfo} */ ( + opt_accountInfo || {})); + // Add call to superclass constructor. + fireauth.AuthUser.base(this, 'constructor'); + /** @private {boolean} Whether popup and redirect is enabled on the user. */ + this.popupRedirectEnabled_ = false; + if (this.authDomain_ && + fireauth.AuthEventManager.ENABLED && + // Make sure popup and redirects are supported in the current environment. + fireauth.util.isPopupRedirectSupported()) { + // Get the Auth event manager associated with this user. + this.authEventManager_ = fireauth.AuthEventManager.getManager( + this.authDomain_, this.apiKey_, this.appName_); + } + /** @private {!Array} The list of + * state change listeners. This is needed to make sure state changes are + * resolved before resolving user API promises. For example redirect + * operations should make sure the associated event ID is saved before + * redirecting. + */ + this.stateChangeListeners_ = []; + /** + * @private {?fireauth.AuthError} The user invalidation error if it exists. + */ + this.userInvalidatedError_ = null; + /** + * @private {!fireauth.ProactiveRefresh} The reference to the proactive token + * refresher utility for the current user. + */ + this.proactiveRefresh_ = this.initializeProactiveRefreshUtility_(); + /** + * @private {!function(!Object)} The handler for user token changes used to + * realign the proactive token refresh with external token refresh calls. + */ + this.userTokenChangeListener_ = goog.bind(this.handleUserTokenChange_, this); + var self = this; + /** @private {?string} The current user's language code. */ + this.languageCode_ = null; + /** + * @private {function(!goog.events.Event)} The on language code changed event + * handler. + */ + this.onLanguageCodeChanged_ = function(event) { + // Update the user language code. + self.setLanguageCode(event.languageCode); + }; + /** + * @private {?goog.events.EventTarget} The language code change event + * dispatcher. + */ + this.languageCodeChangeEventDispatcher_ = null; + + /** @private {!Array} The current Firebase frameworks. */ + this.frameworks_ = []; + /** + * @private {function(!goog.events.Event)} The on framework list changed event + * handler. + */ + this.onFrameworkChanged_ = function(event) { + // Update the Firebase frameworks. + self.setFramework(event.frameworks); + }; + /** + * @private {?goog.events.EventTarget} The framework change event dispatcher. + */ + this.frameworkChangeEventDispatcher_ = null; +}; +goog.inherits(fireauth.AuthUser, goog.events.EventTarget); + + +/** + * Updates the user language code. + * @param {?string} languageCode The current language code to use in user + * requests. + */ +fireauth.AuthUser.prototype.setLanguageCode = function(languageCode) { + // Save current language. + this.languageCode_ = languageCode; + // Update the custom locale header. + this.rpcHandler_.updateCustomLocaleHeader(languageCode); +}; + + +/** @return {?string} The current user's language code. */ +fireauth.AuthUser.prototype.getLanguageCode = function() { + return this.languageCode_; +}; + + +/** + * Listens to language code changes triggered by the provided dispatcher. + * @param {?goog.events.EventTarget} dispatcher The language code changed event + * dispatcher. + */ +fireauth.AuthUser.prototype.setLanguageCodeChangeDispatcher = + function(dispatcher) { + // Remove any previous listener. + if (this.languageCodeChangeEventDispatcher_) { + goog.events.unlisten( + this.languageCodeChangeEventDispatcher_, + fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED, + this.onLanguageCodeChanged_); + } + // Update current dispatcher. + this.languageCodeChangeEventDispatcher_ = dispatcher; + // Using an event listener makes it easy for non-currentUsers to detect + // language changes on the parent Auth instance. A developer could still call + // APIs that require localization on signed out user references. + if (dispatcher) { + goog.events.listen( + dispatcher, + fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED, + this.onLanguageCodeChanged_); + } +}; + + +/** + * Updates the Firebase frameworks on the current user. + * @param {!Array} framework The list of Firebase frameworks. + */ +fireauth.AuthUser.prototype.setFramework = function(framework) { + // Save current frameworks. + this.frameworks_ = framework; + // Update the client version in RPC handler with the new frameworks. + this.rpcHandler_.updateClientVersion(firebase.SDK_VERSION ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + this.frameworks_) : + null); +}; + + +/** @return {!Array} The current Firebase frameworks. */ +fireauth.AuthUser.prototype.getFramework = function() { + return goog.array.clone(this.frameworks_); +}; + + +/** + * Listens to framework changes triggered by the provided dispatcher. + * @param {?goog.events.EventTarget} dispatcher The framework changed event + * dispatcher. + */ +fireauth.AuthUser.prototype.setFrameworkChangeDispatcher = + function(dispatcher) { + // Remove any previous listener. + if (this.frameworkChangeEventDispatcher_) { + goog.events.unlisten( + this.frameworkChangeEventDispatcher_, + fireauth.constants.AuthEventType.FRAMEWORK_CHANGED, + this.onFrameworkChanged_); + } + // Update current dispatcher. + this.frameworkChangeEventDispatcher_ = dispatcher; + // Using an event listener makes it easy for non-currentUsers to detect + // framework changes on the parent Auth instance. + if (dispatcher) { + goog.events.listen( + dispatcher, + fireauth.constants.AuthEventType.FRAMEWORK_CHANGED, + this.onFrameworkChanged_); + } +}; + + +/** + * Handles user token changes. Currently used to realign the proactive token + * refresh internal timing with successful external token refreshes. + * @param {!Object} event The token change event. + * @private + */ +fireauth.AuthUser.prototype.handleUserTokenChange_ = function(event) { + // If an external service refreshes the token, reset the proactive token + // refresh utility in case it is still running so the next run time is + // up to date. + // This will currently also trigger when the proactive refresh succeeds. + // This is not ideal but should not have any downsides. It just adds a + // redundant reset which can be optimized not to run in the future. + if (this.proactiveRefresh_.isRunning()) { + this.proactiveRefresh_.stop(); + this.proactiveRefresh_.start(); + } +}; + + +/** + * @return {!fireauth.Auth} The corresponding Auth instance that created the + * current user. + * @private + */ +fireauth.AuthUser.prototype.getAuth_ = function() { + try { + // Get the Auth instance for the current app identified by the App name. + // This could fail if, for example, the App instance was deleted. + return firebase['app'](this.appName_)['auth'](); + } catch (e) { + // Throw appropriate error. + throw new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'No firebase.auth.Auth instance is available for the Firebase App ' + + '\'' + this.appName_ + '\'!'); + } +}; + + +/** + * Used to initialize the current user's proactive token refresher utility. + * @return {!fireauth.ProactiveRefresh} The user's proactive token refresh + * utility. + * @private + */ +fireauth.AuthUser.prototype.initializeProactiveRefreshUtility_ = function() { + var self = this; + return new fireauth.ProactiveRefresh( + // Force ID token refresh right before expiration. + function() { + // Keep in mind when this fails for any reason other than a network + // error, it will effectively stop the proactive refresh. + return self.getIdToken(true); + }, + // Retry only on network errors. + function(error) { + if (error && error.code == 'auth/network-request-failed') { + return true; + } + return false; + }, + // Return next time to run with offset applied. + function() { + // Get time until expiration minus the refresh offset. + var waitInterval = + self.stsTokenManager_.getExpirationTime() - goog.now() - + fireauth.TokenRefreshTime.OFFSET_DURATION; + // Set to zero if wait interval is negative. + return waitInterval > 0 ? waitInterval : 0; + }, + // Retrial minimum wait. + fireauth.TokenRefreshTime.RETRIAL_MIN_WAIT, + // Retrial maximum wait. + fireauth.TokenRefreshTime.RETRIAL_MAX_WAIT, + // Do not run in background as it is common to have multiple tabs open + // in a browser and this could increase QPS on server. + false); +}; + + +/** Starts token proactive refresh. */ +fireauth.AuthUser.prototype.startProactiveRefresh = function() { + // Only allow if not destroyed and not already started. + if (!this.destroyed_ && !this.proactiveRefresh_.isRunning()) { + this.proactiveRefresh_.start(); + // Unlisten any previous token change listener. + goog.events.unlisten( + this, + fireauth.UserEventType.TOKEN_CHANGED, + this.userTokenChangeListener_); + // Listen to token changes to reset the token refresher. + goog.events.listen( + this, + fireauth.UserEventType.TOKEN_CHANGED, + this.userTokenChangeListener_); + } +}; + + +/** Stops token proactive refresh. */ +fireauth.AuthUser.prototype.stopProactiveRefresh = function() { + // Remove internal token change listener. + goog.events.unlisten( + this, + fireauth.UserEventType.TOKEN_CHANGED, + this.userTokenChangeListener_); + // Stop proactive token refresh. + this.proactiveRefresh_.stop(); +}; + + +/** + * Sets latest access token for the AuthUser object. + * @param {string} lastAccessToken + * @private + */ +fireauth.AuthUser.prototype.setLastAccessToken_ = function(lastAccessToken) { + /** @private {?string} Latest access token. */ + this.lastAccessToken_ = lastAccessToken; + fireauth.object.setReadonlyProperty(this, '_lat', lastAccessToken); +}; + + +/** + * @param {!function(!fireauth.AuthUser):!goog.Promise} listener The listener + * to state changes to add. + */ +fireauth.AuthUser.prototype.addStateChangeListener = function(listener) { + this.stateChangeListeners_.push(listener); +}; + + +/** + * @param {!function(!fireauth.AuthUser):!goog.Promise} listener The listener + * to state changes to remove. + */ +fireauth.AuthUser.prototype.removeStateChangeListener = function(listener) { + goog.array.removeAllIf(this.stateChangeListeners_, function(ele) { + return ele == listener; + }); +}; + + +/** + * Executes all state change listener promises and when all fulfilled, resolves + * with the current user. + * @return {!goog.Promise} A promise that resolves when all state listeners + * fulfilled. + * @private + */ +fireauth.AuthUser.prototype.notifyStateChangeListeners_ = function() { + var promises = []; + var self = this; + for (var i = 0; i < this.stateChangeListeners_.length; i++) { + // Run listener with Auth user instance and add to list of promises. + promises.push(this.stateChangeListeners_[i](this)); + } + return goog.Promise.allSettled(promises).then(function(results) { + // State change errors should be recoverable even if errors occur. + return self; + }); +}; + + +/** + * Sets the user current pending popup event ID. + * @param {string} eventId The pending popup event ID. + */ +fireauth.AuthUser.prototype.setPopupEventId = function(eventId) { + // Saving a popup event in a separate property other than redirectEventId + // would prevent a pending redirect event from being overwritten by a newly + // called popup operation. + this.popupEventId_ = eventId; +}; + + +/** + * @return {?string} The pending popup event ID. + */ +fireauth.AuthUser.prototype.getPopupEventId = function() { + return this.popupEventId_ || null; +}; + + +/** + * Sets the user current pending redirect event ID. + * @param {string} eventId The pending redirect event ID. + */ +fireauth.AuthUser.prototype.setRedirectEventId = function(eventId) { + this.redirectEventId_ = eventId; +}; + + +/** + * @return {?string} The pending redirect event ID. + */ +fireauth.AuthUser.prototype.getRedirectEventId = function() { + return this.redirectEventId_ || null; +}; + + +/** + * Subscribes to Auth event manager to handle popup and redirect events. + * This is an explicit operation as users could exist in temporary states. For + * example a user change could be detected in another tab. When syncing to those + * changes, a temporary user is retrieved from storage and then copied to + * existing user. The temporary user should not subscribe to Auth event changes. + */ +fireauth.AuthUser.prototype.enablePopupRedirect = function() { + // Subscribe to Auth event manager if available. + if (this.authEventManager_ && !this.popupRedirectEnabled_) { + this.popupRedirectEnabled_ = true; + this.authEventManager_.subscribe(this); + } +}; + + +/** + * getAccountInfo users field. + * @const {string} + */ +fireauth.AuthUser.GET_ACCOUNT_INFO_USERS = 'users'; + + +/** + * getAccountInfo response user fields. + * @enum {string} + */ +fireauth.AuthUser.GetAccountInfoField = { + CREATED_AT: 'createdAt', + DISPLAY_NAME: 'displayName', + EMAIL: 'email', + LAST_LOGIN_AT: 'lastLoginAt', + LOCAL_ID: 'localId', + PASSWORD_HASH: 'passwordHash', + PASSWORD_UPDATED_AT: 'passwordUpdatedAt', + PHONE_NUMBER: 'phoneNumber', + PHOTO_URL: 'photoUrl', + PROVIDER_USER_INFO: 'providerUserInfo', + EMAIL_VERIFIED: 'emailVerified' +}; + + +/** + * setAccountInfo response user fields. + * @enum {string} + */ +fireauth.AuthUser.SetAccountInfoField = { + DISPLAY_NAME: 'displayName', + EMAIL: 'email', + PHOTO_URL: 'photoUrl', + PROVIDER_ID: 'providerId', + PROVIDER_USER_INFO: 'providerUserInfo' +}; + + +/** + * getAccountInfo response provider user info fields. + * @enum {string} + */ +fireauth.AuthUser.GetAccountInfoProviderField = { + DISPLAY_NAME: 'displayName', + EMAIL: 'email', + PHOTO_URL: 'photoUrl', + PHONE_NUMBER: 'phoneNumber', + PROVIDER_ID: 'providerId', + RAW_ID: 'rawId' +}; + + +/** + * verifyAssertion response fields. + * @enum {string} + */ +fireauth.AuthUser.VerifyAssertionField = { + ID_TOKEN: 'idToken', + PROVIDER_ID: 'providerId' +}; + + +/** @return {!fireauth.StsTokenManager} The STS token manager instance */ +fireauth.AuthUser.prototype.getStsTokenManager = function() { + return this.stsTokenManager_; +}; + + +/** + * Sets the user account info. + * @param {!fireauth.AuthUser.AccountInfo} accountInfo The account information + * from the default provider. + */ +fireauth.AuthUser.prototype.setAccountInfo = function(accountInfo) { + fireauth.object.setReadonlyProperties(this, { + 'uid': accountInfo['uid'], + 'displayName': accountInfo['displayName'] || null, + 'photoURL': accountInfo['photoURL'] || null, + 'email': accountInfo['email'] || null, + 'emailVerified': accountInfo['emailVerified'] || false, + 'phoneNumber': accountInfo['phoneNumber'] || null, + 'isAnonymous': accountInfo['isAnonymous'] || false, + 'metadata': new fireauth.UserMetadata( + accountInfo['createdAt'], accountInfo['lastLoginAt']), + 'providerData': [] + }); +}; + + +/** + * Type specifying the parameters that can be passed to the + * {@code fireauth.AuthUser} constructor. + * @typedef {{ + * uid: (?string|undefined), + * displayName: (?string|undefined), + * photoURL: (?string|undefined), + * email: (?string|undefined), + * emailVerified: ?boolean, + * phoneNumber: (?string|undefined), + * isAnonymous: ?boolean, + * createdAt: (?string|undefined), + * lastLoginAt: (?string|undefined) + * }} + */ +fireauth.AuthUser.AccountInfo; + + +/** + * The provider for all fireauth.AuthUser objects is 'firebase'. + */ +fireauth.object.setReadonlyProperty(fireauth.AuthUser.prototype, 'providerId', + fireauth.idp.ProviderId.FIREBASE); + + +/** + * Returns nothing. This can be used to consume the output of a Promise. + * @private + */ +fireauth.AuthUser.returnNothing_ = function() { + // Return nothing. Intentionally left empty. +}; + + +/** + * Ensures the user is still logged in before moving to the next promise + * resolution. + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.checkDestroyed_ = function() { + var self = this; + return goog.Promise.resolve().then(function() { + if (self.destroyed_) { + throw new fireauth.AuthError(fireauth.authenum.Error.MODULE_DESTROYED); + } + }); +}; + + +/** + * @return {!Array} The list of provider IDs. + */ +fireauth.AuthUser.prototype.getProviderIds = function() { + return goog.array.map(this['providerData'], function(userInfo) { + return userInfo['providerId']; + }); +}; + + +/** + * Adds the provided user info to list of providers' data. + * @param {?fireauth.AuthUserInfo} providerData Provider data to store for user. + */ +fireauth.AuthUser.prototype.addProviderData = function(providerData) { + if (!providerData) { + return; + } + this.removeProviderData(providerData['providerId']); + this['providerData'].push(providerData); +}; + + +/** + * @param {!fireauth.idp.ProviderId} providerId The provider ID whose + * data should be removed. + */ +fireauth.AuthUser.prototype.removeProviderData = function(providerId) { + goog.array.removeAllIf(this['providerData'], function(userInfo) { + return userInfo['providerId'] == providerId; + }); +}; + + +/** + * @param {string} propName The property name to modify. + * @param {?string|boolean} value The new value to set. + */ +fireauth.AuthUser.prototype.updateProperty = function(propName, value) { + // User ID is required. + if (propName == 'uid' && !value) { + return; + } + if (this.hasOwnProperty(propName)) { + fireauth.object.setReadonlyProperty(this, propName, value); + } +}; + + +/** + * @param {!fireauth.AuthUser} otherUser The other user to compare to. + * @return {boolean} True if both User objects have the same user ID. + */ +fireauth.AuthUser.prototype.hasSameUserIdAs = function(otherUser) { + var thisId = this['uid']; + var thatId = otherUser['uid']; + if (thisId === undefined || thisId === null || thisId === '' || + thatId === undefined || thatId === null || thatId === '') { + return false; + } + return thisId == thatId; +}; + + +/** + * Copies all properties and STS token manager instance from userToCopy to + * current user without triggering any Auth state change or token change + * listener. + * @param {!fireauth.AuthUser} userToCopy The updated user to overwrite current + * user. + */ +fireauth.AuthUser.prototype.copy = function(userToCopy) { + var self = this; + // Copy to self. + if (self == userToCopy) { + return; + } + fireauth.object.setReadonlyProperties(this, { + 'uid': userToCopy['uid'], + 'displayName': userToCopy['displayName'], + 'photoURL': userToCopy['photoURL'], + 'email': userToCopy['email'], + 'emailVerified': userToCopy['emailVerified'], + 'phoneNumber': userToCopy['phoneNumber'], + 'isAnonymous': userToCopy['isAnonymous'], + 'providerData': [] + }); + // This should always be available but just in case there is a conflict with + // a user from an older version. + if (userToCopy['metadata']) { + fireauth.object.setReadonlyProperty( + this, + 'metadata', + /** @type{!fireauth.UserMetadata} */ (userToCopy['metadata']).clone()); + } else { + // User to copy has no metadata. Align with that. + fireauth.object.setReadonlyProperty( + this, 'metadata', new fireauth.UserMetadata()); + } + goog.array.forEach(userToCopy['providerData'], function(userInfo) { + self.addProviderData(userInfo); + }); + this.stsTokenManager_ = userToCopy.getStsTokenManager(); + fireauth.object.setReadonlyProperty( + this, 'refreshToken', this.stsTokenManager_.getRefreshToken()); +}; + + +/** + * Set the Auth user redirect storage manager. + * @param {?fireauth.storage.RedirectUserManager} redirectStorageManager The + * utility used to store or delete the user on redirect. + */ +fireauth.AuthUser.prototype.setRedirectStorageManager = + function(redirectStorageManager) { + /** + * @private {?fireauth.storage.RedirectUserManager} The redirect user storage + * manager. + */ + this.redirectStorageManager_ = redirectStorageManager; +}; + + +/** + * Refreshes the current user, if signed in. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reload = function() { + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_(this.checkDestroyed_().then(function() { + return self.reloadWithoutSaving_() + .then(function() { + return self.notifyStateChangeListeners_(); + }) + .then(fireauth.AuthUser.returnNothing_); + })); +}; + + +/** + * Refreshes the current user, if signed in. + * @return {!goog.Promise} Promise that resolves with the idToken. + * @private + */ +fireauth.AuthUser.prototype.reloadWithoutSaving_ = function() { + var self = this; + // ID token is required to refresh the user's data. + // If this is called after invalidation, getToken will throw the cached error. + return this.getIdToken().then(function(idToken) { + var isAnonymous = self['isAnonymous']; + return self.setUserAccountInfoFromToken_(idToken) + .then(function(user) { + if (!isAnonymous) { + // Preserves the not anonymous status of the stored user, + // even if no more credentials (federated or email/password) + // linked to the user. + self.updateProperty('isAnonymous', false); + } + return idToken; + }); + }); +}; + + +/** + * This operation resolves with the Firebase ID token. + * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @return {!goog.Promise} A Promise that resolves with the ID token. + */ +fireauth.AuthUser.prototype.getIdToken = function(opt_forceRefresh) { + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_(this.checkDestroyed_().then(function() { + return self.stsTokenManager_.getToken(opt_forceRefresh); + }).then(function(response) { + if (!response) { + // If the user exists, the token manager should be initialized. + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + // Only if the access token is refreshed, notify Auth listeners. + if (response['accessToken'] != self.lastAccessToken_) { + self.setLastAccessToken_(response['accessToken']); + // Auth state change, notify listeners. + self.notifyAuthListeners_(); + } + self.updateProperty('refreshToken', response['refreshToken']); + return response['accessToken']; + })); +}; + + +/** + * This operation resolves with the Firebase ID token. It has been deprecated in + * favor of getIdToken. + * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @return {!goog.Promise} A Promise that resolves with the ID token. + */ +fireauth.AuthUser.prototype.getToken = function(opt_forceRefresh) { + fireauth.deprecation.log(fireauth.deprecation.Deprecations.USER_GET_TOKEN); + return this.getIdToken(opt_forceRefresh); +}; + + +/** + * Checks if the error corresponds to a user invalidation action. + * @param {*} error The error returned by a user operation. + * @return {boolean} Whether the user is invalidated based on the error + * provided. + * @private + */ +fireauth.AuthUser.isUserInvalidated_ = function(error) { + return !!(error && + (error.code == 'auth/user-disabled' || + error.code == 'auth/user-token-expired')); +}; + + +/** + * Updates the current tokens using a server response, if new tokens are + * present and are different from the current ones, and notify the Auth + * listeners. + * @param {!Object} response The response from the server. + * @private + */ +fireauth.AuthUser.prototype.updateTokensIfPresent_ = function(response) { + if (response[fireauth.RpcHandler.AuthServerField.ID_TOKEN] && + this.lastAccessToken_ != response[ + fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { + this.stsTokenManager_.parseServerResponse(response); + this.notifyAuthListeners_(); + this.setLastAccessToken_(response[ + fireauth.RpcHandler.AuthServerField.ID_TOKEN]); + // Update refresh token property. + this.updateProperty( + 'refreshToken', this.stsTokenManager_.getRefreshToken()); + } +}; + + +/** + * Called internally on Auth (access token) changes to notify listeners. + * @private + */ +fireauth.AuthUser.prototype.notifyAuthListeners_ = function() { + this.dispatchEvent( + new fireauth.UserEvent(fireauth.UserEventType.TOKEN_CHANGED)); +}; + + +/** + * Called internally on user deletion to notify listeners. + * @private + */ +fireauth.AuthUser.prototype.notifyUserDeletedListeners_ = function() { + this.dispatchEvent( + new fireauth.UserEvent(fireauth.UserEventType.USER_DELETED)); +}; + + +/** + * Called internally on user session invalidation to notify listeners. + * @private + */ +fireauth.AuthUser.prototype.notifyUserInvalidatedListeners_ = function() { + this.dispatchEvent( + new fireauth.UserEvent(fireauth.UserEventType.USER_INVALIDATED)); +}; + + +/** + * Queries the backend using the provided ID token for all linked accounts to + * build the Firebase user object. + * @param {!string} idToken The ID token string. + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.setUserAccountInfoFromToken_ = function(idToken) { + return this.rpcHandler_.getAccountInfoByIdToken(idToken) + .then(goog.bind(this.parseAccountInfo_, this)); +}; + + +/** + * Parses the response from the getAccountInfo endpoint. + * @param {!Object} resp The backend response. + * @private + */ +fireauth.AuthUser.prototype.parseAccountInfo_ = function(resp) { + var users = resp[fireauth.AuthUser.GET_ACCOUNT_INFO_USERS]; + if (!users || !users.length) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + var user = users[0]; + var accountInfo = /** @type {!fireauth.AuthUser.AccountInfo} */ ({ + 'uid': /** @type {string} */ ( + user[fireauth.AuthUser.GetAccountInfoField.LOCAL_ID]), + 'displayName': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.DISPLAY_NAME]), + 'photoURL': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.PHOTO_URL]), + 'email': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.EMAIL]), + 'emailVerified': + !!user[fireauth.AuthUser.GetAccountInfoField.EMAIL_VERIFIED], + 'phoneNumber': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.PHONE_NUMBER]), + 'lastLoginAt': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.LAST_LOGIN_AT]), + 'createdAt': /** @type {?string|undefined} */ ( + user[fireauth.AuthUser.GetAccountInfoField.CREATED_AT]) + }); + this.setAccountInfo(accountInfo); + var linkedAccounts = this.extractLinkedAccounts_(user); + for (var i = 0; i < linkedAccounts.length; i++) { + this.addProviderData(linkedAccounts[i]); + } + // Sets the isAnonymous flag based on email, passwordHash and providerData. + var isAnonymous = !(this['email'] && + user[fireauth.AuthUser.GetAccountInfoField.PASSWORD_HASH]) && + !(this['providerData'] && this['providerData'].length); + this.updateProperty('isAnonymous', isAnonymous); +}; + + +/** + * Extracts the linked accounts from getAccountInfo response and returns an + * array of corresponding provider data. + * @param {!Object} resp The response object. + * @return {!Array.} The linked accounts. + * @private + */ +fireauth.AuthUser.prototype.extractLinkedAccounts_ = function(resp) { + var providerInfo = + resp[fireauth.AuthUser.GetAccountInfoField.PROVIDER_USER_INFO]; + if (!providerInfo || !providerInfo.length) { + return []; + } + + return goog.array.map(providerInfo, function(info) { + return new fireauth.AuthUserInfo( + info[fireauth.AuthUser.GetAccountInfoProviderField.RAW_ID], + info[fireauth.AuthUser.GetAccountInfoProviderField.PROVIDER_ID], + info[fireauth.AuthUser.GetAccountInfoProviderField.EMAIL], + info[fireauth.AuthUser.GetAccountInfoProviderField.DISPLAY_NAME], + info[fireauth.AuthUser.GetAccountInfoProviderField.PHOTO_URL], + info[fireauth.AuthUser.GetAccountInfoProviderField.PHONE_NUMBER]); + }); +}; + + +/** + * Reauthenticates a user using a fresh credential, to be used before operations + * such as updatePassword that require tokens from recent login attempts. It + * also returns any additional user info data or credentials returned form the + * backend. + * @param {!fireauth.AuthCredential} credential + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reauthenticateAndRetrieveDataWithCredential = + function(credential) { + var self = this; + var userCredential = null; + // Register this pending promise but bypass user invalidation check. + return this.registerPendingPromise_( + // Match ID token from credential with the current user UID. + credential.matchIdTokenWithUid(this.rpcHandler_, this['uid']) + .then(function(response) { + // If the credential is valid and matches the current user ID, then + // update the tokens accordingly. + self.updateTokensIfPresent_(response); + // Get user credential. + userCredential = self.getUserCredential_( + response, fireauth.constants.OperationType.REAUTHENTICATE); + // This could potentially validate an invalidated user. This happens in + // the case a password reset was applied. The refresh token is expired. + // Reauthentication should revalidate the user. + // User would remain non current if already signed out, but should be + // enabled again. + self.userInvalidatedError_ = null; + return self.reload(); + }).then(function() { + // Return user credential after reauthenticated user is reloaded. + return userCredential; + }), + // Skip invalidation check as reauthentication could revalidate a user. + true); +}; + + +/** + * Reauthenticates a user using a fresh credential, to be used before operations + * such as updatePassword that require tokens from recent login attempts. + * @param {!fireauth.AuthCredential} credential + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reauthenticateWithCredential = + function(credential) { + // Get reauthenticateAndRetrieveDataWithCredential result and return void. + return this.reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Do not return anything. Promise should resolve with void. + }); +}; + + +/** + * Reloads the user and then checks if a provider is already linked. If so, + * this returns a Promise that rejects. Note that state change listeners are not + * notified on success, so that operations using this can make changes and then + * do one final listener notification. + * @param {string} providerId + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.checkIfAlreadyLinked_ = + function(providerId) { + var self = this; + // Reload first in case the user was updated elsewhere. + return this.reloadWithoutSaving_() + .then(function() { + if (goog.array.contains(self.getProviderIds(), providerId)) { + return self.notifyStateChangeListeners_() + .then(function() { + throw new fireauth.AuthError( + fireauth.authenum.Error.PROVIDER_ALREADY_LINKED); + }); + } + }); +}; + + +/** + * Links a provider to the current user and returns any additional user info + * data or credentials returned form the backend. + * @param {!fireauth.AuthCredential} credential The credential from the Auth + * provider. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.linkAndRetrieveDataWithCredential = + function(credential) { + var self = this; + var userCredential = null; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + this.checkIfAlreadyLinked_(credential['providerId']) + .then(function() { + return self.getIdToken(); + }) + .then(function(idToken) { + return credential.linkToIdToken(self.rpcHandler_, idToken); + }) + .then(function(response) { + // Get user credential. + userCredential = self.getUserCredential_( + response, fireauth.constants.OperationType.LINK); + // Finalize linking. + return self.finalizeLinking_(response); + }) + .then(function(user) { + // Return user credential after finalizing linking. + return userCredential; + }) + ); +}; + + +/** + * Links a provider to the current user. + * @param {!fireauth.AuthCredential} credential The credential from the Auth + * provider. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.linkWithCredential = function(credential) { + // Get linkAndRetrieveDataWithCredential result and return the user only. + return this.linkAndRetrieveDataWithCredential(credential) + .then(function(result) { + return result['user']; + }); +}; + + +/** + * Links a phone number using the App verifier instance and returns a + * promise that resolves with the confirmation result which on confirmation + * will resolve with the UserCredential object. + * @param {string} phoneNumber The phone number to authenticate with. + * @param {!firebase.auth.ApplicationVerifier} appVerifier The application + * verifier. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.linkWithPhoneNumber = + function(phoneNumber, appVerifier) { + var self = this; + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_( + // Check if linked already. If so, throw an error. + // This is redundant but is needed to prevent the need to send the + // SMS (worth the cost). + this.checkIfAlreadyLinked_(fireauth.idp.ProviderId.PHONE) + .then(function() { + return fireauth.ConfirmationResult.initialize( + self.getAuth_(), + phoneNumber, + appVerifier, + // This will check again if the credential is linked. + goog.bind(self.linkAndRetrieveDataWithCredential, self)); + }))); +}; + + +/** + * Reauthenticates a user with a phone number using the App verifier instance + * and returns a promise that resolves with the confirmation result which on + * confirmation will resolve with the UserCredential object. + * @param {string} phoneNumber The phone number to authenticate with. + * @param {!firebase.auth.ApplicationVerifier} appVerifier The application + * verifier. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reauthenticateWithPhoneNumber = + function(phoneNumber, appVerifier) { + var self = this; + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_( + // Wrap this operation in a Promise since self.getAuth_() may throw an + // error synchronously. + goog.Promise.resolve().then(function() { + return fireauth.ConfirmationResult.initialize( + // Get corresponding Auth instance. + self.getAuth_(), + phoneNumber, + appVerifier, + goog.bind(self.reauthenticateAndRetrieveDataWithCredential, + self)); + }), + // Skip invalidation check as reauthentication could revalidate a + // user. + true)); +}; + + +/** + * Converts an ID token response (eg. verifyAssertion) to a UserCredential + * object. + * @param {!Object} idTokenResponse The ID token response. + * @param {!fireauth.constants.OperationType} operationType The operation type + * to set in the user credential. + * @return {!fireauth.AuthEventManager.Result} The UserCredential object + * constructed from the response. + * @private + */ +fireauth.AuthUser.prototype.getUserCredential_ = + function(idTokenResponse, operationType) { + // Get credential if available in the response. + var credential = fireauth.AuthProvider.getCredentialFromResponse( + idTokenResponse); + // Get additional user info data if available in the response. + var additionalUserInfo = fireauth.AdditionalUserInfo.fromPlainObject( + idTokenResponse); + // Return the readonly copy of the user credential object. + return fireauth.object.makeReadonlyCopy({ + // Return the current user reference. + 'user': this, + // Return any credential passed from the backend. + 'credential': credential, + // Return any additional IdP data passed from the backend. + 'additionalUserInfo': additionalUserInfo, + // Return the operation type in the user credential object. + 'operationType': operationType + }); +}; + + +/** + * Finalizes a linking flow, updating idToken and user's data using the + * RPC linking response. + * @param {!Object} response The RPC linking response. + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.finalizeLinking_ = function(response) { + // The response may contain a new access token, + // so we should update them just like a new sign in. + this.updateTokensIfPresent_(response); + // This will take care of saving the updated state. + var self = this; + return this.reload().then(function() { + return self; + }); +}; + + +/** + * Updates the user's email. + * @param {string} newEmail The new email. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.updateEmail = function(newEmail) { + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_(this.getIdToken() + .then(function(idToken) { + return self.rpcHandler_.updateEmail(idToken, newEmail); + }) + .then(function(response) { + // Calls to SetAccountInfo may invalidate old tokens. + self.updateTokensIfPresent_(response); + // Reloads the user to update emailVerified. + return self.reload(); + })); +}; + + +/** + * Updates the user's phone number. + * @param {!fireauth.PhoneAuthCredential} phoneCredential + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.updatePhoneNumber = function(phoneCredential) { + var self = this; + return this.registerPendingPromise_(this.getIdToken() + .then(function(idToken) { + // The backend always overwrites the existing phone number during a + // link operation. + return phoneCredential.linkToIdToken(self.rpcHandler_, idToken); + }) + .then(function(response) { + self.updateTokensIfPresent_(response); + return self.reload(); + })); +}; + + +/** + * Updates the user's password. + * @param {string} newPassword The new password. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.updatePassword = function(newPassword) { + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + this.getIdToken() + .then(function(idToken) { + return self.rpcHandler_.updatePassword(idToken, newPassword); + }) + .then(function(response) { + self.updateTokensIfPresent_(response); + // Reloads the user in case email has also been updated and the user + // was anonymous. + return self.reload(); + })); +}; + + +/** + * Updates the user's profile data. + * @param {!Object} profile The profile data to update. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.updateProfile = function(profile) { + if (profile['displayName'] === undefined && + profile['photoURL'] === undefined) { + // No change, directly return. + return this.checkDestroyed_(); + } + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + this.getIdToken().then(function(idToken) { + // Translate the request into one that the backend accepts. + var profileRequest = { + 'displayName': profile['displayName'], + 'photoUrl': profile['photoURL'] + }; + return self.rpcHandler_.updateProfile(idToken, profileRequest); + }) + .then(function(response) { + // Calls to SetAccountInfo may invalidate old tokens. + self.updateTokensIfPresent_(response); + // Update properties. + self.updateProperty('displayName', + response[fireauth.AuthUser.SetAccountInfoField.DISPLAY_NAME] || + null); + self.updateProperty('photoURL', + response[fireauth.AuthUser.SetAccountInfoField.PHOTO_URL] || null); + goog.array.forEach(self['providerData'], function(userInfo) { + // Check if password provider is linked. + if (userInfo['providerId'] === fireauth.idp.ProviderId.PASSWORD) { + // If so, update both fields in that provider. + fireauth.object.setReadonlyProperty( + userInfo, 'displayName', self['displayName']); + fireauth.object.setReadonlyProperty( + userInfo, 'photoURL', self['photoURL']); + } + }); + // Notify changes and resolve. + return self.notifyStateChangeListeners_(); + }) + .then(fireauth.AuthUser.returnNothing_)); +}; + + +/** + * Unlinks a provider from an account. + * @param {!fireauth.idp.ProviderId} providerId The ID of the provider to + * unlink. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.unlink = function(providerId) { + var self = this; + // Make sure we have updated user providers to avoid removing a linked + // provider that hasn't been updated in current copy of user. + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + this.reloadWithoutSaving_() + .then(function(idToken) { + // Provider already unlinked. + if (!goog.array.contains(self.getProviderIds(), providerId)) { + return self.notifyStateChangeListeners_() + .then(function() { + throw new fireauth.AuthError( + fireauth.authenum.Error.NO_SUCH_PROVIDER); + }); + } + // We delete the providerId given. + return self.rpcHandler_ + .deleteLinkedAccounts(idToken, [providerId]) + .then(function(resp) { + // Construct the set of provider IDs returned by server. + var remainingProviderIds = {}; + var userInfo = resp[fireauth.AuthUser.SetAccountInfoField. + PROVIDER_USER_INFO] || []; + goog.array.forEach(userInfo, function(obj) { + remainingProviderIds[ + obj[fireauth.AuthUser.SetAccountInfoField.PROVIDER_ID]] = + true; + }); + + // Remove all provider data objects where the provider ID no + // longer exists in this user. + goog.array.forEach(self.getProviderIds(), function(pId) { + if (!remainingProviderIds[pId]) { + // This provider no longer linked, remove it from user. + self.removeProviderData(pId); + } + }); + + // Remove the phone number if the phone provider was unlinked. + if (!remainingProviderIds[fireauth.PhoneAuthProvider[ + 'PROVIDER_ID']]) { + fireauth.object.setReadonlyProperty(self, 'phoneNumber', null); + } + + return self.notifyStateChangeListeners_(); + }); + })); +}; + + +/** + * Deletes the user, triggering an Auth token change if successful. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.delete = function() { + // Notice the way of declaring the method, it's to avoid a weird bug on IE8. + var self = this; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + this.getIdToken() + .then(function(idToken) { + return self.rpcHandler_.deleteAccount(idToken); + }) + .then(function() { + self.notifyUserDeletedListeners_(); + })) + .then(function() { + // Destroying after the registered promise is handled ensures it won't + // be canceled. + self.destroy(); + }); +}; + + +/** + * Tells the Auth event manager if this user is the owner of a detected Auth + * event. A user can handle linkWithPopup and linkWithRedirect operations. + * In addition, the event ID should match the user's event IDs. + * @param {!fireauth.AuthEvent.Type} mode The Auth operation mode (popup, + * redirect). + * @param {?string=} opt_eventId The event ID. + * @return {boolean} Whether the Auth event handler can handler the provided + * event. + * @override + */ +fireauth.AuthUser.prototype.canHandleAuthEvent = function(mode, opt_eventId) { + if (mode == fireauth.AuthEvent.Type.LINK_VIA_POPUP && + this.getPopupEventId() == opt_eventId && + this.pendingPopupResolvePromise_) { + // The link via popup event's ID matches the user's popup event ID which + // makes this user the owner of this event. + return true; + } else if (mode == fireauth.AuthEvent.Type.REAUTH_VIA_POPUP && + this.getPopupEventId() == opt_eventId && + this.pendingPopupResolvePromise_) { + // The reauth via popup event's ID matches the user's popup event ID which + // makes this user the owner of this event. + return true; + } else if (mode == fireauth.AuthEvent.Type.LINK_VIA_REDIRECT && + this.getRedirectEventId() == opt_eventId) { + // The link via redirect event's ID matches the user's redirect event ID + // which makes this user the owner of this event. + return true; + } else if (mode == fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT && + this.getRedirectEventId() == opt_eventId) { + // The reauth via redirect event's ID matches the user's redirect event ID + // which makes this user the owner of this event. + return true; + } + return false; +}; + + +/** + * Completes the pending popup operation. If error is not null, rejects with the + * error. Otherwise, it resolves with the popup redirect result. + * @param {!fireauth.AuthEvent.Type} mode The Auth operation mode (popup, + * redirect). + * @param {?fireauth.AuthEventManager.Result} popupRedirectResult The result + * to resolve with when no error supplied. + * @param {?fireauth.AuthError} error When supplied, the promise will reject. + * @param {?string=} opt_eventId The event ID. + * @override + */ +fireauth.AuthUser.prototype.resolvePendingPopupEvent = + function(mode, popupRedirectResult, error, opt_eventId) { + // Only handles popup events with event IDs that match a pending popup ID. + if ((mode != fireauth.AuthEvent.Type.LINK_VIA_POPUP && + mode != fireauth.AuthEvent.Type.REAUTH_VIA_POPUP) || + opt_eventId != this.getPopupEventId()) { + return; + } + if (error && this.pendingPopupRejectPromise_) { + // Reject with error for supplied mode. + this.pendingPopupRejectPromise_(error); + } else if (popupRedirectResult && + !error && + this.pendingPopupResolvePromise_) { + // Resolve with result for supplied mode. + this.pendingPopupResolvePromise_(popupRedirectResult); + } + // Now that event is resolved, delete timeout promise. + if (this.popupTimeoutPromise_) { + this.popupTimeoutPromise_.cancel(); + this.popupTimeoutPromise_ = null; + } + // Delete pending promises. + delete this.pendingPopupResolvePromise_; + delete this.pendingPopupRejectPromise_; +}; + + +/** + * Returns the handler's appropriate popup and redirect sign in operation + * finisher. Can handle link or reauth events that match existing event IDs. + * @param {!fireauth.AuthEvent.Type} mode The Auth operation mode (popup, + * redirect). + * @param {?string=} opt_eventId The optional event ID. + * @return {?function(string, + * string):!goog.Promise} + * @override + */ +fireauth.AuthUser.prototype.getAuthEventHandlerFinisher = + function(mode, opt_eventId) { + if (mode == fireauth.AuthEvent.Type.LINK_VIA_POPUP && + opt_eventId == this.getPopupEventId()) { + // Link with popup ID matches popup event ID. + return goog.bind(this.finishPopupAndRedirectLink, this); + } else if (mode == fireauth.AuthEvent.Type.REAUTH_VIA_POPUP && + opt_eventId == this.getPopupEventId()) { + // Reauth with popup ID matches popup event ID. + return goog.bind(this.finishPopupAndRedirectReauth, this); + } else if (mode == fireauth.AuthEvent.Type.LINK_VIA_REDIRECT && + this.getRedirectEventId() == opt_eventId) { + // Link with redirect ID matches redirect event ID. + return goog.bind(this.finishPopupAndRedirectLink, this); + } else if (mode == fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT && + this.getRedirectEventId() == opt_eventId) { + // Reauth with redirect ID matches redirect event ID. + return goog.bind(this.finishPopupAndRedirectReauth, this); + } + return null; +}; + + +/** + * @return {string} The generated event ID used to identify a popup or redirect + * event. + * @private + */ +fireauth.AuthUser.prototype.generateEventId_ = function() { + return fireauth.util.generateEventId(this['uid'] + ':::'); +}; + + +/** + * Links to Auth provider via popup. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.linkWithPopup = function(provider) { + var self = this; + // Additional check to return to fail when the provider is already linked. + var additionalCheck = function() { + return self.checkIfAlreadyLinked_(provider['providerId']) + .then(function() { + // Notify state listeners after the check as it will update the user + // state. + return self.notifyStateChangeListeners_(); + }); + }; + return this.runOperationWithPopup_( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, provider, additionalCheck, false); +}; + + +/** + * Reauthenticate to Auth provider via popup. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reauthenticateWithPopup = function(provider) { + // No additional check needed before running this operation. + var additionalCheck = function() { + return goog.Promise.resolve(); + }; + return this.runOperationWithPopup_( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + provider, + additionalCheck, + // Do not update token and skip session invalidation check. + true); +}; + + +/** + * Runs a specific OAuth operation using the Auth provider via popup. + * @param {!fireauth.AuthEvent.Type} mode The mode of operation (link or + * reauth). + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {!function():!goog.Promise} additionalCheck The additional check to + * run before proceeding with the popup processing. + * @param {boolean} isReauthOperation whether this is a reauth operation or not. + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.runOperationWithPopup_ = + function(mode, provider, additionalCheck, isReauthOperation) { + // Check if popup and redirect are supported in this environment. + if (!fireauth.util.isPopupRedirectSupported()) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + // Quickly throw user invalidation error if already invalidated. + if (this.userInvalidatedError_ && + // Skip invalidation check as reauthentication could revalidate a user. + !isReauthOperation) { + return goog.Promise.reject(this.userInvalidatedError_); + } + var self = this; + // Popup the window immediately to make sure the browser associates the + // popup with the click that triggered it. + + // Get provider settings. + var settings = fireauth.idp.getIdpSettings(provider['providerId']); + // There could multiple users at the same time and multiple users could have + // the same UID. So try to ensure event ID uniqueness. + var eventId = this.generateEventId_(); + // If incapable of redirecting popup from opener, popup destination URL + // directly. This could also happen in a sandboxed iframe. + var oauthHelperWidgetUrl = null; + if ((!fireauth.util.runsInBackground() || fireauth.util.isIframe()) && + this.authDomain_ && + provider['isOAuthProvider']) { + oauthHelperWidgetUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + this.authDomain_, + this.apiKey_, + this.appName_, + mode, + provider, + null, + eventId, + firebase.SDK_VERSION || null); + } + // The popup must have a name, otherwise when successive popups are triggered + // they will all render in the same instance and none will succeed since the + // popup cancel of first window will close the shared popup window instance. + var popupWin = + fireauth.util.popup( + oauthHelperWidgetUrl, + fireauth.util.generateRandomString(), + settings && settings.popupWidth, + settings && settings.popupHeight); + var p = additionalCheck().then(function() { + // Auth event manager must be available for account linking or + // reauthentication to be possible. + self.getAuthEventManager(); + if (!isReauthOperation) { + // Some operations like reauthenticate do not require this. + return self.getIdToken().then(function(idToken) {}); + } + }).then(function() { + // Process popup request. + return self.authEventManager_.processPopup( + popupWin, mode, provider, eventId, !!oauthHelperWidgetUrl); + }).then(function() { + return new goog.Promise(function(resolve, reject) { + // Expire other pending promises if still available. + self.resolvePendingPopupEvent( + mode, + null, + new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST), + // Existing popup event ID. + self.getPopupEventId()); + // Save current pending promises. + self.pendingPopupResolvePromise_ = resolve; + self.pendingPopupRejectPromise_ = reject; + // Overwrite popup event ID with new one. + self.setPopupEventId(eventId); + // Keep track of timeout promise to cancel it on promise resolution before + // it times out. + self.popupTimeoutPromise_ = + self.authEventManager_.startPopupTimeout( + self, mode, /** @type {!Window} */ (popupWin), eventId); + }); + }).then(function(result) { + // On resolution, close popup if still opened and pass result through. + if (popupWin) { + fireauth.util.closeWindow(popupWin); + } + if (result) { + return fireauth.object.makeReadonlyCopy(result); + } + return null; + }).thenCatch(function(error) { + if (popupWin) { + fireauth.util.closeWindow(popupWin); + } + throw error; + }); + // Register this pending promise. This will also check for user invalidation. + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_( + p, + // Skip invalidation check as reauthentication could revalidate a + // user. + isReauthOperation)); +}; + + +/** + * Links to Auth provider via redirect. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.linkWithRedirect = function(provider) { + var mode = fireauth.AuthEvent.Type.LINK_VIA_REDIRECT; + var self = this; + // Additional check to return to fail when the provider is already linked. + var additionalCheck = function() { + return self.checkIfAlreadyLinked_(provider['providerId']); + }; + return this.runOperationWithRedirect_(mode, provider, additionalCheck, false); +}; + + +/** + * Reauthenticates to Auth provider via redirect. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.reauthenticateWithRedirect = function(provider) { + // No additional check needed. + var additionalCheck = function() { + return goog.Promise.resolve(); + }; + return this.runOperationWithRedirect_( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + provider, + additionalCheck, + // Do not update token and skip session invalidation check. + true); +}; + + + +/** + * Runs a specific OAuth operation using the Auth provider via redirect. + * @param {!fireauth.AuthEvent.Type} mode The mode of operation (link or + * reauth). + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {!function():!goog.Promise} additionalCheck The additional check to + * run before proceeding with the redirect processing. + * @param {boolean} isReauthOperation whether this is a reauth operation or not. + * @return {!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.runOperationWithRedirect_ = + function(mode, provider, additionalCheck, isReauthOperation) { + // Check if popup and redirect are supported in this environment. + if (!fireauth.util.isPopupRedirectSupported()) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + // Quickly throw user invalidation error if already invalidated. + if (this.userInvalidatedError_ && + // Skip invalidation check as reauthentication could revalidate a user. + !isReauthOperation) { + return goog.Promise.reject(this.userInvalidatedError_); + } + var self = this; + var errorThrown = null; + // There could multiple users at the same time and multiple users could have + // the same UID. So try to ensure event ID uniqueness. + var eventId = this.generateEventId_(); + var p = additionalCheck().then(function() { + // Auth event manager must be available for account linking or + // reauthentication to be possible. + self.getAuthEventManager(); + if (!isReauthOperation) { + // Some operations like reauthenticate do not require this. + return self.getIdToken().then(function(idToken) {}); + } + }).then(function() { + // Process redirect operation. + self.setRedirectEventId(eventId); + // Before redirecting save the event ID. + // It is important that the user redirect event ID is updated in storage + // before redirecting. + return self.notifyStateChangeListeners_(); + }).then(function(user) { + if (self.redirectStorageManager_) { + // Save the user before redirecting in case it is not current so that it + // can be retrieved after reloading for linking or reauthentication to + // succeed. + return self.redirectStorageManager_.setRedirectUser(self); + } + return user; + }).then(function(user) { + // Complete the redirect operation. + return self.authEventManager_.processRedirect(mode, provider, eventId); + }).thenCatch(function(error) { + // Catch error if any is generated. + errorThrown = error; + if (self.redirectStorageManager_) { + // If an error is detected, delete the redirected user from storage. + return self.redirectStorageManager_.removeRedirectUser(); + } + // No storage manager, just throw error. + throw errorThrown; + }).then(function() { + // Rethrow the error. + if (errorThrown) { + throw errorThrown; + } + }); + // Register this pending promise. This will also check for user invalidation. + return /** @type {!goog.Promise} */ (this.registerPendingPromise_( + p, + // Skip invalidation check as reauthentication could revalidate a user. + isReauthOperation)); +}; + + +/** + * @return {!fireauth.AuthEventManager} The user's Auth event manager. + */ +fireauth.AuthUser.prototype.getAuthEventManager = function() { + // Either return the manager instance if available, otherwise throw an error. + if (this.authEventManager_ && this.popupRedirectEnabled_) { + return this.authEventManager_; + } else if (this.authEventManager_ && !this.popupRedirectEnabled_) { + // This should not happen as Auth will enable a user after it is created. + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + throw new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN); +}; + + +/** + * Finishes the popup and redirect account linking operations. + * @param {string} requestUri The callback URL with the OAuth response. + * @param {string} sessionId The session ID used to generate the authUri. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.finishPopupAndRedirectLink = + function(requestUri, sessionId) { + var self = this; + // Now that popup has responded, delete popup timeout promise. + if (this.popupTimeoutPromise_) { + this.popupTimeoutPromise_.cancel(); + this.popupTimeoutPromise_ = null; + } + var userCredential = null; + // This routine could be run before init state, make sure it waits for that to + // complete otherwise this would fail as user not loaded from storage yet. + var p = this.getIdToken() + .then(function(idToken) { + var request = { + 'requestUri': requestUri, + 'sessionId': sessionId, + 'idToken': idToken + }; + // This operation should fail if new ID token differs from old one. + // So this can be treate as a profile update operation. + return self.rpcHandler_.verifyAssertionForLinking(request); + }) + .then(function(response) { + // Get user credential. + userCredential = self.getUserCredential_( + response, fireauth.constants.OperationType.LINK); + // Finalizes the linking process. + return self.finalizeLinking_(response); + }) + .then(function(user) { + // Return the user credential response. + return userCredential; + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_(p)); +}; + + +/** + * Finishes the popup and redirect account reauthentication operations. + * @param {string} requestUri The callback URL with the OAuth response. + * @param {string} sessionId The session ID used to generate the authUri. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.finishPopupAndRedirectReauth = + function(requestUri, sessionId) { + var self = this; + // Now that popup has responded, delete popup timeout promise. + if (this.popupTimeoutPromise_) { + this.popupTimeoutPromise_.cancel(); + this.popupTimeoutPromise_ = null; + } + var userCredential = null; + // This routine could be run before init state, make sure it waits for that to + // complete otherwise this would fail as user not loaded from storage yet. + var p = goog.Promise.resolve() + .then(function() { + var request = { + 'requestUri': requestUri, + 'sessionId': sessionId, + }; + // Finish sign in by calling verifyAssertionForExisting and then + // matching the returned ID token's UID with the current user's. + return fireauth.AuthCredential.verifyTokenResponseUid( + self.rpcHandler_.verifyAssertionForExisting(request), + self['uid']); + }).then(function(response) { + // Get credential from response if available. + // Get user credential. + userCredential = self.getUserCredential_( + response, fireauth.constants.OperationType.REAUTHENTICATE); + // If the credential is valid and matches the current user ID, then + // update the tokens accordingly. + self.updateTokensIfPresent_(response); + // This could potentially validate an invalidated user. This happens in + // the case a password reset was applied. The refresh token is expired. + // Reauthentication should revalidate the user. + // User would remain non current if already signed out, but should be + // enabled again. + self.userInvalidatedError_ = null; + return self.reload(); + }) + .then(function() { + // Return the user credential response. + return userCredential; + }); + return /** @type {!goog.Promise} */ ( + this.registerPendingPromise_( + p, + // Skip invalidation check as reauthentication could revalidate a + // user. + true)); +}; + + +/** + * Sends the email verification email to the email in the user's account. + * @param {?Object=} opt_actionCodeSettings The optional action code settings + * object. + * @return {!goog.Promise} + */ +fireauth.AuthUser.prototype.sendEmailVerification = + function(opt_actionCodeSettings) { + var self = this; + var idToken = null; + // Register this pending promise. This will also check for user invalidation. + return this.registerPendingPromise_( + // Wrap in promise as ActionCodeSettings constructor could throw a + // synchronous error if invalid arguments are specified. + this.getIdToken().then(function(latestIdToken) { + idToken = latestIdToken; + if (typeof opt_actionCodeSettings !== 'undefined' && + // Ignore empty objects. + !goog.object.isEmpty(opt_actionCodeSettings)) { + return new fireauth.ActionCodeSettings( + /** @type {!Object} */ (opt_actionCodeSettings)).buildRequest(); + } + return {}; + }) + .then(function(additionalRequestData) { + return self.rpcHandler_.sendEmailVerification( + /** @type {string} */ (idToken), additionalRequestData); + }) + .then(function(email) { + if (self['email'] != email) { + // Our local copy does not have an email. If the email changed, + // reload the user. + return self.reload(); + } + }) + .then(function() { + // Return nothing. + })); +}; + + +/** + * Destroys the user object and makes further operations invalid. Sensitive + * fields (refreshToken) are also cleared. + */ +fireauth.AuthUser.prototype.destroy = function() { + // Cancel all pending promises. + for (var i = 0; i < this.pendingPromises_.length; i++) { + this.pendingPromises_[i].cancel(fireauth.authenum.Error.MODULE_DESTROYED); + } + // Stop listening to language code changes. + this.setLanguageCodeChangeDispatcher(null); + // Stop listening to framework changes. + this.setFrameworkChangeDispatcher(null); + // Empty pending promises array. + this.pendingPromises_ = []; + this.destroyed_ = true; + // Stop proactive refresh if running. + this.stopProactiveRefresh(); + fireauth.object.setReadonlyProperty(this, 'refreshToken', null); + // Make sure the destroyed user is unsubscribed from Auth event handling. + if (this.authEventManager_) { + this.authEventManager_.unsubscribe(this); + } +}; + + +/** + * Takes in a pending promise, saves it and adds a clean up callback which + * forgets the pending promise after it is fulfilled and echoes the promise + * back. If in the process, a user invalidation error is detected, caches the + * error so next time a call is made on the user, the operation will fail with + * the cached error. + * @param {!goog.Promise<*, *>|!goog.Promise} p The pending promise. + * @param {boolean=} opt_skipInvalidationCheck Whether to skip invalidation + * check. + * @return {!goog.Promise<*, *>|!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.registerPendingPromise_ = + function(p, opt_skipInvalidationCheck) { + var self = this; + // Check if user invalidation occurs. + var processedP = this.checkIfInvalidated_(p, opt_skipInvalidationCheck); + // Save created promise in pending list. + this.pendingPromises_.push(processedP); + processedP.thenAlways(function() { + // When fulfilled, remove from pending list. + goog.array.remove(self.pendingPromises_, processedP); + }); + // Return the promise. + return processedP; +}; + + +/** + * Check if user invalidation occurs. If so, it caches the error so it can be + * thrown immediately the next time an operation is run on the user. + * @param {!goog.Promise<*, *>|!goog.Promise} p The pending promise. + * @param {boolean=} opt_skipInvalidationCheck Whether to skip invalidation + * check. + * @return {!goog.Promise<*, *>|!goog.Promise} + * @private + */ +fireauth.AuthUser.prototype.checkIfInvalidated_ = + function(p, opt_skipInvalidationCheck) { + var self = this; + // Already invalidated, reject with token expired error. + // Unless invalidation check is to be skipped. + if (this.userInvalidatedError_ && !opt_skipInvalidationCheck) { + // Cancel pending promise. + p.cancel(); + // Reject with cached error. + return goog.Promise.reject(this.userInvalidatedError_); + } + return p.thenCatch(function(error) { + // Session invalidated. + if (fireauth.AuthUser.isUserInvalidated_(error)) { + // Notify listeners of invalidated session. + if (!self.userInvalidatedError_) { + self.notifyUserInvalidatedListeners_(); + } + // Cache the invalidation error. + self.userInvalidatedError_ = /** @type {!fireauth.AuthError} */ (error); + } + // Rethrow the error. + throw error; + }); +}; + + +/** + * @return {!Object} The object representation of the user instance. + * @override + */ +fireauth.AuthUser.prototype.toJSON = function() { + // Return the plain object representation in case JSON.stringify is called on + // a user instance. + return this.toPlainObject(); +}; + + +/** + * @return {!Object} The object representation of the user instance. + */ +fireauth.AuthUser.prototype.toPlainObject = function() { + var obj = { + 'uid': this['uid'], + 'displayName': this['displayName'], + 'photoURL': this['photoURL'], + 'email': this['email'], + 'emailVerified': this['emailVerified'], + 'phoneNumber': this['phoneNumber'], + 'isAnonymous': this['isAnonymous'], + 'providerData': [], + 'apiKey': this.apiKey_, + 'appName': this.appName_, + 'authDomain': this.authDomain_, + 'stsTokenManager': this.stsTokenManager_.toPlainObject(), + // Redirect event ID must be maintained in case there is a pending redirect + // event. + 'redirectEventId': this.getRedirectEventId() + }; + // Extend user plain object with metadata object. + if (this['metadata']) { + goog.object.extend(obj, this['metadata'].toPlainObject()); + } + goog.array.forEach(this['providerData'], function(userInfo) { + obj['providerData'].push(fireauth.object.makeWritableCopy(userInfo)); + }); + return obj; +}; + + +/** + * Converts a plain user object to {@code fireauth.AuthUser}. + * @param {!Object} user The object representation of the user instance. + * @return {?fireauth.AuthUser} The Firebase user object corresponding to + * object. + */ +fireauth.AuthUser.fromPlainObject = function(user) { + if (!user['apiKey']) { + return null; + } + var options = { + 'apiKey': user['apiKey'], + 'authDomain': user['authDomain'], + 'appName': user['appName'] + }; + // Convert to server response format. Constructor does not take + // stsTokenManager toPlainObject as that format is different than the return + // server response which is always used to initialize a user instance. It is + // also difficult to have toPlainObject equal server response due to expiresIn + // field in server response. toPlainObject will return an expiration time + // instead. + var stsTokenManagerResponse = {}; + if (user['stsTokenManager'] && + user['stsTokenManager']['accessToken'] && + user['stsTokenManager']['expirationTime']) { + stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.ID_TOKEN] = + user['stsTokenManager']['accessToken']; + // Refresh token could be expired. + stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN] = + user['stsTokenManager']['refreshToken'] || null; + stsTokenManagerResponse[fireauth.RpcHandler.AuthServerField.EXPIRES_IN] = + (user['stsTokenManager']['expirationTime'] - goog.now()) / 1000; + } else { + // Token response is a required field. + return null; + } + var firebaseUser = new fireauth.AuthUser(options, + stsTokenManagerResponse, + /** @type {!fireauth.AuthUser.AccountInfo} */ (user)); + if (user['providerData']) { + goog.array.forEach(user['providerData'], function(userInfo) { + if (userInfo) { + firebaseUser.addProviderData(/** @type {!fireauth.AuthUserInfo} */ ( + fireauth.object.makeReadonlyCopy(userInfo))); + } + }); + } + // Redirect event ID must be restored to complete any pending link with + // redirect operation owned by this user. + if (user['redirectEventId']) { + firebaseUser.setRedirectEventId(user['redirectEventId']); + } + return firebaseUser; +}; + + + +/** + * Factory method for initializing a Firebase user object and populating its + * user info. This is the recommended way for initializing a user externally. + * On sign in/up operation, the server returns a token response. The response is + * all that is needed to initialize this user. + * @param {!Object} appOptions The application options. + * @param {!Object} stsTokenResponse The server STS token response. + * @param {?fireauth.storage.RedirectUserManager=} + * opt_redirectStorageManager The utility used to store and delete a user on + * link with redirect. + * @param {?Array=} opt_frameworks The list of frameworks to log on the + * user on initialization. + * @return {!goog.Promise} + */ +fireauth.AuthUser.initializeFromIdTokenResponse = function(appOptions, + stsTokenResponse, opt_redirectStorageManager, opt_frameworks) { + // Initialize the Firebase Auth user. + var user = new fireauth.AuthUser( + appOptions, stsTokenResponse); + // If redirect storage manager provided, set it. + if (opt_redirectStorageManager) { + user.setRedirectStorageManager(opt_redirectStorageManager); + } + // If frameworks provided, set it. + if (opt_frameworks) { + user.setFramework(opt_frameworks); + } + // Updates the user info and data and resolves with a user instance. + return user.reload().then(function() { + return user; + }); +}; diff --git a/packages/auth/src/cacherequest.js b/packages/auth/src/cacherequest.js new file mode 100644 index 00000000000..f568cdeb0b4 --- /dev/null +++ b/packages/auth/src/cacherequest.js @@ -0,0 +1,111 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility for caching requests that return promises, typically + * used for requests that are sent to the server. + */ + +goog.provide('fireauth.CacheRequest'); + +goog.require('goog.Promise'); + + +/** + * This utility caches a function call that returns a promise along with its + * arguments. It gives the user the option to specify the cache duration and + * whether to cach errors as well as the ability to purge cache with its + * contents if needed. + * @constructor @struct + */ +fireauth.CacheRequest = function() { + /** @private {?function(*):!goog.Promise} The function to cache. */ + this.func_ = null; + /** @private {*} The context (this) of the function to cache. */ + this.self_ = null; + /** @private {Array} The array of arguments to run the function with. */ + this.arguments_ = []; + /** @private {?goog.Promise} The cached returned promise result. */ + this.cachedResult_ = null; + /** @private {number} The expiration timestamp of the cached result. */ + this.expirationTime_ = goog.now(); + /** @private {number} The time to live from the caching point in time. */ + this.ttl_ = 0; + /** @private {boolean} Whether to cache errors too. */ + this.cacheErrors_ = false; +}; + + +/** + * @param {function(*):!goog.Promise} func The function to cache. + * @param {*} self The context (this) of the function to cache. + * @param {Array} args The array of arguments to run the function with. + * @param {number} ttl The time to live for any cached results in milliseconds. + * @param {boolean=} opt_cacheErrors Whether to cache errors too. + */ +fireauth.CacheRequest.prototype.cache = + function(func, self, args, ttl, opt_cacheErrors) { + this.func_ = func; + this.self_ = self; + this.arguments_ = args; + this.expirationTime_ = goog.now(); + this.ttl_ = ttl; + this.cacheErrors_ = !!opt_cacheErrors; + +}; + + +/** + * @return {!goog.Promise} The promise that resolves when the function is run + * or the previously cached promise. + */ +fireauth.CacheRequest.prototype.run = function() { + var self = this; + if (!this.func_) { + throw new Error('No available configuration cached!'); + } + // If the result is not cached or the cache result is outdated. + if (!this.cachedResult_ || goog.now() >= this.expirationTime_) { + // Set expiration of current request. + this.expirationTime_ = goog.now() + this.ttl_; + // Get new result and cache it. + this.cachedResult_ = + this.func_.apply(this.self_, this.arguments_).then(function(result) { + // When successful resolution, just return the result which is to be + // cached. + return result; + }).thenCatch(function(error) { + // When an error is thrown. + if (!self.cacheErrors_) { + // Do not cache errors if errors are not to be cached. + // This will bust the cached result. Otherwise the error is cached. + self.expirationTime_ = goog.now(); + } + // Throw the returned error. + throw error; + }); + } + // Return the cached result. + return this.cachedResult_; +}; + + +/** Purges any cached results. */ +fireauth.CacheRequest.prototype.purge = function() { + // Purge the cached results. + this.cachedResult_ = null; + this.expirationTime_ = goog.now(); +}; diff --git a/packages/auth/src/confirmationresult.js b/packages/auth/src/confirmationresult.js new file mode 100644 index 00000000000..12936a156c4 --- /dev/null +++ b/packages/auth/src/confirmationresult.js @@ -0,0 +1,100 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the firebase.auth.ConfirmationResult. This is needed + * to provide first class support for phone Auth API: signInWithPhoneNumber, + * linkWithPhoneNumber and reauthenticateWithPhoneNumber. + */ + +goog.provide('fireauth.ConfirmationResult'); + +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.object'); +goog.require('goog.Promise'); + + +/** + * The confirmation result class. This takes in the verification ID returned + * from the phone Auth provider and the credential resolver to run when + * confirming with a verification code. + * @param {string} verificationId The verification ID returned from the Phone + * Auth provider after sending the verification code. + * @param {!function(!fireauth.AuthCredential): + * !goog.Promise} credentialResolver a + * function that takes in an AuthCredential and returns a promise that + * resolves with a UserCredential object. + * @constructor + */ +fireauth.ConfirmationResult = function(verificationId, credentialResolver) { + /** + * @const @private {!function(!fireauth.AuthCredential): + * !goog.Promise} A function that takes + * in an AuthCredential and returns a promise that resolves with a + * UserCredential object. + */ + this.credentialResolver_ = credentialResolver; + // Set verificationId as read-only property. + fireauth.object.setReadonlyProperty(this, 'verificationId', verificationId); +}; + + +/** + * Confirms the verification code and returns a promise that resolves with the + * User Credential object. + * @param {string} verificationCode The phone Auth verification code to use to + * complete the Auth flow. + * @return {!goog.Promise} + */ +fireauth.ConfirmationResult.prototype.confirm = function(verificationCode) { + // Initialize a phone Auth credential with the verification ID and code. + var credential = fireauth.PhoneAuthProvider.credential( + this['verificationId'], verificationCode); + // Run the credential resolver with the phone Auth credential and return its + // result. + return this.credentialResolver_(credential); +}; + + +/** + * Initializes a ConfirmationResult using the provided phone number, app + * verifier and returns it asynchronously. On code confirmation, the result will + * resolve using the credential resolver provided. + * @param {!fireauth.Auth} auth The corresponding Auth instance. + * @param {string} phoneNumber The phone number to authenticate with. + * @param {!firebase.auth.ApplicationVerifier} appVerifier The application + * verifier. + * @param {!function(!fireauth.AuthCredential): + * !goog.Promise} credentialResolver a + * function that takes in an AuthCredential and returns a promise that + * resolves with a UserCredential object. + * @return {!goog.Promise} + */ +fireauth.ConfirmationResult.initialize = + function(auth, phoneNumber, appVerifier, credentialResolver) { + // Initialize a phone Auth provider instance using the provided Auth + // instance. + var phoneAuthProvider = new fireauth.PhoneAuthProvider(auth); + // Verify the phone number. + return phoneAuthProvider.verifyPhoneNumber(phoneNumber, appVerifier) + .then(function(verificationId) { + // When code is sent and verification ID is returned, initialize a + // ConfirmationResult with the returned verification ID and credential + // resolver, and return that instance. + return new fireauth.ConfirmationResult( + verificationId, credentialResolver); + }); +}; diff --git a/packages/auth/src/cordovahandler.js b/packages/auth/src/cordovahandler.js new file mode 100644 index 00000000000..31ba90c1e30 --- /dev/null +++ b/packages/auth/src/cordovahandler.js @@ -0,0 +1,853 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines Cordova utility and helper functions. + * The following plugins must be installed: + * cordova plugin add cordova-plugin-buildinfo + * cordova plugin add cordova-universal-links-plugin + * cordova plugin add cordova-plugin-browsertab + * cordova plugin add cordova-plugin-inappbrowser + * iOS custom scheme support: + * cordova plugin add cordova-plugin-customurlscheme --variable \ + * URL_SCHEME=com.firebase.example + * Console logging in iOS: + * cordova plugin add cordova-plugin-console + */ + +goog.provide('fireauth.CordovaHandler'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.DynamicLink'); +goog.require('fireauth.OAuthSignInHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.storage.AuthEventManager'); +goog.require('fireauth.storage.OAuthHandlerManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.Uri'); +goog.require('goog.array'); +goog.require('goog.crypt'); +goog.require('goog.crypt.Sha256'); + + +/** + * Cordova environment utility and helper functions. + * @param {string} authDomain The application authDomain. + * @param {string} apiKey The API key. + * @param {string} appName The App name. + * @param {?string=} opt_clientVersion The optional client version string. + * @param {number=} opt_initialTimeout Initial Auth event timeout. + * @param {number=} opt_redirectTimeout Redirect result timeout. + * @param {?string=} opt_endpointId The endpoint ID (staging, test Gaia, etc). + * @constructor + * @implements {fireauth.OAuthSignInHandler} + */ +fireauth.CordovaHandler = function(authDomain, apiKey, appName, + opt_clientVersion, opt_initialTimeout, opt_redirectTimeout, + opt_endpointId) { + /** @private {string} The application authDomain. */ + this.authDomain_ = authDomain; + /** @private {string} The application API key. */ + this.apiKey_ = apiKey; + /** @private {string} The application name. */ + this.appName_ = appName; + /** @private {?string} The client version */ + this.clientVersion_ = opt_clientVersion || null; + /** @private {?string} The Auth endpoint ID. */ + this.endpointId_ = opt_endpointId || null; + /** @private {string} The storage key. */ + this.storageKey_ = fireauth.util.createStorageKey(apiKey, appName); + /** + * @private {!fireauth.storage.OAuthHandlerManager} The OAuth handler + * storage manager reference, used to save a partial Auth event when + * redirect operation is triggered. + */ + this.savePartialEventManager_ = new fireauth.storage.OAuthHandlerManager(); + /** + * @private {!fireauth.storage.AuthEventManager} The Auth event storage + * manager reference. This is used to get back the saved partial Auth + * event and then delete on successful handling. + */ + this.getAndDeletePartialEventManager_ = + new fireauth.storage.AuthEventManager(this.storageKey_); + /** + * @private {?goog.Promise} A promise that resolves with + * the OAuth redirect URL response. + */ + this.initialAuthEvent_ = null; + /** + * @private {!Array} The Auth event + * listeners. + */ + this.authEventListeners_ = []; + /** @private {number} The initial Auth event timeout. */ + this.initialTimeout_ = opt_initialTimeout || + fireauth.CordovaHandler.INITIAL_TIMEOUT_MS_; + /** @private {number} The return to app after redirect timeout. */ + this.redirectTimeout_ = opt_redirectTimeout || + fireauth.CordovaHandler.REDIRECT_TIMEOUT_MS_; + /** + * @private {?goog.Promise} The last pending redirect promise. This is null if + * already completed. + */ + this.pendingRedirect_ = null; + /** + * @private {?Object} The inAppBrowser reference window if available. This is + * relevant to iOS 7 and 8 embedded webviews. + */ + this.inAppBrowserRef_ = null; +}; + + +/** + * The total number of chars used to generate the session ID string. + * @const {number} + * @private + */ +fireauth.CordovaHandler.SESSION_ID_TOTAL_CHARS_ = 20; + + +/** + * The default initial Auth event timeout in ms. + * @const {number} + * @private + */ +fireauth.CordovaHandler.INITIAL_TIMEOUT_MS_ = 500; + + +/** + * The default timeout in milliseconds for a pending redirect operation after + * returning to the app. + * @const {number} + * @private + */ +fireauth.CordovaHandler.REDIRECT_TIMEOUT_MS_ = 2000; + + +/** + * Constructs a Cordova configuration error message. + * @param {?string=} opt_message The optional error message to be used. This + * will override the existing default one. + * @return {!fireauth.AuthError} The Cordova invalid configuration error with + * the custom message provided. If no message is provided, the default + * message is used. + * @private + */ +fireauth.CordovaHandler.getError_ = function(opt_message) { + return new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION, + opt_message); +}; + + +/** + * Initializes the Cordova environment and waits for it to be ready. + * @return {!goog.Promise} A promise that resolves if the current environment is + * a Cordova environment. + * @override + */ +fireauth.CordovaHandler.prototype.initializeAndWait = function() { + if (this.isReady_) { + return this.isReady_; + } + this.isReady_ = fireauth.util.checkIfCordova().then(function() { + // Check all dependencies installed. + // https://github.com/nordnet/cordova-universal-links-plugin + var subscribe = fireauth.util.getObjectRef( + 'universalLinks.subscribe', goog.global); + if (typeof subscribe !== 'function') { + throw fireauth.CordovaHandler.getError_( + 'cordova-universal-links-plugin is not installed'); + } + // https://www.npmjs.com/package/cordova-plugin-buildinfo + var appIdentifier = + fireauth.util.getObjectRef('BuildInfo.packageName', goog.global); + if (typeof appIdentifier === 'undefined') { + throw fireauth.CordovaHandler.getError_( + 'cordova-plugin-buildinfo is not installed'); + } + // https://github.com/google/cordova-plugin-browsertab + var openUrl = fireauth.util.getObjectRef( + 'cordova.plugins.browsertab.openUrl', goog.global); + if (typeof openUrl !== 'function') { + throw fireauth.CordovaHandler.getError_( + 'cordova-plugin-browsertab is not installed'); + } + // https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/ + var openInAppBrowser = fireauth.util.getObjectRef( + 'cordova.InAppBrowser.open', goog.global); + if (typeof openInAppBrowser !== 'function') { + throw fireauth.CordovaHandler.getError_( + 'cordova-plugin-inappbrowser is not installed'); + } + }, function(error) { + // If not supported. + throw new fireauth.AuthError(fireauth.authenum.Error.CORDOVA_NOT_READY); + }); + return this.isReady_; +}; + + +/** + * Generates a session ID. Used to prevent session fixation attacks. + * @param {number} numOfChars The number of characters to generate. + * @return {string} The generated session ID. + * @private + */ +fireauth.CordovaHandler.prototype.generateSessionId_ = function(numOfChars) { + var chars = []; + var allowedChars = + '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + while (numOfChars > 0) { + var index = Math.floor(Math.random() * allowedChars.length); + chars.push(allowedChars.charAt(index)); + numOfChars--; + } + return chars.join(''); +}; + + +/** + * Computes the sha256 hash of a session ID. + * @param {string} str The string to hash. + * @return {string} The hashed string. + * @private + */ +fireauth.CordovaHandler.prototype.computeSecureHash_ = function(str) { + // sha256 the sessionId. This will be passed to the OAuth backend. + // When exchanging the Auth code with a firebase ID token, the raw session ID + // needs to be provided. + var sha256 = new goog.crypt.Sha256(); + sha256.update(str); + return goog.crypt.byteArrayToHex(sha256.digest()); +}; + + +/** + * Waits for popup window to close and time out if the result is unhandled. + * This is not supported in Cordova. + * @param {!Window} popupWin The popup window. + * @param {!function(!fireauth.AuthError)} onError The on error callback. + * @return {!goog.Promise} + * @override + */ +fireauth.CordovaHandler.prototype.startPopupTimeout = + function(popupWin, onError, timeoutDuration) { + // Not supported operation, check processPopup for details. + onError(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + return goog.Promise.resolve(); +}; + + +/** + * Processes the popup request. This is not supported in Cordova. + * @param {?Window} popupWin The popup window reference. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {!function()} onInitialize The function to call on initialization. + * @param {!function(*)} onError The function to call on error. + * @param {string=} opt_eventId The optional event ID. + * @param {boolean=} opt_alreadyRedirected Whether popup is already redirected + * to final destination. + * @return {!goog.Promise} The popup window promise. + * @override + */ +fireauth.CordovaHandler.prototype.processPopup = function( + popupWin, + mode, + provider, + onInitialize, + onError, + opt_eventId, + opt_alreadyRedirected) { + // Popups not supported in Cordova as the activity could be destroyed in + // some cases. Redirect works better as getRedirectResult can be used as a + // fallback to get the result when the activity is detroyed. + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); +}; + + +/** + * @return {boolean} Whether the handler will unload the current page on + * redirect operations. + * @override + */ +fireauth.CordovaHandler.prototype.unloadsOnRedirect = function() { + // Does not necessarily unload the page on redirect. + return false; +}; + + +/** + * @return {boolean} Whether the handler should be initialized early. + * @override + */ +fireauth.CordovaHandler.prototype.shouldBeInitializedEarly = function() { + // Initialize early to detect incoming link. This is not an expensive + // operation, unlike embedding an iframe. + return true; +}; + + +/** + * @return {boolean} Whether the sign-in handler in the current environment + * has volatile session storage. + * @override + */ +fireauth.CordovaHandler.prototype.hasVolatileStorage = function() { + // An activity can be destroyed and thereby sessionStorage wiped out. + return true; +}; + + +/** + * Processes the OAuth redirect request. Will resolve when the OAuth response + * is detected in the incoming link and the corresponding Auth event is + * triggered. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {?string=} opt_eventId The optional event ID. + * @return {!goog.Promise} + * @override + */ +fireauth.CordovaHandler.prototype.processRedirect = function( + mode, + provider, + opt_eventId) { + // If there is already a pending redirect, throw an error. + if (this.pendingRedirect_) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING)); + } + var self = this; + var doc = goog.global.document; + // On close timer promise. + var onClose = null; + // Auth event detection callback; + var authEventCallback = null; + // On resume (return from the redirect operation). + var onResume = null; + // On visibility change used to detect return to app in certain versions, + // currently iOS. + var onVisibilityChange = null; + // When the processRedirect promise completes, clean up any remaining + // temporary listeners and timers. + var cleanup = function() { + // Remove current resume listener. + if (onResume) { + doc.removeEventListener('resume', onResume, false); + } + // Remove visibility change listener. + if (onVisibilityChange) { + doc.removeEventListener('visibilitychange', onVisibilityChange, false); + } + // Cancel onClose promise if not already cancelled. + if (onClose) { + onClose.cancel(); + } + // Remove Auth event callback. + if (authEventCallback) { + self.removeAuthEventListener(authEventCallback); + } + // Clear any pending redirect now that it is completed. + self.pendingRedirect_ = null; + }; + // Save the pending redirect promise and clear it on completion. + this.pendingRedirect_ = goog.Promise.resolve().then(function() { + // Validate provider. + // Fail fast in this case. + fireauth.AuthProvider.checkIfOAuthSupported(provider); + return self.getInitialAuthEvent_(); + }).then(function() { + return self.processRedirectInternal_(mode, provider, opt_eventId); + }).then(function() { + // Wait for result (universal link) before resolving this operation. + // This ensures that if the activity is not destroyed, we can still + // return the result of this operation. + return new goog.Promise(function(resolve, reject) { + /** + * @param {?fireauth.AuthEvent} event The Auth event detected. + * @return {boolean} + */ + authEventCallback = function(event) { + // Auth event detected, resolve promise. + // Close SFSVC if still open. + var closeBrowsertab = fireauth.util.getObjectRef( + 'cordova.plugins.browsertab.close', goog.global); + resolve(); + // Close the SFSVC if it is still open (iOS 9+). + if (typeof closeBrowsertab === 'function') { + closeBrowsertab(); + } + // Close inappbrowser emebedded webview in iOS7 and 8 case if still + // open. + if (self.inAppBrowserRef_ && + typeof self.inAppBrowserRef_['close'] === 'function') { + self.inAppBrowserRef_['close'](); + // Reset reference. + self.inAppBrowserRef_ = null; + } + return false; + }; + // Wait and listen for the operation to complete (Auth event would + // trigger). + self.addAuthEventListener(authEventCallback); + // On resume (return from the redirect operation). + onResume = function() { + // Already resumed. Do not run again. + if (onClose) { + return; + } + // Wait for some time before throwing the error that the flow was + // cancelled by the user. + onClose = goog.Timer.promise(self.redirectTimeout_).then(function() { + // Throw the redirect cancelled by user error. + reject(new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_CANCELLED_BY_USER)); + }); + }; + onVisibilityChange = function() { + // If app is visible, run onResume. Otherwise, ignore. + if (fireauth.util.isAppVisible()) { + onResume(); + } + }; + // Listen to resume event (will trigger when the user returns to the app). + doc.addEventListener('resume', onResume, false); + // Listen to visibility change. This is used for iOS Cordova Safari 7+. + // Does not work in Android stock browser versions older than 4.4. + // We rely on resume event in Android as it works reliably in all + // versions. + if (!fireauth.util.isAndroid()) { + doc.addEventListener('visibilitychange', onVisibilityChange, false); + } + }).thenCatch(function(error) { + // Remove any pending partial event. + return self.getPartialStoredEvent_().then(function() { + throw error; + }); + }); + }).thenAlways(cleanup); + // Return the pending redirect promise. + return this.pendingRedirect_; +}; + +/** + * Processes the OAuth redirect request. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {?string=} opt_eventId The optional event ID. + * @return {!goog.Promise} + * @private + */ +fireauth.CordovaHandler.prototype.processRedirectInternal_ = function( + mode, + provider, + opt_eventId) { + var self = this; + // https://github.com/google/cordova-plugin-browsertab + // Opens chrome custom tab in Android if chrome is installed, + // SFSafariViewController in iOS if supported. + // If the above are not supported, opens the system browser. + // Opening a system browser could result in an app being rejected in the App + // Store. The only solution here is to use an insecure embedded UIWebView. + // This applies to older iOS versions 8 and under. + // Generate a random session ID. + var sessionId = this.generateSessionId_( + fireauth.CordovaHandler.SESSION_ID_TOTAL_CHARS_); + // Create the partial Auth event. + var event = new fireauth.AuthEvent( + mode, + opt_eventId, + null, + sessionId, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Use buildinfo package to get app metadata. + // https://www.npmjs.com/package/cordova-plugin-buildinfo + // Get app identifier. + var appIdentifier = + fireauth.util.getObjectRef('BuildInfo.packageName', goog.global); + // initializeAndWait will ensure this does not happen. + if (typeof appIdentifier !== 'string') { + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION); + } + // Get app display name. + var appDisplayName = + fireauth.util.getObjectRef('BuildInfo.displayName', goog.global); + // Construct additional params to pass to OAuth handler. + var additionalParams = {}; + // Append app identifier. + if (fireauth.util.isIOS()) { + // iOS app. + additionalParams['ibi'] = appIdentifier; + } else if (fireauth.util.isAndroid()) { + // Android app. + additionalParams['apn'] = appIdentifier; + } else { + // This should not happen as Cordova handler should not even be used in this + // case. + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED)); + } + // Pass app display name. + if (appDisplayName) { + additionalParams['appDisplayName'] = appDisplayName; + } + // Hash the session ID and pass it to additional params. + var hashedSessionId = this.computeSecureHash_(sessionId); + // Append session ID. + additionalParams['sessionId'] = hashedSessionId; + // Construct OAuth handler URL. + var oauthHelperWidgetUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + this.authDomain_, + this.apiKey_, + this.appName_, + mode, + provider, + null, + opt_eventId, + this.clientVersion_, + additionalParams, + this.endpointId_); + // Make sure handler initialized and ready. + // This should also ensure all plugins are installed. + return this.initializeAndWait().then(function() { + // Save partial Auth event. + return self.savePartialEventManager_.setAuthEvent(self.storageKey_, event); + }).then(function() { + // initializeAndWait will ensure this plugin is installed. + var isAvailable = /** @type {!function(!function(*))} */ ( + fireauth.util.getObjectRef( + 'cordova.plugins.browsertab.isAvailable', goog.global)); + if (typeof isAvailable !== 'function') { + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION); + } + var openUrl = null; + // Check if browsertab is supported. + isAvailable(function(result) { + if (result) { + // browsertab supported. + openUrl = /** @type {!function(string, ...*)} */ ( + fireauth.util.getObjectRef( + 'cordova.plugins.browsertab.openUrl', goog.global)); + if (typeof openUrl !== 'function') { + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION); + } + // Open OAuth handler. + openUrl(oauthHelperWidgetUrl); + } else { + // browsertab not supported, switch to inappbrowser. + openUrl = /** @type {!function(string, string, string=)} */ ( + fireauth.util.getObjectRef( + 'cordova.InAppBrowser.open', goog.global)); + if (typeof openUrl !== 'function') { + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION); + } + // Open in embedded webview for iOS 7 and 8 as Apple rejects apps that + // switch context. + // _blank opens an embedded webview. + // _system opens the system browser. + // _system (opens a system browser) is used as a fallback when + // browsertab plugin is unable to open a chromecustomtab or SFSVC. + // This has to exclude all iOS older versions where switching to a + // browser is frowned upon by Apple and embedding a UIWebView is the + // only option but is insecure and deprecated by Google for OAuth + // sign-in. This will be applicable in old versions of Android. + self.inAppBrowserRef_ = openUrl( + oauthHelperWidgetUrl, + fireauth.util.isIOS7Or8() ? '_blank' : '_system', + 'location=yes'); + } + }); + }); +}; + + +/** + * Dispatches the detected Auth event to all subscribed listeners. + * @param {!fireauth.AuthEvent} event A detected Auth event. + * @private + */ +fireauth.CordovaHandler.prototype.dispatchEvent_ = function(event) { + for (var i = 0; i < this.authEventListeners_.length; i++) { + try { + this.authEventListeners_[i](event); + } catch (e) { + // If any handler fails, ignore and run next handler. + } + } +}; + + +/** + * Resolves the first redirect Auth event and caches it. + * @return {!goog.Promise} A promise that resolves with the + * initial Auth event response from a redirect operation. Initializes the + * internal Auth event listener which will dispatch Auth events to all + * subscribed listeners. + * @private + */ +fireauth.CordovaHandler.prototype.getInitialAuthEvent_ = function() { + var self = this; + if (!this.initialAuthEvent_) { + // Cache this result so on next call, it is not triggered again. + this.initialAuthEvent_ = this.initializeAndWait().then(function() { + return new goog.Promise(function(resolve, reject) { + /** + * @param {?fireauth.AuthEvent} event The Auth event detected. + * @return {boolean} + */ + var authEventCallback = function(event) { + resolve(event); + // Remove on completion. + self.removeAuthEventListener(authEventCallback); + return false; + }; + // Listen to Auth events. If resolved, resolve promise. + self.addAuthEventListener(authEventCallback); + // This should succeed as initializeAndWait should guarantee plugins are + // ready. + self.setAuthEventListener_(); + }); + }); + } + return this.initialAuthEvent_; +}; + + +/** + * Gets and deletes the current stored partial event from storage. + * @return {!goog.Promise} A promise that resolves with the + * stored Auth event. + * @private + */ +fireauth.CordovaHandler.prototype.getPartialStoredEvent_ = function() { + var event = null; + var self = this; + // Get any saved partial Auth event. + return this.getAndDeletePartialEventManager_.getAuthEvent() + .then(function(authEvent) { + // Save partial event locally. + event = authEvent; + // Delete partial event. + return self.getAndDeletePartialEventManager_.removeAuthEvent(); + }).then(function() { + // Return the locally saved partial event. + return event; + }); +}; + + +/** + * Extracts the Auth event pertaining to the incoming URL. + * @param {!fireauth.AuthEvent} partialEvent The partial Auth event. + * @param {string} url The incoming universal link. + * @return {?fireauth.AuthEvent} The resolved Auth event corresponding to the + * callback URL. This is null if no event is found. + * @private + */ +fireauth.CordovaHandler.prototype.extractAuthEventFromUrl_ = + function(partialEvent, url) { + // Default no redirect event result. + var authEvent = null; + // Parse the deep link within the dynamic link URL. + var callbackUrl = fireauth.DynamicLink.parseDeepLink(url); + // Confirm it is actually a callback URL. + // Currently the universal link will be of this format: + // https:///__/auth/callback + // This is a fake URL but is not intended to take the user anywhere + // and just redirect to the app. + if (callbackUrl.indexOf('/__/auth/callback') != -1) { + // Check if there is an error in the URL. + // This mechanism is also used to pass errors back to the app: + // https:///__/auth/callback?firebaseError= + var uri = goog.Uri.parse(callbackUrl); + // Get the error object corresponding to the stringified error if found. + var errorObject = fireauth.util.parseJSON( + uri.getParameterValue('firebaseError') || null); + var error = typeof errorObject === 'object' ? + fireauth.AuthError.fromPlainObject( + /** @type {?Object} */ (errorObject)) : + null; + if (error) { + // Construct the full failed Auth event. + authEvent = new fireauth.AuthEvent( + partialEvent.getType(), + partialEvent.getEventId(), + null, + null, + error); + } else { + // Construct the full successful Auth event. + authEvent = new fireauth.AuthEvent( + partialEvent.getType(), + partialEvent.getEventId(), + callbackUrl, + partialEvent.getSessionId()); + } + } + return authEvent; +}; + + +/** + * Sets the internal Auth event listener. This listens to incoming universal + * links and on detection, repackages them into an Auth event and then + * dispatches the events in all event listeners. + * @private + */ +fireauth.CordovaHandler.prototype.setAuthEventListener_ = function() { + // https://github.com/nordnet/cordova-universal-links-plugin + var self = this; + // Get universal link subscriber. + var subscribe = fireauth.util.getObjectRef( + 'universalLinks.subscribe', goog.global); + // Should not occur as initializeAndWait will ensure that. + if (typeof subscribe !== 'function') { + // Universal link plugin not installed. + throw new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION); + } + // Universal link plugin installed. + // Default no redirect event result. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var initialResolve = false; + // On initialization, if no incoming universal link detected, trigger + // no Auth event (no redirect operation previously called) after waiting + // for a short period of time. + var noEventTimer = goog.Timer.promise(this.initialTimeout_).then(function() { + // Delete any pending unhandled event. + return self.getPartialStoredEvent_().then(function(event) { + // On timeout trigger noEvent if not already resolved in link + // subscriber. + if (!initialResolve) { + self.dispatchEvent_(noEvent); + } + }); + }); + // No event name needed, subscribe to all incoming universal links. + var universalLinkCb = function(eventData) { + initialResolve = true; + // Cancel no event timer. + if (noEventTimer) { + noEventTimer.cancel(); + } + // Incoming link detected. + // Check for any stored partial event. + self.getPartialStoredEvent_().then(function(event) { + // Initialize to an unknown event. + var authEvent = noEvent; + // Confirm OAuth response included. + if (event && eventData && eventData['url']) { + // Construct complete event. Default to unknown event if none found. + authEvent = self.extractAuthEventFromUrl_(event, eventData['url']) || + noEvent; + } + // Dispatch Auth event. + self.dispatchEvent_(authEvent); + }); + }; + // iOS 7 or 8 custom URL schemes. + // This is also the current default behavior for iOS 9+. + // For this to work, cordova-plugin-customurlscheme needs to be installed. + // https://github.com/EddyVerbruggen/Custom-URL-scheme + // Do not overwrite the existing developer's URL handler. + var existingHandlerOpenURL = goog.global['handleOpenURL']; + goog.global['handleOpenURL'] = function(url) { + var appIdentifier = + fireauth.util.getObjectRef('BuildInfo.packageName', goog.global); + // Apply case insensitive match. While bundle IDs are case sensitive, + // when creating a new app, Apple verifies the Bundle ID using + // case-insensitive search. So it is not possible that an app in the app + // store try to impersonate another one by lower/upper casing characters. + if (url.toLowerCase().indexOf(appIdentifier.toLowerCase() + '://') == 0) { + universalLinkCb({ + 'url': url + }); + } + // Call the developer's handler if it is present. + if (typeof existingHandlerOpenURL === 'function') { + try { + existingHandlerOpenURL(url); + } catch(e) { + // This doesn't swallow the error but also does not interrupt the flow. + console.error(e); + } + } + }; + subscribe(null, universalLinkCb); +}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to add. + * @override + */ +fireauth.CordovaHandler.prototype.addAuthEventListener = function(listener) { + // TODO: consider creating an abstract base class that OAuth handlers + // extend with add, remove Auth event listeners and dispatcher methods. + this.authEventListeners_.push(listener); + // Set internal listener to Auth events. This will be ignored on subsequent + // calls. + this.getInitialAuthEvent_().thenCatch(function(error) { + // Suppress this error as it should be caught through other actionable + // public methods. + // This would typically happen on invalid Cordova setup, when the OAuth + // plugins are not installed. This should still trigger the Auth event + // as developers are not forced to use OAuth sign-in in their Cordova app. + // This is needed for onAuthStateChanged listener to trigger initially. + if (error.code === 'auth/invalid-cordova-configuration') { + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + listener(noEvent); + } + }); +}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to remove. + * @override + */ +fireauth.CordovaHandler.prototype.removeAuthEventListener = function(listener) { + goog.array.removeAllIf(this.authEventListeners_, function(ele) { + return ele == listener; + }); +}; + diff --git a/packages/auth/src/debug.js b/packages/auth/src/debug.js new file mode 100644 index 00000000000..364e0dcd07c --- /dev/null +++ b/packages/auth/src/debug.js @@ -0,0 +1,28 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Redefines various constants for internal testing and debugging. + */ + +/** @suppress {extraRequire} */ +goog.require('fireauth.iframeclient.IfcHandler'); + + +/** @suppress {const|duplicate} */ +fireauth.iframeclient.SCHEME = 'http'; +/** @type {?number} @suppress {const|duplicate} */ +fireauth.iframeclient.PORT_NUMBER = 8080; diff --git a/packages/auth/src/defines.js b/packages/auth/src/defines.js new file mode 100644 index 00000000000..bd52adb282f --- /dev/null +++ b/packages/auth/src/defines.js @@ -0,0 +1,129 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines all common constants and enums used by firebase-auth. + */ + +goog.provide('fireauth.constants'); +goog.provide('fireauth.constants.AuthEventType'); + + +/** + * Enums for authentication operation types. + * @enum {string} + */ +fireauth.constants.OperationType = { + LINK: 'link', + REAUTHENTICATE: 'reauthenticate', + SIGN_IN: 'signIn' +}; + + +/** + * Events dispatched firebase.auth.Auth. + * @enum {string} + */ +fireauth.constants.AuthEventType = { + /** Dispatched when Firebase framework is changed. */ + FRAMEWORK_CHANGED: 'frameworkChanged', + /** Dispatched when language code is changed. */ + LANGUAGE_CODE_CHANGED: 'languageCodeChanged' +}; + + +/** + * The settings of an Auth endpoint. The fields are: + *
    + *
  • firebaseAuthEndpoint: defines the Firebase Auth backend endpoint for + * specified endpoint type.
  • + *
  • secureTokenEndpoint: defines the secure token backend endpoint for + * specified endpoint type.
  • + *
  • id: defines the endpoint identifier.
  • + *
+ * @typedef {{ + * firebaseAuthEndpoint: string, + * secureTokenEndpoint: string, + * id: string + * }} + */ +fireauth.constants.EndpointSettings; + + +/** + * The different endpoints for Firebase Auth backend. + * @enum {!fireauth.constants.EndpointSettings} + */ +fireauth.constants.Endpoint = { + PRODUCTION: { + firebaseAuthEndpoint: 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/', + secureTokenEndpoint: 'https://securetoken.googleapis.com/v1/token', + id: 'p' + }, + STAGING: { + firebaseAuthEndpoint: 'https://staging-www.sandbox.googleapis.com/' + + 'identitytoolkit/v3/relyingparty/', + secureTokenEndpoint: 'https://staging-securetoken.sandbox.googleapis.com' + + '/v1/token', + id: 's' + }, + TEST: { + firebaseAuthEndpoint: 'https://www-googleapis-test.sandbox.google.com/' + + 'identitytoolkit/v3/relyingparty/', + secureTokenEndpoint: 'https://test-securetoken.sandbox.googleapis.com/v1' + + '/token', + id: 't' + } +}; + + +/** + * Returns the endpoint specific RpcHandler configuration. + * @param {?string=} opt_id The identifier of the endpoint type if available. + * @return {?Object|undefined} The RpcHandler endpoint configuration object. + */ +fireauth.constants.getEndpointConfig = function(opt_id) { + for (var endpointKey in fireauth.constants.Endpoint) { + if (fireauth.constants.Endpoint[endpointKey].id === opt_id) { + var endpoint = fireauth.constants.Endpoint[endpointKey]; + return { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + } + } + return null; +}; + + +/** + * Returns the validated endpoint identifier. Undefined if the provided one is + * invalid. + * @param {?string=} opt_id The identifier of the endpoint type if available. + * @return {string|undefined} The validated endpoint ID. If not valid, + * undefined. + */ +fireauth.constants.getEndpointId = function(opt_id) { + if (opt_id && fireauth.constants.getEndpointConfig(opt_id)) { + return opt_id; + } + return undefined; +}; + + +/** @const {string|undefined} The current client endpoint. */ +fireauth.constants.clientEndpoint = fireauth.constants.getEndpointId('__EID__'); diff --git a/packages/auth/src/deprecation.js b/packages/auth/src/deprecation.js new file mode 100644 index 00000000000..ef386b9d3a0 --- /dev/null +++ b/packages/auth/src/deprecation.js @@ -0,0 +1,64 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provides utilities for displaying deprecation notices. + */ +goog.provide('fireauth.deprecation'); +goog.provide('fireauth.deprecation.Deprecations'); +goog.require('fireauth.util'); + + +/** + * An enum of valid notices to display. All deprecation notices must be in this + * enum. Deprecation messages should be unique and provide the full context + * of what is deprecated (e.g. the fully qualified path to a method). + * @enum {string} + */ +fireauth.deprecation.Deprecations = { + USER_GET_TOKEN: 'firebase.User.prototype.getToken is deprecated. Please use' + + ' firebase.User.prototype.getIdToken instead.' +}; + + +/** + * Keeps track of notices that were already displayed. + * @type {!Object} + * @private + */ +fireauth.deprecation.shownMessages_ = {}; + + +/** + * Logs a deprecation notice to the developer. + * @param {!fireauth.deprecation.Deprecations} message + */ +fireauth.deprecation.log = function(message) { + if (fireauth.deprecation.shownMessages_[message]) { + return; + } + fireauth.deprecation.shownMessages_[message] = true; + fireauth.util.consoleWarn(message); +}; + + +/** + * Resets the displayed deprecation notices. + */ +fireauth.deprecation.resetForTesting = function() { + fireauth.deprecation.shownMessages_ = + /** @type {!Object} */ ({}); +}; diff --git a/packages/auth/src/dynamiclink.js b/packages/auth/src/dynamiclink.js new file mode 100644 index 00000000000..25bb006fba3 --- /dev/null +++ b/packages/auth/src/dynamiclink.js @@ -0,0 +1,258 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the Firebase dynamic link constructor. + */ + +goog.provide('fireauth.DynamicLink'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); +goog.require('goog.Uri'); + + +/** + * Dynamic link builder used to help build the FDL link to redirect to an app + * while passing some payload or error. + * @param {?string} fdlDomain The FDL domain. If none is available, custom + * scheme redirects are used. + * @param {!fireauth.DynamicLink.Platform} platform The FDL supported + * platform (Android or iOS). + * @param {string} appIdentifier The app identifier (iOS bundle ID or Android + * package name). + * @param {string} authDomain The Firebase application authDomain. + * @param {string} payload The FDL deep link content. + * @param {?string=} opt_clientId The optional OAuth client ID. + * @constructor + */ +fireauth.DynamicLink = function(fdlDomain, platform, appIdentifier, authDomain, + payload, opt_clientId) { + // The fallback error when the app is not installed on the device. + var defaultError = + new fireauth.AuthError(fireauth.authenum.Error.APP_NOT_INSTALLED); + /** @private {string} The fallback URL when the app is not installed. */ + this.fallbackUrl_ = 'https://' + authDomain + '/__/auth/handler?' + + 'firebaseError=' + encodeURIComponent(/** @type {string} */ ( + fireauth.util.stringifyJSON(defaultError.toPlainObject()))); + fireauth.object.setReadonlyProperty(this, 'fallbackUrl', this.fallbackUrl_); + /** @private {?string} The FDL domain if available. */ + this.fdlDomain_ = fdlDomain; + fireauth.object.setReadonlyProperty(this, 'fdlDomain', fdlDomain); + /** @private {!fireauth.DynamicLink.Platform} The FDL link platform. */ + this.platform_ = platform; + fireauth.object.setReadonlyProperty(this, 'platform', platform); + /** @private {string} The app identifier. */ + this.appIdentifier_ = appIdentifier; + fireauth.object.setReadonlyProperty(this, 'appIdentifier', appIdentifier); + /** @private {string} The Firebase application authDomain. */ + this.authDomain_ = authDomain; + fireauth.object.setReadonlyProperty(this, 'authDomain', authDomain); + /** @private {string} The FDL deep link content. */ + this.link_ = payload; + fireauth.object.setReadonlyProperty(this, 'payload', payload); + /** @private {?string} The application display name. */ + this.appName_ = null; + fireauth.object.setReadonlyProperty(this, 'appName', null); + /** @private {?string} The client ID if available. */ + this.clientId_ = opt_clientId || null; + fireauth.object.setReadonlyProperty(this, 'clientId', this.clientId_); +}; + + +/** + * Sets the app name for the current dynamic link. + * @param {?string|undefined} appName The app name typically displayed in an FDL + * button. + */ +fireauth.DynamicLink.prototype.setAppName = function(appName) { + this.appName_ = appName || null; + fireauth.object.setReadonlyProperty(this, 'appName', appName); +}; + + +/** + * Sets the dynamic link fallback URL overriding the default one. + * @param {string} fallbackUrl The dynamic link fallback URL. + */ +fireauth.DynamicLink.prototype.setFallbackUrl = function(fallbackUrl) { + this.fallbackUrl_ = fallbackUrl; + fireauth.object.setReadonlyProperty(this, 'fallbackUrl', fallbackUrl); +}; + + +/** + * Parses a dynamic link object from an automatic FDL redirect link. + * @param {string} url The URL string to parse and convert to a dynamic link. + * @return {?fireauth.DynamicLink} The corresponding dynamic link if applicable. + */ +fireauth.DynamicLink.fromURL = function(url) { + // This constructs the Dynamic link from the URL provided. + var uri = goog.Uri.parse(url); + var fdlDomain = uri.getParameterValue('fdlDomain'); + var platform = uri.getParameterValue('platform'); + var appIdentifier = uri.getParameterValue('appIdentifier'); + var authDomain = uri.getParameterValue('authDomain'); + var payload = uri.getParameterValue('link'); + var appName = uri.getParameterValue('appName'); + if (fdlDomain && platform && appIdentifier && authDomain && payload && + appName) { + var dl = new fireauth.DynamicLink( + /** @type {string} */ (fdlDomain), + /** @type {!fireauth.DynamicLink.Platform} */ (platform), + /** @type {string} */ (appIdentifier), + /** @type {string} */ (authDomain), + /** @type {string} */ (payload)); + dl.setAppName(appName); + return dl; + } + return null; +}; + + +/** + * @param {string} url The dynamic link URL. + * @return {string} The deep link embedded within the dynamic link. + */ +fireauth.DynamicLink.parseDeepLink = function(url) { + var uri = goog.Uri.parse(url); + var link = uri.getParameterValue('link'); + // Double link case (automatic redirect). + var doubleDeepLink = goog.Uri.parse(link).getParameterValue('link'); + // iOS custom scheme links. + var iOSdeepLink = uri.getParameterValue('deep_link_id'); + var iOSDoubledeepLink = goog.Uri.parse(iOSdeepLink).getParameterValue('link'); + var callbackUrl = + iOSDoubledeepLink || iOSdeepLink || doubleDeepLink || link || url; + return callbackUrl; +}; + + +/** + * The supported FDL platforms. + * @enum {string} + */ +fireauth.DynamicLink.Platform = { + ANDROID: 'android', + IOS: 'ios' +}; + + +/** + * Constructs the common FDL link base used for building the button link or the + * automatic redirect link. + * @param {string} fallbackUrl The fallback URL to use. + * @return {!goog.Uri} The partial URI of the FDL link used to build the final + * button link or the automatic redirect link. + * @private + */ +fireauth.DynamicLink.prototype.constructFdlBase_ = function(fallbackUrl) { + var uri = goog.Uri.create( + 'https', + null, + this.fdlDomain_, + null, + '/'); + if (this.platform_ == fireauth.DynamicLink.Platform.ANDROID) { + uri.setParameterValue('apn', this.appIdentifier_); + uri.setParameterValue('afl', fallbackUrl); + } else if (this.platform_ == fireauth.DynamicLink.Platform.IOS) { + uri.setParameterValue('ibi', this.appIdentifier_); + uri.setParameterValue('ifl', fallbackUrl); + } + return uri; +}; + + +/** + * Constructs the custom scheme URL. This is used when no FDL domain is + * available. + * @return {!goog.Uri} The uri of the dynamic link used to build the final + * button link or the automatic redirect link. + * @private + */ +fireauth.DynamicLink.prototype.constructCustomSchemeUrl_ = function() { + // This mimics the FDL custom scheme URL format. + var uri = goog.Uri.create( + this.clientId_ ? this.clientId_.split('.').reverse().join('.') : + this.appIdentifier_, + null, + // 'firebaseauth' is used in the app verification flow. + // 'google' is used for the Cordova iOS flow. + this.clientId_ ? 'firebaseauth' : 'google', + null, + '/link'); + uri.setParameterValue('deep_link_id', this.link_); + return uri; +}; + + +/** + * @param {boolean=} opt_isAutoRedirect Whether the link is an auto redirect + * link. + * @return {string} The generated dynamic link string. + * @override + */ +fireauth.DynamicLink.prototype.toString = function(opt_isAutoRedirect) { + // When FDL domain is not available, always returns the custom scheme URL. + if (!this.fdlDomain_) { + return this.constructCustomSchemeUrl_().toString(); + } + if (!!opt_isAutoRedirect) { + return this.generateAutomaticRedirectLink_(); + } + return this.generateButtonLink_(); +}; + + +/** + * @return {string} The final FDL button link. + * @private + */ +fireauth.DynamicLink.prototype.generateButtonLink_ = function() { + var fdlLink = this.constructFdlBase_(this.fallbackUrl_); + fdlLink.setParameterValue('link', this.link_); + return fdlLink.toString(); +}; + + +/** + * @return {string} The final FDL automatic redirect link. + * @private + */ +fireauth.DynamicLink.prototype.generateAutomaticRedirectLink_ = + function() { + var doubleDeeplink = goog.Uri.create( + 'https', + null, + this.authDomain_, + null, + '/__/auth/callback'); + doubleDeeplink.setParameterValue('fdlDomain', this.fdlDomain_); + doubleDeeplink.setParameterValue('platform', this.platform_); + doubleDeeplink.setParameterValue('appIdentifier', this.appIdentifier_); + doubleDeeplink.setParameterValue('authDomain', this.authDomain_); + doubleDeeplink.setParameterValue('link', this.link_); + doubleDeeplink.setParameterValue('appName', this.appName_ || ''); + // The fallback URL is the deep link itself. + // This is in case the link fails to be intercepted by the app, FDL will + // redirect to the fallback URL. + var fdlLink = this.constructFdlBase_(doubleDeeplink.toString()); + fdlLink.setParameterValue('link', doubleDeeplink.toString()); + return fdlLink.toString(); +}; diff --git a/packages/auth/src/error_auth.js b/packages/auth/src/error_auth.js new file mode 100644 index 00000000000..0af4a05abf9 --- /dev/null +++ b/packages/auth/src/error_auth.js @@ -0,0 +1,391 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines developer-visible errors for Firebase Auth APIs. + */ + + +goog.provide('fireauth.AuthError'); +goog.provide('fireauth.authenum'); +goog.provide('fireauth.authenum.Error'); + + + +/** + * Error that can be returned to the developer. + * @param {!fireauth.authenum.Error} code The short error code. + * @param {?string=} opt_message The human-readable message. + * @constructor + * @extends {Error} + */ +fireauth.AuthError = function(code, opt_message) { + this['code'] = fireauth.AuthError.ERROR_CODE_PREFIX + code; + this.message = opt_message || fireauth.AuthError.MESSAGES_[code] || ''; +}; +goog.inherits(fireauth.AuthError, Error); + + +/** + * @return {!Object} The plain object form of the error. + */ +fireauth.AuthError.prototype.toPlainObject = function() { + return { + 'code': this['code'], + 'message': this.message + }; +}; + + +/** + * @return {!Object} The plain object form of the error. This is used by + * JSON.toStringify() to return the stringified representation of the error; + * @override + */ +fireauth.AuthError.prototype.toJSON = function() { + // Return the plain object representation in case JSON.stringify is called on + // an auth error instance. + return this.toPlainObject(); +}; + + +/** + * @param {?Object|undefined} response The object response to convert to a + * fireauth.AuthError. + * @return {?fireauth.AuthError} The error representation of the response. + */ +fireauth.AuthError.fromPlainObject = function(response) { + var fullCode = response && response['code']; + if (fullCode) { + // Remove prefix from name. + var code = fullCode.substring( + fireauth.AuthError.ERROR_CODE_PREFIX.length); + return new fireauth.AuthError( + /** @type {fireauth.authenum.Error} */ (code), response['message']); + } + return null; +}; + + +/** + * Takes in an error and translates a specific error code to another one if + * found in the current error. + * @param {*} error The error thrown. + * @param {!fireauth.authenum.Error} fromCode The error code to translate from. + * @param {!fireauth.authenum.Error} toCode The error code to translate to. + * @return {*} The mapped error message. + */ +fireauth.AuthError.translateError = function(error, fromCode, toCode) { + if (error && + error['code'] && + error['code'] == fireauth.AuthError.ERROR_CODE_PREFIX + fromCode) { + // Translate the error to the new one. + return new fireauth.AuthError(toCode); + } + // Return the same error if the fromCode is not found. + return error; +}; + + +/** + * The error prefix for fireauth.Auth errors. + * @protected {string} + */ +fireauth.AuthError.ERROR_CODE_PREFIX = 'auth/'; + + +/** + * Developer facing Firebase Auth error codes. + * @enum {string} + */ +fireauth.authenum.Error = { + ARGUMENT_ERROR: 'argument-error', + APP_NOT_AUTHORIZED: 'app-not-authorized', + APP_NOT_INSTALLED: 'app-not-installed', + CAPTCHA_CHECK_FAILED: 'captcha-check-failed', + CODE_EXPIRED: 'code-expired', + CORDOVA_NOT_READY: 'cordova-not-ready', + CORS_UNSUPPORTED: 'cors-unsupported', + CREDENTIAL_ALREADY_IN_USE: 'credential-already-in-use', + CREDENTIAL_MISMATCH: 'custom-token-mismatch', + CREDENTIAL_TOO_OLD_LOGIN_AGAIN: 'requires-recent-login', + DYNAMIC_LINK_NOT_ACTIVATED: 'dynamic-link-not-activated', + EMAIL_EXISTS: 'email-already-in-use', + EXPIRED_OOB_CODE: 'expired-action-code', + EXPIRED_POPUP_REQUEST: 'cancelled-popup-request', + INTERNAL_ERROR: 'internal-error', + INVALID_API_KEY: 'invalid-api-key', + INVALID_APP_CREDENTIAL: 'invalid-app-credential', + INVALID_APP_ID: 'invalid-app-id', + INVALID_AUTH: 'invalid-user-token', + INVALID_AUTH_EVENT: 'invalid-auth-event', + INVALID_CERT_HASH: 'invalid-cert-hash', + INVALID_CODE: 'invalid-verification-code', + INVALID_CONTINUE_URI: 'invalid-continue-uri', + INVALID_CORDOVA_CONFIGURATION: 'invalid-cordova-configuration', + INVALID_CUSTOM_TOKEN: 'invalid-custom-token', + INVALID_EMAIL: 'invalid-email', + INVALID_IDP_RESPONSE: 'invalid-credential', + INVALID_MESSAGE_PAYLOAD: 'invalid-message-payload', + INVALID_OAUTH_CLIENT_ID: 'invalid-oauth-client-id', + INVALID_OAUTH_PROVIDER: 'invalid-oauth-provider', + INVALID_OOB_CODE: 'invalid-action-code', + INVALID_ORIGIN: 'unauthorized-domain', + INVALID_PASSWORD: 'wrong-password', + INVALID_PERSISTENCE: 'invalid-persistence-type', + INVALID_PHONE_NUMBER: 'invalid-phone-number', + INVALID_RECIPIENT_EMAIL: 'invalid-recipient-email', + INVALID_SENDER: 'invalid-sender', + INVALID_SESSION_INFO: 'invalid-verification-id', + MISSING_ANDROID_PACKAGE_NAME: 'missing-android-pkg-name', + MISSING_APP_CREDENTIAL: 'missing-app-credential', + MISSING_AUTH_DOMAIN: 'auth-domain-config-required', + MISSING_CODE: 'missing-verification-code', + MISSING_CONTINUE_URI: 'missing-continue-uri', + MISSING_IFRAME_START: 'missing-iframe-start', + MISSING_IOS_BUNDLE_ID: 'missing-ios-bundle-id', + MISSING_PHONE_NUMBER: 'missing-phone-number', + MISSING_SESSION_INFO: 'missing-verification-id', + MODULE_DESTROYED: 'app-deleted', + NEED_CONFIRMATION: 'account-exists-with-different-credential', + NETWORK_REQUEST_FAILED: 'network-request-failed', + NO_AUTH_EVENT: 'no-auth-event', + NO_SUCH_PROVIDER: 'no-such-provider', + OPERATION_NOT_ALLOWED: 'operation-not-allowed', + OPERATION_NOT_SUPPORTED: 'operation-not-supported-in-this-environment', + POPUP_BLOCKED: 'popup-blocked', + POPUP_CLOSED_BY_USER: 'popup-closed-by-user', + PROVIDER_ALREADY_LINKED: 'provider-already-linked', + QUOTA_EXCEEDED: 'quota-exceeded', + REDIRECT_CANCELLED_BY_USER: 'redirect-cancelled-by-user', + REDIRECT_OPERATION_PENDING: 'redirect-operation-pending', + TIMEOUT: 'timeout', + TOKEN_EXPIRED: 'user-token-expired', + TOO_MANY_ATTEMPTS_TRY_LATER: 'too-many-requests', + UNAUTHORIZED_DOMAIN: 'unauthorized-continue-uri', + UNSUPPORTED_PERSISTENCE: 'unsupported-persistence-type', + USER_CANCELLED: 'user-cancelled', + USER_DELETED: 'user-not-found', + USER_DISABLED: 'user-disabled', + USER_MISMATCH: 'user-mismatch', + USER_SIGNED_OUT: 'user-signed-out', + WEAK_PASSWORD: 'weak-password', + WEB_STORAGE_UNSUPPORTED: 'web-storage-unsupported' +}; + + +/** + * Map from developer error codes to human-readable error messages. + * @private {!Object} + */ +fireauth.AuthError.MESSAGES_ = {}; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.ARGUMENT_ERROR] = ''; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.APP_NOT_AUTHORIZED] = + 'This app, identified by the domain where it\'s hosted, is not ' + + 'authorized to use Firebase Authentication with the provided API key. ' + + 'Review your key configuration in the Google API console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.APP_NOT_INSTALLED] = + 'The requested mobile application corresponding to the identifier (' + + 'Android package name or iOS bundle ID) provided is not installed on ' + + 'this device.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.CAPTCHA_CHECK_FAILED] = + 'The reCAPTCHA response token provided is either invalid, expired, ' + + 'already used or the domain associated with it does not match the list ' + + 'of whitelisted domains.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.CODE_EXPIRED] = + 'The SMS code has expired. Please re-send the verification code to try ' + + 'again.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.CORDOVA_NOT_READY] = + 'Cordova framework is not ready.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.CORS_UNSUPPORTED] = + 'This browser is not supported.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE] = + 'This credential is already associated with a different user account.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.CREDENTIAL_MISMATCH] = + 'The custom token corresponds to a different audience.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.CREDENTIAL_TOO_OLD_LOGIN_AGAIN] = + 'This operation is sensitive and requires recent authentication. Log in ' + + 'again before retrying this request.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.DYNAMIC_LINK_NOT_ACTIVATED] = 'Please activate ' + + 'Dynamic Links in the Firebase Console and agree to the terms and ' + + 'conditions.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.EMAIL_EXISTS] = + 'The email address is already in use by another account.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.EXPIRED_OOB_CODE] = + 'The action code has expired. '; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.EXPIRED_POPUP_REQUEST] = + 'This operation has been cancelled due to another conflicting popup ' + + 'being opened.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INTERNAL_ERROR] = + 'An internal error has occurred.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_APP_CREDENTIAL] = + 'The phone verification request contains an invalid application verifier.' + + ' The reCAPTCHA token response is either invalid or expired.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_APP_ID] = + 'The mobile app identifier is not registed for the current project.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_AUTH] = + 'The user\'s credential is no longer valid. The user must sign in again.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_AUTH_EVENT] = + 'An internal error has occurred.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_CODE] = + 'The SMS verification code used to create the phone auth credential is ' + + 'invalid. Please resend the verification code sms and be sure use the ' + + 'verification code provided by the user.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_CONTINUE_URI] = + 'The continue URL provided in the request is invalid.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION] = 'The following' + + ' Cordova plugins must be installed to enable OAuth sign-in: ' + + 'cordova-plugin-buildinfo, cordova-universal-links-plugin, ' + + 'cordova-plugin-browsertab, cordova-plugin-inappbrowser and ' + + 'cordova-plugin-customurlscheme.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_CUSTOM_TOKEN] = + 'The custom token format is incorrect. Please check the documentation.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_EMAIL] = + 'The email address is badly formatted.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_API_KEY] = + 'Your API key is invalid, please check you have copied it correctly.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_CERT_HASH] = + 'The SHA-1 certificate hash provided is invalid.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_IDP_RESPONSE] = + 'The supplied auth credential is malformed or has expired.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_PERSISTENCE] = + 'The specified persistence type is invalid. It can only be local, ' + + 'session or none.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_MESSAGE_PAYLOAD] = + 'The email template corresponding to this action contains invalid charac' + + 'ters in its message. Please fix by going to the Auth email templates se' + + 'ction in the Firebase Console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_OAUTH_PROVIDER] = + 'EmailAuthProvider is not supported for this operation. This operation ' + + 'only supports OAuth providers.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_OAUTH_CLIENT_ID] = + 'The OAuth client ID provided is either invalid or does not match the ' + + 'specified API key.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_ORIGIN] = + 'This domain is not authorized for OAuth operations for your Firebase ' + + 'project. Edit the list of authorized domains from the Firebase console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_OOB_CODE] = + 'The action code is invalid. This can happen if the code is malformed, ' + + 'expired, or has already been used.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_PASSWORD] = + 'The password is invalid or the user does not have a password.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_PHONE_NUMBER] = + 'The format of the phone number provided is incorrect. Please enter the ' + + 'phone number in a format that can be parsed into E.164 format. E.164 ' + + 'phone numbers are written in the format [+][country code][subscriber ' + + 'number including area code].'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_RECIPIENT_EMAIL] = + 'The email corresponding to this action failed to send as the provided ' + + 'recipient email address is invalid.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_SENDER] = + 'The email template corresponding to this action contains an invalid sen' + + 'der email or name. Please fix by going to the Auth email templates sect' + + 'ion in the Firebase Console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.INVALID_SESSION_INFO] = + 'The verification ID used to create the phone auth credential is invalid.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME] = 'An Android ' + + 'Package Name must be provided if the Android App is required to be ' + + 'installed.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_AUTH_DOMAIN] = + 'Be sure to include authDomain when calling firebase.initializeApp(), ' + + 'by following the instructions in the Firebase console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_APP_CREDENTIAL] = + 'The phone verification request is missing an application verifier ' + + 'assertion. A reCAPTCHA response token needs to be provided.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_CODE] = + 'The phone auth credential was created with an empty SMS verification ' + + 'code.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_CONTINUE_URI] = + 'A continue URL must be provided in the request.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_IFRAME_START] = + 'An internal error has occurred.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_IOS_BUNDLE_ID] = + 'An iOS Bundle ID must be provided if an App Store ID is provided.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_PHONE_NUMBER] = + 'To send verification codes, provide a phone number for the recipient.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MISSING_SESSION_INFO] = + 'The phone auth credential was created with an empty verification ID.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.MODULE_DESTROYED] = + 'This instance of FirebaseApp has been deleted.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.NEED_CONFIRMATION] = + 'An account already exists with the same email address but different ' + + 'sign-in credentials. Sign in using a provider associated with this ' + + 'email address.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.NETWORK_REQUEST_FAILED] = + 'A network error (such as timeout, interrupted connection or ' + + 'unreachable host) has occurred.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.NO_AUTH_EVENT] = + 'An internal error has occurred.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.NO_SUCH_PROVIDER] = + 'User was not linked to an account with the given provider.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.OPERATION_NOT_ALLOWED] = + 'The given sign-in provider is disabled for this Firebase project. ' + + 'Enable it in the Firebase console, under the sign-in method tab of the ' + + 'Auth section.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.OPERATION_NOT_SUPPORTED] = + 'This operation is not supported in the environment this application is ' + + 'running on. "location.protocol" must be http, https or chrome-extension' + + ' and web storage must be enabled.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.POPUP_BLOCKED] = + 'Unable to establish a connection with the popup. It may have been ' + + 'blocked by the browser.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.POPUP_CLOSED_BY_USER] = + 'The popup has been closed by the user before finalizing the operation.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.PROVIDER_ALREADY_LINKED] = + 'User can only be linked to one identity for the given provider.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.QUOTA_EXCEEDED] = + 'The project\'s quota for this operation has been exceeded.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.REDIRECT_CANCELLED_BY_USER] = + 'The redirect operation has been cancelled by the user before finalizing.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING] = + 'A redirect sign-in operation is already pending.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.TIMEOUT] = + 'The operation has timed out.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.TOKEN_EXPIRED] = + 'The user\'s credential is no longer valid. The user must sign in again.'; +fireauth.AuthError.MESSAGES_[ + fireauth.authenum.Error.TOO_MANY_ATTEMPTS_TRY_LATER] = + 'We have blocked all requests from this device due to unusual activity. ' + + 'Try again later.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.UNAUTHORIZED_DOMAIN] = + 'The domain of the continue URL is not whitelisted. Please whitelist ' + + 'the domain in the Firebase console.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE] = + 'The current environment does not support the specified persistence type.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.USER_CANCELLED] = + 'User did not grant your application the permissions it requested.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.USER_DELETED] = + 'There is no user record corresponding to this identifier. The user may ' + + 'have been deleted.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.USER_DISABLED] = + 'The user account has been disabled by an administrator.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.USER_MISMATCH] = + 'The supplied credentials do not correspond to the previously signed in ' + + 'user.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.USER_SIGNED_OUT] = ''; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.WEAK_PASSWORD] = + 'The password must be 6 characters long or more.'; +fireauth.AuthError.MESSAGES_[fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED] = + 'This browser is not supported or 3rd party cookies and data may be ' + + 'disabled.'; diff --git a/packages/auth/src/error_invalidorigin.js b/packages/auth/src/error_invalidorigin.js new file mode 100644 index 00000000000..0e3c03c9f08 --- /dev/null +++ b/packages/auth/src/error_invalidorigin.js @@ -0,0 +1,81 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the invalid origin error, a subclass of + * fireauth.AuthError. + */ + + +goog.provide('fireauth.InvalidOriginError'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('goog.Uri'); +goog.require('goog.string'); + + + +/** + * Invalid origin error that can be returned to the developer. + * @param {string} origin The invalid domain name. + * @constructor + * @extends {fireauth.AuthError} + */ +fireauth.InvalidOriginError = function(origin) { + var code = fireauth.authenum.Error.INVALID_ORIGIN; + var message = undefined; + var uri = goog.Uri.parse(origin); + // Get domain. + var domain = uri.getDomain(); + // Get scheme. + var scheme = uri.getScheme(); + // Only http, https and chrome-extension currently supported. + if (scheme == 'chrome-extension') { + // Chrome extension whitelisting. + // Replace chrome-extension://CHROME_EXT_ID in error message template. + message = goog.string.subs( + fireauth.InvalidOriginError.CHROME_EXTENSION_MESSAGE_TEMPLATE_, + domain); + } else if (scheme == 'http' || scheme == 'https') { + // Replace domain in error message template. + message = goog.string.subs( + fireauth.InvalidOriginError.HTTP_MESSAGE_TEMPLATE_, + domain); + } else { + // Throw operation not supported when non http, https or Chrome extension + // protocol. + code = fireauth.authenum.Error.OPERATION_NOT_SUPPORTED; + } + fireauth.InvalidOriginError.base(this, 'constructor', code, message); +}; +goog.inherits(fireauth.InvalidOriginError, fireauth.AuthError); + + +/** @private @const {string} The http invalid origin message template. */ +fireauth.InvalidOriginError.HTTP_MESSAGE_TEMPLATE_ = 'This domain (%s) is no' + + 't authorized to run this operation. Add it to the OAuth redirect domain' + + 's list in the Firebase console -> Auth section -> Sign in method tab.'; + + +/** + * @private @const {string} The Chrome extension invalid origin message + * template. + */ +fireauth.InvalidOriginError.CHROME_EXTENSION_MESSAGE_TEMPLATE_ = 'This chrom' + + 'e extension ID (chrome-extension://%s) is not authorized to run this op' + + 'eration. Add it to the OAuth redirect domains list in the Firebase cons' + + 'ole -> Auth section -> Sign in method tab.'; diff --git a/packages/auth/src/error_withcredential.js b/packages/auth/src/error_withcredential.js new file mode 100644 index 00000000000..7ef95b7dc6b --- /dev/null +++ b/packages/auth/src/error_withcredential.js @@ -0,0 +1,143 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the Auth errors that include emails and an Auth + * credential, a subclass of fireauth.AuthError. + */ + + +goog.provide('fireauth.AuthErrorWithCredential'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.object'); +goog.require('goog.object'); + + +/** + * Error with email and credential that can be returned to the developer. + * @param {fireauth.authenum.Error} code The error code. + * @param {?fireauth.AuthErrorWithCredential.CredentialInfo=} opt_credentialInfo + * Additional credential information to associate with the error. + * @param {string=} opt_message The human-readable message. + * @constructor + * @extends {fireauth.AuthError} + */ +fireauth.AuthErrorWithCredential = + function(code, opt_credentialInfo, opt_message) { + fireauth.AuthErrorWithCredential.base( + this, 'constructor', code, opt_message); + var credentialInfo = opt_credentialInfo || {}; + + // These properties are public. + if (credentialInfo.email) { + fireauth.object.setReadonlyProperty(this, 'email', credentialInfo.email); + } + if (credentialInfo.phoneNumber) { + fireauth.object.setReadonlyProperty(this, 'phoneNumber', + credentialInfo.phoneNumber); + } + if (credentialInfo.credential) { + fireauth.object.setReadonlyProperty(this, 'credential', + credentialInfo.credential); + } +}; +goog.inherits(fireauth.AuthErrorWithCredential, fireauth.AuthError); + + +/** + * Additional credential information to associate with an error, so that the + * user does not have to execute the Auth flow again on linking errors. + * @typedef {{ + * email: (?string|undefined), + * phoneNumber: (?string|undefined), + * credential: (?fireauth.AuthCredential|undefined), + * }} + */ +fireauth.AuthErrorWithCredential.CredentialInfo; + + +/** + * @return {!Object} The plain object form of the error. + * @override + */ +fireauth.AuthErrorWithCredential.prototype.toPlainObject = function() { + var obj = { + 'code': this['code'], + 'message': this.message + }; + if (this['email']) { + obj['email'] = this['email']; + } + if (this['phoneNumber']) { + obj['phoneNumber'] = this['phoneNumber']; + } + + var credential = this['credential'] && this['credential'].toPlainObject(); + if (credential){ + goog.object.extend(obj, credential); + } + return obj; +}; + + +/** + * @return {!Object} The plain object form of the error. This is used by + * JSON.toStringify() to return the stringified representation of the error; + * @override + */ +fireauth.AuthErrorWithCredential.prototype.toJSON = function() { + // Return the plain object representation in case JSON.stringify is called on + // an Auth error instance. + return this.toPlainObject(); +}; + + +/** + * @param {?Object|undefined} response The object response to convert to a + * fireauth.AuthErrorWithCredential. + * @return {?fireauth.AuthError} The error representation of the response. + * @override + */ +fireauth.AuthErrorWithCredential.fromPlainObject = function(response) { + // Code included. + if (response['code']) { + var code = response['code'] || ''; + // Remove prefix from name if available. + if (code.indexOf(fireauth.AuthError.ERROR_CODE_PREFIX) == 0) { + code = code.substring(fireauth.AuthError.ERROR_CODE_PREFIX.length); + } + + // Credentials in response. + var credentialInfo = { + credential: fireauth.AuthProvider.getCredentialFromResponse(response) + }; + if (response['email']) { + credentialInfo.email = response['email']; + } else if (response['phoneNumber']) { + credentialInfo.phoneNumber = response['phoneNumber']; + } else { + // Neither email nor phone number are set; return a generic error. + return new fireauth.AuthError(code, response['message'] || undefined); + } + + return new fireauth.AuthErrorWithCredential(code, credentialInfo, + response['message']); + } + // No error or invalid response. + return null; +}; diff --git a/packages/auth/src/exports_auth.js b/packages/auth/src/exports_auth.js new file mode 100644 index 00000000000..d332fcadc74 --- /dev/null +++ b/packages/auth/src/exports_auth.js @@ -0,0 +1,552 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.exports'); + +goog.require('fireauth.Auth'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.AuthUser'); +goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.FacebookAuthProvider'); +goog.require('fireauth.GithubAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.InvalidOriginError'); +goog.require('fireauth.OAuthProvider'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.RecaptchaVerifier'); +goog.require('fireauth.TwitterAuthProvider'); +goog.require('fireauth.args'); +goog.require('fireauth.authStorage.Persistence'); +goog.require('fireauth.exportlib'); +goog.require('fireauth.idp.ProviderId'); +goog.require('goog.Promise'); + + +fireauth.exportlib.exportPrototypeMethods( + fireauth.Auth.prototype, { + applyActionCode: { + name: 'applyActionCode', + args: [fireauth.args.string('code')] + }, + checkActionCode: { + name: 'checkActionCode', + args: [fireauth.args.string('code')] + }, + confirmPasswordReset: { + name: 'confirmPasswordReset', + args: [ + fireauth.args.string('code'), + fireauth.args.string('newPassword') + ] + }, + createUserWithEmailAndPassword: { + name: 'createUserWithEmailAndPassword', + args: [fireauth.args.string('email'), fireauth.args.string('password')] + }, + fetchProvidersForEmail: { + name: 'fetchProvidersForEmail', + args: [fireauth.args.string('email')] + }, + getRedirectResult: { + name: 'getRedirectResult', + args: [] + }, + onAuthStateChanged: { + name: 'onAuthStateChanged', + args: [ + fireauth.args.or( + fireauth.args.object(), + fireauth.args.func(), + 'nextOrObserver'), + fireauth.args.func('opt_error', true), + fireauth.args.func('opt_completed', true) + ] + }, + onIdTokenChanged: { + name: 'onIdTokenChanged', + args: [ + fireauth.args.or( + fireauth.args.object(), + fireauth.args.func(), + 'nextOrObserver'), + fireauth.args.func('opt_error', true), + fireauth.args.func('opt_completed', true) + ] + }, + sendPasswordResetEmail: { + name: 'sendPasswordResetEmail', + args: [ + fireauth.args.string('email'), + fireauth.args.or( + fireauth.args.object('opt_actionCodeSettings', true), + fireauth.args.null(null, true), + 'opt_actionCodeSettings', + true) + ] + }, + setPersistence: { + name: 'setPersistence', + args: [fireauth.args.string('persistence')] + }, + signInAndRetrieveDataWithCredential: { + name: 'signInAndRetrieveDataWithCredential', + args: [fireauth.args.authCredential()] + }, + signInAnonymously: { + name: 'signInAnonymously', + args: [] + }, + signInWithCredential: { + name: 'signInWithCredential', + args: [fireauth.args.authCredential()] + }, + signInWithCustomToken: { + name: 'signInWithCustomToken', + args: [fireauth.args.string('token')] + }, + signInWithEmailAndPassword: { + name: 'signInWithEmailAndPassword', + args: [fireauth.args.string('email'), fireauth.args.string('password')] + }, + signInWithPhoneNumber: { + name: 'signInWithPhoneNumber', + args: [ + fireauth.args.string('phoneNumber'), + fireauth.args.applicationVerifier() + ] + }, + signInWithPopup: { + name: 'signInWithPopup', + args: [fireauth.args.authProvider()] + }, + signInWithRedirect: { + name: 'signInWithRedirect', + args: [fireauth.args.authProvider()] + }, + signOut: { + name: 'signOut', + args: [] + }, + toJSON: { + name: 'toJSON', + // This shouldn't take an argument but a blank string is being passed + // on JSON.stringify and causing this to fail with an argument error. + // So allow an optional string. + args: [fireauth.args.string(null, true)] + }, + useDeviceLanguage: { + name: 'useDeviceLanguage', + args: [] + }, + verifyPasswordResetCode: { + name: 'verifyPasswordResetCode', + args: [fireauth.args.string('code')] + } + }); + +fireauth.exportlib.exportPrototypeProperties( + fireauth.Auth.prototype, { + 'lc': { + name: 'languageCode', + arg: fireauth.args.or( + fireauth.args.string(), + fireauth.args.null(), + 'languageCode') + } + }); + +// Exports firebase.auth.Auth.Persistence. +fireauth.Auth['Persistence'] = fireauth.authStorage.Persistence; +fireauth.Auth['Persistence']['LOCAL'] = fireauth.authStorage.Persistence.LOCAL; +fireauth.Auth['Persistence']['SESSION'] = + fireauth.authStorage.Persistence.SESSION; +fireauth.Auth['Persistence']['NONE'] = fireauth.authStorage.Persistence.NONE; + + +fireauth.exportlib.exportPrototypeMethods( + fireauth.AuthUser.prototype, { + 'delete': { + name: 'delete', + args: [] + }, + getIdToken: { + name: 'getIdToken', + args: [fireauth.args.bool('opt_forceRefresh', true)] + }, + getToken: { + name: 'getToken', + args: [fireauth.args.bool('opt_forceRefresh', true)] + }, + linkAndRetrieveDataWithCredential: { + name: 'linkAndRetrieveDataWithCredential', + args: [fireauth.args.authCredential()] + }, + linkWithCredential: { + name: 'linkWithCredential', + args: [fireauth.args.authCredential()] + }, + linkWithPhoneNumber: { + name: 'linkWithPhoneNumber', + args: [ + fireauth.args.string('phoneNumber'), + fireauth.args.applicationVerifier() + ] + }, + linkWithPopup: { + name: 'linkWithPopup', + args: [fireauth.args.authProvider()] + }, + linkWithRedirect: { + name: 'linkWithRedirect', + args: [fireauth.args.authProvider()] + }, + reauthenticateAndRetrieveDataWithCredential: { + name: 'reauthenticateAndRetrieveDataWithCredential', + args: [fireauth.args.authCredential()] + }, + reauthenticateWithCredential: { + name: 'reauthenticateWithCredential', + args: [fireauth.args.authCredential()] + }, + reauthenticateWithPhoneNumber: { + name: 'reauthenticateWithPhoneNumber', + args: [ + fireauth.args.string('phoneNumber'), + fireauth.args.applicationVerifier() + ] + }, + reauthenticateWithPopup: { + name: 'reauthenticateWithPopup', + args: [fireauth.args.authProvider()] + }, + reauthenticateWithRedirect: { + name: 'reauthenticateWithRedirect', + args: [fireauth.args.authProvider()] + }, + reload: { + name: 'reload', + args: [] + }, + sendEmailVerification: { + name: 'sendEmailVerification', + args: [ + fireauth.args.or( + fireauth.args.object('opt_actionCodeSettings', true), + fireauth.args.null(null, true), + 'opt_actionCodeSettings', + true) + ] + }, + toJSON: { + name: 'toJSON', + // This shouldn't take an argument but a blank string is being passed + // on JSON.stringify and causing this to fail with an argument error. + // So allow an optional string. + args: [fireauth.args.string(null, true)] + }, + unlink: { + name: 'unlink', + args: [fireauth.args.string('provider')] + }, + updateEmail: { + name: 'updateEmail', + args: [fireauth.args.string('email')] + }, + updatePassword: { + name: 'updatePassword', + args: [fireauth.args.string('password')] + }, + updatePhoneNumber: { + name: 'updatePhoneNumber', + args: [fireauth.args.authCredential(fireauth.idp.ProviderId.PHONE)] + }, + updateProfile: { + name: 'updateProfile', + args: [fireauth.args.object('profile')] + } + }); + +fireauth.exportlib.exportPrototypeMethods( + goog.Promise.prototype, { + thenCatch: { + name: 'catch' + }, + then: { + name: 'then' + } + }); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.ConfirmationResult.prototype, { + confirm: { + name: 'confirm', + args: [ + fireauth.args.string('verificationCode') + ] + } + }); + +fireauth.exportlib.exportFunction( + fireauth.EmailAuthProvider, 'credential', + fireauth.EmailAuthProvider.credential, [ + fireauth.args.string('email'), + fireauth.args.string('password') + ]); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.FacebookAuthProvider.prototype, { + addScope: { + name: 'addScope', + args: [fireauth.args.string('scope')] + }, + setCustomParameters: { + name: 'setCustomParameters', + args: [fireauth.args.object('customOAuthParameters')] + } + }); +fireauth.exportlib.exportFunction( + fireauth.FacebookAuthProvider, 'credential', + fireauth.FacebookAuthProvider.credential, [ + fireauth.args.or(fireauth.args.string(), fireauth.args.object(), + 'token') + ]); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.GithubAuthProvider.prototype, { + addScope: { + name: 'addScope', + args: [fireauth.args.string('scope')] + }, + setCustomParameters: { + name: 'setCustomParameters', + args: [fireauth.args.object('customOAuthParameters')] + } + }); +fireauth.exportlib.exportFunction( + fireauth.GithubAuthProvider, 'credential', + fireauth.GithubAuthProvider.credential, [ + fireauth.args.or(fireauth.args.string(), fireauth.args.object(), + 'token') + ]); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.GoogleAuthProvider.prototype, { + addScope: { + name: 'addScope', + args: [fireauth.args.string('scope')] + }, + setCustomParameters: { + name: 'setCustomParameters', + args: [fireauth.args.object('customOAuthParameters')] + } + }); +fireauth.exportlib.exportFunction( + fireauth.GoogleAuthProvider, 'credential', + fireauth.GoogleAuthProvider.credential, [ + fireauth.args.or(fireauth.args.string(), + fireauth.args.or(fireauth.args.object(), fireauth.args.null()), + 'idToken'), + fireauth.args.or(fireauth.args.string(), fireauth.args.null(), + 'accessToken', true) + ]); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.TwitterAuthProvider.prototype, { + setCustomParameters: { + name: 'setCustomParameters', + args: [fireauth.args.object('customOAuthParameters')] + } + }); +fireauth.exportlib.exportFunction( + fireauth.TwitterAuthProvider, 'credential', + fireauth.TwitterAuthProvider.credential, [ + fireauth.args.or(fireauth.args.string(), fireauth.args.object(), + 'token'), + fireauth.args.string('secret', true) + ]); +fireauth.exportlib.exportPrototypeMethods( + fireauth.OAuthProvider.prototype, { + addScope: { + name: 'addScope', + args: [fireauth.args.string('scope')] + }, + credential: { + name: 'credential', + args: [ + fireauth.args.or(fireauth.args.string(), fireauth.args.null(), + 'idToken', true), + fireauth.args.or(fireauth.args.string(), fireauth.args.null(), + 'accessToken', true) + ] + }, + setCustomParameters: { + name: 'setCustomParameters', + args: [fireauth.args.object('customOAuthParameters')] + } + }); +fireauth.exportlib.exportFunction( + fireauth.PhoneAuthProvider, 'credential', + fireauth.PhoneAuthProvider.credential, [ + fireauth.args.string('verificationId'), + fireauth.args.string('verificationCode') + ]); +fireauth.exportlib.exportPrototypeMethods( + fireauth.PhoneAuthProvider.prototype, { + verifyPhoneNumber: { + name: 'verifyPhoneNumber', + args: [ + fireauth.args.string('phoneNumber'), + fireauth.args.applicationVerifier() + ] + } + }); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.AuthError.prototype, { + toJSON: { + name: 'toJSON', + // This shouldn't take an argument but a blank string is being passed + // on JSON.stringify and causing this to fail with an argument error. + // So allow an optional string. + args: [fireauth.args.string(null, true)] + } + }); +fireauth.exportlib.exportPrototypeMethods( + fireauth.AuthErrorWithCredential.prototype, { + toJSON: { + name: 'toJSON', + // This shouldn't take an argument but a blank string is being passed + // on JSON.stringify and causing this to fail with an argument error. + // So allow an optional string. + args: [fireauth.args.string(null, true)] + } + }); +fireauth.exportlib.exportPrototypeMethods( + fireauth.InvalidOriginError.prototype, { + toJSON: { + name: 'toJSON', + // This shouldn't take an argument but a blank string is being passed + // on JSON.stringify and causing this to fail with an argument error. + // So allow an optional string. + args: [fireauth.args.string(null, true)] + } + }); + +fireauth.exportlib.exportPrototypeMethods( + fireauth.RecaptchaVerifier.prototype, { + clear: { + name: 'clear', + args: [] + }, + render: { + name: 'render', + args: [] + }, + verify: { + name: 'verify', + args: [] + } + }); + + +(function() { + if (typeof firebase === 'undefined' || !firebase.INTERNAL || + !firebase.INTERNAL.registerService) { + throw new Error('Cannot find the firebase namespace; be sure to include ' + + 'firebase-app.js before this library.'); + } else { + /** @type {!firebase.ServiceFactory} */ + var factory = function(app, extendApp) { + var auth = new fireauth.Auth(app); + extendApp({ + 'INTERNAL': { + // Extend app.INTERNAL.getUid. + 'getUid': goog.bind(auth.getUid, auth), + 'getToken': goog.bind(auth.getIdTokenInternal, auth), + 'addAuthTokenListener': + goog.bind(auth.addAuthTokenListenerInternal, auth), + 'removeAuthTokenListener': + goog.bind(auth.removeAuthTokenListenerInternal, auth) + } + }); + return auth; + }; + + var namespace = { + 'Auth': fireauth.Auth, + 'Error': fireauth.AuthError + }; + fireauth.exportlib.exportFunction(namespace, + 'EmailAuthProvider', fireauth.EmailAuthProvider, []); + fireauth.exportlib.exportFunction(namespace, + 'FacebookAuthProvider', fireauth.FacebookAuthProvider, []); + fireauth.exportlib.exportFunction(namespace, + 'GithubAuthProvider', fireauth.GithubAuthProvider, []); + fireauth.exportlib.exportFunction(namespace, + 'GoogleAuthProvider', fireauth.GoogleAuthProvider, []); + fireauth.exportlib.exportFunction(namespace, + 'TwitterAuthProvider', fireauth.TwitterAuthProvider, []); + fireauth.exportlib.exportFunction(namespace, + 'OAuthProvider', fireauth.OAuthProvider, [ + fireauth.args.string('providerId') + ]); + fireauth.exportlib.exportFunction(namespace, + 'PhoneAuthProvider', fireauth.PhoneAuthProvider, [ + fireauth.args.firebaseAuth(true) + ]); + fireauth.exportlib.exportFunction(namespace, + 'RecaptchaVerifier', fireauth.RecaptchaVerifier, [ + fireauth.args.or( + fireauth.args.string(), + fireauth.args.element(), + 'recaptchaContainer'), + fireauth.args.object('recaptchaParameters', true), + fireauth.args.firebaseApp(true) + ]); + + // Register Auth service with firebase.App. + firebase.INTERNAL.registerService( + fireauth.exportlib.AUTH_TYPE, + factory, + namespace, + // Initialize Auth when an App is created, so that tokens and Auth state + // listeners are available. + function (event, app) { + if (event === 'create') { + try { + app[fireauth.exportlib.AUTH_TYPE](); + } catch (e) { + // This is a silent operation in the background. If the auth + // initialization fails, it should not cause a fatal error. + // Instead when the developer tries to initialize again manually, + // the error will be thrown. + // One specific use case here is the initialization for the nodejs + // client when no API key is provided. This is commonly used + // for unauthenticated database access. + } + } + } + ); + + + // Expose User as firebase.User. + firebase.INTERNAL.extendNamespace({ + 'User': fireauth.AuthUser + }); + } +})(); diff --git a/packages/auth/src/exports_lib.js b/packages/auth/src/exports_lib.js new file mode 100644 index 00000000000..d5f4014ef28 --- /dev/null +++ b/packages/auth/src/exports_lib.js @@ -0,0 +1,206 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provides utilities for exporting public APIs, with error + * checking. + */ + +goog.provide('fireauth.exportlib'); +goog.provide('fireauth.exportlib.ExportedMethod'); + +goog.require('fireauth.args'); + + +/** + * Type constant for Firebase Auth. + * @const {string} + */ +fireauth.exportlib.AUTH_TYPE = 'auth'; + + +/** + * Represents an exported method, with the exported name of the method and the + * expected arguments to that method. + * @typedef {{ + * name: string, + * args: (Array|null|undefined) + * }} + */ +fireauth.exportlib.ExportedMethod; + + +/** + * Represents an exported property, with the exported name of the property and + * the expected argument to the setter of this property. + * @typedef {{ + * name: string, + * arg: !fireauth.args.Argument + * }} + */ +fireauth.exportlib.ExportedProperty; + + +/** + * Exports prototype methods of an object. + * @param {!Object} protObj The prototype of an object. + * @param {!Object} fnMap The map of + * prototype functions to their export name and expected arguments. + */ +fireauth.exportlib.exportPrototypeMethods = function(protObj, fnMap) { + // This method exports methods by aliasing the unobfuscated function name + // (specified as a string in the "name" field of ExportedMethod) to the + // obfuscated function name (specified as a key of the fnMap object). + // + // To give a concrete example, let's say that we have this method: + // fireauth.Auth.prototype.fetchProvidersForEmail = function() { ... }; + // + // In the exports file, we export as follows: + // fireauth.exportlib.exportPrototypeMethods(fireauth.Auth.prototype, { + // fetchProvidersForEmail: {name: 'fetchProvidersForEmail', args: ...} + // }); + // + // When the compiler obfuscates the code, the code above will become something + // like this: + // fireauth.Auth.prototype.qZ = function() { ... }; + // fireauth.exportlib.exportPrototypeMethods(fireauth.Auth.prototype, { + // qZ: {name: 'fetchProvidersForEmail', args: ...} + // }); + // + // (Of course, fireauth.Auth and fireauth.exportlib.exportPrototypeMethods + // would also be obfuscated). Note that the key in fnMap is obfuscated but the + // "name" field in the ExportedMethod is not. Now, exportPrototypeMethods can + // export fetchProvidersForEmail by reading the key ("qZ") and the "name" + // field ("fetchProvidersForEmail") and essentially executing this: + // fireauth.Auth.prototype['fetchProvidersForEmail'] = + // fireauth.Auth.prototype['qZ']; + for (var obfuscatedFnName in fnMap) { + var unobfuscatedFnName = fnMap[obfuscatedFnName].name; + protObj[unobfuscatedFnName] = + fireauth.exportlib.wrapMethodWithArgumentVerifier_( + unobfuscatedFnName, protObj[obfuscatedFnName], + fnMap[obfuscatedFnName].args); + } +}; + + +/** + * Exports properties of an object. See the docs for exportPrototypeMethods for + * more information about how this works. + * @param {!Object} protObj The prototype of an object. + * @param {!Object} propMap The + * map of properties to their export names. + */ +fireauth.exportlib.exportPrototypeProperties = function(protObj, propMap) { + for (var obfuscatedPropName in propMap) { + var unobfuscatedPropName = propMap[obfuscatedPropName].name; + // Don't alias a property to itself. + // Downside is that argument validation will not be possible. For now, to + // get around it, ensure unobfuscated property names are different + // than the corresponding obfuscated property names. + if (unobfuscatedPropName === obfuscatedPropName) { + continue; + } + // Get the expected argument. + var expectedArg = propMap[obfuscatedPropName].arg; + Object.defineProperty(protObj, unobfuscatedPropName, { + /** + * @this {!Object} + * @return {string} The value of the property. + */ + get: function() { + return this[obfuscatedPropName]; + }, + /** + * @this {!Object} + * @param {string} value The new value of the property. + */ + set: function(value) { + // Validate the argument before setting it. + fireauth.args.validate( + unobfuscatedPropName, [expectedArg], [value], true); + this[obfuscatedPropName] = value; + }, + enumerable: true + }); + } +}; + + +/** + * Export a static method as a public API. + * @param {!Object} parentObj The parent object to patch. + * @param {string} name The public name of the method. + * @param {!Function} func The method. + * @param {?Array=} opt_expectedArgs The expected + * arguments to the method. + */ +fireauth.exportlib.exportFunction = function(parentObj, name, func, + opt_expectedArgs) { + parentObj[name] = fireauth.exportlib.wrapMethodWithArgumentVerifier_( + name, func, opt_expectedArgs); +}; + + +/** + * Wraps a method with a function that first verifies the arguments to the + * method and then calls the original method. + * @param {string} methodName The name of the method, which will be displayed + * on the error message if the arguments are not valid. + * @param {!Function} method The method to be wrapped. + * @param {?Array=} opt_expectedArgs The expected + * arguments. + * @return {!Function} The wrapped method. + * @private + */ +fireauth.exportlib.wrapMethodWithArgumentVerifier_ = function(methodName, + method, opt_expectedArgs) { + if (!opt_expectedArgs) { + return method; + } + var shortName = fireauth.exportlib.extractMethodNameFromFullPath_(methodName); + var wrapper = function() { + var argumentsAsArray = Array.prototype.slice.call(arguments); + fireauth.args.validate(shortName, + /** @type {!Array} */ (opt_expectedArgs), + argumentsAsArray); + return method.apply(this, argumentsAsArray); + }; + // Reattach all static stuff to wrapper. + for (var key in method) { + wrapper[key] = method[key]; + } + // Reattach all prototype stuff to wrapper. + for (var key in method.prototype) { + wrapper.prototype[key] = method.prototype[key]; + } + // Return wrapper with all of method's static and prototype methods and + // properties. + return wrapper; +}; + + +/** + * From a full path to a method (e.g. "fireauth.GoogleAuthProvider.credential"), + * get just the method name ("credential"). + * @param {string} path The full path. + * @return {string} The method name. + * @private + */ +fireauth.exportlib.extractMethodNameFromFullPath_ = function(path) { + var parts = path.split('.'); + return parts[parts.length - 1]; +}; diff --git a/packages/auth/src/exports_unreleased.js b/packages/auth/src/exports_unreleased.js new file mode 100644 index 00000000000..8c644698e70 --- /dev/null +++ b/packages/auth/src/exports_unreleased.js @@ -0,0 +1,25 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Exports symbols for unreleased features. This file is not + * included in the Firebase JS build. + * + * When a feature is to be released in the next Firebase JS release, move the + * export from this file to exports_auth.js. + */ + +goog.provide('fireauth.exportsUnreleased'); diff --git a/packages/auth/src/externs.js b/packages/auth/src/externs.js new file mode 100644 index 00000000000..faefe2524ae --- /dev/null +++ b/packages/auth/src/externs.js @@ -0,0 +1,41 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Firebase Auth-specific externs. + */ + + +/** + * A verifier that asserts that the user calling an API is a real user. + * @interface + */ +firebase.auth.ApplicationVerifier = function() {}; + + +/** + * The type of the ApplicationVerifier assertion, e.g. "recaptcha". + * @type {string} + */ +firebase.auth.ApplicationVerifier.prototype.type; + + +/** + * Returns a promise for the assertion to verify the app identity, e.g. the + * g-recaptcha-response in reCAPTCHA. + * @return {!firebase.Promise} + */ +firebase.auth.ApplicationVerifier.prototype.verify = function() {}; diff --git a/packages/auth/src/idp.js b/packages/auth/src/idp.js new file mode 100644 index 00000000000..2415c96f3ae --- /dev/null +++ b/packages/auth/src/idp.js @@ -0,0 +1,138 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the IdP provider IDs and related settings. + */ + +goog.provide('fireauth.idp'); +goog.provide('fireauth.idp.IdpSettings'); +goog.provide('fireauth.idp.ProviderId'); +goog.provide('fireauth.idp.Settings'); + + +/** + * Enums for supported provider IDs. + * @enum {string} + */ +fireauth.idp.ProviderId = { + ANONYMOUS: 'anonymous', + FACEBOOK: 'facebook.com', + FIREBASE: 'firebase', + GITHUB: 'github.com', + GOOGLE: 'google.com', + PASSWORD: 'password', + PHONE: 'phone', + TWITTER: 'twitter.com' +}; + + +/** + * The settings of an identity provider. The fields are: + *
    + *
  • languageParam: defines the custom OAuth language parameter. + *
  • popupWidth: defines the popup recommended width. + *
  • popupHeight: defines the popup recommended height. + *
  • providerId: defines the provider ID. + *
  • reservedOAuthParameters: defines the list of reserved OAuth parameters. + *
+ * @typedef {{ + * languageParam: (?string|undefined), + * popupWidth: (?number|undefined), + * popupHeight: (?number|undefined), + * providerId: !fireauth.idp.ProviderId, + * reservedOAuthParameters: !Array + * }} + */ +fireauth.idp.IdpSettings; + + +/** + * The list of reserved OAuth 1.0 parameters. + * @const {!Array} + */ +fireauth.idp.RESERVED_OAUTH1_PARAMS = + ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature', + 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', + 'oauth_version']; + + +/** + * The list of reserved OAuth 2.0 parameters. + * @const {!Array} + */ +fireauth.idp.RESERVED_OAUTH2_PARAMS = + ['client_id', 'response_type', 'scope', 'redirect_uri', 'state']; + + +/** + * The recommendations for the different IdP display settings. + * @enum {!fireauth.idp.IdpSettings} + */ +fireauth.idp.Settings = { + FACEBOOK: { + languageParam: 'locale', + popupWidth: 500, + popupHeight: 600, + providerId: fireauth.idp.ProviderId.FACEBOOK, + reservedOAuthParameters: fireauth.idp.RESERVED_OAUTH2_PARAMS + }, + GITHUB: { + languageParam: null, + popupWidth: 500, + popupHeight: 620, + providerId: fireauth.idp.ProviderId.GITHUB, + reservedOAuthParameters: fireauth.idp.RESERVED_OAUTH2_PARAMS + }, + GOOGLE: { + languageParam: 'hl', + popupWidth: 515, + popupHeight: 680, + providerId: fireauth.idp.ProviderId.GOOGLE, + reservedOAuthParameters: fireauth.idp.RESERVED_OAUTH2_PARAMS + }, + TWITTER: { + languageParam: 'lang', + popupWidth: 485, + popupHeight: 705, + providerId: fireauth.idp.ProviderId.TWITTER, + reservedOAuthParameters: fireauth.idp.RESERVED_OAUTH1_PARAMS + } +}; + + +/** + * @param {!fireauth.idp.ProviderId} providerId The requested provider ID. + * @return {?fireauth.idp.Settings} The settings for the requested provider ID. + */ +fireauth.idp.getIdpSettings = function(providerId) { + for (var key in fireauth.idp.Settings) { + if (fireauth.idp.Settings[key].providerId == providerId) { + return fireauth.idp.Settings[key]; + } + } + return null; +}; + + +/** + * @param {!fireauth.idp.ProviderId} providerId The requested provider ID. + * @return {!Array} The list of reserved OAuth parameters. + */ +fireauth.idp.getReservedOAuthParams = function(providerId) { + var settings = fireauth.idp.getIdpSettings(providerId); + return (settings && settings.reservedOAuthParameters) || []; +}; diff --git a/packages/auth/src/idtoken.js b/packages/auth/src/idtoken.js new file mode 100644 index 00000000000..9fd4088c705 --- /dev/null +++ b/packages/auth/src/idtoken.js @@ -0,0 +1,196 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility functions to handle Firebase Auth ID tokens. + */ + +goog.provide('fireauth.IdToken'); + +goog.require('goog.crypt.base64'); + + +/** + * Parses the token string into a {@code Token} object. + * @param {!fireauth.IdToken.JsonToken} token The parsed JSON token. + * @constructor + */ +fireauth.IdToken = function(token) { + /** @private {string} The issuer of the token. */ + this.iss_ = token['iss']; + /** @private {string} The audience of the token. */ + this.aud_ = token['aud']; + /** @private {number} The expire time in seconds of the token. */ + this.exp_ = token['exp']; + /** @private {string} The local user ID of the token. */ + this.localId_ = token['sub']; + var now = goog.now() / 1000; + /** @private {number} The issue time in seconds of the token. */ + this.iat_ = token['iat'] || (now > this.exp_ ? this.exp_ : now); + /** @private {?string} The email address of the token. */ + this.email_ = token['email'] || null; + /** @private {boolean} Whether the user is verified. */ + this.verified_ = !!token['verified']; + /** @private {?string} The provider ID of the token. */ + this.providerId_ = token['provider_id'] || + (token['firebase'] && token['firebase']['sign_in_provider']) || + null; + /** @private {boolean} Whether the user is anonymous. */ + this.anonymous_ = !!token['is_anonymous'] || this.providerId_ == 'anonymous'; + /** @private {?string} The federated ID of the token. */ + this.federatedId_ = token['federated_id'] || null; + /** @private {?string} The display name of the token. */ + this.displayName_ = token['display_name'] || null; + /** @private {?string} The photo URL of the token. */ + this.photoURL_ = token['photo_url'] || null; + /** + * @private {?string} The phone number of the user identified by the token. + */ + this.phoneNumber_ = token['phone_number'] || null; +}; + + +/** + * @typedef {{ + * identities: (?Object|undefined), + * sign_in_provider: (?string|undefined) + * }} + */ +fireauth.IdToken.Firebase; + + +/** + * @typedef {{ + * iss: string, + * aud: string, + * exp: number, + * sub: string, + * iat: (?number|undefined), + * email: (?string|undefined), + * verified: (?boolean|undefined), + * provider_id: (?string|undefined), + * is_anonymous: (?boolean|undefined), + * federated_id: (?string|undefined), + * display_name: (?string|undefined), + * photo_url: (?string|undefined), + * phone_number: (?string|undefined), + * firebase: (?fireauth.IdToken.Firebase|undefined) + * }} + */ +fireauth.IdToken.JsonToken; + + +/** @return {?string} The email address of the account. */ +fireauth.IdToken.prototype.getEmail = function() { + return this.email_; +}; + + +/** @return {number} The expire time in seconds. */ +fireauth.IdToken.prototype.getExp = function() { + return this.exp_; +}; + + +/** @return {?string} The ID of the identity provider. */ +fireauth.IdToken.prototype.getProviderId = function() { + return this.providerId_; +}; + + +/** @return {?string} The display name of the account. */ +fireauth.IdToken.prototype.getDisplayName = function() { + return this.displayName_; +}; + + +/** @return {?string} The photo URL of the account. */ +fireauth.IdToken.prototype.getPhotoUrl = function() { + return this.photoURL_; +}; + + +/** @return {string} The user ID of the account. */ +fireauth.IdToken.prototype.getLocalId = function() { + return this.localId_; +}; + + +/** @return {?string} The federated ID of the account. */ +fireauth.IdToken.prototype.getFederatedId = function() { + return this.federatedId_; +}; + + +/** @return {boolean} Whether the user is anonymous. */ +fireauth.IdToken.prototype.isAnonymous = function() { + return this.anonymous_; +}; + + +/** @return {boolean} Whether the user email is verified. */ +fireauth.IdToken.prototype.isVerified = function() { + return this.verified_; +}; + + +/** @return {boolean} Whether token is expired. */ +fireauth.IdToken.prototype.isExpired = function() { + var now = Math.floor(goog.now() / 1000); + // It is expired if token expiration time is less than current time. + return this.getExp() <= now; +}; + + +/** @return {string} The issuer of the token. */ +fireauth.IdToken.prototype.getIssuer = function() { + return this.iss_; +}; + + +/** @return {?string} The phone number of the account. */ +fireauth.IdToken.prototype.getPhoneNumber = function() { + return this.phoneNumber_; +}; + + +/** + * Parses the JWT token and extracts the information part without verifying the + * token signature. + * @param {string} tokenString The JWT token. + * @return {?fireauth.IdToken} The decoded token. + */ +fireauth.IdToken.parse = function(tokenString) { + // Token format is .. + var fields = tokenString.split('.'); + if (fields.length != 3) { + return null; + } + var jsonInfo = fields[1]; + // Google base64 library does not handle padding. + var padLen = (4 - jsonInfo.length % 4) % 4; + for (var i = 0; i < padLen; i++) { + jsonInfo += '.'; + } + try { + var token = JSON.parse(goog.crypt.base64.decodeString(jsonInfo, true)); + if (token['sub'] && token['iss'] && token['aud'] && token['exp']) { + return new fireauth.IdToken( + /** @type {!fireauth.IdToken.JsonToken} */ (token)); + } + } catch (e) {} + return null; +}; diff --git a/packages/auth/src/iframeclient/ifchandler.js b/packages/auth/src/iframeclient/ifchandler.js new file mode 100644 index 00000000000..3bb8356b303 --- /dev/null +++ b/packages/auth/src/iframeclient/ifchandler.js @@ -0,0 +1,987 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines fireauth.iframeclient.IfcHandler used to communicate + * with the serverless widget. + */ + +goog.provide('fireauth.iframeclient.IfcHandler'); +goog.provide('fireauth.iframeclient.IframeUrlBuilder'); +goog.provide('fireauth.iframeclient.OAuthUrlBuilder'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.InvalidOriginError'); +goog.require('fireauth.OAuthSignInHandler'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.iframeclient.IframeWrapper'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.Uri'); +goog.require('goog.array'); +goog.require('goog.object'); + + +/** + * The OAuth handler and iframe prototcol. + * @const {string} + * @suppress {const|duplicate} + */ +fireauth.iframeclient.SCHEME = 'https'; + + + +/** + * The OAuth handler and iframe port number. + * @const {?number} + * @suppress {const|duplicate} + */ +fireauth.iframeclient.PORT_NUMBER = null; + + + +/** + * The iframe URL builder used to build the iframe widget URL. + * @param {string} authDomain The application authDomain. + * @param {string} apiKey The API key. + * @param {string} appName The App name. + * @constructor + */ +fireauth.iframeclient.IframeUrlBuilder = function(authDomain, apiKey, appName) { + /** @private {string} The application authDomain. */ + this.authDomain_ = authDomain; + /** @private {string} The API key. */ + this.apiKey_ = apiKey; + /** @private {string} The App name. */ + this.appName_ = appName; + /** @private {?string|undefined} The client version. */ + this.v_ = null; + /** + * @private {!goog.Uri} The URI object used to build the iframe URL. + */ + this.uri_ = goog.Uri.create( + fireauth.iframeclient.SCHEME, + null, + this.authDomain_, + fireauth.iframeclient.PORT_NUMBER, + '/__/auth/iframe', + null, + null); + this.uri_.setParameterValue('apiKey', this.apiKey_); + this.uri_.setParameterValue('appName', this.appName_); + /** @private {?string|undefined} The endpoint ID. */ + this.endpointId_ = null; + /** @private {!Array} The list of framework IDs. */ + this.frameworks_ = []; +}; + + +/** + * Sets the client version. + * @param {?string|undefined} v The client version. + * @return {!fireauth.iframeclient.IframeUrlBuilder} The current iframe URL + * builder instance. + */ +fireauth.iframeclient.IframeUrlBuilder.prototype.setVersion = function(v) { + this.v_ = v; + return this; +}; + + +/** + * Sets the endpoint ID. + * @param {?string|undefined} eid The endpoint ID (staging, test Gaia, etc). + * @return {!fireauth.iframeclient.IframeUrlBuilder} The current iframe URL + * builder instance. + */ +fireauth.iframeclient.IframeUrlBuilder.prototype.setEndpointId = function(eid) { + this.endpointId_ = eid; + return this; +}; + + +/** + * Sets the list of frameworks to pass to the iframe. + * @param {?Array|undefined} frameworks The list of frameworks to log. + * @return {!fireauth.iframeclient.IframeUrlBuilder} The current iframe URL + * builder instance. + */ +fireauth.iframeclient.IframeUrlBuilder.prototype.setFrameworks = + function(frameworks) { + this.frameworks_ = goog.array.clone(frameworks || []); + return this; +}; + + +/** + * Modifes the URI with the relevant Auth provider parameters. + * @return {string} The constructed OAuth URL string. + * @override + */ +fireauth.iframeclient.IframeUrlBuilder.prototype.toString = function() { + // Pass the client version if available. + if (this.v_) { + this.uri_.setParameterValue('v', this.v_); + } else { + this.uri_.removeParameter('v'); + } + // Pass the endpoint ID if available. + if (this.endpointId_) { + this.uri_.setParameterValue('eid', this.endpointId_); + } else { + this.uri_.removeParameter('eid'); + } + // Pass the list of frameworks if available. + if (this.frameworks_.length) { + this.uri_.setParameterValue('fw', this.frameworks_.join(',')); + } else { + this.uri_.removeParameter('fw'); + } + return this.uri_.toString(); +}; + + + +/** + * The OAuth URL builder used to build the OAuth handler widget URL. + * @param {string} authDomain The application authDomain. + * @param {string} apiKey The API key. + * @param {string} appName The App name. + * @param {string} authType The Auth operation type. + * @param {!fireauth.AuthProvider} provider The Auth provider that the OAuth + * handler request is built to sign in to. + * @constructor + */ +fireauth.iframeclient.OAuthUrlBuilder = + function(authDomain, apiKey, appName, authType, provider) { + /** @private {string} The application authDomain. */ + this.authDomain_ = authDomain; + /** @private {string} The API key. */ + this.apiKey_ = apiKey; + /** @private {string} The App name. */ + this.appName_ = appName; + /** @private {string} The Auth operation type. */ + this.authType_ = authType; + /** + * @private {?string|undefined} The redirect URL used in redirect operations. + */ + this.redirectUrl_ = null; + /** @private {?string|undefined} The event ID. */ + this.eventId_ = null; + /** @private {?string|undefined} The client version. */ + this.v_ = null; + /** + * @private {!fireauth.AuthProvider} The Firebase Auth provider that the OAuth + * handler request is built to sign in to. + */ + this.provider_ = provider; + /** @private {?string|undefined} The endpoint ID. */ + this.endpointId_ = null; +}; + + +/** + * Sets the redirect URL. + * @param {?string|undefined} redirectUrl The redirect URL used in redirect + * operations. + * @return {!fireauth.iframeclient.OAuthUrlBuilder} The current OAuth URL + * builder instance. + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.setRedirectUrl = + function(redirectUrl) { + this.redirectUrl_ = redirectUrl; + return this; +}; + + +/** + * Sets the event ID. + * @param {?string|undefined} eventId The event ID. + * @return {!fireauth.iframeclient.OAuthUrlBuilder} The current OAuth URL + * builder instance. + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.setEventId = function(eventId) { + this.eventId_ = eventId; + return this; +}; + + +/** + * Sets the client version. + * @param {?string|undefined} v The client version. + * @return {!fireauth.iframeclient.OAuthUrlBuilder} The current OAuth URL + * builder instance. + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.setVersion = function(v) { + this.v_ = v; + return this; +}; + + +/** + * Sets the endpoint ID. + * @param {?string|undefined} eid The endpoint ID (staging, test Gaia, etc). + * @return {!fireauth.iframeclient.OAuthUrlBuilder} The current OAuth URL + * builder instance. + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.setEndpointId = function(eid) { + this.endpointId_ = eid; + return this; +}; + + +/** + * Sets any additional optional parameters. This will overwrite any previously + * set additional parameters. + * @param {?Object|undefined} additionalParams The optional + * additional parameters. + * @return {!fireauth.iframeclient.OAuthUrlBuilder} The current OAuth URL + * builder instance. + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.setAdditionalParameters = + function(additionalParams) { + this.additionalParams_ = goog.object.clone(additionalParams || null); + return this; +}; + + +/** + * Modifies the URI with the relevant Auth provider parameters. + * @return {string} The constructed OAuth URL string. + * @override + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.toString = function() { + var uri = goog.Uri.create( + fireauth.iframeclient.SCHEME, + null, + this.authDomain_, + fireauth.iframeclient.PORT_NUMBER, + '/__/auth/handler', + null, + null); + uri.setParameterValue('apiKey', this.apiKey_); + uri.setParameterValue('appName', this.appName_); + uri.setParameterValue('authType', this.authType_); + + // Add custom parameters for OAuth1/OAuth2 providers. + if (this.provider_['isOAuthProvider']) { + // Set default language if available and no language already set. + /** @type {!fireauth.FederatedProvider} */ (this.provider_) + .setDefaultLanguage(this.getAuthLanguage_()); + uri.setParameterValue('providerId', this.provider_['providerId']); + var customParameters = /** @type {!fireauth.FederatedProvider} */ ( + this.provider_).getCustomParameters(); + if (!goog.object.isEmpty(customParameters)) { + uri.setParameterValue( + 'customParameters', + /** @type {string} */ (fireauth.util.stringifyJSON(customParameters)) + ); + } + } + + // Add scopes for OAuth2 providers. + if (typeof this.provider_.getScopes === 'function') { + var scopes = this.provider_.getScopes(); + if (scopes.length) { + uri.setParameterValue('scopes', scopes.join(',')); + } + } + + if (this.redirectUrl_) { + uri.setParameterValue('redirectUrl', this.redirectUrl_); + } else { + uri.removeParameter('redirectUrl'); + } + if (this.eventId_) { + uri.setParameterValue('eventId', this.eventId_); + } else { + uri.removeParameter('eventId'); + } + // Pass the client version if available. + if (this.v_) { + uri.setParameterValue('v', this.v_); + } else { + uri.removeParameter('v'); + } + if (this.additionalParams_) { + for (var key in this.additionalParams_) { + if (this.additionalParams_.hasOwnProperty(key) && + // Don't overwrite other existing parameters. + !uri.getParameterValue(key)) { + uri.setParameterValue(key, this.additionalParams_[key]); + } + } + } + // Pass the endpoint ID if available. + if (this.endpointId_) { + uri.setParameterValue('eid', this.endpointId_); + } else { + uri.removeParameter('eid'); + } + // Append any framework IDs to the handler URL to log in handler RPC requests. + var frameworks = this.getAuthFrameworks_(); + if (frameworks.length) { + uri.setParameterValue('fw', frameworks.join(',')); + } + return uri.toString(); +}; + + +/** + * Returns the current Auth instance's language code. + * @return {?string} The corresponding language code. + * @private + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.getAuthLanguage_ = function() { + try { + // Get the Auth instance for the current App identified by the App name. + // This could fail if, for example, the App instance was deleted. + return firebase['app'](this.appName_)['auth']().getLanguageCode(); + } catch (e) { + return null; + } +}; + + +/** + * Returns the list of Firebase frameworks used for logging purposes. + * @return {!Array} The list of corresponding Firebase frameworks. + * @private + */ +fireauth.iframeclient.OAuthUrlBuilder.prototype.getAuthFrameworks_ = + function() { + return fireauth.iframeclient.OAuthUrlBuilder.getAuthFrameworksForApp_( + this.appName_); +}; + + +/** + * Returns the list of Firebase frameworks used for logging purposes + * corresponding to the Firebase App name provided. + * @param {string} appName The Firebase App name. + * @return {!Array} The list of corresponding Firebase frameworks. + * @private + */ +fireauth.iframeclient.OAuthUrlBuilder.getAuthFrameworksForApp_ = + function(appName) { + try { + // Get the Auth instance's list of Firebase framework IDs for the current + // App identified by the App name. + // This could fail if, for example, the App instance was deleted. + return firebase['app'](appName)['auth']().getFramework(); + } catch (e) { + return []; + } +}; + + + +/** + * Initializes the ifcHandler which provides the mechanism to listen to Auth + * events on the hidden iframe. + * @param {string} authDomain The firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The App ID for the Auth instance that triggered this + * request. + * @param {?string=} opt_clientVersion The optional client version string. + * @param {?string=} opt_endpointId The endpoint ID (staging, test Gaia, etc). + * @constructor + * @implements {fireauth.OAuthSignInHandler} + */ +fireauth.iframeclient.IfcHandler = function(authDomain, apiKey, appName, + opt_clientVersion, opt_endpointId) { + /** @private {string} The Auth domain. */ + this.authDomain_ = authDomain; + /** @private {string} The API key. */ + this.apiKey_ = apiKey; + /** @private {string} The App name. */ + this.appName_ = appName; + /** @private {?string} The client version. */ + this.clientVersion_ = opt_clientVersion || null; + /** @private {?string} The Auth endpoint ID. */ + this.endpointId_ = opt_endpointId || null; + // Delay RPC handler and iframe URL initialization until needed to ensure + // logged frameworks are propagated to the iframe. + /** @private {?string} The full client version string. */ + this.fullClientVersion_ = null; + /** @private {?string} The iframe URL. */ + this.iframeUrl_ = null; + /** @private {?fireauth.RpcHandler} The RPC handler for provided API key. */ + this.rpcHandler_ = null; + /** + * @private {!Array} The Auth event + * listeners. + */ + this.authEventListeners_ = []; + // Delay origin validator determination until needed, so the error is not + // thrown in the background. This will also prevent the getProjectConfig RPC + // until it is required. + /** @private {?goog.Promise} The origin validator. */ + this.originValidator_ = null; + /** @private {?goog.Promise} The initialization promise. */ + this.isInitialized_ = null; +}; + + +/** + * Validates the provided URL. + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler used to validate the + * requested origin. + * @param {string=} opt_origin The optional page origin. If not provided, the + * window.location.href value is used. + * @return {!goog.Promise} The promise that resolves if the provided origin is + * valid. + * @private + */ +fireauth.iframeclient.IfcHandler.getOriginValidator_ = + function(rpcHandler, opt_origin) { + var origin = opt_origin || fireauth.util.getCurrentUrl(); + return rpcHandler.getAuthorizedDomains().then(function(authorizedDomains) { + if (!fireauth.util.isAuthorizedDomain(authorizedDomains, origin)) { + throw new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + } + }); +}; + + +/** + * Initializes the iframe client wrapper. + * @return {!goog.Promise} The promise that resolves on initialization. + */ +fireauth.iframeclient.IfcHandler.prototype.initialize = function() { + // Already initialized. + if (this.isInitialized_) { + return this.isInitialized_; + } + var self = this; + this.isInitialized_ = fireauth.util.onDomReady().then(function() { + /** + * @private {!fireauth.iframeclient.IframeWrapper} The iframe wrapper + * instance. + */ + self.iframeWrapper_ = new fireauth.iframeclient.IframeWrapper( + self.getIframeUrl()); + // Register all event listeners to Auth event messages sent from Auth + // iframe. + self.registerEvents_(); + }); + return this.isInitialized_; +}; + + +/** + * Waits for popup window to close. When closed start timeout listener for popup + * pending promise. If in the process, it was detected that the iframe does not + * support web storage, the popup is closed and the web storage unsupported + * error is thrown. + * @param {!Window} popupWin The popup window. + * @param {!function(!fireauth.AuthError)} onError The on error callback. + * @param {number} timeoutDuration The time to wait in ms after the popup is + * closed before triggering the popup closed by user error. + * @return {!goog.Promise} + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.startPopupTimeout = + function(popupWin, onError, timeoutDuration) { + // Expire pending timeout promise for popup operation. + var popupClosedByUserError = new fireauth.AuthError( + fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + // If web storage is disabled in the iframe, expire popup timeout quickly with + // this error. + var webStorageNotSupportedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var self = this; + var isResolved = false; + // Wait for the iframe to be ready first. + return this.initializeAndWait().then(function() { + // We do not return isWebStorageSupported() to ensure that this is backward + // compatible. + // Pushing the following client changes before updating the iframe to + // respond to these events would continue to work. + // The downside is that the popup could be closed before this resolves. + // In that case, they would get an error that the popup was closed and not + // the error that web storage is not supported, though that is unlikely + // as isWebStorageSupported should execute faster than the popup timeout. + // If web storage is not supported in the iframe, fail quickly. + self.isWebStorageSupported().then(function(isSupported) { + if (!isSupported) { + // If not supported, close window. + if (popupWin) { + fireauth.util.closeWindow(popupWin); + } + onError(webStorageNotSupportedError); + isResolved = true; + } + }); + }).thenCatch(function(error) { + // Ignore any possible error in iframe embedding. + // These types of errors will be handled in processPopup which will close + // the popup too if that happens. + return; + }).then(function() { + // Skip if already resolved. + if (isResolved) { + return; + } + // After the iframe is ready, wait for popup to close and then start timeout + // check. + return fireauth.util.onPopupClose(popupWin); + }).then(function() { + // Skip if already resolved. + if (isResolved) { + return; + } + return goog.Timer.promise(timeoutDuration).then(function() { + // If this is already resolved or rejected, this will do nothing. + onError(popupClosedByUserError); + }); + }); +}; + + +/** + * @return {boolean} Whether the handler should be initialized early. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.shouldBeInitializedEarly = + function() { + var ua = fireauth.util.getUserAgentString(); + // Cannot run in the background (can't wait for iframe to be embedded + // before triggering popup redirect) and is Safari (can only detect + // localStorage in iframe via change event) => embed iframe ASAP. + // Do the same for mobile browsers on iOS devices as they use the same + // Safari implementation underneath. + return !fireauth.util.runsInBackground(ua) && + !fireauth.util.iframeCanSyncWebStorage(ua); +}; + + +/** + * @return {boolean} Whether the sign-in handler in the current environment + * has volatile session storage. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.hasVolatileStorage = function() { + // Web environment with web storage enabled has stable sessionStorage. + return false; +}; + + +/** + * Processes the popup request. The popup instance must be provided externally + * and on error, the requestor must close the window. + * @param {?Window} popupWin The popup window reference. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {!function()} onInitialize The function to call on initialization. + * @param {!function(*)} onError The function to call on error. + * @param {string=} opt_eventId The optional event ID. + * @param {boolean=} opt_alreadyRedirected Whether popup is already redirected + * to final destination. + * @return {!goog.Promise} The popup window promise. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.processPopup = function( + popupWin, + mode, + provider, + onInitialize, + onError, + opt_eventId, + opt_alreadyRedirected) { + // processPopup is failing since it tries to access popup win when tab can + // not run in background. For now bypass processPopup which runs + // additional origin check not accounted above. Besides, iframe will never + // hand result to parent if origin not whitelisted. + // Error thrown by browser: Unable to establish a connection with the + // popup. It may have been blocked by the browser. + // If popup is null, startPopupTimeout will catch it without having the + // above error getting triggered due to popup access from opener. + + // Reject immediately if the popup is blocked. + if (!popupWin) { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.POPUP_BLOCKED)); + } + // Already redirected and cannot run in the background, resolve quickly while + // initializing. + if (opt_alreadyRedirected && !fireauth.util.runsInBackground()) { + // Initialize first before resolving. + this.initializeAndWait().thenCatch(function(error) { + fireauth.util.closeWindow(popupWin); + onError(error); + }); + onInitialize(); + // Already redirected. + return goog.Promise.resolve(); + } + // If origin validator not determined yet. + if (!this.originValidator_) { + this.originValidator_ = + fireauth.iframeclient.IfcHandler.getOriginValidator_( + this.getRpcHandler_()); + } + var self = this; + return this.originValidator_.then(function() { + // After origin validation, wait for iframe to be ready before redirecting. + var onReady = self.initializeAndWait().thenCatch(function(error) { + fireauth.util.closeWindow(popupWin); + onError(error); + throw error; + }); + onInitialize(); + return onReady; + }).then(function() { + // Popup and redirect operations work for OAuth providers only. + fireauth.AuthProvider.checkIfOAuthSupported(provider); + // Already redirected to intended destination, no need to redirect again. + if (opt_alreadyRedirected) { + return; + } + var oauthHelperWidgetUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + self.authDomain_, + self.apiKey_, + self.appName_, + mode, + provider, + null, + opt_eventId, + self.clientVersion_, + undefined, + self.endpointId_); + // Redirect popup to OAuth helper widget URL. + fireauth.util.goTo(oauthHelperWidgetUrl, /** @type {!Window} */ (popupWin)); + }).thenCatch(function(e) { + // Force another origin validation. + if (e.code == 'auth/network-request-failed') { + self.originValidator_ = null; + } + throw e; + }); +}; + + +/** + * @return {!fireauth.RpcHandler} The RPC handler instance with the relevant + * endpoints, version and frameworks. + * @private + */ +fireauth.iframeclient.IfcHandler.prototype.getRpcHandler_ = function() { + if (!this.rpcHandler_) { + this.fullClientVersion_ = this.clientVersion_ ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + this.clientVersion_, + fireauth.iframeclient.OAuthUrlBuilder.getAuthFrameworksForApp_( + this.appName_)) : + null; + this.rpcHandler_ = new fireauth.RpcHandler( + this.apiKey_, + // Get the client Auth endpoint used. + fireauth.constants.getEndpointConfig(this.endpointId_), + this.fullClientVersion_); + } + return this.rpcHandler_; +}; + + +/** + * Processes the redirect request. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {?string=} opt_eventId The optional event ID. + * @return {!goog.Promise} + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.processRedirect = + function(mode, provider, opt_eventId) { + // If origin validator not determined yet. + if (!this.originValidator_) { + this.originValidator_ = + fireauth.iframeclient.IfcHandler.getOriginValidator_( + this.getRpcHandler_()); + } + var self = this; + // Make sure origin is validated. + return this.originValidator_.then(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + var oauthHelperWidgetUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + self.authDomain_, + self.apiKey_, + self.appName_, + mode, + provider, + fireauth.util.getCurrentUrl(), + opt_eventId, + self.clientVersion_, + undefined, + self.endpointId_); + // Redirect to OAuth helper widget URL. + fireauth.util.goTo(oauthHelperWidgetUrl); + }).thenCatch(function(e) { + // Force another origin validation on network errors. + if (e.code == 'auth/network-request-failed') { + self.originValidator_ = null; + } + throw e; + }); +}; + + +/** @return {string} The iframe URL. */ +fireauth.iframeclient.IfcHandler.prototype.getIframeUrl = function() { + if (!this.iframeUrl_) { + this.iframeUrl_ = fireauth.iframeclient.IfcHandler.getAuthIframeUrl( + this.authDomain_, this.apiKey_, this.appName_, this.clientVersion_, + this.endpointId_, + fireauth.iframeclient.OAuthUrlBuilder.getAuthFrameworksForApp_( + this.appName_)); + } + return this.iframeUrl_; +}; + + +/** + * @return {!goog.Promise} The promise that resolves when the iframe is ready. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.initializeAndWait = function() { + // Initialize if not initialized yet. + var self = this; + return this.initialize().then(function() { + return self.iframeWrapper_.onReady(); + }).thenCatch(function(error) { + // Reset origin validator. + self.originValidator_ = null; + // Reject iframe ready promise with network error. + throw new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + }); +}; + + +/** + * @return {boolean} Whether the handler will unload the current page on + * redirect operations. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.unloadsOnRedirect = function() { + return true; +}; + + +/** + * @param {string} authDomain The Firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The App ID for the Auth instance that triggered this + * request. + * @param {?string=} opt_clientVersion The optional client version string. + * @param {?string=} opt_endpointId The endpoint ID (staging, test Gaia, etc). + * @param {?Array=} opt_frameworks The optional list of framework IDs. + * @return {string} The data iframe src URL. + */ +fireauth.iframeclient.IfcHandler.getAuthIframeUrl = function(authDomain, apiKey, + appName, opt_clientVersion, opt_endpointId, opt_frameworks) { + // OAuth helper iframe URL. + var builder = new fireauth.iframeclient.IframeUrlBuilder( + authDomain, apiKey, appName); + return builder + .setVersion(opt_clientVersion) + .setEndpointId(opt_endpointId) + .setFrameworks(opt_frameworks) + .toString(); +}; + + +/** + * @param {string} authDomain The Firebase authDomain used to determine the + * OAuth helper page domain. + * @param {string} apiKey The API key for sending backend Auth requests. + * @param {string} appName The App ID for the Auth instance that triggered this + * request. + * @param {string} authType The type of operation that depends on OAuth sign in. + * @param {!fireauth.AuthProvider} provider The provider to sign in to. + * @param {?string=} opt_redirectUrl The optional URL to redirect to on OAuth + * sign in completion. + * @param {?string=} opt_eventId The optional event ID to identify on receipt. + * @param {?string=} opt_clientVersion The optional client version string. + * @param {?Object=} opt_additionalParams The optional + * additional parameters. + * @param {?string=} opt_endpointId The endpoint ID (staging, test Gaia, etc). + * @return {string} The OAuth helper widget URL. + */ +fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl = function( + authDomain, + apiKey, + appName, + authType, + provider, + opt_redirectUrl, + opt_eventId, + opt_clientVersion, + opt_additionalParams, + opt_endpointId) { + // OAuth helper widget URL. + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + authDomain, apiKey, appName, authType, provider); + return builder + .setRedirectUrl(opt_redirectUrl) + .setEventId(opt_eventId) + .setVersion(opt_clientVersion) + .setAdditionalParameters(opt_additionalParams) + .setEndpointId(opt_endpointId) + .toString(); +}; + + +/** + * Post message receiver event names. + * @enum {string} + */ +fireauth.iframeclient.IfcHandler.ReceiverEvent = { + AUTH_EVENT: 'authEvent' +}; + + +/** + * Post message sender event names. + * @enum {string} + */ +fireauth.iframeclient.IfcHandler.SenderEvent = { + WEB_STORAGE_SUPPORT_EVENT: 'webStorageSupport' +}; + + +/** + * Post message response field names. + * @enum {string} + */ +fireauth.iframeclient.IfcHandler.Response = { + STATUS: 'status', + AUTH_EVENT: 'authEvent', + WEB_STORAGE_SUPPORT: 'webStorageSupport' +}; + + +/** + * Post message status values. + * @enum {string} + */ +fireauth.iframeclient.IfcHandler.Status = { + ACK: 'ACK', + ERROR: 'ERROR' +}; + + +/** + * Registers all event listeners. + * @private + */ +fireauth.iframeclient.IfcHandler.prototype.registerEvents_ = function() { + // Should be run in initialization. + if (!this.iframeWrapper_) { + throw new Error('IfcHandler must be initialized!'); + } + var self = this; + // Listen to Auth change events emitted from iframe. + this.iframeWrapper_.registerEvent( + fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT, + function(response) { + var resolveResponse = {}; + if (response && + response[fireauth.iframeclient.IfcHandler.Response.AUTH_EVENT]) { + var isHandled = false; + // Get Auth event (plain object). + var authEvent = fireauth.AuthEvent.fromPlainObject( + response[fireauth.iframeclient.IfcHandler.Response.AUTH_EVENT]); + // Trigger Auth change on all listeners. + for (var i = 0; i < self.authEventListeners_.length; i++) { + isHandled = self.authEventListeners_[i](authEvent) || isHandled; + } + // Return ack response to notify sender of success. + resolveResponse = {}; + resolveResponse[fireauth.iframeclient.IfcHandler.Response.STATUS] = + isHandled ? fireauth.iframeclient.IfcHandler.Status.ACK : + fireauth.iframeclient.IfcHandler.Status.ERROR; + return goog.Promise.resolve(resolveResponse); + } + // Return error status if the response is invalid. + resolveResponse[fireauth.iframeclient.IfcHandler.Response.STATUS] = + fireauth.iframeclient.IfcHandler.Status.ERROR; + return goog.Promise.resolve(resolveResponse); + }); +}; + + +/** + * @return {!goog.Promise} Whether web storage is supported in the + * iframe. + */ +fireauth.iframeclient.IfcHandler.prototype.isWebStorageSupported = function() { + var webStorageSupportEvent = + fireauth.iframeclient.IfcHandler.SenderEvent.WEB_STORAGE_SUPPORT_EVENT; + var message = { + 'type': webStorageSupportEvent + }; + var self = this; + // Initialize if not initialized yet. + return this.initialize().then(function() { + return self.iframeWrapper_.sendMessage(message); + }).then(function(response) { + // Parse the response and return the passed web storage support status. + var key = fireauth.iframeclient.IfcHandler.Response.WEB_STORAGE_SUPPORT; + if (response && + response.length && + typeof response[0][key] !== 'undefined') { + return response[0][key]; + } + // Internal error. + throw new Error; + }); +}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to add. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.addAuthEventListener = + function(listener) { + this.authEventListeners_.push(listener); +}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to remove. + * @override + */ +fireauth.iframeclient.IfcHandler.prototype.removeAuthEventListener = + function(listener) { + goog.array.removeAllIf(this.authEventListeners_, function(ele) { + return ele == listener; + }); +}; diff --git a/packages/auth/src/iframeclient/iframewrapper.js b/packages/auth/src/iframeclient/iframewrapper.js new file mode 100644 index 00000000000..c92d289b617 --- /dev/null +++ b/packages/auth/src/iframeclient/iframewrapper.js @@ -0,0 +1,314 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines fireauth.iframeclient.IframeWrapper used to communicate + * with the hidden iframe to detect Auth events. + */ + +goog.provide('fireauth.iframeclient.IframeWrapper'); + +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.net.jsloader'); +goog.require('goog.string.Const'); + + +/** + * Defines the hidden iframe wrapper for cross origin communications. + * @param {string} url The hidden iframe src URL. + * @constructor + */ +fireauth.iframeclient.IframeWrapper = function(url) { + /** @private {string} The hidden iframe URL. */ + this.url_ = url; + + /** + * @type {?gapi.iframes.Iframe} + * @private + */ + this.iframe_ = null; + + /** @private {!goog.Promise} A promise that resolves on iframe open. */ + this.onIframeOpen_ = this.open_(); +}; + + +/** + * @typedef {{ + * type: string + * }} + */ +fireauth.iframeclient.IframeWrapper.Message; + +/** + * Returns URL, src of the hidden iframe. + * @return {string} + * @private + */ +fireauth.iframeclient.IframeWrapper.prototype.getPath_ = function() { + return this.url_; +}; + + +/** + * @return {!goog.Promise} The promise that resolves when the iframe is ready. + */ +fireauth.iframeclient.IframeWrapper.prototype.onReady = function() { + return this.onIframeOpen_; +}; + + +/** + * Returns options used to open the iframe. + * @return {!gapi.iframes.OptionsBag} + * @private + */ +fireauth.iframeclient.IframeWrapper.prototype.getOptions_ = function() { + var options = /** @type {!gapi.iframes.OptionsBag} */ ({ + 'where': document.body, + 'url': this.getPath_(), + 'messageHandlersFilter': /** @type {!gapi.iframes.IframesFilter} */ ( + fireauth.util.getObjectRef( + 'gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER')), + 'attributes': { + 'style': { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + } + }, + 'dontclear': true + }); + return options; +}; + + +/** + * Opens an iframe. + * @return {!goog.Promise} A promise that resolves on successful iframe open. + * @private + */ +fireauth.iframeclient.IframeWrapper.prototype.open_ = function() { + var self = this; + return fireauth.iframeclient.IframeWrapper.loadGApiJs_().then(function() { + return new goog.Promise(function(resolve, reject) { + /** + * @param {?gapi.iframes.Iframe} iframe The new opened iframe. + */ + var onOpen = function(iframe) { + self.iframe_ = iframe; + self.iframe_.restyle({ + // Prevent iframe from closing on mouse out. + 'setHideOnLeave': false + }); + // Confirm iframe is correctly loaded. + // To fallback on failure, set a timeout. + var networkErrorTimer = setTimeout(function() { + reject(new Error('Network Error')); + }, fireauth.iframeclient.IframeWrapper.PING_TIMEOUT_.get()); + // Clear timer and resolve pending iframe ready promise. + var clearTimerAndResolve = function() { + clearTimeout(networkErrorTimer); + resolve(); + }; + // This returns an IThenable. However the reject part does not call + // when the iframe is not loaded. + iframe.ping(clearTimerAndResolve).then( + clearTimerAndResolve, + function(error) { reject(new Error('Network Error')); }); + }; + /** @type {function():!gapi.iframes.Context} */ ( + fireauth.util.getObjectRef('gapi.iframes.getContext'))().open( + self.getOptions_(), onOpen); + }); + }); +}; + + +/** + * @param {!fireauth.iframeclient.IframeWrapper.Message} message to send. + * @return {!goog.Promise} The promise that resolve when message is + * sent. + */ +fireauth.iframeclient.IframeWrapper.prototype.sendMessage = function(message) { + var self = this; + return this.onIframeOpen_.then(function() { + return new goog.Promise(function(resolve, reject) { + self.iframe_.send( + message['type'], + message, + resolve, + /** @type {!gapi.iframes.IframesFilter} */ ( + fireauth.util.getObjectRef( + 'gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER'))); + + }); + }); +}; + + +/** + * Registers a listener to a post message. + * @param {string} eventName The message to register for. + * @param {gapi.iframes.MessageHandler} handler Message handler. + */ +fireauth.iframeclient.IframeWrapper.prototype.registerEvent = + function(eventName, handler) { + var self = this; + this.onIframeOpen_.then(function() { + self.iframe_.register( + eventName, + handler, + /** @type {!gapi.iframes.IframesFilter} */ ( + fireauth.util.getObjectRef( + 'gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER'))); + }); +}; + + +/** + * Unregisters a listener to a post message. + * @param {string} eventName The message to unregister. + * @param {gapi.iframes.MessageHandler} handler Message handler. + */ +fireauth.iframeclient.IframeWrapper.prototype.unregisterEvent = + function(eventName, handler) { + var self = this; + this.onIframeOpen_.then(function() { + self.iframe_.unregister(eventName, handler); + }); +}; + + + +/** @private @const {!goog.string.Const} The GApi loader URL. */ +fireauth.iframeclient.IframeWrapper.GAPI_LOADER_SRC_ = goog.string.Const.from( + 'https://apis.google.com/js/api.js?onload=%{onload}'); + + +/** + * @private @const {!fireauth.util.Delay} The gapi.load network error timeout + * delay with units in ms. + */ +fireauth.iframeclient.IframeWrapper.NETWORK_TIMEOUT_ = + new fireauth.util.Delay(30000, 60000); + + +/** + * @private @const {!fireauth.util.Delay} The iframe ping error timeout delay + * with units in ms. + */ +fireauth.iframeclient.IframeWrapper.PING_TIMEOUT_ = + new fireauth.util.Delay(5000, 15000); + + +/** @private {?goog.Promise} The cached GApi loader promise. */ +fireauth.iframeclient.IframeWrapper.cachedGApiLoader_ = null; + + +/** Resets the cached GApi loader. */ +fireauth.iframeclient.IframeWrapper.resetCachedGApiLoader = function() { + fireauth.iframeclient.IframeWrapper.cachedGApiLoader_ = null; +}; + + + +/** + * Loads the GApi client library if it is not loaded for gapi.iframes usage. + * @return {!goog.Promise} A promise that resolves when gapi.iframes is loaded. + * @private + */ +fireauth.iframeclient.IframeWrapper.loadGApiJs_ = function() { + // If already pending or resolved, return the cached promise. + if (fireauth.iframeclient.IframeWrapper.cachedGApiLoader_) { + return fireauth.iframeclient.IframeWrapper.cachedGApiLoader_; + } + // If there is no cached promise, initialize a new one. + fireauth.iframeclient.IframeWrapper.cachedGApiLoader_ = + new goog.Promise(function(resolve, reject) { + // Offline, fail quickly instead of waiting for request to timeout. + if (!fireauth.util.isOnline()) { + reject(new Error('Network Error')); + return; + } + // Function to run when gapi.load is ready. + var onGapiLoad = function() { + // The developer may have tried to previously run gapi.load and failed. + // Run this to fix that. + fireauth.util.resetUnloadedGapiModules(); + var loader = /** @type {function(string, !Object)} */ ( + fireauth.util.getObjectRef('gapi.load')); + loader('gapi.iframes', { + 'callback': resolve, + 'ontimeout': function() { + // The above reset may be sufficient, but having this reset after + // failure ensures that if the developer calls gapi.load after the + // connection is re-established and before another attempt to embed + // the iframe, it would work and would not be broken because of our + // failed attempt. + // Timeout when gapi.iframes.Iframe not loaded. + fireauth.util.resetUnloadedGapiModules(); + reject(new Error('Network Error')); + }, + 'timeout': fireauth.iframeclient.IframeWrapper.NETWORK_TIMEOUT_.get() + }); + }; + if (fireauth.util.getObjectRef('gapi.iframes.Iframe')) { + // If gapi.iframes.Iframe available, resolve. + resolve(); + } else if (fireauth.util.getObjectRef('gapi.load')) { + // Gapi loader ready, load gapi.iframes. + onGapiLoad(); + } else { + // Create a new iframe callback when this is called so as not to overwrite + // any previous defined callback. This happens if this method is called + // multiple times in parallel and could result in the later callback + // overwriting the previous one. This would end up with a iframe + // timeout. + var cbName = '__iframefcb' + + Math.floor(Math.random() * 1000000).toString(); + // GApi loader not available, dynamically load platform.js. + goog.global[cbName] = function() { + // GApi loader should be ready. + if (fireauth.util.getObjectRef('gapi.load')) { + onGapiLoad(); + } else { + // Gapi loader failed, throw error. + reject(new Error('Network Error')); + } + }; + // Build GApi loader. + var url = goog.html.TrustedResourceUrl.format( + fireauth.iframeclient.IframeWrapper.GAPI_LOADER_SRC_, + {'onload': cbName}); + // Load GApi loader. + var result = goog.Promise.resolve(goog.net.jsloader.safeLoad(url)); + result.thenCatch(function(error) { + // In case library fails to load, typically due to a network error, + // reset cached loader to null to force a refresh on a retrial. + reject(new Error('Network Error')); + }); + } + }).thenCatch(function(error) { + // Reset cached promise to allow for retrial. + fireauth.iframeclient.IframeWrapper.cachedGApiLoader_ = null; + throw error; + }); + return fireauth.iframeclient.IframeWrapper.cachedGApiLoader_; +}; diff --git a/packages/auth/src/oauthhelperstate.js b/packages/auth/src/oauthhelperstate.js new file mode 100644 index 00000000000..6312b130ba3 --- /dev/null +++ b/packages/auth/src/oauthhelperstate.js @@ -0,0 +1,200 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the OAuth helper widget state. + */ + +goog.provide('fireauth.OAuthHelperState'); + +goog.require('fireauth.AuthEvent'); + + +/** + * Defines the OAuth helper widget state. + * @param {string} apiKey The API key. + * @param {string} appName The App name. + * @param {!fireauth.AuthEvent.Type} type The OAuth helper mode + * @param {?string=} opt_eventId The event identifier. + * @param {?string=} opt_redirectUrl The optional redirect URL for redirect + * mode. + * @param {?string=} opt_clientVersion The optional client version. + * @param {?string=} opt_displayName The application display name. + * @param {?string=} opt_apn The optional Android package name. + * @param {?string=} opt_ibi The optional iOS bundle ID. + * @param {?string=} opt_eid The optional Auth endpoint ID. + * @param {?Array=} opt_frameworks The optional list of framework IDs. + * @param {?string=} opt_clientId The optional OAuth client ID. + * @param {?string=} opt_sha1Cert The optional SHA-1 hash of Android cert. + * @constructor + */ +fireauth.OAuthHelperState = function( + apiKey, appName, type, opt_eventId, opt_redirectUrl, opt_clientVersion, + opt_displayName, opt_apn, opt_ibi, opt_eid, opt_frameworks, opt_clientId, + opt_sha1Cert) { + /** @private {string} The API key. */ + this.apiKey_ = apiKey; + /** @private {string} The App name. */ + this.appName_ = appName; + /** @private {!fireauth.AuthEvent.Type} The OAuth helper mode. */ + this.type_ = type; + /** @private {?string} The event identifier. */ + this.eventId_ = opt_eventId || null; + /** @private {?string} The redirect URL for redirect mode. */ + this.redirectUrl_ = opt_redirectUrl || null; + /** @private {?string} The client version. */ + this.clientVersion_ = opt_clientVersion || null; + /** @private {?string} The application display name. */ + this.displayName_ = opt_displayName || null; + /** @private {?string} The Android package name. */ + this.apn_ = opt_apn || null; + /** @private {?string} The iOS bundle ID. */ + this.ibi_ = opt_ibi || null; + /** @private {?string} The endpoint ID. */ + this.eid_ = opt_eid || null; + /** @private {!Array} The list of framework IDs. */ + this.frameworks_ = opt_frameworks || []; + /** @private {?string} The OAuth client ID. */ + this.clientId_ = opt_clientId || null; + /** @private {?string} The SHA-1 hash of Android cert. */ + this.sha1Cert_ = opt_sha1Cert || null; +}; + + +/** @return {?string} The OAuth client ID. */ +fireauth.OAuthHelperState.prototype.getClientId = function() { + return this.clientId_; +}; + + +/** @return {?string} The SHA-1 hash of the Android cert. */ +fireauth.OAuthHelperState.prototype.getSha1Cert = function() { + return this.sha1Cert_; +}; + + +/** @return {!fireauth.AuthEvent.Type} The type of Auth event. */ +fireauth.OAuthHelperState.prototype.getType = function() { + return this.type_; +}; + + +/** @return {?string} The Auth event identifier. */ +fireauth.OAuthHelperState.prototype.getEventId = function() { + return this.eventId_; +}; + + +/** @return {string} The API key. */ +fireauth.OAuthHelperState.prototype.getApiKey = function() { + return this.apiKey_; +}; + + +/** @return {string} The App name. */ +fireauth.OAuthHelperState.prototype.getAppName = function() { + return this.appName_; +}; + + +/** @return {?string} The redirect URL. */ +fireauth.OAuthHelperState.prototype.getRedirectUrl = function() { + return this.redirectUrl_; +}; + + +/** @return {?string} The client version. */ +fireauth.OAuthHelperState.prototype.getClientVersion = function() { + return this.clientVersion_; +}; + + +/** @return {?string} The application display name if available. */ +fireauth.OAuthHelperState.prototype.getDisplayName = function() { + return this.displayName_; +}; + + +/** @return {?string} The Android package name. */ +fireauth.OAuthHelperState.prototype.getApn = function() { + return this.apn_; +}; + + +/** @return {?string} The iOS bundle ID. */ +fireauth.OAuthHelperState.prototype.getIbi = function() { + return this.ibi_; +}; + + +/** @return {?string} The Auth endpoint ID. */ +fireauth.OAuthHelperState.prototype.getEndpointId = function() { + return this.eid_; +}; + + +/** @return {!Array} The list of framework IDs. */ +fireauth.OAuthHelperState.prototype.getFrameworks = function() { + return this.frameworks_; +}; + + +/** @return {!Object} The plain object representation of OAuth helper state. */ +fireauth.OAuthHelperState.prototype.toPlainObject = function() { + return { + 'apiKey': this.apiKey_, + 'appName': this.appName_, + 'type': this.type_, + 'eventId': this.eventId_, + 'redirectUrl': this.redirectUrl_, + 'clientVersion': this.clientVersion_, + 'displayName': this.displayName_, + 'apn': this.apn_, + 'ibi': this.ibi_, + 'eid': this.eid_, + 'fw': this.frameworks_, + 'clientId': this.clientId_, + 'sha1Cert': this.sha1Cert_ + }; +}; + + +/** + * @param {?Object} rawResponse The plain object representation of OAuth helper + * state. + * @return {?fireauth.OAuthHelperState} The OAuth helper state representation of + * plain object. + */ +fireauth.OAuthHelperState.fromPlainObject = function(rawResponse) { + var response = rawResponse || {}; + if (response['type'] && response['apiKey']) { + return new fireauth.OAuthHelperState( + response['apiKey'], + response['appName'] || '', + response['type'], + response['eventId'], + response['redirectUrl'], + response['clientVersion'], + response['displayName'], + response['apn'], + response['ibi'], + response['eid'], + response['fw'], + response['clientId'], + response['sha1Cert']); + } + return null; +}; diff --git a/packages/auth/src/oauthsigninhandler.js b/packages/auth/src/oauthsigninhandler.js new file mode 100644 index 00000000000..80b6292b775 --- /dev/null +++ b/packages/auth/src/oauthsigninhandler.js @@ -0,0 +1,113 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the OAuthSignInHandler interface used to start OAuth + * sign-in flows via popup and redirect and detect incoming OAuth responses. + */ + +goog.provide('fireauth.OAuthSignInHandler'); + +/** + * The interface that represents an OAuth sign-in handler. + * @interface + */ +fireauth.OAuthSignInHandler = function() {}; + + +/** + * @return {boolean} Whether the handler should be initialized early. + */ +fireauth.OAuthSignInHandler.prototype.shouldBeInitializedEarly = function() {}; + + +/** + * @return {boolean} Whether the sign-in handler in the current environment + * has volatile session storage. + */ +fireauth.OAuthSignInHandler.prototype.hasVolatileStorage = function() {}; + + +/** + * @return {!goog.Promise} The promise that resolves when the handler is + * initialized and ready. + */ +fireauth.OAuthSignInHandler.prototype.initializeAndWait = function() {}; + + +/** + * Processes the OAuth popup request. The popup instance must be provided + * externally and on error, the requestor must close the window. + * @param {?Window} popupWin The popup window reference. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {!function()} onInitialize The function to call on initialization. + * @param {!function(*)} onError The function to call on error. + * @param {string=} opt_eventId The optional event ID. + * @param {boolean=} opt_alreadyRedirected Whether popup is already redirected + * to final destination. + * @return {!goog.Promise} The popup window promise. + */ +fireauth.OAuthSignInHandler.prototype.processPopup = function(popupWin, mode, + provider, onInitialize, onError, opt_eventId, opt_alreadyRedirected) {}; + + +/** + * Processes the OAuth redirect request. + * @param {!fireauth.AuthEvent.Type} mode The Auth event type. + * @param {!fireauth.AuthProvider} provider The Auth provider to sign in with. + * @param {?string=} opt_eventId The optional event ID. + * @return {!goog.Promise} + */ +fireauth.OAuthSignInHandler.prototype.processRedirect = + function(mode, provider, opt_eventId) {}; + + +/** + * @return {boolean} Whether the handler will unload the current page on + * redirect operations. + */ +fireauth.OAuthSignInHandler.prototype.unloadsOnRedirect = function() {}; + + +/** + * Waits for popup window to close. When closed start timeout listener for popup + * pending promise. If in the process, an error is detected, the error is + * funnelled back and the popup is closed. + * @param {!Window} popupWin The popup window. + * @param {!function(!fireauth.AuthError)} onError The on error callback. + * @param {number} timeoutDuration The time to wait in ms after the popup is + * closed before triggering the popup closed by user error. + * @return {!goog.Promise} + */ +fireauth.OAuthSignInHandler.prototype.startPopupTimeout = + function(popupWin, onError, timeoutDuration) {}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to add. + */ +fireauth.OAuthSignInHandler.prototype.addAuthEventListener = + function(listener) {}; + + +/** + * @param {!function(?fireauth.AuthEvent):boolean} listener The Auth event + * listener to remove. + */ +fireauth.OAuthSignInHandler.prototype.removeAuthEventListener = + function(listener) {}; diff --git a/packages/auth/src/object.js b/packages/auth/src/object.js new file mode 100644 index 00000000000..dce3c329d6f --- /dev/null +++ b/packages/auth/src/object.js @@ -0,0 +1,214 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provides methods for manipulating objects. + */ + +goog.provide('fireauth.object'); + +goog.require('fireauth.deprecation'); +goog.require('fireauth.deprecation.Deprecations'); + + +/** + * Checks whether the defineProperty method allows to change the value of + * the property. + * @return {boolean} Whether the defineProperty method allows to change the + * value of the property. + * @private + */ +fireauth.object.isReadonlyConfigurable_ = function() { + // Android 2.3 stock browser doesn't allow to change the value of + // a read-only property once defined. + try { + var obj = {}; + Object.defineProperty(obj, 'abcd', { + configurable: true, + enumerable: true, + value: 1 + }); + Object.defineProperty(obj, 'abcd', { + configurable: true, + enumerable: true, + value: 2 + }); + return obj['abcd'] == 2; + } catch (e) { + return false; + } +}; + + +/** + * @private {boolean} Whether the defineProperty method allows to change the + * value of the property. + */ +fireauth.object.readonlyConfigurable_ = + fireauth.object.isReadonlyConfigurable_(); + + +/** + * Defines a property on an object that is not writable by clients. However, the + * property can be overwritten within the Firebase library through subsequent + * calls to setReadonlyProperty. + * + * In browsers that do not support read-only properties (notably IE8 and below), + * fall back to writable properties. + * + * @param {!Object} obj The object to which we add the property. + * @param {string} key The name of the property. + * @param {*} value The desired value. + */ +fireauth.object.setReadonlyProperty = function(obj, key, value) { + if (fireauth.object.readonlyConfigurable_) { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: true, + value: value + }); + } else { + obj[key] = value; + } +}; + + +/** + * Defines a deprecated property, which emits a warning if the developer tries + * to use it. + * + * In browsers that do not support getters, we fall back to a normal property + * with no message. + * + * @param {!Object} obj The object to which we add the property. + * @param {string} key The name of the deprecated property. + * @param {*} value The desired value. + * @param {!fireauth.deprecation.Deprecations} deprecationMessage The + * deprecation warning to display. + */ +fireauth.object.setDeprecatedReadonlyProperty = function(obj, key, value, + deprecationMessage) { + if (fireauth.object.readonlyConfigurable_) { + Object.defineProperty(obj, key, { + configurable: true, + enumerable: true, + get: function() { + fireauth.deprecation.log(deprecationMessage); + return value; + } + }); + } else { + obj[key] = value; + } +}; + + +/** + * Defines properties on an object that are not writable by clients, equivalent + * to many calls to setReadonlyProperty. + * @param {!Object} obj The object to which we add the properties. + * @param {?Object} props An object that maps the keys and values + * that we wish to add. + */ +fireauth.object.setReadonlyProperties = function(obj, props) { + if (!props) { + return; + } + + for (var key in props) { + if (props.hasOwnProperty(key)) { + fireauth.object.setReadonlyProperty(obj, key, props[key]); + } + } +}; + + +/** + * Makes a shallow read-only copy of an object. The writability of any child + * objects will not be affected. + * @param {?Object} obj The object that we wish to copy. + * @return {!Object} + */ +fireauth.object.makeReadonlyCopy = function(obj) { + var output = {}; + fireauth.object.setReadonlyProperties(output, obj); + return output; +}; + + +/** + * Makes a shallow writable copy of a read-only object. The writability of any + * child objects will not be affected. + * @param {?Object} obj The object that we wish to copy. + * @return {!Object} + */ +fireauth.object.makeWritableCopy = function(obj) { + var output = {}; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + output[key] = obj[key]; + } + } + return output; +}; + + +/** + * Returns true if the all the specified fields are present in obj and are not + * null, undefined, or the empty string. If the field list is empty, returns + * true regardless of the value of obj. + * @param {?Object=} opt_obj The object. + * @param {?Array=} opt_fields The desired fields of the object. + * @return {boolean} True if obj has all the specified fields. + */ +fireauth.object.hasNonEmptyFields = function(opt_obj, opt_fields) { + if (!opt_fields || !opt_fields.length) { + return true; + } + if (!opt_obj) { + return false; + } + for (var i = 0; i < opt_fields.length; i++) { + var field = opt_obj[opt_fields[i]]; + if (field === undefined || field === null || field === '') { + return false; + } + } + return true; +}; + + +/** + * Traverses the specified object and creates a read-only deep copy of it. + * This will fail when circular references are contained within the object. + * @param {*} obj The object to make a read-only copy from. + * @return {*} A Read-only copy of the obj specified. + */ +fireauth.object.unsafeCreateReadOnlyCopy = function(obj) { + var copy = obj; + if (typeof obj == 'object' && obj != null) { + // Make the right type of copy. + copy = 'length' in obj ? [] : {}; + // Make a deep copy. + for (var key in obj) { + fireauth.object.setReadonlyProperty( + copy, key, fireauth.object.unsafeCreateReadOnlyCopy(obj[key])); + } + } + // Return the copy. + return copy; +}; + diff --git a/packages/auth/src/proactiverefresh.js b/packages/auth/src/proactiverefresh.js new file mode 100644 index 00000000000..5b262a18c89 --- /dev/null +++ b/packages/auth/src/proactiverefresh.js @@ -0,0 +1,216 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility for proactive refresh with exponential backoff + * algorithm typically used to define a retry policy for certain async + * operations. + */ + +goog.provide('fireauth.ProactiveRefresh'); + +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); + + +/** + * The helper utility used to proactively refresh a certain operation based on + * certain constraints with an exponential backoff retrial policy when + * specific recoverable errors occur. Typically this is used to retry an + * operation on network errors. + * @param {!function():!goog.Promise} operation The promise returning operation + * to run. + * @param {!function(*):boolean} retryPolicy A function that takes in an error + * and returns whether a retry policy should be implemented based on the + * error. If not, the operation will not run again. + * @param {!function():number} getWaitDuration A function that returns the wait + * period before running again. + * @param {number} lowerBound The lower bound duration to wait when an error + * occurs. This is the first interval to wait before rerunning after an + * error is detected. + * @param {number} upperBound The upper bound duration to wait when an error + * keeps occurring. This upper bound should not be exceeded. + * @param {boolean=} opt_runInBackground Whether to run in the background, when + * the tab is not visible. If the refresh should only run when the app is + * visible, the operation will block until the app is visible and then run. + * @constructor @struct @final + */ +fireauth.ProactiveRefresh = function( + operation, + retryPolicy, + getWaitDuration, + lowerBound, + upperBound, + opt_runInBackground) { + /** + * @const @private {!function():!goog.Promise} The promise returning operation + * to run. + */ + this.operation_ = operation; + /** + * @const @private {!function(*):boolean} The function that takes in an error + * and returns whether a retry policy should be implemented based on the + * error caught. + */ + this.retryPolicy_ = retryPolicy; + /** + * @const @private {!function():number} The function that returns the wait + * period after a successful operation before running again. + */ + this.getWaitDuration_ = getWaitDuration; + /** + * @const @private {number} The lower bound duration to wait when an error + * first occurs. + */ + this.lowerBound_ = lowerBound; + /** + * @const @private {number} The upper bound duration to wait when an error + * keeps occurring. This upper bound should not be exceeded. + */ + this.upperBound_ = upperBound; + /** + * @const @private {boolean} Whether to run in the background, when the tab is + * not visible. + */ + this.runInBackground_ = !!opt_runInBackground; + /** + * @private {?goog.Promise} The pending promise for the next operation to run. + */ + this.pending_ = null; + /** + * @private {number} The first wait interval when a new error occurs. + */ + this.nextErrorWaitInterval_ = this.lowerBound_; + // Throw an error if the lower bound is greater than upper bound. + if (this.upperBound_ < this.lowerBound_) { + throw new Error('Proactive refresh lower bound greater than upper bound!'); + } +}; + + +/** Starts the proactive refresh based on the current configuration. */ +fireauth.ProactiveRefresh.prototype.start = function() { + // Set the next error wait interval to the lower bound. On each consecutive + // error, this will double in value until it reaches the upper bound. + this.nextErrorWaitInterval_ = this.lowerBound_; + // Start proactive refresh with clean slate (successful status). + this.process_(true); +}; + + +/** + * Returns the wait duration before the next run depending on the last run + * status. If the last operation has succeeded, returns the getWaitDuration() + * response. Otherwise, doubles the last error wait interval starting from + * lowerBound and up to upperBound. + * @param {boolean} hasSucceeded Whether last run succeeded. + * @return {number} The wait time for the next run. + * @private + */ +fireauth.ProactiveRefresh.prototype.getNextRun_ = function(hasSucceeded) { + if (hasSucceeded) { + // If last operation succeeded, reset next error wait interval and return + // the default wait duration. + this.nextErrorWaitInterval_ = this.lowerBound_; + // Return typical wait duration interval after a successful operation. + return this.getWaitDuration_(); + } else { + // Get next error wait interval. + var currentErrorWaitInterval = this.nextErrorWaitInterval_; + // Double interval for next consecutive error. + this.nextErrorWaitInterval_ *= 2; + // Make sure next wait interval does not exceed the maximum upper bound. + if (this.nextErrorWaitInterval_ > this.upperBound_) { + this.nextErrorWaitInterval_ = this.upperBound_; + } + return currentErrorWaitInterval; + } +}; + + +/** + * Processes one refresh call and sets the timer for the next call based on + * the last recorded result. + * @param {boolean} hasSucceeded Whether last run succeeded. + * @private + */ +fireauth.ProactiveRefresh.prototype.process_ = function(hasSucceeded) { + var self = this; + // Stop any other pending operation. + this.stop(); + // Wait for next scheduled run based on whether an error occurred during last + // run. + this.pending_ = goog.Timer.promise(this.getNextRun_(hasSucceeded)) + .then(function() { + // Block for conditions (if app is required to be visible) to be ready. + return self.waitUntilReady_(); + }) + .then(function() { + // Run the operation. + return self.operation_(); + }) + .then(function() { + // If successful, try again on next cycle with no previous error + // passed. + self.process_(true); + }) + .thenCatch(function(error) { + // If an error occurs, only rerun when the error meets the retry + // policy. + if (self.retryPolicy_(error)) { + // Should retry with error to trigger exponentional backoff. + self.process_(false); + } + // Any other error is considered unrecoverable. Do not try again. + }); +}; + + +/** + * Returns a promise which resolves when the current tab is visible. + * This resolves quickly if refresh is supposed to run in the background too. + * @return {!goog.Promise} The promise that resolves when the tab is visible or + * that requirement is not needed. + * @private + */ +fireauth.ProactiveRefresh.prototype.waitUntilReady_ = function() { + // Wait until app is in foreground if required. + if (this.runInBackground_) { + // If runs in background, resolve quickly. + return goog.Promise.resolve(); + } else { + // Wait for the app to be visible before resolving the promise. + return fireauth.util.onAppVisible(); + } +}; + + +/** Stops the proactive refresh from running again. */ +fireauth.ProactiveRefresh.prototype.stop = function() { + // If there is a pending promise. + if (this.pending_) { + // Cancel the pending promise and nullify it. + this.pending_.cancel(); + this.pending_ = null; + } +}; + + +/** @return {boolean} Whether the proactive refresh is running or not. */ +fireauth.ProactiveRefresh.prototype.isRunning = function() { + return !!this.pending_; +}; diff --git a/packages/auth/src/recaptchaverifier/recaptchaverifier.js b/packages/auth/src/recaptchaverifier/recaptchaverifier.js new file mode 100644 index 00000000000..058d569a490 --- /dev/null +++ b/packages/auth/src/recaptchaverifier/recaptchaverifier.js @@ -0,0 +1,605 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the reCAPTCHA app verifier and its base class. The + * former is currently used for web phone authentication whereas the latter is + * used for the mobile app verification web fallback. + */ +goog.provide('fireauth.BaseRecaptchaVerifier'); +goog.provide('fireauth.RecaptchaVerifier'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.array'); +goog.require('goog.dom'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.net.jsloader'); +goog.require('goog.string.Const'); + + +/** + * Creates the firebase base reCAPTCHA app verifier independent of Firebase + * App or Auth instances. + * + * @param {string} apiKey The API key used to initialize the RPC handler for + * querying the Auth backend. + * @param {!Element|string} container The reCAPTCHA container parameter. This + * has different meaning depending on whether the reCAPTCHA is hidden or + * visible. + * @param {?Object=} opt_parameters The optional reCAPTCHA parameters. + * @param {?(function():?string)=} opt_getLanguageCode The language code getter + * function. + * @param {?string=} opt_clientVersion The optional client version to append to + * RPC header. + * @param {?Object=} opt_rpcHandlerConfig The optional RPC handler + * configuration, typically passed when different Auth endpoints are to be + * used. + * @constructor + */ +fireauth.BaseRecaptchaVerifier = function(apiKey, container, opt_parameters, + opt_getLanguageCode, opt_clientVersion, opt_rpcHandlerConfig) { + // Set the type readonly property needed for full implementation of the + // firebase.auth.ApplicationVerifier interface. + fireauth.object.setReadonlyProperty(this, 'type', 'recaptcha'); + /** + * @private {?goog.Promise} The cached reCAPTCHA ready response. This is + * null until the first time it is triggered or when an error occurs in + * getting ready. + */ + this.cachedReadyPromise_ = null; + /** @private {?number} The reCAPTCHA widget ID. Null when not rendered. */ + this.widgetId_ = null; + /** @private {boolean} Whether the instance is already destroyed. */ + this.destroyed_ = false; + /** @private {!Element|string} The reCAPTCHA container. */ + this.container_ = container; + // If no parameters passed, use default settings. + // Currently, visible recaptcha is the default setting as invisible reCAPTCHA + // is not yet supported by the backend. + /** @private {!Object} The reCAPTCHA parameters. */ + this.parameters_ = opt_parameters || { + 'theme': 'light', + 'type': 'image' + }; + /** @private {!Array|!goog.Promise>} List of + * pending promises. */ + this.pendingPromises_ = []; + if (this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.SITEKEY]) { + // sitekey should not be provided. + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'sitekey should not be provided for reCAPTCHA as one is ' + + 'automatically provisioned for the current project.'); + } + /** @private {boolean} Whether the reCAPTCHA is invisible or not. */ + this.isInvisible_ = + this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.SIZE] === + 'invisible'; + // reCAPTCHA container must be valid and if visible, not empty. + // An invisible reCAPTCHA will not render in its container. That container + // will execute the reCAPTCHA when it is clicked. + if (!goog.dom.getElement(container) || + (!this.isInvisible_ && goog.dom.getElement(container).hasChildNodes())) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'reCAPTCHA container is either not found or already contains inner ' + + 'elements!'); + } + /** + * @private {!fireauth.RpcHandler} The RPC handler for querying the auth + * backend. + */ + this.rpcHandler_ = new fireauth.RpcHandler( + apiKey, + opt_rpcHandlerConfig || null, + opt_clientVersion || null); + /** + * @private {!function():?string} Current language code getter. + */ + this.getLanguageCode_ = opt_getLanguageCode || function() {return null;}; + var self = this; + /** + * @private {!Array} The token change listeners. + */ + this.tokenListeners_ = []; + // Wrap token callback. + var existingCallback = + this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.CALLBACK]; + this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.CALLBACK] = + function(response) { + // Dispatch internal event for the token response. + self.dispatchEvent_(response); + if (typeof existingCallback === 'function') { + existingCallback(response); + } else if (typeof existingCallback === 'string') { + // Check if the provided callback is a global function name. + var cb = fireauth.util.getObjectRef(existingCallback, goog.global); + if (typeof cb === 'function') { + // If so, trigger it. + cb(response); + } + } + }; + // Wrap expired token callback. + var existingExpiredCallback = this.parameters_[ + fireauth.BaseRecaptchaVerifier.ParamName.EXPIRED_CALLBACK]; + this.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.EXPIRED_CALLBACK] = + function() { + // Dispatch internal event for the token expiration. + self.dispatchEvent_(null); + if (typeof existingExpiredCallback === 'function') { + existingExpiredCallback(); + } else if (typeof existingExpiredCallback === 'string') { + // Check if the provided expired callback is a global function name. + var cb = fireauth.util.getObjectRef(existingExpiredCallback, goog.global); + if (typeof cb === 'function') { + // If so, trigger it. + cb(); + } + } + }; +}; + + +/** + * grecaptcha parameter names. + * @enum {string} + */ +fireauth.BaseRecaptchaVerifier.ParamName = { + CALLBACK: 'callback', + EXPIRED_CALLBACK: 'expired-callback', + SITEKEY: 'sitekey', + SIZE: 'size' +}; + + +/** + * Dispatches the token change event to all subscribed listeners. + * @param {?string} token The current detected token, null for none. + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.dispatchEvent_ = function(token) { + for (var i = 0; i < this.tokenListeners_.length; i++) { + try { + this.tokenListeners_[i](token); + } catch (e) { + // If any handler fails, ignore and run next handler. + } + } +}; + + +/** + * Add a reCAPTCHA token change listener. + * @param {!function(?string)} listener The token listener to add. + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.addTokenChangeListener_ = + function(listener) { + this.tokenListeners_.push(listener); +}; + + +/** + * Remove a reCAPTCHA token change listener. + * @param {!function(?string)} listener The token listener to remove. + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.removeTokenChangeListener_ = + function(listener) { + goog.array.removeAllIf(this.tokenListeners_, function(ele) { + return ele == listener; + }); +}; + + +/** + * Takes in a pending promise, saves it and adds a clean up callback which + * forgets the pending promise after it is fulfilled and echoes the promise + * back. + * @param {!goog.Promise<*, *>|!goog.Promise} p The pending promise. + * @return {!goog.Promise<*, *>|!goog.Promise} + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.registerPendingPromise_ = function(p) { + var self = this; + // Save created promise in pending list. + this.pendingPromises_.push(p); + p.thenAlways(function() { + // When fulfilled, remove from pending list. + goog.array.remove(self.pendingPromises_, p); + }); + // Return the promise. + return p; +}; + + +/** @return {boolean} Whether verifier instance has pending promises. */ +fireauth.BaseRecaptchaVerifier.prototype.hasPendingPromises = function() { + return this.pendingPromises_.length != 0; +}; + + +/** + * Gets the current RecaptchaVerifier in a ready state for rendering by first + * checking that the environment supports a reCAPTCHA, loading reCAPTCHA + * dependencies if not already available and then getting the Firebase project's + * provisioned reCAPTCHA configuration. + * @return {!goog.Promise} The promise that resolves when recaptcha + * is ready for rendering. + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.isReady_ = function() { + var self = this; + // If previously called, return the cached response. + if (this.cachedReadyPromise_) { + return this.cachedReadyPromise_; + } + this.cachedReadyPromise_ = this.registerPendingPromise_(goog.Promise.resolve() + .then(function() { + // Verify environment first. + // This is actually not enough as this could be triggered from a worker + // environment, but DOM ready should theoretically not resolve. + if (fireauth.util.isHttpOrHttps()) { + // Wait for DOM to be ready as this feature depends on that. + return fireauth.util.onDomReady(); + } else { + throw new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, + 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + + 'environment.'); + } + }) + .then(function() { + // Load external reCAPTCHA dependencies if not already there, taking + // into account the current language code. + return fireauth.BaseRecaptchaVerifier.Loader.getInstance() + .loadRecaptchaDeps(self.getLanguageCode_()); + }) + .then(function() { + // Load Firebase project's reCAPTCHA configuration. + return self.rpcHandler_.getRecaptchaParam(); + }) + .then(function(result) { + // Update the reCAPTCHA parameters. + self.parameters_[fireauth.BaseRecaptchaVerifier.ParamName.SITEKEY] = + result[fireauth.RpcHandler.AuthServerField.RECAPTCHA_SITE_KEY]; + }).thenCatch(function(error) { + // Anytime an error occurs, reset the cached ready promise to rerun on + // retrial. + self.cachedReadyPromise_ = null; + // Rethrow the error. + throw error; + })); + // Return the cached/pending ready promise. + return this.cachedReadyPromise_; +}; + +/** + * Renders the reCAPTCHA and returns the allocated widget ID. + * @return {!goog.Promise} The promise that resolves with the reCAPTCHA + * widget ID when it is rendered. + */ +fireauth.BaseRecaptchaVerifier.prototype.render = function() { + this.checkIfDestroyed_(); + var self = this; + // Get reCAPTCHA ready. + return this.registerPendingPromise_(this.isReady_().then(function() { + if (self.widgetId_ === null) { + // For a visible reCAPTCHA, embed in a wrapper DIV container to allow + // re-rendering in the same developer provided container. + var container = self.container_; + if (!self.isInvisible_) { + // Get outer container (the developer provided container). + var outerContainer = goog.dom.getElement(container); + // Create wrapper temp DIV container. + container = goog.dom.createDom(goog.dom.TagName.DIV); + // Add temp DIV to outer container. + outerContainer.appendChild(container); + } + // If not initialized, initialize reCAPTCHA and return its widget ID. + self.widgetId_ = grecaptcha.render(container, self.parameters_); + } + return self.widgetId_; + })); +}; + + +/** + * Gets the reCAPTCHA ready and waits for the reCAPTCHA token to be available + * before resolving the promise returned. + * @return {!goog.Promise} The promise that resolves with the reCAPTCHA + * token when reCAPTCHA challenge is solved. + */ +fireauth.BaseRecaptchaVerifier.prototype.verify = function() { + // Fail if reCAPTCHA is already destroyed. + this.checkIfDestroyed_(); + var self = this; + // Render reCAPTCHA. + return this.registerPendingPromise_(this.render().then(function(widgetId) { + return new goog.Promise(function(resolve, reject) { + // Get current reCAPTCHA token. + var recaptchaToken = grecaptcha.getResponse(widgetId); + if (recaptchaToken) { + // Unexpired token already available. Resolve pending promise with that + // token. + resolve(recaptchaToken); + } else { + // No token available. Listen to token change. + var cb = function(token) { + if (!token) { + // Ignore token expirations. + return; + } + // Remove temporary token change listener. + self.removeTokenChangeListener_(cb); + // Resolve with new token. + resolve(token); + }; + // Add temporary token change listener. + self.addTokenChangeListener_(cb); + if (self.isInvisible_) { + // Execute invisible reCAPTCHA to force a challenge. + // This should do nothing if already triggered either by developer or + // by a button click. + grecaptcha.execute(/** @type {number} */ (self.widgetId_)); + } + } + }); + })); +}; + + +/** + * Resets the reCAPTCHA widget. + */ +fireauth.BaseRecaptchaVerifier.prototype.reset = function() { + this.checkIfDestroyed_(); + if (this.widgetId_ !== null) { + grecaptcha.reset(this.widgetId_); + } +}; + + +/** + * Throws an error if the reCAPTCHA verifier is already cleared. + * @private + */ +fireauth.BaseRecaptchaVerifier.prototype.checkIfDestroyed_ = function() { + if (this.destroyed_) { + throw new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'RecaptchaVerifier instance has been destroyed.'); + } +}; + + +/** + * Removes the reCAPTCHA from the DOM. + */ +fireauth.BaseRecaptchaVerifier.prototype.clear = function() { + this.checkIfDestroyed_(); + this.destroyed_ = true; + // Decrement reCAPTCHA instance counter. + fireauth.BaseRecaptchaVerifier.Loader.getInstance().clearSingleRecaptcha(); + // Cancel all pending promises. + for (var i = 0; i < this.pendingPromises_.length; i++) { + this.pendingPromises_[i].cancel( + 'RecaptchaVerifier instance has been destroyed.'); + } + if (!this.isInvisible_) { + goog.dom.removeChildren(goog.dom.getElement(this.container_)); + } +}; + + +/** @private @const {!goog.string.Const} The reCAPTCHA javascript source URL. */ +fireauth.BaseRecaptchaVerifier.RECAPTCHA_SRC_ = goog.string.Const.from( + 'https://www.google.com/recaptcha/api.js?onload=%{onload}&render=explicit' + + '&hl=%{hl}'); + + +/** + * Utility to help load reCAPTCHA dependencies for specified languages. + * @constructor + */ +fireauth.BaseRecaptchaVerifier.Loader = function() { + /** + * @private {number} The reCAPTCHA instance counter. This is used to track the + * number of reCAPTCHAs rendered on the page. This is needed to allow + * localization of the reCAPTCHA. Localization is applied by loading the + * grecaptcha SDK with the hl field provided. However, this will break + * existing reCAPTCHAs. So we should only support i18n when there are no + * other widgets rendered on this screen. If the developer is already + * using reCAPTCHA in another context, we will disable localization so we + * don't accidentally break existing reCAPTCHA widgets. + */ + this.counter_ = goog.global['grecaptcha'] ? Infinity : 0; + /** @private {?string} The current reCAPTCHA language code. */ + this.hl_ = null; + /** @private {string} The reCAPTCHA callback name. */ + this.cbName_ = '__rcb' + Math.floor(Math.random() * 1000000).toString(); +}; + + +/** + * Loads the grecaptcha client library if it is not loaded and returns a promise + * that resolves on success. If the right conditions are available, will reload + * the dependencies for a specified language code. + * @param {?string} hl The reCAPTCHA language code. + * @return {!goog.Promise} A promise that resolves when grecaptcha is loaded. + */ +fireauth.BaseRecaptchaVerifier.Loader.prototype.loadRecaptchaDeps = + function(hl) { + var self = this; + return new goog.Promise(function(resolve, reject) { + // Offline, fail quickly instead of waiting for request to timeout. + if (!fireauth.util.isOnline()) { + reject(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + return; + } + // Load grecaptcha SDK if not already loaded or language changed since last + // load and no other rendered reCAPTCHA is visible, + if (!goog.global['grecaptcha'] || (hl !== self.hl_ && !self.counter_)) { + // reCAPTCHA saves the onload function and applies it on subsequent + // reloads. This means that the callback name has to remain the same. + goog.global[self.cbName_] = function() { + if (!goog.global['grecaptcha']) { + // This should not happen. + reject(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR)); + } else { + // Update the current language code. + self.hl_ = hl; + var render = goog.global['grecaptcha']['render']; + // Wrap grecaptcha.render to keep track of rendered grecaptcha. This + // helps detect if the developer rendered a non + // firebase.auth.RecaptchaVerifier reCAPTCHA. + goog.global['grecaptcha']['render'] = + function(container, parameters) { + var widgetId = render(container, parameters); + // Increment only after render succeeds, in case an error is thrown + // during rendering. + self.counter_++; + return widgetId; + }; + resolve(); + } + delete goog.global[self.cbName_]; + }; + // Construct reCAPTCHA URL and on load, run the temporary function. + var url = goog.html.TrustedResourceUrl.format( + fireauth.BaseRecaptchaVerifier.RECAPTCHA_SRC_, + {'onload': self.cbName_, 'hl': hl || ''}); + // TODO: eventually, replace all dependencies on goog.net.jsloader. + goog.Promise.resolve(goog.net.jsloader.safeLoad(url)) + .thenCatch(function(error) { + // In case library fails to load, typically due to a network error, + // reset cached loader to null to force a refresh on a retrial. + reject(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'Unable to load external reCAPTCHA dependencies!')); + }); + } else { + resolve(); + } + }); +}; + + +/** Decrements the reCAPTCHA instance counter. */ +fireauth.BaseRecaptchaVerifier.Loader.prototype.clearSingleRecaptcha = + function() { + this.counter_--; +}; + + +/** + * @private {?fireauth.BaseRecaptchaVerifier.Loader} The singleton instance for + * reCAPTCHA dependency loader. + */ +fireauth.BaseRecaptchaVerifier.Loader.instance_ = null; + + +/** + * @return {!fireauth.BaseRecaptchaVerifier.Loader} The singleton reCAPTCHA + * dependency loader instance. + */ +fireauth.BaseRecaptchaVerifier.Loader.getInstance = function() { + // Check if there is an existing instance. Otherwise create one and cache it. + if (!fireauth.BaseRecaptchaVerifier.Loader.instance_) { + fireauth.BaseRecaptchaVerifier.Loader.instance_ = + new fireauth.BaseRecaptchaVerifier.Loader(); + } + return fireauth.BaseRecaptchaVerifier.Loader.instance_; +}; + + +/** + * Creates the Firebase reCAPTCHA app verifier, publicly available, for the + * Firebase app provided, used for web phone authentication. + * This is a subclass of fireauth.BaseRecaptchaVerifier. + * + * @param {!Element|string} container The reCAPTCHA container parameter. This + * has different meaning depending on whether the reCAPTCHA is hidden or + * visible. + * @param {?Object=} opt_parameters The optional reCAPTCHA parameters. + * @param {?firebase.app.App=} opt_app The corresponding Firebase app. + * @constructor + * @extends {fireauth.BaseRecaptchaVerifier} + */ +fireauth.RecaptchaVerifier = function(container, opt_parameters, opt_app) { + var apiKey; + try { + /** @private {!firebase.app.App} The corresponding Firebase app instance. */ + this.app_ = opt_app || firebase.app(); + } catch (error) { + throw new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'No firebase.app.App instance is currently initialized.'); + } + // API key is required for web client RPC calls. + if (this.app_.options && this.app_.options['apiKey']) { + apiKey = this.app_.options['apiKey']; + } else { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY); + } + var self = this; + // Construct the language code getter based on the underlying Auth instance. + var getLanguageCode = function() { + var languageCode; + // Get the latest language setting. + // reCAPTCHA does not support updating the language of an already + // rendered reCAPTCHA. Reloading the SDK with the new hl will break + // the existing rendered localized reCAPTCHA. We will need to + // document that a new fireauth.BaseRecaptchaVerifier instance needs + // to be instantiated after the language is updated. Otherwise, the + // old language code will remain active on the existing instance. + try { + languageCode = self.app_['auth']().getLanguageCode(); + } catch (e) { + languageCode = null; + } + return languageCode; + }; + // Get the framework version from Auth instance. + var frameworkVersion = null; + try { + frameworkVersion = this.app_['auth']().getFramework(); + } catch (e) { + // Do nothing. + } + // Get the client version based on the Firebase JS version. + var clientFullVersion = firebase.SDK_VERSION ? + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + frameworkVersion) : + null; + // Call the superclass constructor with the computed API key, reCAPTCHA + // container, optional parameters, language code getter, Firebase JS client + // version and the current client configuration endpoints. + fireauth.RecaptchaVerifier.base(this, 'constructor', apiKey, + container, opt_parameters, getLanguageCode, clientFullVersion, + fireauth.constants.getEndpointConfig(fireauth.constants.clientEndpoint)); +}; +goog.inherits(fireauth.RecaptchaVerifier, fireauth.BaseRecaptchaVerifier); diff --git a/packages/auth/src/rpchandler.js b/packages/auth/src/rpchandler.js new file mode 100644 index 00000000000..e064c4f0438 --- /dev/null +++ b/packages/auth/src/rpchandler.js @@ -0,0 +1,2220 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility for handling RPC requests to server. + */ +goog.provide('fireauth.RpcHandler'); +goog.provide('fireauth.RpcHandler.ApiMethodHandler'); +goog.provide('fireauth.RpcHandler.VerifyAssertionData'); +goog.provide('fireauth.XmlHttpFactory'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.idp.ProviderId'); +goog.require('fireauth.object'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.format.EmailAddress'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.json'); +goog.require('goog.net.CorsXmlHttpFactory'); +goog.require('goog.net.EventType'); +goog.require('goog.net.XhrIo'); +goog.require('goog.net.XmlHttpFactory'); +goog.require('goog.net.jsloader'); +goog.require('goog.object'); +goog.require('goog.string.Const'); + + + +/** + * Firebase Auth XmlHttpRequest factory. This is useful for environments like + * Node.js where XMLHttpRequest does not exist. XmlHttpFactory would be + * initialized using the polyfill XMLHttpRequest module. + * @param {function(new:XMLHttpRequest)} xmlHttpRequest The xmlHttpRequest + * constructor. + * @constructor + * @extends {goog.net.XmlHttpFactory} + * @final + */ +fireauth.XmlHttpFactory = function(xmlHttpRequest) { + /** + * @private {function(new:XMLHttpRequest)} The underlying XHR reference. + */ + this.xmlHttpRequest_ = xmlHttpRequest; + fireauth.XmlHttpFactory.base(this, 'constructor'); +}; +goog.inherits(fireauth.XmlHttpFactory, goog.net.XmlHttpFactory); + + +/** + * @return {!goog.net.XhrLike|!XMLHttpRequest} A new XhrLike instance. + * @override + */ +fireauth.XmlHttpFactory.prototype.createInstance = function() { + return new this.xmlHttpRequest_(); +}; + + +/** + * @return {!Object} Options describing how XHR objects obtained from this + * factory should be used. + * @override + */ +fireauth.XmlHttpFactory.prototype.internalGetOptions = function() { + return {}; +}; + + + +/** + * Creates an RPC request handler for the project specified by the API key. + * + * @param {string} apiKey The API key. + * @param {?Object=} opt_config The RPC request processor configuration. + * @param {?string=} opt_firebaseClientVersion The optional Firebase client + * version to log with requests to Firebase Auth server. + * @constructor + */ +fireauth.RpcHandler = function(apiKey, opt_config, opt_firebaseClientVersion) { + // Get XMLHttpRequest reference. + var XMLHttpRequest = fireauth.util.getXMLHttpRequest(); + if (!XMLHttpRequest) { + // In a Node.js environment, xmlhttprequest module needs to be required. + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'The XMLHttpRequest compatibility library was not found.'); + } + this.apiKey_ = apiKey; + var config = opt_config || {}; + this.secureTokenEndpoint_ = config['secureTokenEndpoint'] || + fireauth.RpcHandler.SECURE_TOKEN_ENDPOINT_; + /** + * @private @const {!fireauth.util.Delay} The delay for secure token endpoint + * network timeout. + */ + this.secureTokenTimeout_ = config['secureTokenTimeout'] || + fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_TIMEOUT_; + this.secureTokenHeaders_ = goog.object.clone( + config['secureTokenHeaders'] || + fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_HEADERS_); + this.firebaseEndpoint_ = config['firebaseEndpoint'] || + fireauth.RpcHandler.FIREBASE_ENDPOINT_; + /** + * @private @const {!fireauth.util.Delay} The delay for Firebase Auth endpoint + * network timeout. + */ + this.firebaseTimeout_ = config['firebaseTimeout'] || + fireauth.RpcHandler.DEFAULT_FIREBASE_TIMEOUT_; + this.firebaseHeaders_ = goog.object.clone( + config['firebaseHeaders'] || + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_); + // If Firebase client version needs to be logged too. + if (opt_firebaseClientVersion) { + // Log client version for Firebase Auth server. + this.firebaseHeaders_['X-Client-Version'] = opt_firebaseClientVersion; + // Log client version for securetoken server. + this.secureTokenHeaders_['X-Client-Version'] = opt_firebaseClientVersion; + } + /** @const @private {!goog.net.CorsXmlHttpFactory} The CORS XHR factory. */ + this.corsXhrFactory_ = new goog.net.CorsXmlHttpFactory(); + /** @const @private {!goog.net.XmlHttpFactory} The XHR factory. */ + this.xhrFactory_ = new fireauth.XmlHttpFactory(XMLHttpRequest); +}; + + +/** + * Enums for HTTP request methods. + * @enum {string} + */ +fireauth.RpcHandler.HttpMethod = { + POST: 'POST', + GET: 'GET' +}; + + +/** + * Firebase Auth server error codes. + * @enum {string} + */ +fireauth.RpcHandler.ServerError = { + CAPTCHA_CHECK_FAILED: 'CAPTCHA_CHECK_FAILED', + CORS_UNSUPPORTED: 'CORS_UNSUPPORTED', + CREDENTIAL_MISMATCH: 'CREDENTIAL_MISMATCH', + CREDENTIAL_TOO_OLD_LOGIN_AGAIN: 'CREDENTIAL_TOO_OLD_LOGIN_AGAIN', + DYNAMIC_LINK_NOT_ACTIVATED: 'DYNAMIC_LINK_NOT_ACTIVATED', + EMAIL_EXISTS: 'EMAIL_EXISTS', + EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', + EXPIRED_OOB_CODE: 'EXPIRED_OOB_CODE', + FEDERATED_USER_ID_ALREADY_LINKED: 'FEDERATED_USER_ID_ALREADY_LINKED', + INVALID_APP_CREDENTIAL: 'INVALID_APP_CREDENTIAL', + INVALID_APP_ID: 'INVALID_APP_ID', + INVALID_CERT_HASH: 'INVALID_CERT_HASH', + INVALID_CODE: 'INVALID_CODE', + INVALID_CONTINUE_URI: 'INVALID_CONTINUE_URI', + INVALID_CUSTOM_TOKEN: 'INVALID_CUSTOM_TOKEN', + INVALID_EMAIL: 'INVALID_EMAIL', + INVALID_ID_TOKEN: 'INVALID_ID_TOKEN', + INVALID_IDP_RESPONSE: 'INVALID_IDP_RESPONSE', + INVALID_IDENTIFIER: 'INVALID_IDENTIFIER', + INVALID_MESSAGE_PAYLOAD: 'INVALID_MESSAGE_PAYLOAD', + INVALID_OAUTH_CLIENT_ID: 'INVALID_OAUTH_CLIENT_ID', + INVALID_OOB_CODE: 'INVALID_OOB_CODE', + INVALID_PASSWORD: 'INVALID_PASSWORD', + INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER', + INVALID_RECIPIENT_EMAIL: 'INVALID_RECIPIENT_EMAIL', + INVALID_SENDER: 'INVALID_SENDER', + INVALID_SESSION_INFO: 'INVALID_SESSION_INFO', + INVALID_TEMPORARY_PROOF: 'INVALID_TEMPORARY_PROOF', + MISSING_ANDROID_PACKAGE_NAME: 'MISSING_ANDROID_PACKAGE_NAME', + MISSING_APP_CREDENTIAL: 'MISSING_APP_CREDENTIAL', + MISSING_CODE: 'MISSING_CODE', + MISSING_CONTINUE_URI: 'MISSING_CONTINUE_URI', + MISSING_CUSTOM_TOKEN: 'MISSING_CUSTOM_TOKEN', + MISSING_IOS_BUNDLE_ID: 'MISSING_IOS_BUNDLE_ID', + MISSING_OOB_CODE: 'MISSING_OOB_CODE', + MISSING_PASSWORD: 'MISSING_PASSWORD', + MISSING_PHONE_NUMBER: 'MISSING_PHONE_NUMBER', + MISSING_SESSION_INFO: 'MISSING_SESSION_INFO', + OPERATION_NOT_ALLOWED: 'OPERATION_NOT_ALLOWED', + PASSWORD_LOGIN_DISABLED: 'PASSWORD_LOGIN_DISABLED', + QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', + SESSION_EXPIRED: 'SESSION_EXPIRED', + TOKEN_EXPIRED: 'TOKEN_EXPIRED', + TOO_MANY_ATTEMPTS_TRY_LATER: 'TOO_MANY_ATTEMPTS_TRY_LATER', + UNAUTHORIZED_DOMAIN: 'UNAUTHORIZED_DOMAIN', + USER_CANCELLED: 'USER_CANCELLED', + USER_DISABLED: 'USER_DISABLED', + USER_NOT_FOUND: 'USER_NOT_FOUND', + WEAK_PASSWORD: 'WEAK_PASSWORD' +}; + + +/** + * A map of server error codes to client errors. + * @typedef {!Object< + * !fireauth.RpcHandler.ServerError, !fireauth.authenum.Error>} + */ +fireauth.RpcHandler.ServerErrorMap; + + +/** + * Firebase Auth response field names. + * @enum {string} + */ +fireauth.RpcHandler.AuthServerField = { + ALL_PROVIDERS: 'allProviders', + AUTH_URI: 'authUri', + AUTHORIZED_DOMAINS: 'authorizedDomains', + DYNAMIC_LINKS_DOMAIN: 'dynamicLinksDomain', + EMAIL: 'email', + ERROR_MESSAGE: 'errorMessage', + EXPIRES_IN: 'expiresIn', + ID_TOKEN: 'idToken', + NEED_CONFIRMATION: 'needConfirmation', + RECAPTCHA_SITE_KEY: 'recaptchaSiteKey', + REFRESH_TOKEN: 'refreshToken', + SESSION_ID: 'sessionId', + SESSION_INFO: 'sessionInfo', + TEMPORARY_PROOF: 'temporaryProof' +}; + + +/** + * Firebase Auth getOobConfirmationCode requestType possible values. + * @enum {string} + */ +fireauth.RpcHandler.GetOobCodeRequestType = { + NEW_EMAIL_ACCEPT: 'NEW_EMAIL_ACCEPT', + PASSWORD_RESET: 'PASSWORD_RESET', + VERIFY_EMAIL: 'VERIFY_EMAIL' +}; + + +/** + * Firebase Auth response field names. + * @enum {string} + */ +fireauth.RpcHandler.StsServerField = { + ACCESS_TOKEN: 'access_token', + EXPIRES_IN: 'expires_in', + REFRESH_TOKEN: 'refresh_token' +}; + + +/** + * @return {string} The API key. + */ +fireauth.RpcHandler.prototype.getApiKey = function() { + return this.apiKey_; +}; + + +/** + * The Firebase custom locale header. + * @const {string} + * @private + */ +fireauth.RpcHandler.FIREBASE_LOCALE_KEY_ = 'X-Firebase-Locale'; + + +/** + * The secure token endpoint. + * @const {string} + * @private + */ +fireauth.RpcHandler.SECURE_TOKEN_ENDPOINT_ = + 'https://securetoken.googleapis.com/v1/token'; + + +/** + * The default timeout delay (units in milliseconds) for requests sending to + * STS token endpoint. + * @const {!fireauth.util.Delay} + * @private + */ +fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_TIMEOUT_ = + new fireauth.util.Delay(30000, 60000); + + +/** + * The STS token RPC content headers. + * @const {!Object} + * @private + */ +fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_HEADERS_ = { + 'Content-Type': 'application/x-www-form-urlencoded' +}; + + +/** + * The Firebase endpoint. + * @const {string} + * @private + */ +fireauth.RpcHandler.FIREBASE_ENDPOINT_ = + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/'; + + +/** + * The default timeout delay (units in milliseconds) for requests sending to + * Firebase endpoint. + * @const {!fireauth.util.Delay} + * @private + */ +fireauth.RpcHandler.DEFAULT_FIREBASE_TIMEOUT_ = + new fireauth.util.Delay(30000, 60000); + + +/** + * The Firebase RPC content headers. + * @const {!Object} + * @private + */ +fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_ = { + 'Content-Type': 'application/json' +}; + + +/** + * Updates the custom locale header. + * @param {?string} languageCode The new languageCode. + */ +fireauth.RpcHandler.prototype.updateCustomLocaleHeader = + function(languageCode) { + if (languageCode) { + // If a language code is provided, add it to the header. + this.firebaseHeaders_[fireauth.RpcHandler.FIREBASE_LOCALE_KEY_] = + languageCode; + } else { + // Otherwise remove the custom locale header. + delete this.firebaseHeaders_[fireauth.RpcHandler.FIREBASE_LOCALE_KEY_]; + } +}; + + +/** + * Updates the X-Client-Version in the header. + * @param {?string} clientVersion The new client version. + */ +fireauth.RpcHandler.prototype.updateClientVersion = function(clientVersion) { + if (clientVersion) { + // Update client version for Firebase Auth server. + this.firebaseHeaders_['X-Client-Version'] = clientVersion; + // Update client version for securetoken server. + this.secureTokenHeaders_['X-Client-Version'] = clientVersion; + } else { + // Remove client version from header. + delete this.firebaseHeaders_['X-Client-Version']; + delete this.secureTokenHeaders_['X-Client-Version']; + } +}; + + +/** + * Sends XhrIo request using goog.net.XhrIo. + * @param {string} url The URL to make a request to. + * @param {function(?Object)=} opt_callback The callback to run on completion. + * @param {fireauth.RpcHandler.HttpMethod=} opt_httpMethod The HTTP send method. + * @param {?ArrayBuffer|?ArrayBufferView|?Blob|?Document|?FormData|string=} + * opt_data The request content. + * @param {?Object=} opt_headers The request content headers. + * @param {number=} opt_timeout The request timeout. + * @private + */ +fireauth.RpcHandler.prototype.sendXhr_ = function( + url, + opt_callback, + opt_httpMethod, + opt_data, + opt_headers, + opt_timeout) { + // Offline, fail quickly instead of waiting for request to timeout. + if (!fireauth.util.isOnline()) { + if (opt_callback) { + opt_callback(null); + } + return; + } + var sendXhr; + if (fireauth.util.supportsCors()) { + // If supports CORS use goog.net.XhrIo. + sendXhr = goog.bind(this.sendXhrUsingXhrIo_, this); + } else { + // Load gapi.client.request and gapi.auth dependency dynamically. + if (!fireauth.RpcHandler.loadGApi_) { + fireauth.RpcHandler.loadGApi_ = + new goog.Promise(function(resolve, reject) { + // On load, resolve. + fireauth.RpcHandler.loadGApiJs_(resolve, reject); + }); + } + // If does not support CORS, use gapi.client.request. + sendXhr = goog.bind(this.sendXhrUsingGApiClient_, this); + } + sendXhr( + url, opt_callback, opt_httpMethod, opt_data, opt_headers, opt_timeout); +}; + + +/** + * Sends XhrIo request using goog.net.XhrIo. + * @param {string} url The URL to make a request to. + * @param {function(?Object)=} opt_callback The callback to run on completion. + * @param {fireauth.RpcHandler.HttpMethod=} opt_httpMethod The HTTP send method. + * @param {?ArrayBuffer|?ArrayBufferView|?Blob|?Document|?FormData|string=} + * opt_data The request content. + * @param {?Object=} opt_headers The request content headers. + * @param {number=} opt_timeout The request timeout. + * @private + */ +fireauth.RpcHandler.prototype.sendXhrUsingXhrIo_ = function( + url, + opt_callback, + opt_httpMethod, + opt_data, + opt_headers, + opt_timeout) { + // Send XHR request. CORS does not apply in native environments so don't use + // CorsXmlHttpFactory in those cases. + // For a Node.js environment use the fireauth.XmlHttpFactory instance. + var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; + var xhrIo = fireauth.util.isNativeEnvironment() ? + (isNode ? new goog.net.XhrIo(this.xhrFactory_) : new goog.net.XhrIo()) : + new goog.net.XhrIo(this.corsXhrFactory_); + + // xhrIo.setTimeoutInterval not working in IE10 and IE11, handle manually. + var requestTimeout; + if (opt_timeout) { + xhrIo.setTimeoutInterval(opt_timeout); + requestTimeout = setTimeout(function() { + xhrIo.dispatchEvent(goog.net.EventType.TIMEOUT); + }, opt_timeout); + } + // Run callback function on completion. + xhrIo.listen( + goog.net.EventType.COMPLETE, + /** @this {goog.net.XhrIo} */ + function() { + // Clear timeout timer. + if (requestTimeout) { + clearTimeout(requestTimeout); + } + // Response assumed to be in json format. If not, catch, log error and + // pass null to callback. + var response = null; + try { + // Do not use this.responseJson() as it uses goog.json.parse + // underneath. Internal goog.json.parse parsing uses eval and since + // recommended Content Security Policy does not allow unsafe-eval, + // this is failing and throwing an error in chrome extensions and + // warnings else where. Use native parsing instead via JSON.parse. + response = JSON.parse(this.getResponseText()) || null; + } catch (e) { + response = null; + } + if (opt_callback) { + opt_callback(/** @type {?Object} */ (response)); + } + }); + // Dispose xhrIo on ready. + xhrIo.listenOnce( + goog.net.EventType.READY, + /** @this {goog.net.XhrIo} */ + function() { + // Clear timeout timer. + if (requestTimeout) { + clearTimeout(requestTimeout); + } + // Dispose xhrIo. + this.dispose(); + }); + // Listen to timeout error. + // This should work when request is aborted too. + xhrIo.listenOnce( + goog.net.EventType.TIMEOUT, + /** @this {goog.net.XhrIo} */ + function() { + // Clear timeout timer. + if (requestTimeout) { + clearTimeout(requestTimeout); + } + // Dispose xhrIo. + this.dispose(); + // The request timed out. + if (opt_callback) { + opt_callback(null); + } + }); + xhrIo.send(url, opt_httpMethod, opt_data, opt_headers); +}; + + +/** + * @const {!goog.string.Const} The GApi client library URL. + * @private + */ +fireauth.RpcHandler.GAPI_SRC_ = goog.string.Const.from( + 'https://apis.google.com/js/client.js?onload=%{onload}'); + + +/** + * @const {string} + * @private + */ +fireauth.RpcHandler.GAPI_CALLBACK_NAME_ = + '__fcb' + Math.floor(Math.random() * 1000000).toString(); + + +/** + * Loads the GApi client library if it is not loaded. + * @param {function()} callback The callback to invoke once it's loaded. + * @param {function(?Object)} errback The error callback. + * @private + */ +fireauth.RpcHandler.loadGApiJs_ = function(callback, errback) { + // If gapi.client.request not available, load it dynamically. + if (!((window['gapi'] || {})['client'] || {})['request']) { + goog.global[fireauth.RpcHandler.GAPI_CALLBACK_NAME_] = function() { + // Callback will be called by GApi, test properly loaded here instead of + // after jsloader resolves. + if (!((window['gapi'] || {})['client'] || {})['request']) { + errback(new Error(fireauth.RpcHandler.ServerError.CORS_UNSUPPORTED)); + } else { + callback(); + } + }; + var url = goog.html.TrustedResourceUrl.format( + fireauth.RpcHandler.GAPI_SRC_, + {'onload': fireauth.RpcHandler.GAPI_CALLBACK_NAME_}); + // TODO: replace goog.net.jsloader with our own script includer. + var result = goog.net.jsloader.safeLoad(url); + result.addErrback(function() { + // In case file fails to load. + errback(new Error(fireauth.RpcHandler.ServerError.CORS_UNSUPPORTED)); + }); + } else { + callback(); + } +}; + + +/** + * Sends XhrIo request using gapi.client. + * @param {string} url The URL to make a request to. + * @param {function(?Object)=} opt_callback The callback to run on completion. + * @param {fireauth.RpcHandler.HttpMethod=} opt_httpMethod The HTTP send method. + * @param {?ArrayBuffer|?ArrayBufferView|?Blob|?Document|?FormData|string=} + * opt_data The request content. + * @param {?Object=} opt_headers The request content headers. + * @param {number=} opt_timeout The request timeout. + * @private + */ +fireauth.RpcHandler.prototype.sendXhrUsingGApiClient_ = function( + url, + opt_callback, + opt_httpMethod, + opt_data, + opt_headers, + opt_timeout) { + var self = this; + // Wait for GApi dependency to load. + fireauth.RpcHandler.loadGApi_.then(function() { + window['gapi']['client']['setApiKey'](self.getApiKey()); + // GApi maintains the Auth result and automatically append the Auth token to + // all outgoing requests. Firebase Auth requests will be rejected if there + // are others scopes (e.g. google plus) for the Auth token. Need to empty + // the token before call gitkit api. Restored in callback. + var oauth2Token = window['gapi']['auth']['getToken'](); + window['gapi']['auth']['setToken'](null); + window['gapi']['client']['request']({ + 'path': url, + 'method': opt_httpMethod, + 'body': opt_data, + 'headers': opt_headers, + // This needs to be set to none, otherwise the access token will be passed + // in the header field causing apiary to complain. + 'authType': 'none', + 'callback': function(response) { + window['gapi']['auth']['setToken'](oauth2Token); + if (opt_callback) { + opt_callback(response); + } + } + }); + }).thenCatch(function(error) { + // Catches failure to support CORS and propagates it. + if (opt_callback) { + // Simulate backend server error to be caught by upper layer. + opt_callback({ + 'error': { + 'message': (error && error['message']) || + fireauth.RpcHandler.ServerError.CORS_UNSUPPORTED + } + }); + } + }); +}; + + +/** + * Validates the request for the STS access token. + * + * @param {?Object} data The STS token request body. + * @return {boolean} Whether the request is valid. + * @private + */ +fireauth.RpcHandler.prototype.validateStsTokenRequest_ = function(data) { + if (data['grant_type'] == 'refresh_token' && data['refresh_token']) { + // Exchange refresh token. + return true; + } else if (data['grant_type'] == 'authorization_code' && data['code']) { + // Exchange ID token. + return true; + } else { + // Invalid. + return false; + } +}; + + +/** + * Handles the request for the STS access token. + * + * @param {!Object} data The STS token request body. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.requestStsToken = function(data) { + var self = this; + return new goog.Promise(function(resolve, reject) { + if (self.validateStsTokenRequest_(data)) { + self.sendXhr_( + self.secureTokenEndpoint_ + '?key=' + + encodeURIComponent(self.getApiKey()), + function(response) { + if (!response) { + // An unparseable response from the XHR most likely indicates some + // problem with the network. + reject(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + } else if (fireauth.RpcHandler.hasError_(response)) { + reject(fireauth.RpcHandler.getDeveloperError_(response)); + } else if ( + !response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN] || + !response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN]) { + reject(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR)); + } else { + resolve(response); + } + }, + fireauth.RpcHandler.HttpMethod.POST, + goog.Uri.QueryData.createFromMap(data).toString(), + self.secureTokenHeaders_, + self.secureTokenTimeout_.get()); + } else { + reject(new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + } + }); +}; + + +/** + * @param {!Object} data The object to serialize. + * @return {string} The serialized object with null, undefined and empty string + * values removed. + * @private + */ +fireauth.RpcHandler.serialize_ = function(data) { + // goog.json.serialize converts undefined values to null. + // This helper removes all empty strings, nulls and undefined from serialized + // object. + // Serialize trimmed data. + return goog.json.serialize(fireauth.util.copyWithoutNullsOrUndefined(data)); +}; + + +/** + * Creates and executes a request for the given API method. + * @param {string} method The API method. + * @param {!fireauth.RpcHandler.HttpMethod} httpMethod The http request method. + * @param {!Object} data The data for the API request. In the case of a GET + * request, the contents of this object will be form encoded and appended + * to the query string of the URL. No post body is sent in that case. If an + * object value is specified, it will be converted to a string: + * encodeURIComponent(String(value)). + * @param {?fireauth.RpcHandler.ServerErrorMap=} opt_customErrorMap A map + * of server error codes to client errors to override default error + * handling. + * @param {boolean=} opt_cachebuster Whether to append a unique string to + * request to force backend to return an uncached response to request. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.requestFirebaseEndpoint = function( + method, httpMethod, data, opt_customErrorMap, opt_cachebuster) { + var self = this; + // Construct endpoint URL. + var uri = goog.Uri.parse(this.firebaseEndpoint_ + method); + uri.setParameterValue('key', this.getApiKey()); + // Check whether to append cachebuster to request. + if (opt_cachebuster) { + uri.setParameterValue('cb', goog.now().toString()); + } + // Firebase allows GET endpoints. + var isGet = httpMethod == fireauth.RpcHandler.HttpMethod.GET; + if (isGet) { + // For GET HTTP method, append data to query string. + for (var key in data) { + if (data.hasOwnProperty(key)) { + uri.setParameterValue(key, data[key]); + } + } + } + return new goog.Promise(function(resolve, reject) { + self.sendXhr_( + uri.toString(), + function(response) { + if (!response) { + // An unparseable response from the XHR most likely indicates some + // problem with the network. + reject(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + } else if (fireauth.RpcHandler.hasError_(response)) { + reject(fireauth.RpcHandler.getDeveloperError_(response, + opt_customErrorMap || {})); + } else { + resolve(response); + } + }, + httpMethod, + // No post body data in GET requests. + isGet ? undefined : fireauth.RpcHandler.serialize_(data), + self.firebaseHeaders_, + self.firebaseTimeout_.get()); + }); +}; + + +/** + * Verifies that the request has a valid email set. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateRequestHasEmail_ = function(request) { + if (!goog.format.EmailAddress.isValidAddrSpec(request['email'])) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL); + } +}; + + +/** + * Verifies that the response has a valid email set. + * @param {!Object} response + * @private + */ +fireauth.RpcHandler.validateResponseHasEmail_ = function(response) { + if (!goog.format.EmailAddress.isValidAddrSpec(response['email'])) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Verifies that the an email is valid, if it is there. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateEmailIfPresent_ = function(request) { + if ('email' in request) { + fireauth.RpcHandler.validateRequestHasEmail_(request); + } +}; + + +/** + * @param {string} providerId The provider ID. + * @param {?Array=} opt_additionalScopes The list of scope strings. + * @return {?string} The IDP and its comma separated scope strings serialized. + * @private + */ +fireauth.RpcHandler.getAdditionalScopes_ = + function(providerId, opt_additionalScopes) { + var scopes = {}; + if (opt_additionalScopes && opt_additionalScopes.length) { + scopes[providerId] = opt_additionalScopes.join(','); + // Return stringified scopes. + return goog.json.serialize(scopes); + } + return null; +}; + + +/** + * Validates a response from getAuthUri. + * @param {?Object} response The getAuthUri response data. + * @private + */ +fireauth.RpcHandler.validateGetAuthResponse_ = function(response) { + if (!response[fireauth.RpcHandler.AuthServerField.AUTH_URI] || + !response[fireauth.RpcHandler.AuthServerField.SESSION_ID]) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Requests createAuthUri endpoint to retrieve the authUri and session ID for + * the start of an OAuth handshake. + * @param {string} providerId The provider ID. + * @param {string} continueUri The IdP callback URL. + * @param {?Object=} opt_customParameters The optional OAuth custom parameters + * plain object. + * @param {?Array=} opt_additionalScopes The list of scope strings. + * @param {?string=} opt_email The optional email. + * @param {?string=} opt_sessionId The optional session ID. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.getAuthUri = function( + providerId, + continueUri, + opt_customParameters, + opt_additionalScopes, + opt_email, + opt_sessionId) { + var request = { + 'identifier': opt_email, + 'providerId': providerId, + 'continueUri': continueUri, + 'customParameter': opt_customParameters || {}, + 'oauthScope': fireauth.RpcHandler.getAdditionalScopes_( + providerId, opt_additionalScopes), + 'sessionId': opt_sessionId + }; + // When sessionId is provided, mobile flow (Cordova) is being used, force + // code flow and not implicit flow. All other providers use code flow by + // default. + if (opt_sessionId && providerId == fireauth.idp.ProviderId.GOOGLE) { + request['authFlowType'] = 'CODE_FLOW'; + } + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.GET_AUTH_URI, + request); +}; + + +/** + * Gets the list of IDPs that can be used to log in for the given identifier. + * @param {string} identifier The identifier, such as an email address. + * @return {!goog.Promise>} + */ +fireauth.RpcHandler.prototype.fetchProvidersForIdentifier = + function(identifier) { + // createAuthUri returns an error if continue URI is not http or https. + // For environments like Cordova, Chrome extensions, native frameworks, file + // systems, etc, use http://localhost as continue URL. + var continueUri = fireauth.util.isHttpOrHttps() ? + fireauth.util.getCurrentUrl() : 'http://localhost'; + var request = { + 'identifier': identifier, + 'continueUri': continueUri + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.CREATE_AUTH_URI, request) + .then(function(response) { + return response[fireauth.RpcHandler.AuthServerField.ALL_PROVIDERS] || + []; + }); +}; + + +/** + * Gets the list of authorized domains for the specified project. + * @return {!goog.Promise>} + */ +fireauth.RpcHandler.prototype.getAuthorizedDomains = function() { + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.GET_PROJECT_CONFIG, {}) + .then(function(response) { + return response[ + fireauth.RpcHandler.AuthServerField.AUTHORIZED_DOMAINS] || []; + }); +}; + + +/** + * Gets the reCAPTCHA parameters needed to render the project's provisioned + * reCAPTCHA. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.getRecaptchaParam = function() { + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.GET_RECAPTCHA_PARAM, {}); +}; + + +/** + * Gets the list of authorized domains for the specified project. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.getDynamicLinkDomain = function() { + var request = { + 'returnDynamicLink': true + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.RETURN_DYNAMIC_LINK, request); +}; + + +/** + * Checks if the provided iOS bundle ID belongs to the project as specified by + * the API key. + * @param {string} iosBundleId The iOS bundle ID to check. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.isIosBundleIdValid = function(iosBundleId) { + var request = { + 'iosBundleId': iosBundleId + }; + // This will either resolve if the identifier is valid or throw INVALID_APP_ID + // if not. + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.GET_PROJECT_CONFIG, request) + .then(function(result) { + // Do not return anything. + }); +}; + + +/** + * Checks if the provided Android package name belongs to the project as + * specified by the API key. + * @param {string} androidPackageName The iOS bundle ID to check. + * @param {?string=} opt_sha1Cert The optional SHA-1 Android cert to check. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.isAndroidPackageNameValid = + function(androidPackageName, opt_sha1Cert) { + var request = { + 'androidPackageName': androidPackageName + }; + // This is relevant for the native Android SDK flow. + // This will redirect to an FDL domain owned by GMScore instead of + // the developer's FDL domain as is done for Cordova apps. + if (!!opt_sha1Cert) { + request['sha1Cert'] = opt_sha1Cert; + } + // When no sha1Cert is passed, this will either resolve if the identifier is + // valid or throw INVALID_APP_ID if not. + // When sha1Cert is also passed, this will either resolve or fail with an + // INVALID_CERT_HASH error. + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.GET_PROJECT_CONFIG, request) + .then(function(result) { + // Do not return anything. + }); +}; + + +/** + * Checks if the provided OAuth client ID belongs to the project as specified by + * the API key. + * @param {string} clientId The OAuth client ID to check. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.isOAuthClientIdValid = function(clientId) { + var request = { + 'clientId': clientId + }; + // This will either resolve if the client ID is valid or throw + // INVALID_OAUTH_CLIENT_ID if not. + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.GET_PROJECT_CONFIG, request) + .then(function(result) { + // Do not return anything. + }); +}; + + +/** + * Requests getAccountInfo endpoint using an ID token. + * @param {string} idToken The ID token. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.getAccountInfoByIdToken = function(idToken) { + var request = {'idToken': idToken}; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.GET_ACCOUNT_INFO, + request); +}; + + +/** + * Validates a request to sign in with email and password. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateVerifyCustomTokenRequest_ = function(request) { + if (!request['token']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_CUSTOM_TOKEN); + } +}; + + +/** + * Verifies a custom token and returns a Promise that resolves with the ID + * token. + * @param {string} token The custom token. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyCustomToken = function(token) { + var request = {'token': token}; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.VERIFY_CUSTOM_TOKEN, + request); +}; + + +/** + * Validates a request to sign in with email and password. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateVerifyPasswordRequest_ = function(request) { + fireauth.RpcHandler.validateRequestHasEmail_(request); + if (!request['password']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_PASSWORD); + } +}; + + +/** + * Verifies a password and returns a Promise that resolves with the ID + * token. + * @param {string} email The email address. + * @param {string} password The entered password. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyPassword = function(email, password) { + var request = { + 'email': email, + 'password': password + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.VERIFY_PASSWORD, + request); +}; + + +/** + * Validates a response that should contain an ID token. + * @param {?Object} response The server response data. + * @private + */ +fireauth.RpcHandler.validateIdTokenResponse_ = function(response) { + if (!response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Validates a getRecaptchaParam response. + * @param {?Object} response The server response data. + * @private + */ +fireauth.RpcHandler.validateGetRecaptchaParamResponse_ = function(response) { + // Both are required. This could change though. + if (!response[fireauth.RpcHandler.AuthServerField.RECAPTCHA_SITE_KEY]) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Validates a request that sends the verification ID and code for a sign in/up + * phone Auth flow. + * @param {!Object} request The server request object. + * @private + */ +fireauth.RpcHandler.validateVerifyPhoneNumberRequest_ = function(request) { + // There are 2 cases here: + // case 1: sessionInfo and code + // case 2: phoneNumber and temporaryProof + if (request['phoneNumber'] || request['temporaryProof']) { + // Case 2. Both phoneNumber and temporaryProof should be set. + if (!request['phoneNumber'] || !request['temporaryProof']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + } else { + // Otherwise it's case 1, so we expect sessionInfo and code. + if (!request['sessionInfo']) { + throw new fireauth.AuthError( + fireauth.authenum.Error.MISSING_SESSION_INFO); + } + if (!request['code']) { + throw new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE); + } + } +}; + + +/** + * Validates a request that sends the verification ID and code for a link/update + * phone Auth flow. + * @param {!Object} request The server request object. + * @private + */ +fireauth.RpcHandler.validateVerifyPhoneNumberLinkRequest_ = function(request) { + // idToken should be required here. + if (!request['idToken']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + // The other request parameters match the sign in flow. + fireauth.RpcHandler.validateVerifyPhoneNumberRequest_(request); +}; + + +/** + * Validates a request to create an email and password account. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateCreateAccountRequest_ = function(request) { + fireauth.RpcHandler.validateRequestHasEmail_(request); + if (!request['password']) { + throw new fireauth.AuthError(fireauth.authenum.Error.WEAK_PASSWORD); + } +}; + + +/** + * Creates an email/password account. Returns a Promise that resolves with the + * ID token. + * @param {string} email The email address of the account. + * @param {string} password The password. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.createAccount = function(email, password) { + var request = { + 'email': email, + 'password': password + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.CREATE_ACCOUNT, + request); +}; + + +/** + * Signs in a user as anonymous. Returns a Promise that resolves with the + * ID token. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.signInAnonymously = function() { + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.SIGN_IN_ANONYMOUSLY, {}); +}; + + +/** + * Deletes the user's account corresponding to the idToken given. + * @param {string} idToken The idToken of the user. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.deleteAccount = function(idToken) { + var request = { + 'idToken': idToken + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.DELETE_ACCOUNT, + request); +}; + + +/** + * Requests setAccountInfo endpoint for updateEmail operation. + * @param {string} idToken The ID token. + * @param {string} newEmail The new email. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.updateEmail = function(idToken, newEmail) { + var request = { + 'idToken': idToken, + 'email': newEmail + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.SET_ACCOUNT_INFO, + request); +}; + + +/** + * Validates a setAccountInfo request that updates the password. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateSetAccountInfoSensitive_ = function(request) { + fireauth.RpcHandler.validateEmailIfPresent_(request); + if (!request['password']) { + throw new fireauth.AuthError(fireauth.authenum.Error.WEAK_PASSWORD); + } +}; + + +/** + * Requests setAccountInfo endpoint for updatePassword operation. + * @param {string} idToken The ID token. + * @param {string} newPassword The new password. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.updatePassword = function(idToken, newPassword) { + var request = { + 'idToken': idToken, + 'password': newPassword + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.SET_ACCOUNT_INFO_SENSITIVE, request); +}; + + +/** + * Requests setAccountInfo endpoint to set the email and password. This can be + * used to link an existing account to a new email and password account. + * @param {string} idToken The ID token. + * @param {string} newEmail The new email. + * @param {string} newPassword The new password. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.updateEmailAndPassword = function(idToken, + newEmail, newPassword) { + var request = { + 'idToken': idToken, + 'email': newEmail, + 'password': newPassword + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.SET_ACCOUNT_INFO_SENSITIVE, request); +}; + + +/** + * Maps the name of a field in the account info object to the backend enum + * value, for deletion of profile fields. + * @private {!Object} + */ +fireauth.RpcHandler.PROFILE_FIELD_TO_ENUM_NAME_ = { + 'displayName': 'DISPLAY_NAME', + 'photoUrl': 'PHOTO_URL' +}; + + +/** + * Updates the profile of the user. When resolved, promise returns a response + * similar to that of getAccountInfo. + * @param {string} idToken The ID token of the user whose profile is changing. + * @param {!Object} profileData The new profile data. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.updateProfile = function(idToken, profileData) { + var data = { + 'idToken': idToken + }; + var fieldsToDelete = []; + + // Copy over the relevant fields from profileData, or explicitly flag a field + // for deletion if null is passed as the value. Note that this currently only + // checks profileData to the first level. + goog.object.forEach(fireauth.RpcHandler.PROFILE_FIELD_TO_ENUM_NAME_, + function(enumName, fieldName) { + var fieldValue = profileData[fieldName]; + if (fieldValue === null) { + // If null is explicitly provided, delete the field. + fieldsToDelete.push(enumName); + } else if (fieldName in profileData) { + // If the field is explicitly set, send it to the backend. + data[fieldName] = fieldValue; + } + }); + if (fieldsToDelete.length) { + data['deleteAttribute'] = fieldsToDelete; + } + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.SET_ACCOUNT_INFO, data); +}; + + +/** + * Validates a request for an email action code for password reset. + * @param {!Object} request The getOobCode request data for password reset. + * @private + */ +fireauth.RpcHandler.validateOobCodeRequest_ = function(request) { + if (request['requestType'] != + fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } + fireauth.RpcHandler.validateRequestHasEmail_(request); +}; + + +/** + * Validates a request for an email action code for password reset. + * @param {!Object} request The getOobCode request data for password reset. + * @private + */ +fireauth.RpcHandler.validateEmailVerificationCodeRequest_ = function(request) { + if (request['requestType'] != + fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Requests getOobCode endpoint for password reset, returns promise that + * resolves with user's email. + * @param {string} email The email account with the password to be reset. + * @param {!Object} additionalRequestData Additional data to add to the request. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.sendPasswordResetEmail = + function(email, additionalRequestData) { + var request = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': email + }; + // Extend the original request with the additional data. + goog.object.extend(request, additionalRequestData); + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.GET_OOB_CODE, request); +}; + + +/** + * Requests getOobCode endpoint for email verification, returns promise that + * resolves with user's email. + * @param {string} idToken The idToken of the user confirming his email. + * @param {!Object} additionalRequestData Additional data to add to the request. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.sendEmailVerification = + function(idToken, additionalRequestData) { + var request = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken + }; + // Extend the original request with the additional data. + goog.object.extend(request, additionalRequestData); + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.GET_EMAIL_VERIFICATION_CODE, request); +}; + + +/** + * Requests sendVerificationCode endpoint for verifying the user's ownership of + * a phone number. It resolves with a sessionInfo (verificationId). + * @param {!Object} request The verification request which contains a phone + * number and an assertion. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.sendVerificationCode = function(request) { + // In the future, we could support other types of assertions so for now, + // we are keeping the request an object. + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.SEND_VERIFICATION_CODE, request); +}; + + +/** + * Requests verifyPhoneNumber endpoint for sign in/sign up phone number + * authentication flow and resolves with the STS token response. + * @param {!Object} request The phone number ID and code to exchange for a + * Firebase Auth session. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyPhoneNumber = function(request) { + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_PHONE_NUMBER, request); +}; + + +/** + * Requests verifyPhoneNumber endpoint for link/update phone number + * authentication flow and resolves with the STS token response. + * @param {!Object} request The phone number ID and code to exchange for a + * Firebase Auth session. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyPhoneNumberForLinking = function(request) { + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_PHONE_NUMBER_FOR_LINKING, request); +}; + + +/** + * Validates a response to a phone number linking request. + * @param {?Object} response The server response data. + * @private + */ +fireauth.RpcHandler.validateVerifyPhoneNumberForLinkingResponse_ = + function(response) { + if (response[fireauth.RpcHandler.AuthServerField.TEMPORARY_PROOF]) { + response['code'] = fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE; + throw fireauth.AuthErrorWithCredential.fromPlainObject(response); + } + + // If there's no temporary proof, then we expect the request to have + // succeeded and returned an ID token. + fireauth.RpcHandler.validateIdTokenResponse_(response); +}; + + +/** + * Requests verifyPhoneNumber endpoint for reauthenticating with a phone number + * and resolves with the STS token response. + * @param {!Object} request The phone number ID, code, and current ID token to + * exchange for a refreshed Firebase Auth session. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyPhoneNumberForExisting = function(request) { + request['operation'] = 'REAUTH'; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_PHONE_NUMBER_FOR_EXISTING, + request); +}; + + +/** + * The custom error map for reauth with verifyPhoneNumber. + * @private {!fireauth.RpcHandler.ServerErrorMap} + */ +fireauth.RpcHandler.verifyPhoneNumberForExistingErrorMap_ = {}; + +// For most RPCs, the backend error USER_NOT_FOUND means that the sent STS +// token is invalid. However, for this specific case, USER_NOT_FOUND actually +// means that the sent credential is invalid. +fireauth.RpcHandler.verifyPhoneNumberForExistingErrorMap_[ + fireauth.RpcHandler.ServerError.USER_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + + +/** + * Validates a request to deleteLinkedAccounts. + * @param {?Object} request + * @private + */ +fireauth.RpcHandler.validateDeleteLinkedAccountsRequest_ = function(request) { + if (!goog.isArray(request['deleteProvider'])) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Updates the providers for the account associated with the idToken. + * @param {string} idToken The ID token. + * @param {!Array} providersToDelete The array of providers to delete. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.deleteLinkedAccounts = + function(idToken, providersToDelete) { + var request = { + 'idToken': idToken, + 'deleteProvider': providersToDelete + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.DELETE_LINKED_ACCOUNTS, + request); +}; + + +/** + * Validates a verifyAssertion request. + * @param {?Object} request The verifyAssertion request data. + * @private + */ +fireauth.RpcHandler.validateVerifyAssertionRequest_ = function(request) { + // Either (requestUri and sessionId) or (requestUri and postBody) are + // required. + if (!request['requestUri'] || + (!request['sessionId'] && !request['postBody'])) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Validates a response from verifyAssertionForExisting. + * @param {?Object} response The verifyAssertionForExisting response data. + * @private + */ +fireauth.RpcHandler.validateVerifyAssertionForExistingResponse_ = + function(response) { + // When returnIdpCredential is set to true and the account is new, no error + // is thrown but an errorMessage is added to the response. No idToken is + // passed. + if (response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE] && + response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE] == + fireauth.RpcHandler.ServerError.USER_NOT_FOUND) { + // This corresponds to user-not-found. + throw new fireauth.AuthError(fireauth.authenum.Error.USER_DELETED); + } + // Need confirmation should not be returned when do not create new user flag + // is set. + // If no error found and ID token is missing, throw an internal error. + if (!response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Validates a response from verifyAssertion. + * @param {?Object} response The verifyAssertion response data. + * @private + */ +fireauth.RpcHandler.validateVerifyAssertionResponse_ = function(response) { + var error = null; + if (response[fireauth.RpcHandler.AuthServerField.NEED_CONFIRMATION]) { + // Account linking required, previously logged in to another account + // with same email. User must authenticate they are owners of the + // first account. + // If enough info for Auth linking error, throw an instance of Auth linking + // error. This will be used by developer after reauthenticating with email + // provided by error to link using the credentials in Auth linking error. + // If missing information, return regular Auth error. + response['code'] = fireauth.authenum.Error.NEED_CONFIRMATION; + error = fireauth.AuthErrorWithCredential.fromPlainObject(response); + } else if (response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE] == + fireauth.RpcHandler.ServerError.FEDERATED_USER_ID_ALREADY_LINKED) { + // When FEDERATED_USER_ID_ALREADY_LINKED returned in error message, auth + // credential and email will also be returned, throw relevant error in that + // case. + // In this case the developer needs to signInWithCredential to the returned + // credentials. + response['code'] = fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE; + error = fireauth.AuthErrorWithCredential.fromPlainObject(response); + } else if (response[fireauth.RpcHandler.AuthServerField.ERROR_MESSAGE] == + fireauth.RpcHandler.ServerError.EMAIL_EXISTS) { + // When EMAIL_EXISTS returned in error message, Auth credential and email + // will also be returned, throw relevant error in that case. + // In this case, the developers needs to sign in the user to the original + // owner of the account and then link to the returned credential here. + response['code'] = fireauth.authenum.Error.EMAIL_EXISTS; + error = fireauth.AuthErrorWithCredential.fromPlainObject(response); + } + // If error found, throw it. + if (error) { + throw error; + } + // If no error found and ID token is missing, throw an internal error. + if (!response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Validates a verifyAssertion with linking request. + * @param {?Object} request The verifyAssertion request data. + * @private + */ +fireauth.RpcHandler.validateVerifyAssertionLinkRequest_ = function(request) { + // idToken with either (requestUri and sessionId) or (requestUri and postBody) + // are required. + fireauth.RpcHandler.validateVerifyAssertionRequest_(request); + if (!request['idToken']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * @typedef {{ + * autoCreate: (boolean|undefined), + * requestUri: string, + * postBody: (?string|undefined), + * pendingIdToken: (?string|undefined), + * sessionId: (?string|undefined), + * idToken: (?string|undefined), + * returnIdpCredential: (boolean|undefined) + * }} + */ +fireauth.RpcHandler.VerifyAssertionData; + + +/** + * Requests verifyAssertion endpoint. When resolved, promise returns the whole + * response. + * @param {!fireauth.RpcHandler.VerifyAssertionData} request + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyAssertion = function(request) { + // Force Auth credential to be returned on the following errors: + // FEDERATED_USER_ID_ALREADY_LINKED + // EMAIL_EXISTS + request['returnIdpCredential'] = true; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_ASSERTION, + request); +}; + + +/** + * Requests verifyAssertion endpoint for federated account linking. When + * resolved, promise returns the whole response. + * @param {!fireauth.RpcHandler.VerifyAssertionData} request + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyAssertionForLinking = function(request) { + // Force Auth credential to be returned on the following errors: + // FEDERATED_USER_ID_ALREADY_LINKED + // EMAIL_EXISTS + request['returnIdpCredential'] = true; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_ASSERTION_FOR_LINKING, + request); +}; + + +/** + * Requests verifyAssertion endpoint for an existing federated account. When + * resolved, promise returns the whole response. If not existing, a + * user-not-found error is thrown. + * @param {!fireauth.RpcHandler.VerifyAssertionData} request + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.verifyAssertionForExisting = function(request) { + // Since we are setting returnIdpCredential to true, a response will be + // returned even though the account doesn't exist but an error message is + // appended with value set to USER_NOT_FOUND. If this flag is not passed, only + // the USER_NOT_FOUND error is thrown without any response. + request['returnIdpCredential'] = true; + // Do not create a new account if the user doesn't exist. + request['autoCreate'] = false; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.VERIFY_ASSERTION_FOR_EXISTING, + request); +}; + + +/** + * Validates a request that should contain an action code. + * @param {!Object} request + * @private + */ +fireauth.RpcHandler.validateApplyActionCodeRequest_ = function(request) { + if (!request['oobCode']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INVALID_OOB_CODE); + } +}; + + +/** + * Validates that a checkActionCode response contains the email and requestType + * fields. + * @param {!Object} response The raw response returned by the server. + * @private + */ +fireauth.RpcHandler.validateCheckActionCodeResponse_ = function(response) { + // If the code is invalid, usually a clear error would be returned. + // In this case, something unexpected happened. + // Both fields are required. + if (!response['email'] || !response['requestType']) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + } +}; + + +/** + * Requests resetPassword endpoint for password reset, returns promise that + * resolves with user's email. + * @param {string} code The email action code to confirm for password reset. + * @param {string} newPassword The new password. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.confirmPasswordReset = + function(code, newPassword) { + var request = { + 'oobCode': code, + 'newPassword': newPassword + }; + return this.invokeRpc_(fireauth.RpcHandler.ApiMethod.RESET_PASSWORD, request); +}; + + +/** + * Checks the validity of an email action code and returns the response + * received. + * @param {string} code The email action code to check. + * @return {!goog.Promise} + */ +fireauth.RpcHandler.prototype.checkActionCode = function(code) { + var request = { + 'oobCode': code + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.CHECK_ACTION_CODE, request); +}; + + +/** + * Applies an out-of-band email action code, such as an email verification code. + * @param {string} code The email action code. + * @return {!goog.Promise} A promise that resolves with the user's + * email. + */ +fireauth.RpcHandler.prototype.applyActionCode = function(code) { + var request = { + 'oobCode': code + }; + return this.invokeRpc_( + fireauth.RpcHandler.ApiMethod.APPLY_OOB_CODE, request); +}; + + +/** + * The specification of an RPC call. The fields are: + *
    + *
  • cachebuster: defines whether to send a unique string with request to + * force the backend to return an uncached response to request. + *
  • customErrorMap: A map of backend error codes to client-side errors. + * Any entries set here override the default handling of the backend error + * code. + *
  • endpoint: defines the backend endpoint to call. + *
  • httpMethod: defines the HTTP method to use, defaulting to POST if not + * specified. + *
  • requestRequiredFields: an array of the fields that are required in the + * request. The RPC call will fail with an INTERNAL_ERROR error if a + * required field is not present or if it is null, undefined, or the empty + * string. + *
  • requestValidator: a function that takes in the request object and throws + * an error if the request is invalid. + *
  • responseValidator: a function that takes in the response object and + * throws an error if the response is invalid. + *
  • responseField: the field of the response object that will be returned + * from the RPC call. If no field is specified, the entire response object + * will be returned. + *
  • returnSecureToken: Set to true to explicitly request STS tokens instead + * of legacy Google Identity Toolkit tokens from the backend. + *
+ * @typedef {{ + * cachebuster: (boolean|undefined), + * customErrorMap: (!fireauth.RpcHandler.ServerErrorMap|undefined), + * endpoint: string, + * httpMethod: (!fireauth.RpcHandler.HttpMethod|undefined), + * requestRequiredFields: (!Array|undefined), + * requestValidator: (function(!Object):void|undefined), + * responseValidator: (function(!Object):void|undefined), + * responseField: (string|undefined), + * returnSecureToken: (boolean|undefined) + * }} + */ +fireauth.RpcHandler.ApiMethodHandler; + + +/** + * The specifications for the backend API methods. + * @enum {!fireauth.RpcHandler.ApiMethodHandler} + */ +fireauth.RpcHandler.ApiMethod = { + APPLY_OOB_CODE: { + endpoint: 'setAccountInfo', + requestValidator: fireauth.RpcHandler.validateApplyActionCodeRequest_, + responseField: fireauth.RpcHandler.AuthServerField.EMAIL + }, + CHECK_ACTION_CODE: { + endpoint: 'resetPassword', + requestValidator: fireauth.RpcHandler.validateApplyActionCodeRequest_, + responseValidator: fireauth.RpcHandler.validateCheckActionCodeResponse_ + }, + CREATE_ACCOUNT: { + endpoint: 'signupNewUser', + requestValidator: fireauth.RpcHandler.validateCreateAccountRequest_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, + CREATE_AUTH_URI: { + endpoint: 'createAuthUri' + }, + DELETE_ACCOUNT: { + endpoint: 'deleteAccount', + requestRequiredFields: ['idToken'] + }, + DELETE_LINKED_ACCOUNTS: { + endpoint: 'setAccountInfo', + requestRequiredFields: ['idToken', 'deleteProvider'], + requestValidator: fireauth.RpcHandler.validateDeleteLinkedAccountsRequest_ + }, + GET_ACCOUNT_INFO: { + endpoint: 'getAccountInfo' + }, + GET_AUTH_URI: { + endpoint: 'createAuthUri', + requestRequiredFields: ['continueUri', 'providerId'], + responseValidator: fireauth.RpcHandler.validateGetAuthResponse_ + }, + GET_EMAIL_VERIFICATION_CODE: { + endpoint: 'getOobConfirmationCode', + requestRequiredFields: ['idToken', 'requestType'], + requestValidator: fireauth.RpcHandler.validateEmailVerificationCodeRequest_, + responseField: fireauth.RpcHandler.AuthServerField.EMAIL + }, + GET_OOB_CODE: { + endpoint: 'getOobConfirmationCode', + requestRequiredFields: ['requestType'], + requestValidator: fireauth.RpcHandler.validateOobCodeRequest_, + responseField: fireauth.RpcHandler.AuthServerField.EMAIL + }, + GET_PROJECT_CONFIG: { + // Microsoft edge caching bug. There are two getProjectConfig API calls, + // first from top level window and then from iframe. The second call has a + // response of 304 which means it's a cached response. We suspect the call + // from iframe is reusing the response from the first call and checks the + // allowed origin in the cached response, which only contains the domain for + // the top level window. + cachebuster: true, + endpoint: 'getProjectConfig', + httpMethod: fireauth.RpcHandler.HttpMethod.GET + }, + GET_RECAPTCHA_PARAM: { + cachebuster: true, + endpoint: 'getRecaptchaParam', + httpMethod: fireauth.RpcHandler.HttpMethod.GET, + responseValidator: fireauth.RpcHandler.validateGetRecaptchaParamResponse_ + }, + RESET_PASSWORD: { + endpoint: 'resetPassword', + requestValidator: fireauth.RpcHandler.validateApplyActionCodeRequest_, + responseField: fireauth.RpcHandler.AuthServerField.EMAIL + }, + RETURN_DYNAMIC_LINK: { + cachebuster: true, + endpoint: 'getProjectConfig', + httpMethod: fireauth.RpcHandler.HttpMethod.GET, + responseField: fireauth.RpcHandler.AuthServerField.DYNAMIC_LINKS_DOMAIN + }, + SEND_VERIFICATION_CODE: { + endpoint: 'sendVerificationCode', + // Currently only reCAPTCHA tokens supported. + requestRequiredFields: ['phoneNumber', 'recaptchaToken'], + responseField: fireauth.RpcHandler.AuthServerField.SESSION_INFO + }, + SET_ACCOUNT_INFO: { + endpoint: 'setAccountInfo', + requestRequiredFields: ['idToken'], + requestValidator: fireauth.RpcHandler.validateEmailIfPresent_, + returnSecureToken: true // Maybe updating email will invalidate token in the + // future, this will prevent breaking the client. + }, + SET_ACCOUNT_INFO_SENSITIVE: { + endpoint: 'setAccountInfo', + requestRequiredFields: ['idToken'], + requestValidator: fireauth.RpcHandler.validateSetAccountInfoSensitive_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true // Updating password will send back new sts tokens. + }, + SIGN_IN_ANONYMOUSLY: { + endpoint: 'signupNewUser', + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, + VERIFY_ASSERTION: { + endpoint: 'verifyAssertion', + requestValidator: fireauth.RpcHandler.validateVerifyAssertionRequest_, + responseValidator: fireauth.RpcHandler.validateVerifyAssertionResponse_, + returnSecureToken: true + }, + VERIFY_ASSERTION_FOR_EXISTING: { + endpoint: 'verifyAssertion', + requestValidator: fireauth.RpcHandler.validateVerifyAssertionRequest_, + responseValidator: + fireauth.RpcHandler.validateVerifyAssertionForExistingResponse_, + returnSecureToken: true + }, + VERIFY_ASSERTION_FOR_LINKING: { + endpoint: 'verifyAssertion', + requestValidator: fireauth.RpcHandler.validateVerifyAssertionLinkRequest_, + responseValidator: fireauth.RpcHandler.validateVerifyAssertionResponse_, + returnSecureToken: true + }, + VERIFY_CUSTOM_TOKEN: { + endpoint: 'verifyCustomToken', + requestValidator: fireauth.RpcHandler.validateVerifyCustomTokenRequest_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, + VERIFY_PASSWORD: { + endpoint: 'verifyPassword', + requestValidator: fireauth.RpcHandler.validateVerifyPasswordRequest_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_, + returnSecureToken: true + }, + VERIFY_PHONE_NUMBER: { + endpoint: 'verifyPhoneNumber', + requestValidator: fireauth.RpcHandler.validateVerifyPhoneNumberRequest_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_ + }, + VERIFY_PHONE_NUMBER_FOR_LINKING: { + endpoint: 'verifyPhoneNumber', + requestValidator: fireauth.RpcHandler.validateVerifyPhoneNumberLinkRequest_, + responseValidator: + fireauth.RpcHandler.validateVerifyPhoneNumberForLinkingResponse_ + }, + VERIFY_PHONE_NUMBER_FOR_EXISTING: { + customErrorMap: fireauth.RpcHandler.verifyPhoneNumberForExistingErrorMap_, + endpoint: 'verifyPhoneNumber', + requestValidator: fireauth.RpcHandler.validateVerifyPhoneNumberRequest_, + responseValidator: fireauth.RpcHandler.validateIdTokenResponse_ + } +}; + + +/** + * @const {string} The parameter to send to the backend to specify that the + * client accepts STS tokens directly from Firebear backends. + * @private + */ +fireauth.RpcHandler.USE_STS_TOKEN_PARAM_ = 'returnSecureToken'; + + +/** + * Invokes an RPC method according to the specification defined by + * {@code fireauth.RpcHandler.ApiMethod}. + * @param {!fireauth.RpcHandler.ApiMethod} method The method to invoke. + * @param {!Object} request The input data to the method. + * @return {!goog.Promise} A promise that resolves with the results of the RPC. + * The format of the results can be modified in + * {@code fireauth.RpcHandler.ApiMethod}. + * @private + */ +fireauth.RpcHandler.prototype.invokeRpc_ = function(method, request) { + if (!fireauth.object.hasNonEmptyFields( + request, method.requestRequiredFields)) { + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR)); + } + + var httpMethod = method.httpMethod || fireauth.RpcHandler.HttpMethod.POST; + var self = this; + var response; + return goog.Promise.resolve(request) + .then(method.requestValidator) + .then(function() { + if (method.returnSecureToken) { + // Signal that the client accepts STS tokens, for the legacy Google + // Identity Toolkit token to STS token migration. + request[fireauth.RpcHandler.USE_STS_TOKEN_PARAM_] = true; + } + return self.requestFirebaseEndpoint(method.endpoint, httpMethod, + request, method.customErrorMap, method.cachebuster || false); + }) + .then(function(tempResponse) { + response = tempResponse; + return response; + }) + .then(method.responseValidator) + .then(function() { + if (!method.responseField) { + return response; + } + if (!(method.responseField in response)) { + throw new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + } + return response[method.responseField]; + }); +}; + + +/** + * Checks if the server response contains errors. + * @param {!Object} resp The API response. + * @return {boolean} {@code true} if the response contains errors. + * @private + */ +fireauth.RpcHandler.hasError_ = function(resp) { + return !!resp['error']; +}; + + +/** + * Converts a server response with errors to a developer-facing AuthError. + * @param {!Object} response The server response. + * @param {?fireauth.RpcHandler.ServerErrorMap=} opt_customErrorMap A map of + * backend error codes to client-side errors. Any entries set here + * override the default handling of the backend error code. + * @return {!fireauth.AuthError} The corresponding error object. + * @private + */ +fireauth.RpcHandler.getDeveloperError_ = + function(response, opt_customErrorMap) { + var errorMessage; + var apiaryError = fireauth.RpcHandler.getApiaryError_(response); + if (apiaryError) { + return apiaryError; + } + + var serverErrorCode = fireauth.RpcHandler.getErrorCode_(response); + + var errorMap = {}; + + // Custom token errors. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CUSTOM_TOKEN] = + fireauth.authenum.Error.INVALID_CUSTOM_TOKEN; + errorMap[fireauth.RpcHandler.ServerError.CREDENTIAL_MISMATCH] = + fireauth.authenum.Error.CREDENTIAL_MISMATCH; + // This can only happen if the SDK sends a bad request. + errorMap[fireauth.RpcHandler.ServerError.MISSING_CUSTOM_TOKEN] = + fireauth.authenum.Error.INTERNAL_ERROR; + + // Create Auth URI errors. + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDENTIFIER] = + fireauth.authenum.Error.INVALID_EMAIL; + // This can only happen if the SDK sends a bad request. + errorMap[fireauth.RpcHandler.ServerError.MISSING_CONTINUE_URI] = + fireauth.authenum.Error.INTERNAL_ERROR; + + // Sign in with email and password errors (some apply to sign up too). + errorMap[fireauth.RpcHandler.ServerError.INVALID_EMAIL] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_PASSWORD] = + fireauth.authenum.Error.INVALID_PASSWORD; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + // This can only happen if the SDK sends a bad request. + errorMap[fireauth.RpcHandler.ServerError.MISSING_PASSWORD] = + fireauth.authenum.Error.INTERNAL_ERROR; + + // Sign up with email and password errors. + errorMap[fireauth.RpcHandler.ServerError.EMAIL_EXISTS] = + fireauth.authenum.Error.EMAIL_EXISTS; + errorMap[fireauth.RpcHandler.ServerError.PASSWORD_LOGIN_DISABLED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + + // Verify assertion for sign in with credential errors: + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDP_RESPONSE] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.FEDERATED_USER_ID_ALREADY_LINKED] = + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE; + + // Email template errors while sending emails: + errorMap[fireauth.RpcHandler.ServerError.INVALID_MESSAGE_PAYLOAD] = + fireauth.authenum.Error.INVALID_MESSAGE_PAYLOAD; + errorMap[fireauth.RpcHandler.ServerError.INVALID_RECIPIENT_EMAIL] = + fireauth.authenum.Error.INVALID_RECIPIENT_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SENDER] = + fireauth.authenum.Error.INVALID_SENDER; + + // Send Password reset email errors: + errorMap[fireauth.RpcHandler.ServerError.EMAIL_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + + // Reset password errors: + errorMap[fireauth.RpcHandler.ServerError.EXPIRED_OOB_CODE] = + fireauth.authenum.Error.EXPIRED_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_OOB_CODE] = + fireauth.authenum.Error.INVALID_OOB_CODE; + // This can only happen if the SDK sends a bad request. + errorMap[fireauth.RpcHandler.ServerError.MISSING_OOB_CODE] = + fireauth.authenum.Error.INTERNAL_ERROR; + + // Operations that require ID token in request: + errorMap[fireauth.RpcHandler.ServerError.CREDENTIAL_TOO_OLD_LOGIN_AGAIN] = + fireauth.authenum.Error.CREDENTIAL_TOO_OLD_LOGIN_AGAIN; + errorMap[fireauth.RpcHandler.ServerError.INVALID_ID_TOKEN] = + fireauth.authenum.Error.INVALID_AUTH; + errorMap[fireauth.RpcHandler.ServerError.TOKEN_EXPIRED] = + fireauth.authenum.Error.TOKEN_EXPIRED; + errorMap[fireauth.RpcHandler.ServerError.USER_NOT_FOUND] = + fireauth.authenum.Error.TOKEN_EXPIRED; + + // CORS issues. + errorMap[fireauth.RpcHandler.ServerError.CORS_UNSUPPORTED] = + fireauth.authenum.Error.CORS_UNSUPPORTED; + + // Dynamic link not activated. + errorMap[fireauth.RpcHandler.ServerError.DYNAMIC_LINK_NOT_ACTIVATED] = + fireauth.authenum.Error.DYNAMIC_LINK_NOT_ACTIVATED; + + // iosBundleId or androidPackageName not valid error. + errorMap[fireauth.RpcHandler.ServerError.INVALID_APP_ID] = + fireauth.authenum.Error.INVALID_APP_ID; + + // Other errors. + errorMap[fireauth.RpcHandler.ServerError.TOO_MANY_ATTEMPTS_TRY_LATER] = + fireauth.authenum.Error.TOO_MANY_ATTEMPTS_TRY_LATER; + errorMap[fireauth.RpcHandler.ServerError.WEAK_PASSWORD] = + fireauth.authenum.Error.WEAK_PASSWORD; + errorMap[fireauth.RpcHandler.ServerError.OPERATION_NOT_ALLOWED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + errorMap[fireauth.RpcHandler.ServerError.USER_CANCELLED] = + fireauth.authenum.Error.USER_CANCELLED; + + // Phone Auth related errors. + errorMap[fireauth.RpcHandler.ServerError.CAPTCHA_CHECK_FAILED] = + fireauth.authenum.Error.CAPTCHA_CHECK_FAILED; + errorMap[fireauth.RpcHandler.ServerError.INVALID_APP_CREDENTIAL] = + fireauth.authenum.Error.INVALID_APP_CREDENTIAL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_CODE] = + fireauth.authenum.Error.INVALID_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_PHONE_NUMBER] = + fireauth.authenum.Error.INVALID_PHONE_NUMBER; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SESSION_INFO] = + fireauth.authenum.Error.INVALID_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.INVALID_TEMPORARY_PROOF] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_APP_CREDENTIAL] = + fireauth.authenum.Error.MISSING_APP_CREDENTIAL; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CODE] = + fireauth.authenum.Error.MISSING_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_PHONE_NUMBER] = + fireauth.authenum.Error.MISSING_PHONE_NUMBER; + errorMap[fireauth.RpcHandler.ServerError.MISSING_SESSION_INFO] = + fireauth.authenum.Error.MISSING_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.QUOTA_EXCEEDED] = + fireauth.authenum.Error.QUOTA_EXCEEDED; + errorMap[fireauth.RpcHandler.ServerError.SESSION_EXPIRED] = + fireauth.authenum.Error.CODE_EXPIRED; + + // Other action code errors when additional settings passed. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CONTINUE_URI] = + fireauth.authenum.Error.INVALID_CONTINUE_URI; + // MISSING_CONTINUE_URI is getting mapped to INTERNAL_ERROR above. + // This is OK as this error will be caught by client side validation. + errorMap[fireauth.RpcHandler.ServerError.MISSING_ANDROID_PACKAGE_NAME] = + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME; + errorMap[fireauth.RpcHandler.ServerError.MISSING_IOS_BUNDLE_ID] = + fireauth.authenum.Error.MISSING_IOS_BUNDLE_ID; + errorMap[fireauth.RpcHandler.ServerError.UNAUTHORIZED_DOMAIN] = + fireauth.authenum.Error.UNAUTHORIZED_DOMAIN; + + // getProjectConfig errors when clientId is passed. + errorMap[fireauth.RpcHandler.ServerError.INVALID_OAUTH_CLIENT_ID] = + fireauth.authenum.Error.INVALID_OAUTH_CLIENT_ID; + // getProjectConfig errors when sha1Cert is passed. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CERT_HASH] = + fireauth.authenum.Error.INVALID_CERT_HASH; + + // Override errors set in the custom map. + var customErrorMap = opt_customErrorMap || {}; + goog.object.extend(errorMap, customErrorMap); + + // Get detailed message if available. + errorMessage = fireauth.RpcHandler.getErrorCodeDetails(serverErrorCode); + + // Handle backend errors where the error code can be a prefix of the message + // (e.g. "WEAK_PASSWORD : Password should be at least 6 characters"). + // Use the details after the colon as the error message. If none available, + // pass undefined, which will default to the client hard coded error messages. + for (var prefixCode in errorMap) { + if (serverErrorCode.indexOf(prefixCode) === 0) { + return new fireauth.AuthError(errorMap[prefixCode], errorMessage); + } + } + + // No error message found, return the serialized response as the message. + // This is likely to be an Apiary error for unexpected cases like keyExpired, + // etc. + if (!errorMessage && response) { + errorMessage = fireauth.util.stringifyJSON(response); + } + // The backend returned some error we don't recognize; this is an error on + // our side. + return new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, errorMessage); +}; + + +/** + * @param {string} serverMessage The server error code. + * @return {string|undefined} The detailed error code message. + */ +fireauth.RpcHandler.getErrorCodeDetails = function(serverMessage) { + // Use the error details part as the autherror message. + // For a message INVALID_CUSTOM_TOKEN : [error detail here], + // The Auth error message should be [error detail here]. + // No space should be contained in the error code, otherwise no detailed error + // message returned. + var matches = serverMessage.match(/^[^\s]+\s*:\s*(.*)$/); + if (matches && matches.length > 1) { + return matches[1]; + } + return undefined; +}; + + +/** + * Gets the Apiary error from a backend response, if applicable. + * @param {!Object} response The API response. + * @return {?fireauth.AuthError} The error, if applicable. + * @private + */ +fireauth.RpcHandler.getApiaryError_ = function(response) { + var error = response['error'] && response['error']['errors'] && + response['error']['errors'][0] || {}; + var reason = error['reason'] || ''; + + var errorReasonMap = { + 'keyInvalid': fireauth.authenum.Error.INVALID_API_KEY, + 'ipRefererBlocked': fireauth.authenum.Error.APP_NOT_AUTHORIZED + }; + + if (errorReasonMap[reason]) { + return new fireauth.AuthError(errorReasonMap[reason]); + } + + return null; +}; + + +/** + * Gets the server error code from the response. + * @param {!Object} resp The API response. + * @return {string} The error code if present. + * @private + */ +fireauth.RpcHandler.getErrorCode_ = function(resp) { + return (resp['error'] && resp['error']['message']) || ''; +}; diff --git a/packages/auth/src/storage/asyncstorage.js b/packages/auth/src/storage/asyncstorage.js new file mode 100644 index 00000000000..e5cb678e20e --- /dev/null +++ b/packages/auth/src/storage/asyncstorage.js @@ -0,0 +1,106 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.AsyncStorage'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.Storage'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); + + +/** + * AsyncStorage provides an interface to the React Native AsyncStorage API. + * @param {!Object=} opt_asyncStorage The AsyncStorage API. If not provided + * this method will attempt to fetch an implementation from + * firebase.INTERNAL.reactNative. + * @constructor + * @implements {fireauth.storage.Storage} + * @see https://facebook.github.io/react-native/docs/asyncstorage.html + */ +fireauth.storage.AsyncStorage = function(opt_asyncStorage) { + /** + * The underlying storage instance for persistent data. + * @private + */ + this.storage_ = + opt_asyncStorage || (firebase.INTERNAL['reactNative'] && + firebase.INTERNAL['reactNative']['AsyncStorage']); + + if (!this.storage_) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'The React Native compatibility library was not found.'); + } +}; + + +/** + * Retrieves the value stored at the key. + * @param {string} key + * @return {!goog.Promise<*>} + * @override + */ +fireauth.storage.AsyncStorage.prototype.get = function(key) { + return goog.Promise.resolve(this.storage_['getItem'](key)) + .then(function(val) { + return val && fireauth.util.parseJSON(val); + }); +}; + + +/** + * Stores the value at the specified key. + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + * @override + */ +fireauth.storage.AsyncStorage.prototype.set = function(key, value) { + return goog.Promise.resolve( + this.storage_['setItem'](key, fireauth.util.stringifyJSON(value))); +}; + + +/** + * Removes the value at the specified key. + * @param {string} key + * @return {!goog.Promise} + * @override + */ +fireauth.storage.AsyncStorage.prototype.remove = function(key) { + return goog.Promise.resolve(this.storage_['removeItem'](key)); +}; + + +/** + * Does nothing. AsyncStorage does not support storage events, + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.AsyncStorage.prototype.addStorageListener = function( + listener) {}; + + +/** + * Does nothing. AsyncStorage does not support storage events, + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.AsyncStorage.prototype.removeStorageListener = function( + listener) {}; diff --git a/packages/auth/src/storage/factory.js b/packages/auth/src/storage/factory.js new file mode 100644 index 00000000000..bc5e9884c92 --- /dev/null +++ b/packages/auth/src/storage/factory.js @@ -0,0 +1,128 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.Factory'); +goog.provide('fireauth.storage.Factory.EnvConfig'); + +goog.require('fireauth.storage.AsyncStorage'); +goog.require('fireauth.storage.InMemoryStorage'); +goog.require('fireauth.storage.IndexedDB'); +goog.require('fireauth.storage.LocalStorage'); +goog.require('fireauth.storage.NullStorage'); +goog.require('fireauth.storage.SessionStorage'); +goog.require('fireauth.util'); + + +/** + * Factory manages the storage implementations and determines the correct one + * for the current environment. + * @param {!fireauth.storage.Factory.EnvConfigType} env The storage + * configuration for the current environment. + * @constructor + */ +fireauth.storage.Factory = function(env) { + /** @const @private {!fireauth.storage.Factory.EnvConfigType} */ + this.env_ = env; +}; + + +/** + * Construct the singleton instance of the Factory, automatically detecting + * the current environment. + * @return {!fireauth.storage.Factory} + */ +fireauth.storage.Factory.getInstance = function() { + if (!fireauth.storage.Factory.instance_) { + fireauth.storage.Factory.instance_ = + new fireauth.storage.Factory(fireauth.storage.Factory.getEnvConfig()); + } + return fireauth.storage.Factory.instance_; +}; + + +/** + * @typedef {{ + * persistent: function(new:fireauth.storage.Storage), + * temporary: function(new:fireauth.storage.Storage) + * }} + */ +fireauth.storage.Factory.EnvConfigType; + + +/** + * Configurations of storage for different environments. + * @enum {!fireauth.storage.Factory.EnvConfigType} + */ +fireauth.storage.Factory.EnvConfig = { + BROWSER: { + persistent: fireauth.storage.LocalStorage, + temporary: fireauth.storage.SessionStorage + }, + NODE: { + persistent: fireauth.storage.LocalStorage, + temporary: fireauth.storage.SessionStorage + }, + REACT_NATIVE: { + persistent: fireauth.storage.AsyncStorage, + temporary: fireauth.storage.NullStorage + } +}; + + +/** + * Detects the current environment and returns the appropriate environment + * configuration. + * @return {!fireauth.storage.Factory.EnvConfigType} + */ +fireauth.storage.Factory.getEnvConfig = function() { + var envMap = {}; + envMap[fireauth.util.Env.BROWSER] = + fireauth.storage.Factory.EnvConfig.BROWSER; + envMap[fireauth.util.Env.NODE] = + fireauth.storage.Factory.EnvConfig.NODE; + envMap[fireauth.util.Env.REACT_NATIVE] = + fireauth.storage.Factory.EnvConfig.REACT_NATIVE; + return envMap[fireauth.util.getEnvironment()]; +}; + + +/** + * @return {!fireauth.storage.Storage} The persistent storage instance. + */ +fireauth.storage.Factory.prototype.makePersistentStorage = function() { + if (fireauth.util.isLocalStorageNotSynchronized()) { + // In a browser environment, when an iframe and a popup web storage are not + // synchronized, use the indexedDB fireauth.storage.Storage implementation. + return fireauth.storage.IndexedDB.getFireauthManager(); + } + return new this.env_.persistent(); +}; + + +/** + * @return {!fireauth.storage.Storage} The temporary storage instance. + */ +fireauth.storage.Factory.prototype.makeTemporaryStorage = function() { + return new this.env_.temporary(); +}; + + +/** + * @return {!fireauth.storage.Storage} An in memory storage instance. + */ +fireauth.storage.Factory.prototype.makeInMemoryStorage = function() { + return new fireauth.storage.InMemoryStorage(); +}; diff --git a/packages/auth/src/storage/indexeddb.js b/packages/auth/src/storage/indexeddb.js new file mode 100644 index 00000000000..cc3f008eabb --- /dev/null +++ b/packages/auth/src/storage/indexeddb.js @@ -0,0 +1,520 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines a local storage interface with an indexedDB + * implementation to be used as a fallback with browsers that do not synchronize + * local storage changes between different windows of the same origin. + */ + +goog.provide('fireauth.storage.IndexedDB'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.Storage'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.array'); + + + +/** + * Initialize an indexedDB local storage manager used to mimic local storage + * using an indexedDB underlying implementation including the ability to listen + * to storage changes by key similar to localstorage storage event. + * @param {string} dbName The indexedDB database name where all local storage + * data is to be stored. + * @param {string} objectStoreName The indexedDB object store name where all + * local storage data is to be stored. + * @param {string} dataKeyPath The indexedDB object store index name used to key + * all local storage data. + * @param {string} valueKeyPath The indexedDB object store value field for each + * entry. + * @param {number} version The indexedDB database version number. + * @param {?IDBFactory=} opt_indexedDB The optional IndexedDB factory object. + * @implements {fireauth.storage.Storage} + * @constructor + */ +fireauth.storage.IndexedDB = function( + dbName, + objectStoreName, + dataKeyPath, + valueKeyPath, + version, + opt_indexedDB) { + // indexedDB not available, fail hard. + if (!fireauth.storage.IndexedDB.isAvailable()) { + throw new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + } + /** + * @const @private {string} The indexedDB database name where all local + * storage data is to be stored. + */ + this.dbName_ = dbName; + /** + * @const @private {string} The indexedDB object store name where all local + * storage data is to be stored. + */ + this.objectStoreName_ = objectStoreName; + /** + * @const @private {string} The indexedDB object store index name used to key + * all local storage data. + */ + this.dataKeyPath_ = dataKeyPath; + /** + * @const @private {string} The indexedDB object store value field for each + * entry. + */ + this.valueKeyPath_ = valueKeyPath; + /** @const @private {number} The indexedDB database version number. */ + this.version_ = version; + /** @private {!Object.} The local indexedDB map copy. */ + this.localMap_ = {}; + /** + * @private {!Array)>} Listeners to storage events. + */ + this.storageListeners_ = []; + /** @private {number} The indexedDB pending write operations tracker. */ + this.pendingOpsTracker_ = 0; + /** @private {!IDBFactory} The indexedDB factory object. */ + this.indexedDB_ = /** @type {!IDBFactory} */ ( + opt_indexedDB || goog.global.indexedDB); +}; + + + +/** + * The indexedDB database name where all local storage data is to be stored. + * @private @const {string} + */ +fireauth.storage.IndexedDB.DB_NAME_ = 'firebaseLocalStorageDb'; + + +/** + * The indexedDB object store name where all local storage data is to be stored. + * @private @const {string} + */ +fireauth.storage.IndexedDB.DATA_OBJECT_STORE_NAME_ = 'firebaseLocalStorage'; + + +/** + * The indexedDB object store index name used to key all local storage data. + * @private @const {string} + */ +fireauth.storage.IndexedDB.DATA_KEY_PATH_ = 'fbase_key'; + + +/** + * The indexedDB object store value field for each entry. + * @private @const {string} + */ +fireauth.storage.IndexedDB.VALUE_KEY_PATH_ = 'value'; + + +/** + * The indexedDB database version number. + * @private @const {number} + */ +fireauth.storage.IndexedDB.VERSION_ = 1; + + +/** + * The indexedDB polling delay time in milliseconds. + * @private @const {number} + */ +fireauth.storage.IndexedDB.POLLING_DELAY_ = 800; + + +/** + * The indexedDB polling stop error. + * @private @const {string} + */ +fireauth.storage.IndexedDB.STOP_ERROR_ = 'STOP_EVENT'; + + + +/** + * @return {!fireauth.storage.IndexedDB} The Firebase Auth indexedDB + * local storage manager. + */ +fireauth.storage.IndexedDB.getFireauthManager = function() { + if (!fireauth.storage.IndexedDB.managerInstance_) { + fireauth.storage.IndexedDB.managerInstance_ = + new fireauth.storage.IndexedDB( + fireauth.storage.IndexedDB.DB_NAME_, + fireauth.storage.IndexedDB.DATA_OBJECT_STORE_NAME_, + fireauth.storage.IndexedDB.DATA_KEY_PATH_, + fireauth.storage.IndexedDB.VALUE_KEY_PATH_, + fireauth.storage.IndexedDB.VERSION_); + } + return fireauth.storage.IndexedDB.managerInstance_; +}; + + + +/** + * Initializes The indexedDB database, creates it if not already created and + * opens it. + * @return {!goog.Promise} A promise for the database object. + * @private + */ +fireauth.storage.IndexedDB.prototype.initializeDb_ = function() { + var self = this; + return new goog.Promise(function(resolve, reject) { + var request = self.indexedDB_.open(self.dbName_, self.version_); + request.onerror = function(event) { + reject(new Error(event.target.errorCode)); + }; + request.onupgradeneeded = function(event) { + var db = event.target.result; + try { + db.createObjectStore( + self.objectStoreName_, + { + 'keyPath': self.dataKeyPath_ + }); + } catch (e) { + reject(e); + } + }; + request.onsuccess = function(event) { + var db = event.target.result; + resolve(db); + }; + }); +}; + + +/** + * Checks if indexedDB is intialized, if so, the callback is run, otherwise, + * it waits for the db to initialize and then runs the callback function. + * @return {!goog.Promise} A promise for the initialized indexedDB + * database. + * @private + */ +fireauth.storage.IndexedDB.prototype.initializeDbAndRun_ = + function() { + if (!this.initPromise_) { + this.initPromise_ = this.initializeDb_(); + } + return this.initPromise_; +}; + + +/** + * @return {boolean} Whether indexedDB is available or not. + */ +fireauth.storage.IndexedDB.isAvailable = function() { + return !!window.indexedDB; +}; + + +/** + * Creates a reference for the local storage indexedDB object store and returns + * it. + * @param {!IDBTransaction} tx The IDB transaction instance. + * @return {!IDBObjectStore} The indexedDB object store. + * @private + */ +fireauth.storage.IndexedDB.prototype.getDataObjectStore_ = + function(tx) { + return tx.objectStore(this.objectStoreName_); +}; + + +/** + * Creates an IDB transaction and returns it. + * @param {!IDBDatabase} db The indexedDB instance. + * @param {boolean} isReadWrite Whether the current indexedDB operation is a + * read/write operation or not. + * @return {!IDBTransaction} The requested IDB transaction instance. + * @private + */ +fireauth.storage.IndexedDB.prototype.getTransaction_ = + function(db, isReadWrite) { + var tx = db.transaction( + [this.objectStoreName_], + isReadWrite ? 'readwrite' : 'readonly'); + return tx; +}; + + +/** + * @param {!IDBRequest} request The IDB request instance. + * @return {!goog.Promise} The promise to resolve on transaction completion. + * @private + */ +fireauth.storage.IndexedDB.prototype.onIDBRequest_ = + function(request) { + return new goog.Promise(function(resolve, reject) { + request.onsuccess = function(event) { + if (event && event.target) { + resolve(event.target.result); + } else { + resolve(); + } + }; + request.onerror = function(event) { + reject(new Error(event.target.errorCode)); + }; + }); +}; + + +/** + * Sets the item's identified by the key provided to the value passed. If the + * item does not exist, it is created. An optional callback is run on success. + * @param {string} key The storage key for the item to set. If the item exists, + * it is updated, otherwise created. + * @param {*} value The value to store for the item to set. + * @return {!goog.Promise} A promise that resolves on operation success. + * @override + */ +fireauth.storage.IndexedDB.prototype.set = function(key, value) { + var isLocked = false; + var dbTemp; + var self = this; + return this.initializeDbAndRun_() + .then(function(db) { + dbTemp = db; + var objectStore = self.getDataObjectStore_( + self.getTransaction_(dbTemp, true)); + return self.onIDBRequest_(objectStore.get(key)); + }) + .then(function(data) { + var objectStore = self.getDataObjectStore_( + self.getTransaction_(dbTemp, true)); + if (data) { + // Update the value(s) in the object that you want to change + data.value = value; + // Put this updated object back into the database. + return self.onIDBRequest_(objectStore.put(data)); + } + self.pendingOpsTracker_++; + isLocked = true; + var obj = {}; + obj[self.dataKeyPath_] = key; + obj[self.valueKeyPath_] = value; + return self.onIDBRequest_(objectStore.add(obj)); + }) + .then(function() { + // Save in local copy to avoid triggering false external event. + self.localMap_[key] = value; + }) + .thenAlways(function() { + if (isLocked) { + self.pendingOpsTracker_--; + } + }); +}; + + +/** + * Retrieves a stored item identified by the key provided asynchronously. + * The value is passed to the callback function provided. + * @param {string} key The storage key for the item to fetch. + * @return {!goog.Promise} A promise that resolves with the item's value, or + * null if the item is not found. + * @override + */ +fireauth.storage.IndexedDB.prototype.get = function(key) { + var self = this; + return this.initializeDbAndRun_() + .then(function(db) { + return self.onIDBRequest_( + self.getDataObjectStore_(self.getTransaction_(db, false)).get(key)); + }) + .then(function(response) { + return response && response.value; + }); +}; + + +/** + * Deletes the item identified by the key provided and on success, runs the + * optional callback. + * @param {string} key The storage key for the item to remove. + * @return {!goog.Promise} A promise that resolves on operation success. + * @override + */ +fireauth.storage.IndexedDB.prototype.remove = function(key) { + var isLocked = false; + var self = this; + return this.initializeDbAndRun_() + .then(function(db) { + isLocked = true; + self.pendingOpsTracker_++; + return self.onIDBRequest_( + self.getDataObjectStore_( + self.getTransaction_(db, true))['delete'](key)); + }).then(function() { + // Delete from local copy to avoid triggering false external event. + delete self.localMap_[key]; + }).thenAlways(function() { + if (isLocked) { + self.pendingOpsTracker_--; + } + }); +}; + + +/** + * @return {!goog.Promise>} A promise that resolved with all the + * storage keys that have changed. + * @private + */ +fireauth.storage.IndexedDB.prototype.sync_ = function() { + var self = this; + return this.initializeDbAndRun_() + .then(function(db) { + var objectStore = + self.getDataObjectStore_(self.getTransaction_(db, false)); + if (objectStore['getAll']) { + // Get all keys and value pairs using getAll if supported. + return self.onIDBRequest_(objectStore['getAll']()); + } else { + // If getAll isn't supported, fallback to cursor. + return new goog.Promise(function(resolve, reject) { + var res = []; + var request = objectStore.openCursor(); + request.onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + res.push(cursor.value); + cursor['continue'](); + } else { + resolve(res); + } + }; + request.onerror = function(event) { + reject(new Error(event.target.errorCode)); + }; + }); + } + }).then(function(res) { + var centralCopy = {}; + // List of keys differing from central copy. + var diffKeys = []; + // Build central copy (external copy). + if (self.pendingOpsTracker_ == 0) { + for (var i = 0; i < res.length; i++) { + centralCopy[res[i][self.dataKeyPath_]] = + res[i][self.valueKeyPath_]; + } + // Get diff of central copy and local copy. + diffKeys = fireauth.util.getKeyDiff(self.localMap_, centralCopy); + // Update local copy. + self.localMap_ = centralCopy; + } + // Return modified keys. + return diffKeys; + }); +}; + + +/** + * Adds a listener to storage event change. + * @param {function(!Array)} listener The storage event listener. + * @override + */ +fireauth.storage.IndexedDB.prototype.addStorageListener = + function(listener) { + // First listener, start listeners. + if (this.storageListeners_.length == 0) { + this.startListeners_(); + } + this.storageListeners_.push(listener); +}; + + +/** + * Removes a listener to storage event change. + * @param {function(!Array)} listener The storage event listener. + * @override + */ +fireauth.storage.IndexedDB.prototype.removeStorageListener = + function(listener) { + goog.array.removeAllIf( + this.storageListeners_, + function(ele) { + return ele == listener; + }); + // No more listeners, stop. + if (this.storageListeners_.length == 0) { + this.stopListeners_(); + } +}; + + +/** + * Removes all listeners to storage event change. + */ +fireauth.storage.IndexedDB.prototype.removeAllStorageListeners = + function() { + this.storageListeners_ = []; + // No more listeners, stop. + this.stopListeners_(); +}; + + +/** + * Starts the listener to storage events. + * @private + */ +fireauth.storage.IndexedDB.prototype.startListeners_ = function() { + var self = this; + // Stop any previous listeners. + this.stopListeners_(); + // Repeat sync every fireauth.storage.IndexedDB.POLLING_DELAY_ ms. + var repeat = function() { + self.poll_ = + goog.Timer.promise(fireauth.storage.IndexedDB.POLLING_DELAY_) + .then(goog.bind(self.sync_, self)) + .then(function(keys) { + // If keys modified, call listeners. + if (keys.length > 0) { + goog.array.forEach( + self.storageListeners_, + function(listener) { + listener(keys); + }); + } + }) + .then(repeat) + .thenCatch(function(error) { + // Do not repeat if cancelled externally. + if (error.message != fireauth.storage.IndexedDB.STOP_ERROR_) { + repeat(); + } + }); + return self.poll_; + }; + repeat(); +}; + + +/** + * Stops the listener to storage events. + * @private + */ +fireauth.storage.IndexedDB.prototype.stopListeners_ = function() { + if (this.poll_) { + // Cancel polling function. + this.poll_.cancel(fireauth.storage.IndexedDB.STOP_ERROR_); + } +}; diff --git a/packages/auth/src/storage/inmemorystorage.js b/packages/auth/src/storage/inmemorystorage.js new file mode 100644 index 00000000000..5b751576dac --- /dev/null +++ b/packages/auth/src/storage/inmemorystorage.js @@ -0,0 +1,88 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.InMemoryStorage'); + +goog.require('goog.Promise'); + + + +/** + * InMemoryStorage provides an implementation of Storage that will only persist + * data in memory. This data is volatile and in a browser environment, will + * be lost on page unload and will only be available in the current window. + * This is a useful fallback for browsers where web storage is disabled or + * environments where the preferred storage mechanism is not available or not + * supported. + * @constructor + * @implements {fireauth.storage.Storage} + */ +fireauth.storage.InMemoryStorage = function() { + /** @private {!Object} The object where we store values. */ + this.storage_ = {}; +}; + + +/** + * @param {string} key + * @return {!goog.Promise<*>} + * @override + */ +fireauth.storage.InMemoryStorage.prototype.get = function(key) { + return goog.Promise.resolve(/** @type {*} */ (this.storage_[key])); +}; + + +/** + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + * @override + */ +fireauth.storage.InMemoryStorage.prototype.set = function(key, value) { + this.storage_[key] = value; + return goog.Promise.resolve(); +}; + + +/** + * @param {string} key + * @return {!goog.Promise} + * @override + */ +fireauth.storage.InMemoryStorage.prototype.remove = function(key) { + delete this.storage_[key]; + return goog.Promise.resolve(); +}; + + +/** + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.InMemoryStorage.prototype.addStorageListener = + function(listener) { +}; + + +/** + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.InMemoryStorage.prototype.removeStorageListener = function( + listener) {}; diff --git a/packages/auth/src/storage/localstorage.js b/packages/auth/src/storage/localstorage.js new file mode 100644 index 00000000000..bee8103d15b --- /dev/null +++ b/packages/auth/src/storage/localstorage.js @@ -0,0 +1,172 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.LocalStorage'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.Storage'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.events'); + + + +/** + * LocalStorage provides an interface to localStorage, the persistent Web + * Storage API. + * @constructor + * @implements {fireauth.storage.Storage} + */ +fireauth.storage.LocalStorage = function() { + // Check is localStorage available in the current environment. + if (!fireauth.storage.LocalStorage.isAvailable()) { + // In a Node.js environment, dom-storage module needs to be required. + if (fireauth.util.getEnvironment() == fireauth.util.Env.NODE) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'The LocalStorage compatibility library was not found.'); + } + throw new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + } + + /** + * The underlying storage instance for persistent data. + * @private {!Storage} + */ + this.storage_ = /** @type {!Storage} */ ( + fireauth.storage.LocalStorage.getGlobalStorage() || + firebase.INTERNAL['node']['localStorage']); +}; + + +/** @return {?Storage|undefined} The global localStorage instance. */ +fireauth.storage.LocalStorage.getGlobalStorage = function() { + return goog.global['localStorage']; +}; + + +/** + * The key used to check if the storage instance is available. + * @private {string} + * @const + */ +fireauth.storage.LocalStorage.STORAGE_AVAILABLE_KEY_ = '__sak'; + + +/** @return {boolean} Whether localStorage is available. */ +fireauth.storage.LocalStorage.isAvailable = function() { + // In Node.js localStorage is polyfilled. + var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; + // Either window should provide this storage mechanism or in case of Node.js, + // firebase.INTERNAL should provide it. + var storage = fireauth.storage.LocalStorage.getGlobalStorage() || + (isNode && + firebase.INTERNAL['node'] && + firebase.INTERNAL['node']['localStorage']); + if (!storage) { + return false; + } + try { + // setItem will throw an exception if we cannot access web storage (e.g., + // Safari in private mode). + storage.setItem(fireauth.storage.LocalStorage.STORAGE_AVAILABLE_KEY_, '1'); + storage.removeItem(fireauth.storage.LocalStorage.STORAGE_AVAILABLE_KEY_); + return true; + } catch (e) { + return false; + } +}; + + +/** + * Retrieves the value stored at the key. + * @param {string} key + * @return {!goog.Promise<*>} + * @override + */ +fireauth.storage.LocalStorage.prototype.get = function(key) { + var self = this; + return goog.Promise.resolve() + .then(function() { + var json = self.storage_.getItem(key); + return fireauth.util.parseJSON(json); + }); +}; + + +/** + * Stores the value at the specified key. + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + * @override + */ +fireauth.storage.LocalStorage.prototype.set = function(key, value) { + var self = this; + return goog.Promise.resolve() + .then(function() { + var obj = fireauth.util.stringifyJSON(value); + if (goog.isNull(obj)) { + self.remove(key); + } else { + self.storage_.setItem(key, obj); + } + }); +}; + + +/** + * Removes the value at the specified key. + * @param {string} key + * @return {!goog.Promise} + * @override + */ +fireauth.storage.LocalStorage.prototype.remove = function(key) { + var self = this; + return goog.Promise.resolve() + .then(function() { + self.storage_.removeItem(key); + }); +}; + + +/** + * Adds a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.LocalStorage.prototype.addStorageListener = function( + listener) { + if (goog.global['window']) { + goog.events.listen(goog.global['window'], 'storage', listener); + } +}; + + +/** + * Removes a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.LocalStorage.prototype.removeStorageListener = function( + listener) { + if (goog.global['window']) { + goog.events.unlisten(goog.global['window'], 'storage', listener); + } +}; diff --git a/packages/auth/src/storage/nullstorage.js b/packages/auth/src/storage/nullstorage.js new file mode 100644 index 00000000000..9070dbc0cc7 --- /dev/null +++ b/packages/auth/src/storage/nullstorage.js @@ -0,0 +1,81 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.NullStorage'); + +goog.require('goog.Promise'); + + + +/** + * NullStorage provides an implementation of Storage that does always returns + * null. This can be used if a type of storage is unsupported on a platform. + * @constructor + * @implements {fireauth.storage.Storage} + */ +fireauth.storage.NullStorage = function() { + /** @private {!Object} The object where we store values. */ + this.storage_ = {}; +}; + + +/** + * @param {string} key + * @return {!goog.Promise<*>} + * @override + */ +fireauth.storage.NullStorage.prototype.get = function(key) { + return goog.Promise.resolve(/** @type {*} */ (null)); +}; + + +/** + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + * @override + */ +fireauth.storage.NullStorage.prototype.set = function(key, value) { + return goog.Promise.resolve(); +}; + + +/** + * @param {string} key + * @return {!goog.Promise} + * @override + */ +fireauth.storage.NullStorage.prototype.remove = function(key) { + return goog.Promise.resolve(); +}; + + +/** + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.NullStorage.prototype.addStorageListener = function(listener) { +}; + + +/** + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.NullStorage.prototype.removeStorageListener = function( + listener) {}; diff --git a/packages/auth/src/storage/sessionstorage.js b/packages/auth/src/storage/sessionstorage.js new file mode 100644 index 00000000000..c1e02225e92 --- /dev/null +++ b/packages/auth/src/storage/sessionstorage.js @@ -0,0 +1,164 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.SessionStorage'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.Storage'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); + + + +/** + * SessionStorage provides an interface to sessionStorage, the temporary web + * storage API. + * @constructor + * @implements {fireauth.storage.Storage} + */ +fireauth.storage.SessionStorage = function() { + // Check is sessionStorage available in the current environment. + if (!fireauth.storage.SessionStorage.isAvailable()) { + // In a Node.js environment, dom-storage module needs to be required. + if (fireauth.util.getEnvironment() == fireauth.util.Env.NODE) { + throw new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + 'The SessionStorage compatibility library was not found.'); + } + throw new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + } + + /** + * The underlying storage instance for temporary data. + * @private {!Storage} + */ + this.storage_ = /** @type {!Storage} */ ( + fireauth.storage.SessionStorage.getGlobalStorage() || + firebase.INTERNAL['node']['sessionStorage']); +}; + + +/** @return {?Storage|undefined} The global sessionStorage instance. */ +fireauth.storage.SessionStorage.getGlobalStorage = function() { + return goog.global['sessionStorage']; +}; + + +/** + * The key used to check if the storage instance is available. + * @private {string} + * @const + */ +fireauth.storage.SessionStorage.STORAGE_AVAILABLE_KEY_ = '__sak'; + + +/** @return {boolean} Whether sessionStorage is available. */ +fireauth.storage.SessionStorage.isAvailable = function() { + // In Node.js sessionStorage is polyfilled. + var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; + // Either window should provide this storage mechanism or in case of Node.js, + // firebase.INTERNAL should provide it. + var storage = fireauth.storage.SessionStorage.getGlobalStorage() || + (isNode && + firebase.INTERNAL['node'] && + firebase.INTERNAL['node']['sessionStorage']); + if (!storage) { + return false; + } + try { + // setItem will throw an exception if we cannot access web storage (e.g., + // Safari in private mode). + storage.setItem( + fireauth.storage.SessionStorage.STORAGE_AVAILABLE_KEY_, '1'); + storage.removeItem(fireauth.storage.SessionStorage.STORAGE_AVAILABLE_KEY_); + return true; + } catch (e) { + return false; + } +}; + + +/** + * Retrieves the value stored at the key. + * @param {string} key + * @return {!goog.Promise<*>} + * @override + */ +fireauth.storage.SessionStorage.prototype.get = function(key) { + var self = this; + return goog.Promise.resolve() + .then(function() { + var json = self.storage_.getItem(key); + return fireauth.util.parseJSON(json); + }); +}; + + +/** + * Stores the value at the specified key. + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + * @override + */ +fireauth.storage.SessionStorage.prototype.set = function(key, value) { + var self = this; + return goog.Promise.resolve() + .then(function() { + var obj = fireauth.util.stringifyJSON(value); + if (goog.isNull(obj)) { + self.remove(key); + } else { + self.storage_.setItem(key, obj); + } + }); +}; + + +/** + * Removes the value at the specified key. + * @param {string} key + * @return {!goog.Promise} + * @override + */ +fireauth.storage.SessionStorage.prototype.remove = function(key) { + var self = this; + return goog.Promise.resolve() + .then(function() { + self.storage_.removeItem(key); + }); +}; + + +/** + * Adds a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.SessionStorage.prototype.addStorageListener = function( + listener) {}; + + +/** + * Removes a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)} listener The storage event + * listener. + * @override + */ +fireauth.storage.SessionStorage.prototype.removeStorageListener = function( + listener) {}; diff --git a/packages/auth/src/storage/storage.js b/packages/auth/src/storage/storage.js new file mode 100644 index 00000000000..3a61342e647 --- /dev/null +++ b/packages/auth/src/storage/storage.js @@ -0,0 +1,67 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.Storage'); + + + +/** + * Defines a generic interface to storage APIs across platforms. + * @interface + */ +fireauth.storage.Storage = function() {}; + + +/** + * Retrieves the value stored at the key. + * @param {string} key + * @return {!goog.Promise<*>} + */ +fireauth.storage.Storage.prototype.get = function(key) {}; + + +/** + * Stores the value at the specified key. + * @param {string} key + * @param {*} value + * @return {!goog.Promise} + */ +fireauth.storage.Storage.prototype.set = function(key, value) {}; + + +/** + * Removes the value at the specified key. + * @param {string} key + * @return {!goog.Promise} + */ +fireauth.storage.Storage.prototype.remove = function(key) {}; + + +/** + * Adds a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)|function(!Array)} + * listener The storage event listener. + */ +fireauth.storage.Storage.prototype.addStorageListener = function(listener) {}; + + +/** + * Removes a listener to storage event change. + * @param {function(!goog.events.BrowserEvent)|function(!Array)} + * listener The storage event listener. + */ +fireauth.storage.Storage.prototype.removeStorageListener = function(listener) { +}; diff --git a/packages/auth/src/storageautheventmanager.js b/packages/auth/src/storageautheventmanager.js new file mode 100644 index 00000000000..5a94699f74c --- /dev/null +++ b/packages/auth/src/storageautheventmanager.js @@ -0,0 +1,132 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the fireauth.storage.AuthEventManager class used by + * the iframe to retrieve and delete Auth events triggered through an OAuth + * flow. + */ + +goog.provide('fireauth.storage.AuthEventManager'); +goog.provide('fireauth.storage.AuthEventManager.Keys'); + +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.authStorage'); + + +/** + * Defines the Auth event storage manager. It provides methods to + * load and delete Auth events as well as listen to external OAuth changes on + * them. + * @param {string} appId The Auth event's application ID. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @constructor @struct @final + */ +fireauth.storage.AuthEventManager = function(appId, opt_manager) { + /** @const @private{string} appId The Auth event's application ID. */ + this.appId_ = appId; + /** + * @const @private{!fireauth.authStorage.Manager} The underlying storage + * manager. + */ + this.manager_ = opt_manager || fireauth.authStorage.Manager.getInstance(); +}; + + +/** + * Valid keys for Auth event manager data. + * @enum {!fireauth.authStorage.Key} + */ +fireauth.storage.AuthEventManager.Keys = { + AUTH_EVENT: { + name: 'authEvent', + persistent: fireauth.authStorage.Persistence.LOCAL + }, + REDIRECT_EVENT: { + name: 'redirectEvent', + persistent: fireauth.authStorage.Persistence.SESSION + } +}; + + +/** + * @return {!goog.Promise} A promise that resolves on + * success with the stored Auth event. + */ +fireauth.storage.AuthEventManager.prototype.getAuthEvent = function() { + return this.manager_.get( + fireauth.storage.AuthEventManager.Keys.AUTH_EVENT, this.appId_) + .then(function(response) { + return fireauth.AuthEvent.fromPlainObject(response); + }); +}; + + +/** + * Removes the identifier's Auth event if it exists. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.AuthEventManager.prototype.removeAuthEvent = function() { + return this.manager_.remove( + fireauth.storage.AuthEventManager.Keys.AUTH_EVENT, this.appId_); +}; + + +/** + * Adds a listener to Auth event for App ID provided. + * @param {!function()} listener The listener to run on Auth event. + */ +fireauth.storage.AuthEventManager.prototype.addAuthEventListener = + function(listener) { + this.manager_.addListener( + fireauth.storage.AuthEventManager.Keys.AUTH_EVENT, this.appId_, listener); +}; + + +/** + * Removes a listener to Auth event for App ID provided. + * @param {!function()} listener The listener to run on Auth event. + */ +fireauth.storage.AuthEventManager.prototype.removeAuthEventListener = + function(listener) { + this.manager_.removeListener( + fireauth.storage.AuthEventManager.Keys.AUTH_EVENT, this.appId_, listener); +}; + + +/** + * @return {!goog.Promise} A promise that resolves on + * success with the stored redirect Auth event. + */ +fireauth.storage.AuthEventManager.prototype.getRedirectEvent = + function() { + return this.manager_.get( + fireauth.storage.AuthEventManager.Keys.REDIRECT_EVENT, + this.appId_).then(function(response) { + return fireauth.AuthEvent.fromPlainObject(response); + }); +}; + + +/** + * Removes the identifier's redirect Auth event if it exists. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.AuthEventManager.prototype.removeRedirectEvent = function() { + return this.manager_.remove( + fireauth.storage.AuthEventManager.Keys.REDIRECT_EVENT, this.appId_); +}; diff --git a/packages/auth/src/storageoauthhandlermanager.js b/packages/auth/src/storageoauthhandlermanager.js new file mode 100644 index 00000000000..47fad9d18b4 --- /dev/null +++ b/packages/auth/src/storageoauthhandlermanager.js @@ -0,0 +1,165 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the fireauth.storage.OAuthHandlerManager class which + * provides utilities to the OAuth handler widget to set Auth events after an + * IDP sign in attempt and to store state during the OAuth handshake with IDP. + */ + +goog.provide('fireauth.storage.OAuthHandlerManager'); + +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.OAuthHelperState'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.storage.AuthEventManager.Keys'); + + +/** + * Defines the OAuth handler storage manager. It provides methods to + * store, load and delete OAuth handler widget state, properties and setting + * Auth events. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @constructor @struct @final + */ +fireauth.storage.OAuthHandlerManager = function(opt_manager) { + /** + * @const @private{!fireauth.authStorage.Manager} The underlying storage + * manager. + */ + this.manager_ = opt_manager || fireauth.authStorage.Manager.getInstance(); +}; + + +/** + * Valid keys for OAuth handler manager data. + * @private @enum {!fireauth.authStorage.Key} + */ +fireauth.storage.OAuthHandlerManager.Keys_ = { + OAUTH_HELPER_STATE: { + name: 'oauthHelperState', + persistent: fireauth.authStorage.Persistence.SESSION + }, + SESSION_ID: { + name: 'sessionId', + persistent: fireauth.authStorage.Persistence.SESSION + } +}; + + +/** + * @param {string} appId The Auth state's application ID. + * @return {!goog.Promise} A promise that resolves on success + * with the stored session ID. + */ +fireauth.storage.OAuthHandlerManager.prototype.getSessionId = function(appId) { + return this.manager_.get( + fireauth.storage.OAuthHandlerManager.Keys_.SESSION_ID, appId); +}; + + +/** + * Removes the session ID string if it exists. + * @param {string} appId The Auth state's application ID. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.removeSessionId = + function(appId) { + return this.manager_.remove( + fireauth.storage.OAuthHandlerManager.Keys_.SESSION_ID, appId); +}; + + +/** + * Stores the session ID string. + * @param {string} appId The Auth state's application ID. + * @param {string} sessionId The session ID string to store. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.setSessionId = + function(appId, sessionId) { + return this.manager_.set( + fireauth.storage.OAuthHandlerManager.Keys_.SESSION_ID, sessionId, appId); +}; + + +/** + * @return {!goog.Promise} A promise that resolves + * on success with the stored OAuth helper state. + */ +fireauth.storage.OAuthHandlerManager.prototype.getOAuthHelperState = + function() { + return this.manager_.get( + fireauth.storage.OAuthHandlerManager.Keys_.OAUTH_HELPER_STATE) + .then(function(response) { + return fireauth.OAuthHelperState.fromPlainObject(response); + }); +}; + + +/** + * Removes the current OAuth helper state if it exists. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.removeOAuthHelperState = + function() { + return this.manager_.remove( + fireauth.storage.OAuthHandlerManager.Keys_.OAUTH_HELPER_STATE); +}; + + +/** + * Stores the current OAuth helper state. + * @param {!fireauth.OAuthHelperState} state The OAuth helper state. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.setOAuthHelperState = + function(state) { + return this.manager_.set( + fireauth.storage.OAuthHandlerManager.Keys_.OAUTH_HELPER_STATE, + state.toPlainObject()); +}; + + +/** + * Stores the Auth event for specified identifier. + * @param {string} appId The Auth state's application ID. + * @param {!fireauth.AuthEvent} authEvent The Auth event. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.setAuthEvent = + function(appId, authEvent) { + return this.manager_.set( + fireauth.storage.AuthEventManager.Keys.AUTH_EVENT, + authEvent.toPlainObject(), + appId); +}; + + +/** + * Stores the redirect Auth event for specified identifier. + * @param {string} appId The Auth state's application ID. + * @param {!fireauth.AuthEvent} authEvent The redirect Auth event. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.OAuthHandlerManager.prototype.setRedirectEvent = + function(appId, authEvent) { + return this.manager_.set( + fireauth.storage.AuthEventManager.Keys.REDIRECT_EVENT, + authEvent.toPlainObject(), + appId); +}; diff --git a/packages/auth/src/storagependingredirectmanager.js b/packages/auth/src/storagependingredirectmanager.js new file mode 100644 index 00000000000..82a6306d72b --- /dev/null +++ b/packages/auth/src/storagependingredirectmanager.js @@ -0,0 +1,101 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the fireauth.storage.PendingRedirectManager class which + * provides utilities to store, retrieve and delete the state of whether there + * is a pending redirect operation previously triggered. + */ + +goog.provide('fireauth.storage.PendingRedirectManager'); + +goog.require('fireauth.authStorage'); + + +/** + * Defines the pending redirect storage manager. It provides methods + * to store, retrieve and delete the state of whether there is a pending + * redirect operation previously triggered. + * @param {string} appId The Auth state's application ID. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @constructor @struct @final + */ +fireauth.storage.PendingRedirectManager = function(appId, opt_manager) { + /** @const @private{string} appId The Auth state's application ID. */ + this.appId_ = appId; + /** + * @const @private{!fireauth.authStorage.Manager} The underlying storage + * manager. + */ + this.manager_ = opt_manager || fireauth.authStorage.Manager.getInstance(); +}; + + +/** + * @const @private{!string} The pending redirect flag. + */ +fireauth.storage.PendingRedirectManager.PENDING_FLAG_ = 'pending'; + + +/** + * @const @private{!fireauth.authStorage.Key} The pending redirect status + * storage identifier key. + */ +fireauth.storage.PendingRedirectManager.PENDING_REDIRECT_KEY_ = { + name: 'pendingRedirect', + persistent: fireauth.authStorage.Persistence.SESSION +}; + + +/** + * Stores the pending redirect operation for the provided application ID. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.PendingRedirectManager.prototype.setPendingStatus = + function() { + return this.manager_.set( + fireauth.storage.PendingRedirectManager.PENDING_REDIRECT_KEY_, + fireauth.storage.PendingRedirectManager.PENDING_FLAG_, + this.appId_); +}; + + +/** + * Removes the stored pending redirect operation for provided app ID. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.PendingRedirectManager.prototype.removePendingStatus = + function() { + return this.manager_.remove( + fireauth.storage.PendingRedirectManager.PENDING_REDIRECT_KEY_, + this.appId_); +}; + + +/** + * @return {!goog.Promise} A promise that resolves with a boolean + * whether there is a pending redirect operaiton for the provided app ID. + */ +fireauth.storage.PendingRedirectManager.prototype.getPendingStatus = + function() { + return this.manager_.get( + fireauth.storage.PendingRedirectManager.PENDING_REDIRECT_KEY_, + this.appId_).then(function(response) { + return response == + fireauth.storage.PendingRedirectManager.PENDING_FLAG_; + }); +}; diff --git a/packages/auth/src/storageredirectusermanager.js b/packages/auth/src/storageredirectusermanager.js new file mode 100644 index 00000000000..528904a8d87 --- /dev/null +++ b/packages/auth/src/storageredirectusermanager.js @@ -0,0 +1,102 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the fireauth.storage.RedirectUserManager class which + * provides utilities to store, retrieve and delete an Auth user during a + * redirect operation. + */ + +goog.provide('fireauth.storage.RedirectUserManager'); + +goog.require('fireauth.AuthUser'); +goog.require('fireauth.authStorage'); + + +/** + * Defines the Auth redirect user storage manager. It provides methods + * to store, load and delete a user going through a link with redirect + * operation. + * @param {string} appId The Auth state's application ID. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @constructor @struct @final + */ +fireauth.storage.RedirectUserManager = function(appId, opt_manager) { + /** @const @private{string} appId The Auth state's application ID. */ + this.appId_ = appId; + /** + * @const @private{!fireauth.authStorage.Manager} The underlying storage + * manager. + */ + this.manager_ = opt_manager || fireauth.authStorage.Manager.getInstance(); +}; + + +/** + * @const @private{!fireauth.authStorage.Key} The Auth redirect user storage + * identifier. + */ +fireauth.storage.RedirectUserManager.REDIRECT_USER_KEY_ = { + name: 'redirectUser', + persistent: fireauth.authStorage.Persistence.SESSION +}; + + +/** + * Stores the user being redirected for the provided application ID. + * @param {!fireauth.AuthUser} redirectUser The user being redirected. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.RedirectUserManager.prototype.setRedirectUser = + function(redirectUser) { + return this.manager_.set( + fireauth.storage.RedirectUserManager.REDIRECT_USER_KEY_, + redirectUser.toPlainObject(), + this.appId_); +}; + + +/** + * Removes the stored redirected user for provided app ID. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.RedirectUserManager.prototype.removeRedirectUser = + function() { + return this.manager_.remove( + fireauth.storage.RedirectUserManager.REDIRECT_USER_KEY_, this.appId_); +}; + + +/** + * @param {?string=} opt_authDomain The optional Auth domain to override if + * provided. + * @return {!goog.Promise} A promise that resolves with + * the stored redirected user for the provided app ID. + */ +fireauth.storage.RedirectUserManager.prototype.getRedirectUser = + function(opt_authDomain) { + return this.manager_.get( + fireauth.storage.RedirectUserManager.REDIRECT_USER_KEY_, this.appId_) + .then(function(response) { + // If potential user saved, override Auth domain if authDomain is + // provided. + if (response && opt_authDomain) { + response['authDomain'] = opt_authDomain; + } + return fireauth.AuthUser.fromPlainObject(response || {}); + }); +}; diff --git a/packages/auth/src/storageusermanager.js b/packages/auth/src/storageusermanager.js new file mode 100644 index 00000000000..bb05eee0612 --- /dev/null +++ b/packages/auth/src/storageusermanager.js @@ -0,0 +1,476 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines the fireauth.storage.UserManager class which provides + * utilities to retrieve, store and delete the currently logged in user and to + * listen to external authentication changes for the same app. + * With the ability to modify Auth state persistence. The behavior is as + * follows: + * Common cases: + *
    + *
  • Initially, local and session storage will be checked and the state will + * be loaded from there without changing it unless the developer calls + * setPersistence explicitly. The requirement is that at any time, Auth + * state can be saved using one type only of persistence and never more than + * one.
  • + *
  • If the developer tries to sign in with no persistence specified, the + * default setting will be used (local in a browser).
  • + *
  • If the user is not signed in and persistence is set, any future sign-in + * attempt will use that type of persistence.
  • + *
  • If the user is signed in and the developer then switches persistence, + * that existing signed in user will change persistence to the new one. All + * future sign-in attempts will use that same persistence.
  • + *
  • When signInWithRedirect is called, the current persistence type is passed + * along with that request and on redirect back to app will pass that type + * to determine how that state is saved (overriding the default). If the + * persistence is explicitly specified on that page, it will change that + * redirected Auth state persistence. This is the only time the persistence + * is passed from one page to another. + * So internally, on redirect, the redirect state is retrieved and then we + * check: If the persistence was explicitly provided, we override the + * previous type and save the Auth state using that. If no persistence was + * explicitly provided, we use the previous persistence type that was passed + * in the redirect response.
  • + *
+ * Behavior across tabs: + *
    + *
  • User can sign in using session storage on multiple tabs. Each tab cannot + * see the state of the other tab.
  • + *
  • Any attempt to sign in using local storage will be detected and + * synchronized on all tabs. If the user was previously signed in on a + * specific tab using session storage, that state will be cleared.
  • + *
  • If the user was previously signed in using local storage and then signs + * in using session storage, the user will be signed in on the current tab + * only and signed out on all other tabs.
  • + *
  • Similar logic is applied to the ‘none’ state. In one tab, switching to + * ‘none’ state will delete any previously saved state in ‘local’ + * persistence in other tabs.
  • + *
+ */ + +goog.provide('fireauth.storage.UserManager'); + +goog.require('fireauth.AuthUser'); +goog.require('fireauth.authStorage'); + + +/** + * Defines the Auth user storage manager. It provides methods to + * store, load and delete an authenticated current user. It also provides + * methods to listen to external user changes (updates, sign in, sign out, etc.) + * @param {string} appId The Auth state's application ID. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @constructor @struct @final + */ +fireauth.storage.UserManager = function(appId, opt_manager) { + /** @const @private{string} appId The Auth state's application ID. */ + this.appId_ = appId; + /** + * @const @private{!fireauth.authStorage.Manager} The underlying storage + * manager. + */ + this.manager_ = opt_manager || fireauth.authStorage.Manager.getInstance(); + /** + * @private {?fireauth.authStorage.Key} The current Auth user storage + * identifier. + */ + this.currentAuthUserKey_ = null; + /** + * @private {!goog.Promise} Storage operation serializer promise. This will + * initialize the current persistence used and clean up any duplicate + * states or temporary values (persistence for pending redirect). + * Afterwards this is used to queue storage requests to make sure + * storage operations are always synchronized and read/write events are + * processed on the same storage. + */ + this.onReady_ = this.initialize_(); + // This internal listener will always run before the external ones. + // This is needed to queue processing of this first before any getCurrentUser + // is called from external listeners. + this.manager_.addListener( + fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.LOCAL), + this.appId_, + goog.bind(this.switchToLocalOnExternalEvent_, this)); +}; + + +/** + * Switches to local storage on external storage event. This will happen when + * state is specified as local in an external tab while it is none or session + * in the current one. If a user signs in in an external tab, the current window + * should detect this, clear existing storage and switch to local storage. + * @private + */ +fireauth.storage.UserManager.prototype.switchToLocalOnExternalEvent_ = + function() { + var self = this; + var localKey = fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.LOCAL); + // Wait for any pending operation to finish first. + // Block next read/write operation until persistence is transitioned to + // local. + this.waitForReady_(function() { + return goog.Promise.resolve().then(function() { + // In current persistence is not already local. + if (self.currentAuthUserKey_ && + self.currentAuthUserKey_.persistent != + fireauth.authStorage.Persistence.LOCAL) { + // Check if new current user is available in local storage. + return self.manager_.get(localKey, self.appId_); + } + return null; + }).then(function(response) { + // Sign in on an external tab. + if (response) { + // Remove any existing non-local user. + return self.removeAllExcept_( + fireauth.authStorage.Persistence.LOCAL).then(function() { + // Set persistence to local. + self.currentAuthUserKey_ = localKey; + }); + } + }); + }); +}; + + +/** + * Removes all states stored in all supported persistence types excluding the + * specified one. + * @param {?fireauth.authStorage.Persistence} persistence The type of storage + * persistence to switch to. + * @return {!goog.Promise} The promise that resolves when all stored values are + * removed for types of storage excluding specified persistence. This helps + * ensure there is always one type of persistence at any time. + * @private + */ +fireauth.storage.UserManager.prototype.removeAllExcept_ = + function(persistence) { + var promises = []; + // Queue all promises to remove current user in any other persistence type. + for (var key in fireauth.authStorage.Persistence) { + // Skip specified persistence. + if (fireauth.authStorage.Persistence[key] !== persistence) { + var storageKey = fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence[key]); + promises.push(this.manager_.remove( + /** @type {!fireauth.authStorage.Key} */ (storageKey), + this.appId_)); + } + } + // Clear persistence key (only useful for initial load upon returning from a + // a redirect sign-in operation). + promises.push(this.manager_.remove( + fireauth.storage.UserManager.PERSISTENCE_KEY_, + this.appId_)); + return goog.Promise.all(promises); +}; + + +/** + * Initializes the current persistence state. This will check the 3 supported + * types. The first one that is found will be the current persistence. All + * others will be cleared. If none is found we check PERSISTENCE_KEY_ which + * when specified means that the operation is returning from a + * signInWithRedirect call. This persistence will be applied. + * Otherwise the default local persistence is used. + * @return {!goog.Promise} A promise that resolves when the current persistence + * is resolved. + * @private + */ +fireauth.storage.UserManager.prototype.initialize_ = function() { + var self = this; + // Local key. + var localKey = fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.LOCAL); + // Session key. + var sessionKey = fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.SESSION); + // In memory key. This is unlikely to contain anything on load. + var inMemoryKey = fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.NONE); + // Check if state is stored in session storage. + return this.manager_.get(sessionKey, this.appId_).then(function(response) { + if (response) { + // Session storage is being used. + return sessionKey; + } else { + // Session storage is empty. Check in memory storage. + return self.manager_.get(inMemoryKey, self.appId_) + .then(function(response) { + if (response) { + // In memory storage being used. + return inMemoryKey; + } else { + // Check local storage. + return self.manager_.get(localKey, self.appId_) + .then(function(response) { + if (response) { + // Local storage being used. + return localKey; + } else { + // Nothing found in any supported storage. + // Check current user persistence in storage. + return self.manager_.get( + fireauth.storage.UserManager.PERSISTENCE_KEY_, + self.appId_).then(function(persistence) { + if (persistence) { + // Sign in with redirect operation, apply this + // persistence to any current user. + return fireauth.storage.UserManager + .getAuthUserKey_(persistence); + } else { + // No persistence found, use the default. + return localKey; + } + }); + } + }); + } + }); + } + }).then(function(currentKey) { + // Set current key according to the persistence detected. + self.currentAuthUserKey_ = currentKey; + // Make sure only one state available. Clean up everything else. + return self.removeAllExcept_(currentKey.persistent); + }).thenCatch(function(error) { + // If an error occurs in the process and no current key detected, set to + // persistence value to default. + if (!self.currentAuthUserKey_) { + self.currentAuthUserKey_ = localKey; + } + }); +}; + + +/** + * @const @private {string} The Auth current user storage identifier name. + */ +fireauth.storage.UserManager.AUTH_USER_KEY_NAME_ = 'authUser'; + + +/** + * @const @private{!fireauth.authStorage.Key} The Auth user storage persistence + * identifier. This is needed to remember the previous persistence state for + * sign-in with redirect. + */ +fireauth.storage.UserManager.PERSISTENCE_KEY_ = { + name: 'persistence', + persistent: fireauth.authStorage.Persistence.SESSION +}; + + +/** + * Returns the Auth user key corresponding to the persistence type provided. + * @param {!fireauth.authStorage.Persistence} persistence The key for the + * specified type of persistence. + * @return {!fireauth.authStorage.Key} The corresponding Auth user storage + * identifier. + * @private + */ +fireauth.storage.UserManager.getAuthUserKey_ = function(persistence) { + return { + name: fireauth.storage.UserManager.AUTH_USER_KEY_NAME_, + persistent: persistence + }; +}; + + +/** + * Sets the persistence to the specified type. + * If an existing user already is in storage, it copies that value to the new + * storage and clears all the others. + * @param {!fireauth.authStorage.Persistence} persistence The type of storage + * persistence to switch to. + * @return {!goog.Promise} A promise that resolves when persistence change is + * applied. + */ +fireauth.storage.UserManager.prototype.setPersistence = function(persistence) { + var currentUser = null; + var self = this; + // Validate the persistence type provided. This will throw a synchronous error + // if invalid. + fireauth.authStorage.validatePersistenceArgument(persistence); + // Wait for turn in queue. + return this.waitForReady_(function() { + // If persistence hasn't changed, do nothing. + if (persistence != self.currentAuthUserKey_.persistent) { + // Persistence changed. Copy from current storage to new one. + return self.manager_.get( + /** @type {!fireauth.authStorage.Key} */ (self.currentAuthUserKey_), + self.appId_).then(function(result) { + // Save current user. + currentUser = result; + // Clear from current storage. + return self.removeAllExcept_(persistence); + }).then(function() { + // Update persistence key to the new one. + self.currentAuthUserKey_ = + fireauth.storage.UserManager.getAuthUserKey_(persistence); + // Copy current storage type to the new one. + if (currentUser) { + return self.manager_.set( + /** @type {!fireauth.authStorage.Key} */ ( + self.currentAuthUserKey_), + currentUser, + self.appId_); + } + }); + } + // No change in persistence type. + return goog.Promise.resolve(); + }); +}; + + +/** + * Saves the current persistence type so it can be retrieved after a page + * redirect. This is relevant for signInWithRedirect. + * @return {!goog.Promise} Promise that resolve when current persistence is + * saved. + */ +fireauth.storage.UserManager.prototype.savePersistenceForRedirect = function() { + var self = this; + return this.waitForReady_(function() { + // Save persistence to survive redirect. + return self.manager_.set( + fireauth.storage.UserManager.PERSISTENCE_KEY_, + self.currentAuthUserKey_.persistent, + self.appId_); + }); +}; + + +/** + * Stores the current Auth user for the provided application ID. + * @param {!fireauth.AuthUser} currentUser The app current Auth user to save. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.UserManager.prototype.setCurrentUser = function(currentUser) { + var self = this; + // Wait for any pending persistence change to be resolved. + return this.waitForReady_(function() { + return self.manager_.set( + /** @type {!fireauth.authStorage.Key} */ (self.currentAuthUserKey_), + currentUser.toPlainObject(), + self.appId_); + }); +}; + + +/** + * Removes the stored current user for provided app ID. + * @return {!goog.Promise} A promise that resolves on success. + */ +fireauth.storage.UserManager.prototype.removeCurrentUser = function() { + var self = this; + // Wait for any pending persistence change to be resolved. + return this.waitForReady_(function() { + return self.manager_.remove( + /** @type {!fireauth.authStorage.Key} */ (self.currentAuthUserKey_), + self.appId_); + }); +}; + + +/** + * @param {?string=} opt_authDomain The optional Auth domain to override if + * provided. + * @return {!goog.Promise} A promise that resolves with + * the stored current user for the provided app ID. + */ +fireauth.storage.UserManager.prototype.getCurrentUser = + function(opt_authDomain) { + var self = this; + // Wait for any pending persistence change to be resolved. + return this.waitForReady_(function() { + return self.manager_.get( + /** @type {!fireauth.authStorage.Key} */ (self.currentAuthUserKey_), + self.appId_).then(function(response) { + // If potential user saved, override Auth domain if authDomain is + // provided. + // This is useful in cases where on one page the developer initializes + // the Auth instance without authDomain and signs in user using + // headless methods. On another page, Auth is initialized with + // authDomain for the purpose of linking with a popup. The loaded user + // (stored without the authDomain) must have this field updated with + // the current authDomain. + if (response && opt_authDomain) { + response['authDomain'] = opt_authDomain; + } + return fireauth.AuthUser.fromPlainObject(response || {}); + }); + }); +}; + + +/** + * Serializes storage access operations especially since persistence + * could be updated from one type to the other while read/write operations + * occur. + * @param {function():!goog.Promise} cb The promise return callback to chain + * when pending operations are resolved. + * @return {!goog.Promise} The resulting promise that resolves when provided + * promise finally resolves. + * @template T + * @private + */ +fireauth.storage.UserManager.prototype.waitForReady_ = function(cb) { + // Wait for any pending persistence change to be resolved before running + // storage related operation. Chain to onReady so next call will wait for + // this operation to resolve. + // While an error is unlikely, run callback even if it happens, otherwise + // no storage related event will be allowed to complete after an error. + this.onReady_ = this.onReady_.then(cb, cb); + return this.onReady_; +}; + + +/** + * Adds a listener to Auth current user change event for app ID provided. + * @param {!function()} listener The listener to run on current user change + * event. + */ +fireauth.storage.UserManager.prototype.addCurrentUserChangeListener = + function(listener) { + // When this is triggered, getCurrentUser is called, that will have to wait + // for switchToLocalOnExternalEvent_ to resolve which is ahead of it in the + // queue. + this.manager_.addListener( + fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.LOCAL), + this.appId_, + listener); +}; + + +/** + * Removes a listener to Auth current user change event for app ID provided. + * @param {!function()} listener The listener to remove from current user change + * event changes. + */ +fireauth.storage.UserManager.prototype.removeCurrentUserChangeListener = + function(listener) { + this.manager_.removeListener( + fireauth.storage.UserManager.getAuthUserKey_( + fireauth.authStorage.Persistence.LOCAL), + this.appId_, + listener); +}; diff --git a/packages/auth/src/token.js b/packages/auth/src/token.js new file mode 100644 index 00000000000..d518f70c7f8 --- /dev/null +++ b/packages/auth/src/token.js @@ -0,0 +1,279 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Utility class to retrieve and cache STS token. + */ +goog.provide('fireauth.StsTokenManager'); +goog.provide('fireauth.StsTokenManager.Response'); +goog.provide('fireauth.StsTokenManager.ResponseData'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('goog.Promise'); +goog.require('goog.asserts'); + + + +/** + * Creates STS token manager. + * + * @param {!fireauth.RpcHandler} rpcHandler Handler for RPC requests. + * @constructor + */ +fireauth.StsTokenManager = function(rpcHandler) { + /** + * @const @private {!fireauth.RpcHandler} The RPC handler used to request STS + * tokens. + */ + this.rpcHandler_ = rpcHandler; + /** @private {?string} The STS refresh token. */ + this.refreshToken_ = null; + /** @private {?string} The STS ID token. */ + this.accessToken_ = null; + /** @private {number} The STS expiration timestamp. */ + this.expirationTime_ = 0; +}; + + +/** + * @return {!Object} The plain object representation of the STS token manager. + */ +fireauth.StsTokenManager.prototype.toPlainObject = function() { + return { + 'apiKey': this.rpcHandler_.getApiKey(), + 'refreshToken': this.refreshToken_, + 'accessToken': this.accessToken_, + 'expirationTime': this.expirationTime_ + }; +}; + + +/** + * @param {!fireauth.RpcHandler} rpcHandler The RPC handler for the token + * manager. + * @param {?Object} obj The plain object whose STS token manager instance is to + * be returned. + * @return {?fireauth.StsTokenManager} The STS token manager instance from the + * plain object provided using the RPC handler provided. + */ +fireauth.StsTokenManager.fromPlainObject = function(rpcHandler, obj) { + var stsTokenManager = null; + if (obj && obj['apiKey']) { + // These should be always equals and must be enforced in internal use. + goog.asserts.assert(obj['apiKey'] == rpcHandler.getApiKey()); + stsTokenManager = new fireauth.StsTokenManager(rpcHandler); + stsTokenManager.setRefreshToken(obj['refreshToken']); + stsTokenManager.setAccessToken( + obj['accessToken'], obj['expirationTime'] || 0); + } + return stsTokenManager; +}; + + +/** + * @typedef {{ + * accessToken: (?string), + * expirationTime: (number), + * refreshToken: (?string) + * }} + */ +fireauth.StsTokenManager.Response; + + +/** + * @typedef {{ + * access_token: (?string|undefined), + * expires_in: (number|undefined), + * refresh_token: (?string|undefined) + * }} + */ +fireauth.StsTokenManager.ResponseData; + + +/** + * @param {?string} refreshToken The STS refresh token. + */ +fireauth.StsTokenManager.prototype.setRefreshToken = function(refreshToken) { + this.refreshToken_ = refreshToken; +}; + + +/** + * @param {?string} accessToken The STS access token. + * @param {number} expirationTime The STS token expiration time. + */ +fireauth.StsTokenManager.prototype.setAccessToken = function( + accessToken, expirationTime) { + this.accessToken_ = accessToken; + this.expirationTime_ = expirationTime; +}; + + +/** + * @return {?string} The refresh token. + */ +fireauth.StsTokenManager.prototype.getRefreshToken = function() { + return this.refreshToken_; +}; + + +/** + * @return {number} The STS access token expiration time. + */ +fireauth.StsTokenManager.prototype.getExpirationTime = function() { + return this.expirationTime_; +}; + + +/** + * The number of milliseconds before the official expiration time of a token + * to refresh that token, to provide a buffer for RPCs to complete. + * @const {number} + * @private + */ +fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER_ = 30 * 1000; + + +/** + * @return {boolean} Whether the STS access token is expired or not. + * @private + */ +fireauth.StsTokenManager.prototype.isExpired_ = function() { + return goog.now() > + this.expirationTime_ - fireauth.StsTokenManager.TOKEN_REFRESH_BUFFER_; +}; + + +/** + * Parses a response from the server that contains STS tokens (e.g. from + * VerifyAssertion or VerifyPassword) and save the access token, refresh token, + * and expiration time. + * @param {!Object} response The backend response. + * @return {!string} The STS access token. + */ +fireauth.StsTokenManager.prototype.parseServerResponse = function(response) { + var idToken = response[fireauth.RpcHandler.AuthServerField.ID_TOKEN]; + var refreshToken = + response[fireauth.RpcHandler.AuthServerField.REFRESH_TOKEN]; + var expirationTime = fireauth.StsTokenManager.calcOffsetTimestamp_( + response[fireauth.RpcHandler.AuthServerField.EXPIRES_IN]); + this.setAccessToken(idToken, expirationTime); + this.setRefreshToken(refreshToken); + return idToken; +}; + + +/** + * @param {number|string} offset The offset to add to the current time, in + * seconds. + * @return {number} The timestamp corresponding to the current time plus offset. + * @private + */ +fireauth.StsTokenManager.calcOffsetTimestamp_ = function(offset) { + return goog.now() + parseInt(offset, 10) * 1000; +}; + + +/** + * Exchanges the current refresh token with an access and refresh token. + * @return {!goog.Promise} + * @private + */ +fireauth.StsTokenManager.prototype.exchangeRefreshToken_ = function() { + var data = { + 'grant_type': 'refresh_token', + 'refresh_token': this.refreshToken_ + }; + return this.requestToken_(data); +}; + + +/** + * Sends a request to STS token endpoint for an access/refresh token. + * @param {!Object} data The request data to send to STS token endpoint. + * @return {!goog.Promise} + * @private + */ +fireauth.StsTokenManager.prototype.requestToken_ = function(data) { + var self = this; + // Send RPC request to STS token endpoint. + return this.rpcHandler_.requestStsToken(data).then(function(resp) { + var response = /** @type {!fireauth.StsTokenManager.ResponseData} */ (resp); + self.accessToken_ = + response[fireauth.RpcHandler.StsServerField.ACCESS_TOKEN]; + // Update expiration time. + self.expirationTime_ = fireauth.StsTokenManager.calcOffsetTimestamp_( + response[fireauth.RpcHandler.StsServerField.EXPIRES_IN]); + self.refreshToken_ = + response[fireauth.RpcHandler.StsServerField.REFRESH_TOKEN]; + return /** @type {fireauth.StsTokenManager.Response} */ ({ + 'accessToken': self.accessToken_, + 'expirationTime': self.expirationTime_, + 'refreshToken': self.refreshToken_ + }); + }).thenCatch(function(error) { + // Refresh token expired or user deleted. In this case, reset refresh token + // to prevent sending the request again to the STS server unless + // the token is manually updated, perhaps via successful reauthentication. + if (error['code'] == 'auth/user-token-expired') { + self.refreshToken_ = null; + } + throw error; + }); +}; + + +/** @return {boolean} Whether the refresh token is expired. */ +fireauth.StsTokenManager.prototype.isRefreshTokenExpired = function() { + return !!(this.accessToken_ && !this.refreshToken_); +}; + + +/** + * Returns an STS token. If the cached one is unexpired it is directly returned. + * Otherwise the existing ID token or refresh token is exchanged for a new one. + * If there is no user signed in, returns null. + * + * @param {boolean=} opt_forceRefresh Whether to force refresh token exchange. + * @return {!goog.Promise} + */ +fireauth.StsTokenManager.prototype.getToken = function(opt_forceRefresh) { + var self = this; + var forceRefresh = !!opt_forceRefresh; + // Refresh token is expired. + if (this.isRefreshTokenExpired()) { + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED)); + } + if (!forceRefresh && this.accessToken_ && !this.isExpired_()) { + // Cached STS access token not expired, return it. + return /** @type {!goog.Promise} */ (goog.Promise.resolve({ + 'accessToken': self.accessToken_, + 'expirationTime': self.expirationTime_, + 'refreshToken': self.refreshToken_ + })); + } else if (this.refreshToken_) { + // Expired but refresh token available, exchange refresh token for STS + // token. + return this.exchangeRefreshToken_(); + } else { + // No token, return null token. + return goog.Promise.resolve( + /** @type {?fireauth.StsTokenManager.Response} */ (null)); + } +}; diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js new file mode 100644 index 00000000000..c311559a0d8 --- /dev/null +++ b/packages/auth/src/utils.js @@ -0,0 +1,1337 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Defines utility and helper functions. + */ + +goog.provide('fireauth.util'); + +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.Uri'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.html.SafeUrl'); +goog.require('goog.json'); +goog.require('goog.object'); +goog.require('goog.string'); +goog.require('goog.userAgent'); +goog.require('goog.window'); + + +/** @suppress {duplicate} Suppress variable 'angular' first declared. */ +var angular; + +/** + * Checks whether the user agent is IE11. + * @return {boolean} True if it is IE11. + */ +fireauth.util.isIe11 = function() { + return goog.userAgent.IE && + !!goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE == 11; +}; + + +/** + * Checks whether the user agent is IE10. + * @return {boolean} True if it is IE10. + */ +fireauth.util.isIe10 = function() { + return goog.userAgent.IE && + !!goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE == 10; +}; + + +/** + * Checks whether the user agent is Edge. + * @param {string} userAgent The browser user agent string. + * @return {boolean} True if it is Edge. + */ +fireauth.util.isEdge = function(userAgent) { + return /Edge\/\d+/.test(userAgent); +}; + + +/** + * @param {?string=} opt_userAgent The navigator user agent. + * @return {boolean} Whether local storage is not synchronized between an iframe + * and a popup of the same domain. + */ +fireauth.util.isLocalStorageNotSynchronized = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + return fireauth.util.isIe11() || fireauth.util.isEdge(ua); +}; + + +/** @return {string} The current URL. */ +fireauth.util.getCurrentUrl = function() { + return (goog.global['window'] && goog.global['window']['location']['href']) || + ''; +}; + + +/** + * @param {string} requestUri The request URI to send in verifyAssertion + * request. + * @return {string} The sanitized URI, in this case it undoes the hashbang + * angularJs routing changes to request URI. + */ +fireauth.util.sanitizeRequestUri = function(requestUri) { + // If AngularJS is included. + if (typeof angular != 'undefined') { + // Remove hashbang modifications from URL. + requestUri = requestUri.replace('#/', '#').replace('#!/', '#'); + } + return requestUri; +}; + + +/** + * @param {?string} url The target URL. When !url, redirects to a blank page. + * @param {!Window=} opt_window The optional window to redirect to target URL. + * @param {boolean=} opt_bypassCheck Whether to bypass check. Used for custom + * scheme redirects. + */ +fireauth.util.goTo = function(url, opt_window, opt_bypassCheck) { + var win = opt_window || goog.global['window']; + // No URL, redirect to blank page. + var finalUrl = 'about:blank'; + // Popping up a window and then assigning its URL seems to cause some weird + // error. Fixed by setting win.location.href for now in IE browsers. + // Bug was detected in Edge and IE9. + if (url && !opt_bypassCheck) { + // We cannot use goog.dom.safe.setLocationHref since it tries to read + // popup.location from a different origin, which is an error in IE. + // (In Chrome, popup.location is just an empty Location object) + finalUrl = goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url)); + } + win.location.href = finalUrl; +}; + + +/** + * @param {string} url The target URL. + * @param {!Window=} opt_window The optional window to replace with target URL. + * @param {boolean=} opt_bypassCheck Whether to bypass check. Used for custom + * scheme redirects. + */ +fireauth.util.replaceCurrentUrl = function(url, opt_window, opt_bypassCheck) { + var win = opt_window || goog.global['window']; + if (!opt_bypassCheck) { + win.location.replace( + goog.html.SafeUrl.unwrap(goog.html.SafeUrl.sanitize(url))); + } else { + win.location.replace(url); + } +}; + + +/** + * @param {!Object} a The first object. + * @param {!Object} b The second object. + * @return {!Array} The list of keys that are different between both + * objects provided. + */ +fireauth.util.getKeyDiff = function(a, b) { + var diff = []; + for (var k in a) { + if (!(k in b)) { + diff.push(k); + } else if (typeof a[k] != typeof b[k]) { + diff.push(k); + } else if (goog.isArray(a[k])) { + if (!goog.object.equals(a[k], b[k])) { + diff.push(k); + } + } else if (typeof a[k] == 'object' && a[k] != null && b[k] != null) { + if (fireauth.util.getKeyDiff( + a[k], b[k]).length > 0) { + diff.push(k); + } + } else if (a[k] !== b[k]) { + diff.push(k); + } + } + for (var k in b) { + if (!(k in a)) { + diff.push(k); + } + } + return diff; +}; + + +/** + * @param {?string=} opt_userAgent The navigator user agent. + * @return {?number} The Chrome version, null if the user agent is not Chrome. + */ +fireauth.util.getChromeVersion = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + var browserName = fireauth.util.getBrowserName(ua); + // Confirm current browser is Chrome. + if (browserName != fireauth.util.BrowserName.CHROME) { + return null; + } + var matches = ua.match(/\sChrome\/(\d+)/i); + if (matches && matches.length == 2) { + return parseInt(matches[1], 10); + } + return null; +}; + + +/** + * Detects CORS support. + * @param {?string=} opt_userAgent The navigator user agent. + * @return {boolean} True if the browser supports CORS. + */ +fireauth.util.supportsCors = function(opt_userAgent) { + // Chrome 7 has CORS issues, pick 30 as upper limit. + var chromeVersion = fireauth.util.getChromeVersion(opt_userAgent); + if (chromeVersion && chromeVersion < 30) { + return false; + } + // Among all other supported browsers, only IE8 and IE9 don't support CORS. + return !goog.userAgent.IE || // Not IE. + !goog.userAgent.DOCUMENT_MODE || // No document mode == IE Edge. + goog.userAgent.DOCUMENT_MODE > 9; +}; + + +/** + * Detects whether browser is running on a mobile device. + * @param {?string=} opt_userAgent The navigator user agent. + * @return {boolean} True if the browser is running on a mobile device. + */ +fireauth.util.isMobileBrowser = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + var uaLower = ua.toLowerCase(); + // TODO: implement getBrowserName equivalent for OS. + if (uaLower.match(/android/) || + uaLower.match(/webos/) || + uaLower.match(/iphone|ipad|ipod/) || + uaLower.match(/blackberry/) || + uaLower.match(/windows phone/) || + uaLower.match(/iemobile/)) { + return true; + } + return false; +}; + + +/** + * Closes the provided window. + * @param {?Window=} opt_window The optional window to close. The current window + * is used if none is provided. + */ +fireauth.util.closeWindow = function(opt_window) { + var win = opt_window || goog.global['window']; + // In some browsers, in certain cases after the window closes, as seen in + // Samsung Galaxy S3 Android 4.4.2 stock browser, the win object here is an + // empty object {}. Try to catch the failure and ignore it. + try { + win.close(); + } catch(e) {} +}; + + +/** + * Opens a popup window. + * @param {?string=} opt_url initial URL of the popup window + * @param {string=} opt_name title of the popup + * @param {?number=} opt_width width of the popup + * @param {?number=} opt_height height of the popup + * @return {?Window} Returns the window object that was opened. This returns + * null if a popup blocker prevented the window from being + * opened. + */ +fireauth.util.popup = function(opt_url, opt_name, opt_width, opt_height) { + var width = opt_width || 500; + var height = opt_height || 600; + var top = (window.screen.availHeight - height) / 2; + var left = (window.screen.availWidth - width) / 2; + var options = { + 'width': width, + 'height': height, + 'top': top > 0 ? top : 0, + 'left': left > 0 ? left : 0, + 'location': true, + 'resizable': true, + 'statusbar': true, + 'toolbar': false + }; + // Chrome iOS 7 and 8 is returning an undefined popup win when target is + // specified, even though the popup is not necessarily blocked. + var ua = fireauth.util.getUserAgentString().toLowerCase(); + if (opt_name) { + options['target'] = opt_name; + // This will force a new window on each call, achieving the same effect as + // passing a random name on each call. + if (goog.string.contains(ua, 'crios/')) { + options['target'] = '_blank'; + } + } + var browserName = fireauth.util.getBrowserName( + fireauth.util.getUserAgentString()); + if (browserName == fireauth.util.BrowserName.FIREFOX) { + // Firefox complains when invalid URLs are popped out. Hacky way to bypass. + opt_url = opt_url || 'http://localhost'; + // Firefox disables by default scrolling on popup windows, which can create + // issues when the user has many Google accounts, for instance. + options['scrollbars'] = true; + } + // about:blank getting sanitized causing browsers like IE/Edge to display + // brief error message before redirecting to handler. + var newWin = goog.window.open(opt_url || '', options); + if (newWin) { + // Flaky on IE edge, encapsulate with a try and catch. + try { + newWin.focus(); + } catch (e) {} + } + return newWin; +}; + + +/** + * The default value for the popup wait cycle in ms. + * @const {number} + * @private + */ +fireauth.util.POPUP_WAIT_CYCLE_MS_ = 2000; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the popup requires a delay before closing itself. + */ +fireauth.util.requiresPopupDelay = function(opt_userAgent) { + // TODO: remove this hack when CriOS behavior is fixed in iOS. + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + // Was observed in iOS 10.2 Chrome version 55.0.2883.79. + // Apply to Chrome 55+ iOS 10+ to ensure future Chrome versions or iOS 10 + // minor updates do not suddenly resurface this bug. Revisit this check on + // next CriOS update. + var matches = ua.match(/OS (\d+)_.*CriOS\/(\d+)\./i); + if (matches && matches.length > 2) { + // iOS 10+ && chrome 55+. + return parseInt(matches[1], 10) >= 10 && parseInt(matches[2], 10) >= 55; + } + return false; +}; + + +/** + * @param {?Window} win The popup window to check. + * @param {number=} opt_stepDuration The duration of each wait cycle before + * checking that window is closed. + * @return {!goog.Promise} The promise to resolve when window is + * closed. + */ +fireauth.util.onPopupClose = function(win, opt_stepDuration) { + var stepDuration = opt_stepDuration || fireauth.util.POPUP_WAIT_CYCLE_MS_; + return new goog.Promise(function(resolve, reject) { + // Function to repeat each stepDuration. + var repeat = function() { + goog.Timer.promise(stepDuration).then(function() { + // After wait, check if window is closed. + if (!win || win.closed) { + // If so, resolve. + resolve(); + } else { + // Call repeat again. + return repeat(); + } + }); + }; + return repeat(); + }); +}; + + +/** + * @param {!Array} authorizedDomains List of authorized domains. + * @param {string} url The URL to check. + * @return {boolean} Whether the passed domain is an authorized one. + */ +fireauth.util.isAuthorizedDomain = function(authorizedDomains, url) { + var uri = goog.Uri.parse(url); + var scheme = uri.getScheme(); + var domain = uri.getDomain(); + for (var i = 0; i < authorizedDomains.length; i++) { + // Currently this corresponds to: domain.com = *://*.domain.com:* or + // exact domain match. + // In the case of Chrome extensions, the authorizedDomain will be formatted + // as 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456'. + // The URL to check must have a chrome extension scheme and the domain + // must be an exact match domain == 'abcdefghijklmnopqrstuvwxyz123456'. + if (fireauth.util.matchDomain(authorizedDomains[i], domain, scheme)) { + return true; + } + } + return false; +}; + + +/** + * Represents the dimensions of an entity (width and height). + * @typedef {{ + * width: number, + * height: number + * }} + */ +fireauth.util.Dimensions; + + +/** + * @param {?Window=} opt_window The optional window whose dimensions are to be + * returned. The current window is used if not found. + * @return {?fireauth.util.Dimensions} The requested window dimensions if + * available. + */ +fireauth.util.getWindowDimensions = function(opt_window) { + var win = opt_window || goog.global['window']; + if (win && win['innerWidth'] && win['innerHeight']) { + return { + 'width': parseFloat(win['innerWidth']), + 'height': parseFloat(win['innerHeight']) + }; + } + return null; +}; + + +/** + * RegExp to detect if the domain given is an IP address. This is only used + * for validating http and https schemes. + * + * It does not strictly validate if the IP is a real IP address, but as the + * matchDomain method tests against a set of valid domains (extracted from the + * window's current URL), it is sufficient. + * + * @const {!RegExp} + * @private + */ +fireauth.util.IP_ADDRESS_REGEXP_ = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; + + +/** + * @param {string} domainPattern The domain pattern to match. + * @param {string} domain The domain to check. It is assumed that it is a valid + * domain, not a user provided one. + * @param {string} scheme The scheme of the domain to check. + * @return {boolean} Whether the provided domain matches the domain pattern. + */ +fireauth.util.matchDomain = function(domainPattern, domain, scheme) { + // Chrome extension matching. + if (domainPattern.indexOf('chrome-extension://') == 0) { + var chromeExtUri = goog.Uri.parse(domainPattern); + // Domain must match and the current scheme must be a Chrome extension. + return chromeExtUri.getDomain() == domain && scheme == 'chrome-extension'; + } else if (scheme != 'http' && scheme != 'https') { + // Any other scheme that is not http or https cannot be whitelisted. + return false; + } else { + // domainPattern must not contain a scheme and the current scheme must be + // either http or https. + // Check if authorized domain pattern is an IP address. + if (fireauth.util.IP_ADDRESS_REGEXP_.test(domainPattern)) { + // The domain has to be exactly equal to the pattern, as an IP domain will + // only contain the IP, no extra character. + return domain == domainPattern; + } + // Dots in pattern should be escaped. + var escapedDomainPattern = domainPattern.split('.').join('\\.'); + // Non ip address domains. + // domain.com = *.domain.com OR domain.com + var re = new RegExp( + '^(.+\\.' + escapedDomainPattern + '|' + + escapedDomainPattern + ')$', 'i'); + return re.test(domain); + } +}; + + +/** + * @return {!goog.Promise} A promise that resolves when DOM is ready. + */ +fireauth.util.onDomReady = function() { + var resolver = null; + return new goog.Promise(function(resolve, reject) { + var doc = goog.global.document; + // If document already loaded, resolve immediately. + if (doc.readyState == 'complete') { + resolve(); + } else { + // Document not ready, wait for load before resolving. + // Save resolver, so we can remove listener in case it was externally + // cancelled. + resolver = function() { + resolve(); + }; + goog.events.listenOnce(window, goog.events.EventType.LOAD, resolver); + } + }).thenCatch(function(error) { + // In case this promise was cancelled, make sure it unlistens to load. + goog.events.unlisten(window, goog.events.EventType.LOAD, resolver); + throw error; + }); +}; + + +/** + * The default ondeviceready Cordova timeout in ms. + * @const {number} + * @private + */ +fireauth.util.CORDOVA_ONDEVICEREADY_TIMEOUT_MS_ = 1000; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @param {number=} opt_timeout The optional timeout in ms for deviceready + * event to resolve. + * @return {!goog.Promise} A promise that resolves if the current environment is + * a Cordova environment. + */ +fireauth.util.checkIfCordova = function(opt_userAgent, opt_timeout) { + // Errors generated are internal and should be converted if needed to + // developer facing Firebase errors. + // Only supported in Android/iOS environment. + if (fireauth.util.isAndroidOrIosFileEnvironment(opt_userAgent)) { + return fireauth.util.onDomReady().then(function() { + return new goog.Promise(function(resolve, reject) { + var doc = goog.global.document; + var timeoutId = setTimeout(function() { + reject(new Error('Cordova framework is not ready.')); + }, opt_timeout || fireauth.util.CORDOVA_ONDEVICEREADY_TIMEOUT_MS_); + // This should resolve immediately after DOM ready. + doc.addEventListener('deviceready', function() { + clearTimeout(timeoutId); + resolve(); + }, false); + }); + }); + } + return goog.Promise.reject( + new Error('Cordova must run in an Android or iOS file scheme.')); +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the app is rendered in a mobile iOS or Android file + * environment. + */ +fireauth.util.isAndroidOrIosFileEnvironment = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + return !!(fireauth.util.getCurrentScheme() === 'file:' && + ua.toLowerCase().match(/iphone|ipad|ipod|android/)); +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the app is rendered in a mobile iOS 7 or 8 browser. + */ +fireauth.util.isIOS7Or8 = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + return !!(ua.match(/(iPad|iPhone|iPod).*OS 7_\d/i) || + ua.match(/(iPad|iPhone|iPod).*OS 8_\d/i)); +}; + + +/** + * @return {boolean} Whether browser is Safari or an iOS browser and page is + * embedded in an iframe. Local Storage does not synchronize with an iframe + * embedded on a page in a different domain but will still trigger storage + * event with storage changes. + */ +fireauth.util.isSafariLocalStorageNotSynced = function() { + var ua = fireauth.util.getUserAgentString(); + // Safari or iOS browser and embedded in an iframe. + if (!fireauth.util.iframeCanSyncWebStorage(ua) && fireauth.util.isIframe()) { + return true; + } + return false; +}; + + +/** + * @param {?Window=} opt_win Optional window to check whether it is an iframe. + * If not provided, the current window is checked. + * @return {boolean} Whether web page is running in an iframe. + */ +fireauth.util.isIframe = function(opt_win) { + var win = opt_win || goog.global['window']; + try { + // Check that the current window is not the top window. + // If so, return true. + return !!(win && win != win['top']); + } catch (e) { + return false; + } +}; + + +/** + * @param {?Window=} opt_win Optional window to check whether it has an opener + * that is an iframe. + * @return {boolean} Whether the web page was opened from an iframe. + */ +fireauth.util.isOpenerAnIframe = function(opt_win) { + var win = opt_win || goog.global['window']; + try { + // Get the opener if available. + var opener = win && win['opener']; + // Check if the opener is an iframe. If so, return true. + // Confirm opener is available, otherwise the current window is checked + // instead. + return !!(opener && fireauth.util.isIframe(opener)); + } catch (e) { + return false; + } +}; + + +/** + * Enum for the runtime environment. + * @enum {string} + */ +fireauth.util.Env = { + BROWSER: 'Browser', + NODE: 'Node', + REACT_NATIVE: 'ReactNative' +}; + + +/** + * @return {!fireauth.util.Env} The current runtime environment. + */ +fireauth.util.getEnvironment = function() { + if (firebase.INTERNAL.hasOwnProperty('reactNative')) { + return fireauth.util.Env.REACT_NATIVE; + } else if (firebase.INTERNAL.hasOwnProperty('node')) { + // browserify seems to keep the process property in some cases even though + // the library is browser only. Use this check instead to reliably detect + // a Node.js environment. + return fireauth.util.Env.NODE; + } + // The default is a browser environment. + return fireauth.util.Env.BROWSER; +}; + + +/** + * @return {boolean} Whether the environment is a native environment, where + * CORS checks do not apply. + */ +fireauth.util.isNativeEnvironment = function() { + var environment = fireauth.util.getEnvironment(); + return environment === fireauth.util.Env.REACT_NATIVE || + environment === fireauth.util.Env.NODE; +}; + + +/** + * The separator for storage keys to concatenate App name and API key. + * @const {string} + * @private + */ +fireauth.util.STORAGE_KEY_SEPARATOR_ = ':'; + + +/** + * @param {string} apiKey The API Key of the app. + * @param {string} appName The App name. + * @return {string} The key used for identifying the app owner of the user. + */ +fireauth.util.createStorageKey = function(apiKey, appName) { + return apiKey + fireauth.util.STORAGE_KEY_SEPARATOR_ + appName; +}; + + +/** @return {string} a long random character string. */ +fireauth.util.generateRandomString = function() { + return Math.floor(Math.random() * 1000000000).toString(); +}; + + +/** + * Enums for Browser name. + * @enum {string} + */ +fireauth.util.BrowserName = { + ANDROID: 'Android', + BLACKBERRY: 'Blackberry', + EDGE: 'Edge', + FIREFOX: 'Firefox', + IE: 'IE', + IEMOBILE: 'IEMobile', + OPERA: 'Opera', + OTHER: 'Other', + CHROME: 'Chrome', + SAFARI: 'Safari', + SILK: 'Silk', + WEBOS: 'Webos' +}; + + +/** + * @param {string} userAgent The navigator user agent string. + * @return {string} The browser name, eg Safari, Firefox, etc. + */ +fireauth.util.getBrowserName = function(userAgent) { + var ua = userAgent.toLowerCase(); + if (goog.string.contains(ua, 'opera/') || + goog.string.contains(ua, 'opr/') || + goog.string.contains(ua, 'opios/')) { + return fireauth.util.BrowserName.OPERA; + } else if (goog.string.contains(ua, 'iemobile')) { + // Windows phone IEMobile browser. + return fireauth.util.BrowserName.IEMOBILE; + } else if (goog.string.contains(ua, 'msie') || + goog.string.contains(ua, 'trident/')) { + return fireauth.util.BrowserName.IE; + } else if (goog.string.contains(ua, 'edge/')) { + return fireauth.util.BrowserName.EDGE; + } else if (goog.string.contains(ua, 'firefox/')) { + return fireauth.util.BrowserName.FIREFOX; + } else if (goog.string.contains(ua, 'silk/')) { + return fireauth.util.BrowserName.SILK; + } else if (goog.string.contains(ua, 'blackberry')) { + // Blackberry browser. + return fireauth.util.BrowserName.BLACKBERRY; + } else if (goog.string.contains(ua, 'webos')) { + // WebOS default browser. + return fireauth.util.BrowserName.WEBOS; + } else if (goog.string.contains(ua, 'safari/') && + !goog.string.contains(ua, 'chrome/') && + !goog.string.contains(ua, 'crios/') && + !goog.string.contains(ua, 'android')) { + return fireauth.util.BrowserName.SAFARI; + } else if ((goog.string.contains(ua, 'chrome/') || + goog.string.contains(ua, 'crios/')) && + !goog.string.contains(ua, 'edge/')) { + return fireauth.util.BrowserName.CHROME; + } else if (goog.string.contains(ua, 'android')) { + // Android stock browser. + return fireauth.util.BrowserName.ANDROID; + } else { + // Most modern browsers have name/version at end of user agent string. + var re = new RegExp('([a-zA-Z\\d\\.]+)\/[a-zA-Z\\d\\.]*$'); + var matches = userAgent.match(re); + if (matches && matches.length == 2) { + return matches[1]; + } + } + return fireauth.util.BrowserName.OTHER; +}; + + +/** + * Enums for client implementation name. + * @enum {string} + */ +fireauth.util.ClientImplementation = { + JSCORE: 'JsCore', + OAUTH_HANDLER: 'Handler', + OAUTH_IFRAME: 'Iframe' +}; + + +/** + * Enums for the framework ID to be logged in RPC header. + * Future frameworks to possibly add: angularfire, polymerfire, reactfire, etc. + * @enum {string}. + */ +fireauth.util.Framework = { + // No other framework used. + DEFAULT: 'FirebaseCore-web', + // Firebase Auth used with FirebaseUI-web. + FIREBASEUI: 'FirebaseUI-web' +}; + + +/** + * @param {!Array} providedFrameworks List of framework ID strings. + * @return {!Array} List of supported framework IDs + * with no duplicates. + */ +fireauth.util.getFrameworkIds = function(providedFrameworks) { + var frameworkVersion = []; + var frameworkSet = {}; + for (var key in fireauth.util.Framework) { + frameworkSet[fireauth.util.Framework[key]] = true; + } + for (var i = 0; i < providedFrameworks.length; i++) { + if (typeof frameworkSet[providedFrameworks[i]] !== 'undefined') { + // Delete it from set to prevent duplications. + delete frameworkSet[providedFrameworks[i]]; + frameworkVersion.push(providedFrameworks[i]); + } + } + // Sort alphabetically so that "FirebaseCore-web,FirebaseUI-web" and + // "FirebaseUI-web,FirebaseCore-web" aren't viewed as different. + frameworkVersion.sort(); + return frameworkVersion; +}; + + +/** + * @param {!fireauth.util.ClientImplementation} clientImplementation The client + * implementation. + * @param {string} clientVersion The client version. + * @param {?Array=} opt_frameworkVersion The framework version. + * @param {?string=} opt_userAgent The optional user agent. + * @return {string} The full client SDK version. + */ +fireauth.util.getClientVersion = function(clientImplementation, clientVersion, + opt_frameworkVersion, opt_userAgent) { + var frameworkVersion = fireauth.util.getFrameworkIds( + opt_frameworkVersion || []); + if (!frameworkVersion.length) { + frameworkVersion = [fireauth.util.Framework.DEFAULT]; + } + var environment = fireauth.util.getEnvironment(); + var reportedEnvironment = ''; + if (environment === fireauth.util.Env.BROWSER) { + // In a browser environment, report the browser name. + var userAgent = opt_userAgent || fireauth.util.getUserAgentString(); + reportedEnvironment = fireauth.util.getBrowserName(userAgent); + } else { + // Otherwise, just report the environment name. + reportedEnvironment = environment; + } + // The format to be followed: + // ${browserName}/${clientImplementation}/${clientVersion}/${frameworkVersion} + // As multiple Firebase frameworks/libraries can be used, join their IDs with + // a comma. + return reportedEnvironment + '/' + clientImplementation + + '/' + clientVersion + '/' + frameworkVersion.join(','); +}; + + +/** + * @return {string} The user agent string reported by the environment, or the + * empty string if not available. + */ +fireauth.util.getUserAgentString = function() { + return (goog.global['navigator'] && goog.global['navigator']['userAgent']) || + ''; +}; + + +/** + * @param {string} varStrName The variable string name. + * @param {?Object=} opt_scope The optional scope where to look in. The default + * is window. + * @return {*} The reference if found. + */ +fireauth.util.getObjectRef = function(varStrName, opt_scope) { + var pieces = varStrName.split('.'); + var last = opt_scope || goog.global; + for (var i = 0; + i < pieces.length && typeof last == 'object' && last != null; + i++) { + last = last[pieces[i]]; + } + // Last hasn't reached the end yet, return undefined. + if (i != pieces.length) { + last = undefined; + } + return last; +}; + + +/** @return {boolean} Whether web storage is supported. */ +fireauth.util.isWebStorageSupported = function() { + try { + var storage = goog.global['localStorage']; + var key = fireauth.util.generateEventId(); + if (storage) { + // setItem will throw an exception if we cannot access WebStorage (e.g., + // Safari in private mode). + storage['setItem'](key, '1'); + storage['removeItem'](key); + // For browsers where iframe web storage does not synchronize with a popup + // of the same domain, indexedDB is used for persistent storage. These + // browsers include IE11 and Edge. + // Make sure it is supported (IE11 and Edge private mode does not support + // that). + if (fireauth.util.isLocalStorageNotSynchronized()) { + // In such browsers, if indexedDB is not supported, an iframe cannot be + // notified of the popup sign in result. + return !!goog.global['indexedDB']; + } + return true; + } + } catch (e) {} + return false; +}; + + +/** + * This guards against leaking Cordova support before official launch. + * This field will be removed or updated to return true when the new feature is + * ready for launch. + * @return {boolean} Whether Cordova OAuth support is enabled. + */ +fireauth.util.isCordovaOAuthEnabled = function() { + return false; +}; + + +/** + * @return {boolean} Whether popup and redirect operations are supported in the + * current environment. + */ +fireauth.util.isPopupRedirectSupported = function() { + // Popup and redirect are supported in an environment where the container + // origin can be securely whitelisted. + return (fireauth.util.isHttpOrHttps() || + fireauth.util.isChromeExtension() || + fireauth.util.isAndroidOrIosFileEnvironment()) && + // React Native with remote debugging reports its location.protocol as + // http. + !fireauth.util.isNativeEnvironment() && + // Local storage has to be supported for browser popup and redirect + // operations to work. + fireauth.util.isWebStorageSupported(); +}; + + +/** + * @return {boolean} Whether the current environment is http or https. + */ +fireauth.util.isHttpOrHttps = function() { + return fireauth.util.getCurrentScheme() === 'http:' || + fireauth.util.getCurrentScheme() === 'https:'; +}; + + +/** @return {?string} The current URL scheme. */ +fireauth.util.getCurrentScheme = function() { + return (goog.global['location'] && goog.global['location']['protocol']) || + null; +}; + + +/** + * Checks whether the current page is a Chrome extension. + * @return {boolean} Whether the current page is a Chrome extension. + */ +fireauth.util.isChromeExtension = function() { + return fireauth.util.getCurrentScheme() === 'chrome-extension:'; +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the current browser is running in an iOS + * environment. + */ +fireauth.util.isIOS = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + return !!ua.toLowerCase().match(/iphone|ipad|ipod/); +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the current browser is running in an Android + * environment. + */ +fireauth.util.isAndroid = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + return !!ua.toLowerCase().match(/android/); +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether the opener of a popup cannot communicate with the + * popup while it is in the foreground. + */ +fireauth.util.runsInBackground = function(opt_userAgent) { + // TODO: split this check into 2, one check that opener can access + // popup, another check that storage synchronizes between popup and opener. + // Popup events fail in iOS version 7 (lowest version we currently support) + // browsers. When the popup is triggered, the opener is unable to redirect + // the popup url, close the popup and in some cases will miss the storage + // event triggered when localStorage is changed. + // Extend this to all mobile devices. This behavior is more likely to work + // cross mobile platforms. + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + if (fireauth.util.isMobileBrowser(ua)) { + return false; + } else if (fireauth.util.getBrowserName(ua) == + fireauth.util.BrowserName.FIREFOX) { + // Latest version of Firefox 47.0 does not allow you to access properties on + // the popup window from the opener. + return false; + } + return true; +}; + + +/** + * Stringifies an object, retuning null if the object is not defined. + * @param {*} obj The raw object. + * @return {?string} The JSON-serialized object. + */ +fireauth.util.stringifyJSON = function(obj) { + if (typeof obj === 'undefined') { + return null; + } + return goog.json.serialize(obj); +}; + + +/** + * @param {!Object} obj The original object. + * @return {!Object} A copy of the original object with all entries that are + * null or undefined removed. + */ +fireauth.util.copyWithoutNullsOrUndefined = function(obj) { + // The processed copy to return. + var trimmedObj = {}; + // Remove all empty fields from data, allow zero and false booleans. + for (var key in obj) { + if (obj.hasOwnProperty(key) && + obj[key] !== null && + obj[key] !== undefined) { + trimmedObj[key] = obj[key]; + } + } + return trimmedObj; +}; + + +/** + * Removes all key/pairs with the specified keys from the given object. + * @param {!Object} obj The object to process. + * @param {!Array} keys The list of keys to remove. + * @return {!Object} The object with the keys removed. + */ +fireauth.util.removeEntriesWithKeys = function(obj, keys) { + // Clone object. + var copy = goog.object.clone(obj); + // Traverse keys. + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + // If key found in object, remove it. + if (key in copy) { + delete copy[key]; + } + } + // Returned filtered copy. + return copy; +}; + + +/** + * Parses a JSON string, returning undefined if null is passed. + * @param {?string} json The JSON-serialized object. + * @return {*} The raw object. + */ +fireauth.util.parseJSON = function(json) { + if (goog.isNull(json)) { + return undefined; + } + + // Do not use goog.json.parse since it uses eval underneath to support old + // browsers that do not provide JSON.parse. The recommended Content Security + // Policy does not allow unsafe-eval in some environments like Chrome + // extensions. Usage of eval is not recommend in Chrome in general. + // Use native parsing instead via JSON.parse. This is provided in our list + // of supported browsers. + return JSON.parse(json); +}; + + +/** + * @return {?function(new:XMLHttpRequest)|undefined} The current environment + * XMLHttpRequest. + */ +fireauth.util.getXMLHttpRequest = function() { + // In Node.js XMLHttpRequest is polyfilled. + var isNode = fireauth.util.getEnvironment() == fireauth.util.Env.NODE; + var XMLHttpRequest = goog.global['XMLHttpRequest'] || + (isNode && + firebase.INTERNAL['node'] && + firebase.INTERNAL['node']['XMLHttpRequest']); + return XMLHttpRequest; +}; + + +/** + * @param {?string=} opt_prefix An optional prefix string to prepend to ID. + * @return {string} The generated event ID used to identify a generic event. + */ +fireauth.util.generateEventId = function(opt_prefix) { + return opt_prefix ? opt_prefix : '' + + Math.floor(Math.random() * 1000000000).toString(); +}; + + +/** + * @param {?string=} opt_userAgent The optional user agent. + * @return {boolean} Whether an embedded iframe can sync to web storage changes. + * Web storage sync fails in Safari desktop browsers and iOS mobile + * browsers. + */ +fireauth.util.iframeCanSyncWebStorage = function(opt_userAgent) { + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + if (fireauth.util.getBrowserName(ua) == fireauth.util.BrowserName.SAFARI || + ua.toLowerCase().match(/iphone|ipad|ipod/)) { + return false; + } + return true; +}; + + +/** + * Reset unlaoded GApi modules. If gapi.load fails due to a network error, + * it will stop working after a retrial. This is a hack to fix this issue. + */ +fireauth.util.resetUnloadedGapiModules = function() { + // Clear last failed gapi.load state to force next gapi.load to first + // load the failed gapi.iframes module. + // Get gapix.beacon context. + var beacon = goog.global['___jsl']; + // Get current hint. + if (beacon && beacon['H']) { + // Get gapi hint. + for (var hint in beacon['H']) { + // Requested modules. + beacon['H'][hint]['r'] = beacon['H'][hint]['r'] || []; + // Loaded modules. + beacon['H'][hint]['L'] = beacon['H'][hint]['L'] || []; + // Set requested modules to a copy of the loaded modules. + beacon['H'][hint]['r'] = beacon['H'][hint]['L'].concat(); + // Clear pending callbacks. + if (beacon['CP']) { + for (var i = 0; i < beacon['CP'].length; i++) { + // Remove all failed pending callbacks. + beacon['CP'][i] = null; + } + } + } + } +}; + + +/** + * Returns whether the current device is a mobile device. Mobile browsers and + * React-Native environments are considered mobile devices. + * @param {?string=} opt_userAgent The optional navigator user agent. + * @param {?fireauth.util.Env=} opt_env The optional environment. + * @return {boolean} Whether the current device is a mobile device or not. + */ +fireauth.util.isMobileDevice = function(opt_userAgent, opt_env) { + // Get user agent. + var ua = opt_userAgent || fireauth.util.getUserAgentString(); + // Get environment. + var environment = opt_env || fireauth.util.getEnvironment(); + return fireauth.util.isMobileBrowser(ua) || + environment === fireauth.util.Env.REACT_NATIVE; +}; + + +/** + * @param {?Object=} opt_navigator The optional navigator object typically used + * for testing. + * @return {boolean} Whether the app is currently online. If offline, false is + * returned. If this cannot be determined, true is returned. + */ +fireauth.util.isOnline = function(opt_navigator) { + var navigator = opt_navigator || goog.global['navigator']; + if (navigator && + typeof navigator['onLine'] === 'boolean' && + // Apply only for traditional web apps and Chrome extensions. + // This is especially true for Cordova apps which have unreliable + // navigator.onLine behavior unless cordova-plugin-network-information is + // installed which overwrites the native navigator.onLine value and + // defines navigator.connection. + (fireauth.util.isHttpOrHttps() || + fireauth.util.isChromeExtension() || + typeof navigator['connection'] !== 'undefined')) { + return navigator['onLine']; + } + // If we can't determine the state, assume it is online. + return true; +}; + + +/** + * @param {?Object=} opt_navigator The object with navigator data, defaulting + * to window.navigator if unspecified. + * @return {?string} The user's preferred language. Returns null if + */ +fireauth.util.getUserLanguage = function(opt_navigator) { + var navigator = opt_navigator || goog.global['navigator']; + if (!navigator) { + return null; + } + return ( + // Most reliable, but only supported in Chrome/Firefox. + navigator['languages'] && navigator['languages'][0] || + // Supported in most browsers, but returns the language of the browser + // UI, not the language set in browser settings. + navigator['language'] || + // IE <= 10. + navigator['userLanguage'] || + // Couldn't determine language. + null + ); +}; + + +/** + * A structure to help pick between a range of long and short delay durations + * depending on the current environment. In general, the long delay is used for + * mobile environments whereas short delays are used for desktop environments. + * @param {number} shortDelay The short delay duration. + * @param {number} longDelay The long delay duration. + * @param {?string=} opt_userAgent The optional navigator user agent. + * @param {?fireauth.util.Env=} opt_env The optional environment. + * @constructor + */ +fireauth.util.Delay = function(shortDelay, longDelay, opt_userAgent, opt_env) { + // Internal error when improperly initialized. + if (shortDelay > longDelay) { + throw new Error('Short delay should be less than long delay!'); + } + /** + * @private @const {number} The short duration delay used for desktop + * environments. + */ + this.shortDelay_ = shortDelay; + /** + * @private @const {number} The long duration delay used for mobile + * environments. + */ + this.longDelay_ = longDelay; + /** @private @const {boolean} Whether the environment is a mobile one. */ + this.isMobile_ = fireauth.util.isMobileDevice(opt_userAgent, opt_env); +}; + + +/** + * @return {number} The delay that matches with the current environment. + */ +fireauth.util.Delay.prototype.get = function() { + // If running in a mobile environment, return the long delay, otherwise + // return the short delay. + // This could be improved in the future to dynamically change based on other + // variables instead of just reading the current environment. + return this.isMobile_ ? this.longDelay_ : this.shortDelay_; +}; + + +/** + * @return {boolean} Whether the app is visible in the foreground. This uses + * document.visibilityState. For browsers that do not support it, this is + * always true. + */ +fireauth.util.isAppVisible = function() { + // https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilityState + var doc = goog.global.document; + // Check if supported. + if (doc && typeof doc['visibilityState'] !== 'undefined') { + // Check if visible. + return doc['visibilityState'] == 'visible'; + } + // API not supported in current browser, default to true. + return true; +}; + + +/** + * @return {!goog.Promise} A promise that resolves when the app is visible in + * the foreground. + */ +fireauth.util.onAppVisible = function() { + var doc = goog.global.document; + // Visibility change listener reference. + var onVisibilityChange = null; + if (fireauth.util.isAppVisible() || !doc) { + // Visible or non browser environment. + return goog.Promise.resolve(); + } else { + // Invisible and in browser environment. + return new goog.Promise(function(resolve, reject) { + // On visibility change listener. + onVisibilityChange = function(event) { + // App is visible. + if (fireauth.util.isAppVisible()) { + // Unregister event listener. + doc.removeEventListener( + 'visibilitychange', onVisibilityChange, false); + // Resolve promise. + resolve(); + } + }; + // Listen to visibility change. + doc.addEventListener('visibilitychange', onVisibilityChange, false); + }).thenCatch(function(error) { + // In case this promise was cancelled, make sure it unlistens to + // visibilitychange event. + doc.removeEventListener('visibilitychange', onVisibilityChange, false); + // Rethrow the same error. + throw error; + }); + } +}; + + +/** + * Logs a warning message to the console, if the console is available. + * @param {string} message + */ +fireauth.util.consoleWarn = function(message) { + if (typeof console !== 'undefined' && typeof console.warn === 'function') { + console.warn(message); + } +}; + + +/** + * Parses a UTC time stamp string or number and returns the corresponding UTC + * date string if valid. Otherwise, returns null. + * @param {?string|number} utcTimestamp The UTC timestamp number or string. + * @return {?string} The corresponding UTC date string. Null if invalid. + */ +fireauth.util.utcTimestampToDateString = function(utcTimestamp) { + try { + // Convert to date object. + var date = new Date(parseInt(utcTimestamp, 10)); + // Test date is valid. + if (!isNaN(date.getTime()) && + // Confirm that utcTimestamp is numeric. + goog.string.isNumeric(utcTimestamp)) { + // Convert to UTC date string. + return date.toUTCString(); + } + } catch (e) { + // Do nothing. null will be returned. + } + return null; +}; diff --git a/packages/auth/test/actioncodeinfo_test.js b/packages/auth/test/actioncodeinfo_test.js new file mode 100644 index 00000000000..6ecb1c90fa5 --- /dev/null +++ b/packages/auth/test/actioncodeinfo_test.js @@ -0,0 +1,141 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for actioncodeinfo.js. + */ + +goog.provide('fireauth.ActionCodeInfoTest'); + +goog.require('fireauth.ActionCodeInfo'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.ActionCodeInfoTest'); + + + +var verifyEmailServerResponse = { + 'kind': 'identitytoolkit#ResetPasswordResponse', + 'email': 'user@example.com', + 'requestType': 'VERIFY_EMAIL' +}; +var passwordResetServerResponse = { + 'kind': 'identitytoolkit#ResetPasswordResponse', + 'email': 'user@example.com', + 'requestType': 'PASSWORD_RESET' +}; +var recoverEmailServerResponse = { + 'kind': 'identitytoolkit#ResetPasswordResponse', + 'email': 'user@example.com', + 'newEmail': 'newUser@example.com', + 'requestType': 'RECOVER_EMAIL' +}; + + +function testActionCodeInfo_invalid_missingOperation() { + assertThrows(function() { + new fireauth.ActionCodeInfo({'email': 'user@example.com'}); + }); +} + + +function testActionCodeInfo_invalid_missingEmail() { + assertThrows(function() { + new fireauth.ActionCodeInfo({'requestType': 'PASSWORD_RESET'}); + }); +} + + +function testActionCodeInfo_verifyEmail() { + var expectedData = {email: 'user@example.com', fromEmail: null}; + var actionCodeInfo = new fireauth.ActionCodeInfo(verifyEmailServerResponse); + + // Check operation. + assertEquals('VERIFY_EMAIL', actionCodeInfo['operation']); + // Property should be read-only. + actionCodeInfo['operation'] = 'BLA'; + assertEquals('VERIFY_EMAIL', actionCodeInfo['operation']); + + // Check data. + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + // Property should be read-only. + actionCodeInfo['data']['email'] = 'other@example.com'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + actionCodeInfo['data'] = 'BLA'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); +} + + +function testActionCodeInfo_passwordReset() { + var expectedData = {email: 'user@example.com', fromEmail: null}; + var actionCodeInfo = new fireauth.ActionCodeInfo(passwordResetServerResponse); + + // Check operation. + assertEquals('PASSWORD_RESET', actionCodeInfo['operation']); + // Property should be read-only. + actionCodeInfo['operation'] = 'BLA'; + assertEquals('PASSWORD_RESET', actionCodeInfo['operation']); + + // Check data. + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + // Property should be read-only. + actionCodeInfo['data']['email'] = 'other@example.com'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + actionCodeInfo['data'] = 'BLA'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); +} + + +function testActionCodeInfo_recoverEmail() { + var expectedData = { + email: 'user@example.com', + fromEmail: 'newUser@example.com' + }; + var actionCodeInfo = new fireauth.ActionCodeInfo(recoverEmailServerResponse); + + // Check operation. + assertEquals('RECOVER_EMAIL', actionCodeInfo['operation']); + // Property should be read-only. + actionCodeInfo['operation'] = 'BLA'; + assertEquals('RECOVER_EMAIL', actionCodeInfo['operation']); + + // Check data. + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + // Property should be read-only. + actionCodeInfo['data']['email'] = 'other@example.com'; + actionCodeInfo['data']['fromEmail'] = 'unknown@example.com'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); + actionCodeInfo['data'] = 'BLA'; + assertObjectEquals( + expectedData, + actionCodeInfo['data']); +} diff --git a/packages/auth/test/actioncodesettings_test.js b/packages/auth/test/actioncodesettings_test.js new file mode 100644 index 00000000000..45af297aa7d --- /dev/null +++ b/packages/auth/test/actioncodesettings_test.js @@ -0,0 +1,253 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for actioncodesettings.js. + */ + +goog.provide('fireauth.ActionCodeSettingsTest'); + +goog.require('fireauth.ActionCodeSettings'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.ActionCodeSettingsTest'); + + +/** + * Asserts that the provided settings will throw an error in action code + * settings initialization. + * @param {!Object} settings The settings object to test for expected errors. + * @param {string} expectedCode The expected error code thrown. + */ +function assertActionCodeSettingsErrorThrown(settings, expectedCode) { + var error = assertThrows(function() { + new fireauth.ActionCodeSettings(settings); + }); + assertEquals(expectedCode, error.code); +} + + +function testActionCodeSettings_success_allParameters() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': 'com.example.ios' + }, + 'android': { + 'packageName': 'com.example.android', + 'installApp': true, + 'minimumVersion': '12' + }, + 'handleCodeInApp': true + }; + var expectedRequest = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + var actionCodeSettings = new fireauth.ActionCodeSettings(settings); + assertObjectEquals(expectedRequest, actionCodeSettings.buildRequest()); +} + + +function testActionCodeSettings_success_partialParameters() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + // Will be ignored. + 'iOS': {}, + 'android': {} + }; + var expectedRequest = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'canHandleCodeInApp': false + }; + var actionCodeSettings = new fireauth.ActionCodeSettings(settings); + assertObjectEquals(expectedRequest, actionCodeSettings.buildRequest()); +} + + +function testActionCodeSettings_success_partialParameters_android() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'packageName': 'com.example.android' + } + }; + var expectedRequest = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': false, + 'canHandleCodeInApp': false + }; + var actionCodeSettings = new fireauth.ActionCodeSettings(settings); + assertObjectEquals(expectedRequest, actionCodeSettings.buildRequest()); +} + + +function testActionCodeSettings_success_partialParameters_ios() { + var settings = { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': 'com.example.ios' + } + }; + var expectedRequest = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'canHandleCodeInApp': false + }; + var actionCodeSettings = new fireauth.ActionCodeSettings(settings); + assertObjectEquals(expectedRequest, actionCodeSettings.buildRequest()); +} + + +function testActionCodeSettings_error_continueUrl() { + // Missing continue URL. + assertActionCodeSettingsErrorThrown( + { + 'android': { + 'packageName': 'com.example.android' + } + }, + 'auth/missing-continue-uri'); + // Invalid continue URL. + assertActionCodeSettingsErrorThrown( + { + 'url': '', + 'android': { + 'packageName': 'com.example.android' + } + }, + 'auth/invalid-continue-uri'); + // Invalid continue URL. + assertActionCodeSettingsErrorThrown( + { + 'url': ['https://www.example.com/?state=abc'], + 'android': { + 'packageName': 'com.example.android' + } + }, + 'auth/invalid-continue-uri'); +} + + +function testActionCodeSettings_error_canHandleCodeInApp() { + // Can handle code in app but no app specified. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'handleCodeInApp': true + }, + 'auth/argument-error'); + // Non-boolean can handle code in app. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'handleCodeInApp': 'false' + }, + 'auth/argument-error'); +} + + +function testActionCodeSettings_error_android() { + // Invalid Android field. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': 'bla' + }, + 'auth/argument-error'); + // Android package name set to empty string. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'packageName': '' + } + }, + 'auth/argument-error'); + // Android package missing when other Android parameters specified. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'installApp': true, + 'minimumVersion': '12' + } + }, + 'auth/missing-android-pkg-name'); + // Invalid Android package name. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'packageName': {} + } + }, + 'auth/argument-error'); + // Invalid installApp field. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'packageName': 'com.example.android', + 'installApp': 'bla' + } + }, + 'auth/argument-error'); + // Invalid minimumVersion field. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'android': { + 'packageName': 'com.example.android', + 'minimumVersion': false + } + }, + 'auth/argument-error'); +} + + +function testActionCodeSettings_error_ios() { + // Invalid iOS field. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'iOS': 'bla' + }, + 'auth/argument-error'); + // iOS bundle ID set to empty string. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': '' + } + }, + 'auth/argument-error'); + // Invalid iOS bundle ID. + assertActionCodeSettingsErrorThrown( + { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': {} + } + }, + 'auth/argument-error'); +} diff --git a/packages/auth/test/additionaluserinfo_test.js b/packages/auth/test/additionaluserinfo_test.js new file mode 100644 index 00000000000..aabb045fd77 --- /dev/null +++ b/packages/auth/test/additionaluserinfo_test.js @@ -0,0 +1,440 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for additionaluserinfo.js + */ + +goog.provide('fireauth.AdditionalUserInfoTest'); + +goog.require('fireauth.AdditionalUserInfo'); +goog.require('fireauth.FacebookAdditionalUserInfo'); +goog.require('fireauth.FederatedAdditionalUserInfo'); +goog.require('fireauth.GenericAdditionalUserInfo'); +goog.require('fireauth.GithubAdditionalUserInfo'); +goog.require('fireauth.GoogleAdditionalUserInfo'); +goog.require('fireauth.TwitterAdditionalUserInfo'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.AdditionalUserInfoTest'); + + +// iss: "https://securetoken.google.com/projectId" +// aud: "projectId" +// auth_time: 1506050282 +// user_id: "123456" +// sub: "123456" +// iat: 1506050283 +// exp: 1506053883 +// email: "user@example.com" +// email_verified: false +// phone_number: "+11234567890" +// firebase: {identities: {phone: ["+11234567890"], +// email: ["user@example.com"] +// }, sign_in_provider: "phone"} +var tokenPhone = 'HEAD.ew0KICAiaXNzIjogImh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLm' + + 'NvbS9wcm9qZWN0SWQiLA0KICAiYXVkIjogInByb2plY3RJZCIsDQogICJhdXRoX3RpbWUiOi' + + 'AxNTA2MDUwMjgyLA0KICAidXNlcl9pZCI6ICIxMjM0NTYiLA0KICAic3ViIjogIjEyMzQ1Ni' + + 'IsDQogICJpYXQiOiAxNTA2MDUwMjgzLA0KICAiZXhwIjogMTUwNjA1Mzg4MywNCiAgImVtYW' + + 'lsIjogInVzZXJAZXhhbXBsZS5jb20iLA0KICAiZW1haWxfdmVyaWZpZWQiOiBmYWxzZSwNCi' + + 'AgInBob25lX251bWJlciI6ICIrMTEyMzQ1Njc4OTAiLA0KICAiZmlyZWJhc2UiOiB7DQogIC' + + 'AgImlkZW50aXRpZXMiOiB7DQogICAgICAicGhvbmUiOiBbDQogICAgICAgICIrMTEyMzQ1Nj' + + 'c4OTAiDQogICAgICBdLA0KICAgICAgImVtYWlsIjogWw0KICAgICAgICAidXNlckBleGFtcG' + + 'xlLmNvbSINCiAgICAgIF0NCiAgICB9LA0KICAgICJzaWduX2luX3Byb3ZpZGVyIjogInBob2' + + '5lIg0KICB9DQp9.SIGNATURE'; + +// Typical verifyPhoneNumber response. +var verifyPhoneNumberResponse = { + 'idToken': tokenPhone, + 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600', + 'localId': '123456', + 'isNewUser': true, + 'phoneNumber': '+11234567890' +}; + +// Expected generic additional user info object for the above verifyPhoneNumber +// response. +var expectedGenericAdditionalUserInfo = { + 'isNewUser': true, + 'providerId': 'phone' +}; + +// Typical minimal verifyAssertion response for generic IdP user with no profile +// data. +var noProfileVerifyAssertion = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'idToken': 'ID_TOKEN', + 'isNewUser': true, + 'providerId': 'noprofile.com' +}; + +// Expected federated additional user info object with no profile. +var expectedNoProfileAdditionalUserInfo = { + 'isNewUser': true, + 'providerId': 'noprofile.com', + 'profile' : {} +}; + +// Typical verifyAssertion response for google user. +var googleVerifyAssertion = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'isNewUser': true, + 'idToken': 'ID_TOKEN', + 'providerId': 'google.com', + 'rawUserInfo': '{"kind":"plus#person","displayName":"John Doe","na' + + 'me":{"givenName":"John","familyName":"Doe"},"language":"en","isPl' + + 'usUser":true,"url":"https://plus.google.com/abcdefghijklmnopqrstu' + + '","image":{"url":"https://lh5.googleusercontent.com/123456789012/' + + 'abcdefghijklmnopqrstuvwxyz/12345678/photo.jpg?sz=50","isDefault":' + + 'false},"placesLived":[{"primary":true,"value":"Mountain View, CA"' + + '}],"emails":[{"type":"account","value":"dummyuser1234567@gmail.co' + + 'm"}],"ageRange":{"min":21},"verified":false,"circledByCount":0,"i' + + 'd":"abcdefghijklmnopqrstu","objectType":"person"}' +}; + +// Expected Google additional user info object. +var expectedGoogleAdditionalUserInfo = { + 'providerId': 'google.com', + 'isNewUser': true, + 'profile' : { + 'kind': 'plus#person', + 'displayName': 'John Doe', + 'name': { + 'givenName': 'John', + 'familyName': 'Doe' + }, + 'language': 'en', + 'isPlusUser': true, + 'url': 'https://plus.google.com/abcdefghijklmnopqrstu', + 'image': { + 'url': 'https://lh5.googleusercontent.com/123456789012/abcdefghijklmno' + + 'pqrstuvwxyz/12345678/photo.jpg?sz=50', + 'isDefault': false + }, + 'placesLived': [ + { + 'primary': true, + 'value': 'Mountain View, CA' + } + ], + 'emails': [ + { + 'type': 'account', + 'value': 'dummyuser1234567@gmail.com' + } + ], + 'ageRange': { + 'min': 21 + }, + 'verified': false, + 'circledByCount': 0, + 'id': 'abcdefghijklmnopqrstu', + 'objectType': 'person' + } +}; + +// Typical verifyAssertion response for Facebook user. +var facebookVerifyAssertion = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'isNewUser': true, + 'idToken': 'ID_TOKEN', + 'providerId': 'facebook.com', + 'rawUserInfo': '{"updated_time":"2015-09-14T19:51:07+0000","gender' + + '":"male","timezone":-7,"link":"https://www.facebook.com/abcdefghi' + + 'jklmnopqr/1234567890123456/","verified":true,"last_name":"Do","loc' + + 'ale":"en_US","picture":{"data":{"is_silhouette":true,"url":"https' + + '://scontent.xx.fbcdn.net/v.jpg"}},"age_range":{"min":21},"name":"' + + 'John Do","id":"1234567890123456","first_name":"John","email":"dumm' + + 'yuser1234567@gmail.com"}' +}; + +// Expected Facebook additional user info object. +var expectedFacebookAdditionalUserInfo = { + 'providerId': 'facebook.com', + 'isNewUser': true, + 'profile' : { + 'updated_time': '2015-09-14T19:51:07+0000', + 'gender': 'male', + 'timezone': -7, + 'link': 'https://www.facebook.com/abcdefghijklmnopqr/1234567890123456/', + 'verified': true, + 'last_name': 'Do', + 'locale': 'en_US', + 'picture': { + 'data': { + 'is_silhouette': true, + 'url': 'https://scontent.xx.fbcdn.net/v.jpg' + } + }, + 'age_range': { + 'min': 21 + }, + 'name': 'John Do', + 'id': '1234567890123456', + 'first_name': 'John', + 'email': 'dummyuser1234567@gmail.com' + } +}; + +// Typical verifyAssertion response for Twitter user. +var twitterVerifyAssertion = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'isNewUser': false, + 'idToken': 'ID_TOKEN', + 'providerId': 'twitter.com', + 'screenName': 'twitterxy', + 'rawUserInfo': '{"utc_offset":null,"friends_count":10,"profile_ima' + + 'ge_url_https":"https://abs.twimg.com/sticky/default_profile_image' + + 's/default_profile_3_normal.png","listed_count":0,"profile_backgro' + + 'und_image_url":"http://abs.twimg.com/images/themes/theme1/bg.png"' + + ',"default_profile_image":true,"favourites_count":0,"description":' + + '"","created_at":"Thu Mar 26 03:05:49 +0000 2015","is_translator":' + + 'false,"profile_background_image_url_https":"https://abs.twimg.com' + + '/images/themes/theme1/bg.png","protected":false,"screen_name":"tw' + + 'itterxy","id_str":"1234567890","profile_link_color":"0084B4","is_' + + 'translation_enabled":false,"id":1234567890,"geo_enabled":false,"p' + + 'rofile_background_color":"C0DEED","lang":"en","has_extended_profi' + + 'le":false,"profile_sidebar_border_color":"C0DEED","profile_text_c' + + 'olor":"333333","verified":false,"profile_image_url":"http://abs.t' + + 'wimg.com/sticky/default_profile_images/default_profile_3_normal.p' + + 'ng","time_zone":null,"url":null,"contributors_enabled":false,"pro' + + 'file_background_tile":false,"entities":{"description":{"urls":[]}' + + '},"statuses_count":0,"follow_request_sent":false,"followers_count":' + + '1,"profile_use_background_image":true,"default_profile":true,"follo' + + 'wing":false,"name":"John Doe","location":"","profile_sidebar_fill_c' + + 'olor":"DDEEF6","notifications":false}' +}; + +// Expected Twitter additional user info object. +var expectedTwitterAdditionalUserInfo = { + 'isNewUser': false, + 'providerId': 'twitter.com', + 'username': 'twitterxy', + 'profile' : { + 'utc_offset': null, + 'friends_count': 10, + 'profile_image_url_https': 'https://abs.twimg.com/sticky/default_profile' + + '_images/default_profile_3_normal.png', + 'listed_count': 0, + 'profile_background_image_url': 'http://abs.twimg.com/images/themes/them' + + 'e1/bg.png', + 'default_profile_image': true, + 'favourites_count': 0, + 'description': '', + 'created_at': 'Thu Mar 26 03:05:49 +0000 2015', + 'is_translator': false, + 'profile_background_image_url_https': 'https://abs.twimg.com/images/them' + + 'es/theme1/bg.png', + 'protected': false, + 'screen_name': 'twitterxy', + 'id_str': '1234567890', + 'profile_link_color': '0084B4', + 'is_translation_enabled': false, + 'id': 1234567890, + 'geo_enabled': false, + 'profile_background_color': 'C0DEED', + 'lang': 'en', + 'has_extended_profile': false, + 'profile_sidebar_border_color': 'C0DEED', + 'profile_text_color': '333333', + 'verified': false, + 'profile_image_url': 'http://abs.twimg.com/sticky/default_profile_images' + + '/default_profile_3_normal.png', + 'time_zone': null, + 'url': null, + 'contributors_enabled': false, + 'profile_background_tile': false, + 'entities': { + 'description': { + 'urls': [] + } + }, + 'statuses_count': 0, + 'follow_request_sent': false, + 'followers_count': 1, + 'profile_use_background_image': true, + 'default_profile': true, + 'following': false, + 'name': 'John Doe', + 'location': '', + 'profile_sidebar_fill_color': 'DDEEF6', + 'notifications': false + } +}; + +// Typical verifyAssertion response for GitHub user. +var githubVerifyAssertion = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'idToken': 'ID_TOKEN', + 'isNewUser': false, + 'providerId': 'github.com', + 'rawUserInfo': '{"gists_url":"https://api.github.com/users/uid1234' + + '567890/gists{/gist_id}","repos_url":"https://api.github.com/users' + + '/uid1234567890/repos","following_url":"https://api.github.com/use' + + 'rs/uid1234567890/following{/other_user}","bio":null,"created_at":' + + '"2015-07-23T21:49:36Z","login":"uid1234567890","type":"User","blo' + + 'g":null,"subscriptions_url":"https://api.github.com/users/uid1234' + + '567890/subscriptions","updated_at":"2016-06-21T20:22:45Z","site_a' + + 'dmin":false,"company":null,"id":13474811,"public_repos":0,"gravat' + + 'ar_id":"","email":null,"organizations_url":"https://api.github.co' + + 'm/users/uid1234567890/orgs","hireable":null,"starred_url":"https:' + + '//api.github.com/users/uid1234567890/starred{/owner}{/repo}","fol' + + 'lowers_url":"https://api.github.com/users/uid1234567890/followers' + + '","public_gists":0,"url":"https://api.github.com/users/uid1234567' + + '890","received_events_url":"https://api.github.com/users/uid12345' + + '67890/received_events","followers":0,"avatar_url":"https://avatar' + + 's.githubusercontent.com/u/12345678?v\\u003d3","events_url":"https' + + '://api.github.com/users/uid1234567890/events{/privacy}","html_url' + + '":"https://github.com/uid1234567890","following":0,"name":null,"l' + + 'ocation":null}' +}; + +// Expected GitHub additional user info object. +var expectedGithubAdditionalUserInfo = { + 'isNewUser': false, + 'providerId': 'github.com', + 'username': 'uid1234567890', + 'profile' : { + 'gists_url': 'https://api.github.com/users/uid1234567890/gists{/gist_id}', + 'repos_url': 'https://api.github.com/users/uid1234567890/repos', + 'following_url': 'https://api.github.com/users/uid1234567890/following{/' + + 'other_user}', + 'bio': null, + 'created_at': '2015-07-23T21:49:36Z', + 'login': 'uid1234567890', + 'type': 'User', + 'blog': null, + 'subscriptions_url':'https://api.github.com/users/uid1234567890/subscrip' + + 'tions', + 'updated_at': '2016-06-21T20:22:45Z', + 'site_admin': false, + 'company': null, + 'id': 13474811, + 'public_repos': 0, + 'gravatar_id': '', + 'email': null, + 'organizations_url': 'https://api.github.com/users/uid1234567890/orgs', + 'hireable': null, + 'starred_url': 'https://api.github.com/users/uid1234567890/starred{/owne' + + 'r}{/repo}', + 'followers_url': 'https://api.github.com/users/uid1234567890/followers', + 'public_gists': 0, + 'url': 'https://api.github.com/users/uid1234567890', + 'received_events_url': 'https://api.github.com/users/uid1234567890/recei' + + 'ved_events', + 'followers': 0, + 'avatar_url': 'https://avatars.githubusercontent.com/u/12345678?v=3', + 'events_url': 'https://api.github.com/users/uid1234567890/events{/privacy}', + 'html_url': 'https://github.com/uid1234567890', + 'following': 0, + 'name': null, + 'location': null + } +}; + + +function testInvalidAdditionalUserInfo() { + var invalid = {}; + try { + new fireauth.FederatedAdditionalUserInfo(invalid); + fail('Initializing an invalid additional user info object should fail.'); + } catch (e) { + assertEquals('Invalid additional user info!', e.message); + } + assertNull(fireauth.AdditionalUserInfo.fromPlainObject(invalid)); +} + + +function testGenericAdditionalUserInfo() { + var genericAdditionalUserInfo = new fireauth.GenericAdditionalUserInfo( + verifyPhoneNumberResponse); + assertObjectEquals( + expectedGenericAdditionalUserInfo, + genericAdditionalUserInfo); + assertObjectEquals( + genericAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(verifyPhoneNumberResponse)); +} + + +function testFederatedAdditionalUserInfo_withProfile() { + var federatedAdditionalUserInfo = + new fireauth.FederatedAdditionalUserInfo(facebookVerifyAssertion); + assertObjectEquals( + expectedFacebookAdditionalUserInfo, + federatedAdditionalUserInfo); +} + + +function testFederatedAdditionalUserInfo_noProfile() { + var noProfileAdditionalUserInfo = + new fireauth.FederatedAdditionalUserInfo(noProfileVerifyAssertion); + assertObjectEquals( + expectedNoProfileAdditionalUserInfo, + noProfileAdditionalUserInfo); + assertObjectEquals( + noProfileAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(noProfileVerifyAssertion)); +} + + +function testFacebookAdditionalUserInfo() { + var facebookAdditionalUserInfo = + new fireauth.FacebookAdditionalUserInfo(facebookVerifyAssertion); + assertObjectEquals( + expectedFacebookAdditionalUserInfo, + facebookAdditionalUserInfo); + assertObjectEquals( + facebookAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(facebookVerifyAssertion)); +} + + +function testGithubAdditionalUserInfo() { + var githubAdditionalUserInfo = + new fireauth.GithubAdditionalUserInfo(githubVerifyAssertion); + assertObjectEquals( + expectedGithubAdditionalUserInfo, + githubAdditionalUserInfo); + assertObjectEquals( + githubAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(githubVerifyAssertion)); +} + + +function testGoogleAdditionalUserInfo() { + var googleAdditionalUserInfo = + new fireauth.GoogleAdditionalUserInfo(googleVerifyAssertion); + assertObjectEquals( + expectedGoogleAdditionalUserInfo, + googleAdditionalUserInfo); + assertObjectEquals( + googleAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(googleVerifyAssertion)); +} + + +function testTwitterAdditionalUserInfo() { + var twitterAdditionalUserInfo = + new fireauth.TwitterAdditionalUserInfo(twitterVerifyAssertion); + assertObjectEquals( + expectedTwitterAdditionalUserInfo, + twitterAdditionalUserInfo); + assertObjectEquals( + twitterAdditionalUserInfo, + fireauth.AdditionalUserInfo.fromPlainObject(twitterVerifyAssertion)); +} diff --git a/packages/auth/test/args_test.js b/packages/auth/test/args_test.js new file mode 100644 index 00000000000..b35a727b71e --- /dev/null +++ b/packages/auth/test/args_test.js @@ -0,0 +1,596 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.argsTest'); + +goog.require('fireauth.Auth'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.args'); +goog.require('goog.Promise'); +goog.require('goog.dom'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.argsTest'); + + +var app = firebase.initializeApp({ + apiKey: 'myApiKey' +}); +var auth = new fireauth.Auth(app); + + +function testValidate_valid_noArgs() { + fireauth.args.validate('myFunc', [], []); +} + + +function testValidate_valid_oneArg() { + fireauth.args.validate('myFunc', [fireauth.args.string('foo')], ['bar']); +} + + +function testValidate_valid_multipleArgs() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.bool('emailVerified') + ]; + var args = ['foo@bar.com', false]; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_valid_optionalArgs_absent() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.bool('emailVerified', true) + ]; + var args = ['foo@bar.com']; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_valid_optionalArgs_present() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.bool('emailVerified', true) + ]; + var args = ['foo@bar.com', true]; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_invalid_type() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [fireauth.args.string('name')], [13]); + }); + assertEquals('auth/argument-error', error.code); + assertEquals('myFunc failed: First argument "name" must be a valid ' + + 'string.', error.message); +} + + +function testValidate_invalid_isSetter() { + var error = assertThrows(function() { + fireauth.args.validate( + 'name', [fireauth.args.string('name')], [13], true); + }); + assertEquals('auth/argument-error', error.code); + assertEquals('name failed: "name" must be a valid string.', error.message); +} + + +function testValidate_invalid_onlyOneValid() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password') + ]; + var args = ['foo@bar.com', 13]; + fireauth.args.validate('yourFunc', expectedArgs, args); + }); + assertEquals('yourFunc failed: Second argument "password" must be a valid ' + + 'string.', error.message); +} + + +function testValidate_invalid_noname() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [fireauth.args.string()], [13]); + }); + assertEquals('auth/argument-error', error.code); + assertEquals('myFunc failed: First argument must be a valid ' + + 'string.', error.message); +} + + +function testValidate_argNumber_tooFew() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password') + ]; + var args = ['foo@bar.com']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: Expected 2 arguments but got 1.', + error.message); +} + + +function testValidate_argNumber_tooMany() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password') + ]; + var args = ['foo@bar.com', 'hunter2', 'whatamidoinghere']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: Expected 2 arguments but got 3.', + error.message); +} + + +function testValidate_argNumber_oneValid() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email') + ]; + var args = ['foo@bar.com', 'whatamidoinghere']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: Expected 1 argument but got 2.', + error.message); +} + + +function testValidate_argNumber_validRange() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password', true) + ]; + var args = ['foo@bar.com', 'hunter2', 'whatamidoinghere']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: Expected 1-2 arguments but got 3.', + error.message); +} + + +function testValidate_authCredential_valid() { + var expectedArgs = [fireauth.args.authCredential()]; + var args = [fireauth.GoogleAuthProvider.credential('foo')]; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_authCredential_invalid() { + var error = assertThrows(function() { + var expectedArgs = [fireauth.args.authCredential()]; + var args = ['foo']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: First argument "credential" must be a valid ' + + 'credential.', error.message); +} + + +function testValidate_authCredential_null() { + var error = assertThrows(function() { + var expectedArgs = [fireauth.args.authCredential()]; + var args = [null]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: First argument "credential" must be a valid ' + + 'credential.', error.message); +} + + +function testValidate_authCredential_withType_valid() { + var expectedArgs = [fireauth.args.authCredential('phone')]; + var args = [fireauth.PhoneAuthProvider.credential('id', 'code')]; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_authCredential_withType_invalid() { + var expectedMessage = 'myFunc failed: First argument "phoneCredential" ' + + 'must be a valid phone credential.'; + var expectedArgs = [fireauth.args.authCredential('phone')]; + var error = assertThrows(function() { + var args = [fireauth.GoogleAuthProvider.credential('foo')]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals(expectedMessage, error.message); + + error = assertThrows(function() { + var args = [ + fireauth.EmailAuthProvider.credential('me@example.com', '123123') + ]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals(expectedMessage, error.message); + + error = assertThrows(function() { + var args = ['I am the wrong type']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals(expectedMessage, error.message); +} + + +function testValidate_authCredential_withTypeAndName_invalid() { + var expectedMessage = 'myFunc failed: First argument "myArgName" ' + + 'must be a valid google credential.'; + var expectedArgs = [fireauth.args.authCredential('google', 'myArgName')]; + var error = assertThrows(function() { + var args = [fireauth.GoogleAuthProvider.credential('foo')]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals(expectedMessage, error.message); +} + + +function testValidate_authProvider_valid() { + var expectedArgs = [fireauth.args.authProvider()]; + var args = [new fireauth.GoogleAuthProvider()]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Test with email and password Auth provider. + args = [new fireauth.EmailAuthProvider()]; + fireauth.args.validate('myFunc', expectedArgs, args); +} + + +function testValidate_authProvider_invalid() { + var error = assertThrows(function() { + var expectedArgs = [fireauth.args.authProvider()]; + var args = [fireauth.GoogleAuthProvider.credential('thisisntaprovider')]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('myFunc failed: First argument "authProvider" must be a valid ' + + 'Auth provider.', error.message); +} + + +function testValidate_or_valid_first() { + fireauth.args.validate('myFunc', [ + fireauth.args.or(fireauth.args.string(), fireauth.args.bool()) + ], ['foo']); +} + + +function testValidate_or_valid_second() { + fireauth.args.validate('myFunc', [ + fireauth.args.or(fireauth.args.string(), fireauth.args.bool()) + ], [true]); +} + + +function testValidate_or_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.or(fireauth.args.string(), fireauth.args.bool(), + 'strOrBool') + ], [13]); + }); + assertEquals('auth/argument-error', error.code); + assertEquals('myFunc failed: First argument "strOrBool" must be a valid ' + + 'string or a boolean.', error.message); +} + + +function testValidate_or_nested_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.or( + fireauth.args.or(fireauth.args.string(), fireauth.args.null()), + fireauth.args.bool(), + 'strOrBoolOrNull') + ], [true]); +} + + +function testValidate_or_nested_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.or( + fireauth.args.or(fireauth.args.string(), fireauth.args.bool()), + fireauth.args.null(), + 'strOrBoolOrNull') + ], [5]); + }); + assertEquals('myFunc failed: First argument "strOrBoolOrNull" must be a ' + + 'valid string or a boolean or null.', error.message); +} + + +function testValidate_or_nested_invalid_secondArgNested() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.or( + fireauth.args.string(), + fireauth.args.or(fireauth.args.bool(), fireauth.args.null()), + 'strOrBoolOrNull') + ], [5]); + }); + assertEquals('myFunc failed: First argument "strOrBoolOrNull" must be a ' + + 'valid string or a boolean or null.', error.message); +} + + +function testValidate_number_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.number('myNumber') + ], [-12.4]); +} + + +function testValidate_number_valid_optional() { + fireauth.args.validate('myFunc', [ + fireauth.args.number('myNumber', true) + ], []); +} + + +function testValidate_number_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.number('myNumber') + ], ['13']); + }); + assertEquals('myFunc failed: First argument "myNumber" must be a valid ' + + 'number.', error.message); +} + + +function testValidate_object_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.object('myObject') + ], [{'foo': 1}]); +} + + +function testValidate_object_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.object('myObject') + ], [13]); + }); + assertEquals('myFunc failed: First argument "myObject" must be a valid ' + + 'object.', error.message); +} + + +function testValidate_function_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.func('myCallback') + ], [function() {}]); +} + + +function testValidate_function_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.func('myCallback') + ], [{}]); + }); + assertEquals('myFunc failed: First argument "myCallback" must be a ' + + 'function.', error.message); +} + + +function testValidate_null_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.null() + ], [null]); +} + + +function testValidate_null_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.null('noArg') + ], ['something']); + }); + assertEquals('myFunc failed: First argument "noArg" must be null.', + error.message); +} + + +function testValidate_firebaseAuth_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.firebaseAuth() + ], [auth]); +} + + +function testValidate_firebaseAuth_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.firebaseAuth() + ], [{'some': 'thing'}]); + }); + assertEquals('myFunc failed: First argument "auth" must be an instance of ' + + 'Firebase Auth.', error.message); +} + + +function testValidate_element_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.element('myElement') + ], [goog.dom.createDom(goog.dom.TagName.DIV)]); +} + + +function testValidate_element_valid_optional() { + fireauth.args.validate('myFunc', [ + fireauth.args.element('myElement', true) + ], []); +} + + +function testValidate_element_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.element('myElement') + ], [13]); + }); + assertEquals('myFunc failed: First argument "myElement" must be an HTML ' + + 'element.', error.message); +} + + +function testValidate_firebaseApp_valid() { + fireauth.args.validate('myFunc', [ + fireauth.args.firebaseApp() + ], [app]); +} + + +function testValidate_firebaseApp_valid_optional() { + fireauth.args.validate('myFunc', [ + fireauth.args.firebaseApp(true) + ], []); +} + + +function testValidate_firebaseApp_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.firebaseApp() + ], [{'some': 'thing'}]); + }); + assertEquals('myFunc failed: First argument "app" must be an instance of ' + + 'Firebase App.', error.message); +} + + +function testValidate_appVerifier_valid() { + var appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve('assertion'); + } + }; + fireauth.args.validate('myFunc', [ + fireauth.args.applicationVerifier() + ], [appVerifier]); +} + + +function testValidate_appVerifier_invalid() { + var error = assertThrows(function() { + fireauth.args.validate('myFunc', [ + fireauth.args.applicationVerifier() + ], [{'some': 'thing'}]); + }); + assertEquals('myFunc failed: First argument "applicationVerifier" must be ' + + 'an implementation of firebase.auth.ApplicationVerifier.', error.message); +} + + +function testValidate_requiredArgAfterOptional() { + var error = assertThrows(function() { + var expectedArgs = [ + fireauth.args.string('email', true), + fireauth.args.string('password'), + fireauth.args.string('emailVerified', true) + ]; + var args = ['foo@bar.com', 'hunter2', false]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('auth/internal-error', error.code); +} + + +function testValidate_optionalUndefined_single() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password'), + fireauth.args.bool('emailVerified', true) + ]; + // Optional valid parameter explicitly passed. + var args = ['foo@bar.com', 'hunter2', false]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional valid parameter not passed. + args = ['foo@bar.com', 'hunter2']; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional valid parameter passed as undefined. + args = ['foo@bar.com', 'hunter2', undefined]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional parameter passed as invalid. + var error = assertThrows(function() { + args = ['foo@bar.com', 'hunter2', 'invalid']; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('auth/argument-error', error.code); +} + + +function testValidate_optionalUndefined_multiple() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password'), + fireauth.args.bool('emailVerified', true), + fireauth.args.bool('anonymous', true) + ]; + // Optional valid parameters explicitly passed. + var args = ['foo@bar.com', 'hunter2', false, true]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional valid parameter not passed. + args = ['foo@bar.com', 'hunter2']; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional valid parameters passed as undefined. + args = ['foo@bar.com', 'hunter2', undefined, undefined]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional valid parameter passed as undefined and then a valid parameter + // passed. + args = ['foo@bar.com', 'hunter2', undefined, false]; + fireauth.args.validate('myFunc', expectedArgs, args); + // Optional parameter passed as invalid. In this case null is invalid. + var error = assertThrows(function() { + args = ['foo@bar.com', 'hunter2', undefined, null]; + fireauth.args.validate('myFunc', expectedArgs, args); + }); + assertEquals('auth/argument-error', error.code); +} + + +function testValidate_argumentsObj() { + function fooFunc() { + var expectedArgs = [ + fireauth.args.string('email'), + fireauth.args.string('password') + ]; + fireauth.args.validate('myFunc', expectedArgs, arguments); + } + fooFunc('me@site.com', 'myPassword'); +} + + +function testValidate_argumentsObj_invalid() { + var error = assertThrows(function() { + function fooFunc() { + fireauth.args.validate('fooFunc', [fireauth.args.string('name')], + arguments); + } + fooFunc(13); + }); + assertEquals('fooFunc failed: First argument "name" must be a valid ' + + 'string.', error.message); +} diff --git a/packages/auth/test/auth_test.js b/packages/auth/test/auth_test.js new file mode 100644 index 00000000000..ab046791bff --- /dev/null +++ b/packages/auth/test/auth_test.js @@ -0,0 +1,8386 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for auth.js + */ + +goog.provide('fireauth.AuthTest'); + +goog.require('fireauth.ActionCodeInfo'); +goog.require('fireauth.ActionCodeSettings'); +goog.require('fireauth.Auth'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthEventManager'); +goog.require('fireauth.AuthUser'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.StsTokenManager'); +goog.require('fireauth.UserEventType'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.constants'); +/** @suppress {extraRequire} Needed for firebase.app().auth() */ +goog.require('fireauth.exports'); +goog.require('fireauth.idp'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.object'); +goog.require('fireauth.storage.PendingRedirectManager'); +goog.require('fireauth.storage.RedirectUserManager'); +goog.require('fireauth.storage.UserManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.Uri'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.AuthTest'); + + +var appId1 = 'appId1'; +var appId2 = 'appId2'; +var auth1 = null; +var auth2 = null; +var app1 = null; +var app2 = null; +var authUi1 = null; +var authUi2 = null; +var config1 = null; +var config2 = null; +var config3 = null; +var rpcHandler = null; +var token = null; +var accountInfo = { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/' + + 'default_profile_3_normal.png', + 'emailVerified': true +}; +// accountInfo in the format of a getAccountInfo response. +var getAccountInfoResponse = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/' + + 'default_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] +}; +// A sample JWT, along with its decoded contents. +var idTokenGmail = { + jwt: 'HEADER.ew0KICAiaXNzIjogIkdJVGtpdCIsDQogICJleHAiOiAxMzI2NDM5' + + 'MDQ0LA0KICAic3ViIjogIjY3OSIsDQogICJhdWQiOiAiMjA0MjQxNjMxNjg2IiwNCiAgImZl' + + 'ZGVyYXRlZF9pZCI6ICJodHRwczovL3d3dy5nb29nbGUuY29tL2FjY291bnRzLzEyMzQ1Njc4' + + 'OSIsDQogICJwcm92aWRlcl9pZCI6ICJnbWFpbC5jb20iLA0KICAiZW1haWwiOiAidGVzdDEy' + + 'MzQ1NkBnbWFpbC5jb20iDQp9.SIGNATURE', + data: { + exp: 1326439044, + sub: '679', + aud: '204241631686', + provider_id: 'gmail.com', + email: 'test123456@gmail.com', + federated_id: 'https://www.google.com/accounts/123456789' + } +}; +var expectedTokenResponse; +var expectedTokenResponse2; +var expectedTokenResponse3; +var expectedTokenResponseWithIdPData; +var expectedAdditionalUserInfo; +var expectedGoogleCredential; +var now = 1449534145526; + +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +var mockControl; +var ignoreArgument; + +var stubs = new goog.testing.PropertyReplacer(); +var angular = {}; +var currentUserStorageManager; +var redirectUserStorageManager; +var timeoutDelay = 30000; +var clock; + +var actionCodeSettings = { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': 'com.example.ios' + }, + 'android': { + 'packageName': 'com.example.android', + 'installApp': true, + 'minimumVersion': '12' + }, + 'handleCodeInApp': true +}; + + +function setUp() { + // Disable Auth event manager for testing unless needed. + fireauth.AuthEventManager.ENABLED = false; + // Assume origin is a valid one. + simulateWhitelistedOrigin(); + // Simulate tab can run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // In case the tests are run from an iframe. + stubs.replace( + fireauth.util, + 'isIframe', + function() { + return false; + }); + // Called on init state when a user is logged in. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + stubs.replace( + fireauth.util, + 'getCurrentUrl', + function() {return 'http://localhost';}); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Initialize App and Auth instances. + config1 = { + apiKey: 'apiKey1' + }; + config2 = { + apiKey: 'apiKey2' + }; + config3 = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + // Same as config3 but with a different authDomain. + config4 = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain2.firebaseapp.com', + 'appName': 'appId1' + }; + expectedTokenResponse = { + 'idToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600' + }; + expectedTokenResponse2 = { + 'idToken': 'ID_TOKEN2', + 'refreshToken': 'REFRESH_TOKEN2', + 'expiresIn': '3600' + }; + expectedTokenResponse3 = { + 'idToken': 'ID_TOKEN3', + 'refreshToken': 'REFRESH_TOKEN3', + 'expiresIn': '3600' + }; + expectedTokenResponseWithIdPData = { + 'idToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600', + // Credential returned. + 'providerId': 'google.com', + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken', + 'oauthExpireIn': 3600, + // Additional user info data. + 'rawUserInfo': '{"kind":"plus#person","displayName":"John Doe","na' + + 'me":{"givenName":"John","familyName":"Doe"}}' + }; + expectedAdditionalUserInfo = { + 'profile': { + 'kind': 'plus#person', + 'displayName': 'John Doe', + 'name': { + 'givenName': 'John', + 'familyName': 'Doe' + } + }, + 'providerId': 'google.com', + 'isNewUser': false + }; + expectedGoogleCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + rpcHandler = new fireauth.RpcHandler('apiKey1'); + token = new fireauth.StsTokenManager(rpcHandler); + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', now + 3600 * 1000); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +function tearDown() { + // Delete all Firebase apps created + var promises = []; + for (var i = 0; i < firebase.apps.length; i++) { + promises.push(firebase.apps[i].delete()); + } + if (promises.length) { + // Wait for all Firebase apps to be deleted. + asyncTestCase.waitForSignals(1); + goog.Promise.all(promises).then(function() { + // Dispose clock then. Disposing before will throw an error in IE 11. + goog.dispose(clock); + asyncTestCase.signal(); + }); + if (clock) { + // Some IE browsers like IE 11, native promise hangs if this is not called + // when clock is mocked. + // app.delete() will hang (it uses the native Promise). + clock.tick(); + } + } else if (clock) { + // No Firebase apps created, dispose clock immediately. + goog.dispose(clock); + } + + fireauth.AuthEventManager.manager_ = {}; + window.localStorage.clear(); + window.sessionStorage.clear(); + rpcHandler = null; + token = null; + if (auth1) { + auth1.delete(); + } + auth1 = null; + if (auth2) { + auth2.delete(); + } + auth2 = null; + app1 = null; + app2 = null; + config1 = null; + config2 = null; + config3 = null; + stubs.reset(); + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } + delete fireauth.authStorage.Manager.instance_; + currentUserStorageManager = null; + redirectUserStorageManager = null; +} + + +function testInitializeApp_noApiKey() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_API_KEY); + // Initialize app without an API key. + var appWithoutApiKey = firebase.initializeApp({}, 'appWithoutApiKey'); + // Initialization without API key should not throw an Auth error. + // Only on Auth explicit initialization will the error be visibly thrown. + try { + appWithoutApiKey.auth(); + fail('Auth initialization should fail due to missing api key.'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(expectedError, e); + } +} + + +/** + * Assert the Auth token listener is called once. + * @param {!fireauth.Auth} auth The Auth instance. + */ +function assertAuthTokenListenerCalledOnce(auth) { + var calls = 0; + asyncTestCase.waitForSignals(1); + auth.addAuthTokenListener(function(token) { + // Should only trigger once after init state. + calls++; + assertEquals(1, calls); + asyncTestCase.signal(); + }); +} + + +/** Simulates that local storage synchronizes across tabs. */ +function simulateLocalStorageSynchronized() { + stubs.replace( + fireauth.util, + 'isIe11', + function() {return false;}); + stubs.replace( + fireauth.util, + 'isEdge', + function() {return false;}); + // Simulate tab can run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); +} + + +/** + * Simulates current origin is whitelisted for popups and redirects. + */ +function simulateWhitelistedOrigin() { + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); +} + + +/** + * Asserts that two users are equivalent. Plain assertObjectEquals may not work + * as the expiration time may sometimes be off by a second. This takes that into + * account. + * @param {!fireauth.AuthUser} expected + * @param {!fireauth.AuthUser} actual + */ +function assertUserEquals(expected, actual) { + var expectedObj = expected.toPlainObject(); + var actualObj = actual.toPlainObject(); + var delta = expectedObj['stsTokenManager']['expirationTime'] - + actualObj['stsTokenManager']['expirationTime']; + // Confirm expiration times are close enough. + // The conversion back and forth from and to plain object, could result in + // some negligible difference. + assertTrue(Math.abs(delta) <= 1000); + // Overwrite expiration times. + expectedObj['stsTokenManager']['expirationTime'] = 0; + actualObj['stsTokenManager']['expirationTime'] = 0; + assertObjectEquals(expectedObj, actualObj); +} + + +function testAuth_noApiKey() { + try { + app1 = firebase.initializeApp({}, appId1); + app1.auth(); + fail('Should have thrown an error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY), e); + } +} + + +function testToJson_noUser() { + // Test toJSON with no user signed in. + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + var authPlainObject = { + 'apiKey': config1['apiKey'], + 'authDomain': config1['authDomain'], + 'appName': appId1, + 'currentUser': null + }; + assertObjectEquals(authPlainObject, auth1.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(auth1), JSON.stringify(auth1.toJSON())); +} + + +function testToJson_withUser() { + // Test toJSON with a user signed in. + stubs.reset(); + fireauth.AuthEventManager.ENABLED = false; + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Reload will be called on init. + return goog.Promise.resolve(); + }); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var storageKey = fireauth.util.createStorageKey(config1['apiKey'], appId1); + var authPlainObject = { + 'apiKey': config1['apiKey'], + 'authDomain': config1['authDomain'], + 'appName': appId1, + 'currentUser': user1.toJSON() + }; + currentUserStorageManager = new fireauth.storage.UserManager(storageKey); + // Simulate logged in user, save to storage, it will be picked up on init + // Auth state. + currentUserStorageManager.setCurrentUser(user1).then(function() { + auth1 = app1.auth(); + auth1.onIdTokenChanged(function(user) { + assertObjectEquals(authPlainObject, auth1.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(auth1), JSON.stringify(auth1.toJSON())); + asyncTestCase.signal(); + }); + }); +} + + +function testGetStorageKey() { + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertEquals(config1['apiKey'] + ':' + appId1, auth1.getStorageKey()); +} + + +function testAuth_initListeners_disabled() { + // Test with init listener disabled. + app1 = firebase.initializeApp(config1, appId1); + app2 = firebase.initializeApp(config2, appId2); + auth1 = app1.auth(); + auth2 = app2.auth(); + assertObjectEquals(app1, auth1.app_()); + assertObjectEquals(app2, auth2.app_()); +} + + +function testApp() { + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertObjectEquals(app1, auth1.app_()); +} + + +function testGetRpcHandler() { + app1 = firebase.initializeApp(config1, appId1); + app2 = firebase.initializeApp(config2, appId2); + auth1 = app1.auth(); + auth2 = app2.auth(); + assertNotNull(auth1.getRpcHandler()); + assertNotNull(auth2.getRpcHandler()); + assertEquals('apiKey1', auth1.getRpcHandler().getApiKey()); + assertEquals('apiKey2', auth2.getRpcHandler().getApiKey()); +} + + +function testAuth_rpcHandlerEndpoints() { + // Confirm expected endpoint config passed to underlying RPC handler. + var endpoint = fireauth.constants.Endpoint.STAGING; + var endpointConfig = { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + stubs.replace( + fireauth.constants, + 'getEndpointConfig', + function(opt_id) { + return endpointConfig; + }); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor(config1['apiKey'], endpointConfig, ignoreArgument) + .$returns(rpcHandler); + mockControl.$replayAll(); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); +} + + +function testCurrentUser() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var user = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + auth1.setCurrentUser_(user); + assertUserEquals(user, auth1['currentUser']); +} + + +function testLogFramework() { + // Helper function to get the client version for the test. + var getVersion = function(frameworks) { + return fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + frameworks); + }; + // Pipe through all framework IDs. + stubs.replace( + fireauth.util, + 'getFrameworkIds', + function(providedFrameworks) { + return providedFrameworks; + }); + // Listen to all client version update calls on RpcHandler. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateClientVersion', + goog.testing.recordFunction()); + var handler = goog.testing.recordFunction(); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + + // Listen to all frameworkChanged events dispatched by the Auth instance. + goog.events.listen( + auth1, + fireauth.constants.AuthEventType.FRAMEWORK_CHANGED, + handler); + assertArrayEquals([], auth1.getFramework()); + assertEquals(0, handler.getCallCount()); + assertEquals( + 0, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + + // Add version and confirm event triggered and client version updated in + // RpcHandler. + auth1.logFramework('angularfire'); + assertArrayEquals(['angularfire'], auth1.getFramework()); + assertEquals(1, handler.getCallCount()); + assertArrayEquals( + ['angularfire'], handler.getLastCall().getArgument(0).frameworks); + assertEquals( + 1, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion(['angularfire']), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); + + // Add another version and confirm event triggered and client version updated + // in RpcHandler. + auth1.logFramework('firebaseui'); + assertArrayEquals(['angularfire', 'firebaseui'], auth1.getFramework()); + assertEquals(2, handler.getCallCount()); + assertArrayEquals( + ['angularfire', 'firebaseui'], + handler.getLastCall().getArgument(0).frameworks); + assertEquals( + 2, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion(['angularfire', 'firebaseui']), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); +} + + +function testInternalLogFramework() { + // Record all calls to logFramework. + stubs.replace( + fireauth.Auth.prototype, + 'logFramework', + goog.testing.recordFunction()); + // Confirm INTERNAL.logFramework calls logFramework. + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertEquals(0, fireauth.Auth.prototype.logFramework.getCallCount()); + auth1.INTERNAL.logFramework('firebaseui'); + assertEquals(1, fireauth.Auth.prototype.logFramework.getCallCount()); + assertEquals( + 'firebaseui', + fireauth.Auth.prototype.logFramework.getLastCall().getArgument(0)); +} + + +function testUseDeviceLanguage() { + // Listen to all custom locale header calls on RpcHandler. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateCustomLocaleHeader', + goog.testing.recordFunction()); + var handler = goog.testing.recordFunction(); + stubs.replace(fireauth.util, 'getUserLanguage', function() { + return 'de'; + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + // Listen to all languageCodeChanged events dispatched by the Auth instance. + goog.events.listen( + auth1, + fireauth.constants.AuthEventType.LANGUAGE_CODE_CHANGED, + handler); + assertNull(auth1.languageCode); + assertEquals(0, handler.getCallCount()); + assertEquals( + 0, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + + // Update to English and confirm event triggered and custom locale updated in + // RpcHandler. + auth1.languageCode = 'en'; + assertEquals('en', auth1.languageCode); + assertEquals(1, handler.getCallCount()); + assertEquals('en', handler.getLastCall().getArgument(0).languageCode); + assertEquals( + 1, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'en', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + + // Update to device language and confirm event triggered and custom locale + // updated in RpcHandler. + auth1.useDeviceLanguage(); + assertEquals('de', auth1.languageCode); + assertEquals(2, handler.getCallCount()); + assertEquals('de', handler.getLastCall().getArgument(0).languageCode); + assertEquals( + 2, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'de', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + + // Developer should still be able to set the language code. + // Update to French and confirm event triggered and custom locale updated in + // RpcHandler. + auth1.languageCode = 'fr'; + assertEquals('fr', auth1.languageCode); + assertEquals(3, handler.getCallCount()); + assertEquals('fr', handler.getLastCall().getArgument(0).languageCode); + assertEquals( + 3, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'fr', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + + // Switch back to device language. + auth1.useDeviceLanguage(); + assertEquals('de', auth1.languageCode); + assertEquals(4, handler.getCallCount()); + assertEquals('de', handler.getLastCall().getArgument(0).languageCode); + assertEquals( + 4, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'de', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + + // Changing to the same language should not trigger any change. + auth1.languageCode = 'de'; + assertEquals(4, handler.getCallCount()); + assertEquals( + 4, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + + // Update to null and confirm event triggered and custom locale updated in + // RpcHandler. + auth1.languageCode = null; + assertNull(auth1.languageCode); + assertEquals(5, handler.getCallCount()); + assertNull(handler.getLastCall().getArgument(0).languageCode); + assertEquals( + 5, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertNull( + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); +} + + +/** + * Test Auth state listeners triggered on listener add even when initial state + * is null. However it will only first trigger when state is resolved. + */ +function testAddAuthTokenListener_initialNullState() { + var user = new fireauth.AuthUser(config1, expectedTokenResponse, accountInfo); + stubs.reset(); + // Simulate no state returned. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(null); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Suppress addStateChangeListener. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) {}); + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Generate new token on next call to trigger listeners. + return goog.Promise.resolve({ + accessToken: 'accessToken', + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + var listener1 = mockControl.createFunctionMock('listener1'); + var listener2 = mockControl.createFunctionMock('listener2'); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + listener1(null).$does(function() { + // Should be triggered after state is resolved. + assertEquals(0, marker); + // Increment marker. + marker++; + auth1.addAuthTokenListener(listener2); + }); + listener2(null).$does(function() { + // Should be triggered after listener2 is added. + assertEquals(1, marker); + // Increment marker. + marker++; + // Auth state change notification should also trigger immediately now. + // Simulate Auth event to trigger both listeners. + auth1.setCurrentUser_(user); + user.getIdToken(); + }); + listener1('accessToken').$does(function() { + // Marker should confirm listener triggered after notifyAuthListeners_. + assertEquals(2, marker); + asyncTestCase.signal(); + }); + listener2('accessToken').$does(function() { + // Marker should confirm listener triggered after notifyAuthListeners_. + assertEquals(2, marker); + asyncTestCase.signal(); + }); + mockControl.$replayAll(); + // Wait for last 2 expected listener calls. + asyncTestCase.waitForSignals(2); + // Keep track of what is triggering the events. + var marker = 0; + // Test listeners called when state first determined. + auth1.addAuthTokenListener(listener1); +} + + +/** + * Test Auth state listeners triggered on listener add even when initial state + * is not null (signed in user). However it will only first trigger when state + * is resolved. + */ +function testAddAuthTokenListener_initialValidState() { + var user = new fireauth.AuthUser(config1, expectedTokenResponse, accountInfo); + stubs.reset(); + // Simulate valid state returned. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(user); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Suppress addStateChangeListener. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) {}); + // Simulate available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Internally calls Auth user listeners. + return goog.Promise.resolve(); + }); + var currentAccessToken = 'ID_TOKEN'; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Generate new token on next call to trigger listeners. + return goog.Promise.resolve({ + accessToken: currentAccessToken, + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + // Keep track of what is triggering the events. + var marker = 0; + var listener1 = mockControl.createFunctionMock('listener1'); + var listener2 = mockControl.createFunctionMock('listener2'); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + listener1('ID_TOKEN').$does(function() { + // Should be triggered after state is resolved. + assertEquals(0, marker); + marker++; + // Now that state is determined, adding a new listener should resolve + // immediately. + auth1.addAuthTokenListener(listener2); + }); + listener2('ID_TOKEN').$does(function() { + // Should be triggered after listener2 is added. + assertEquals(1, marker); + // Increment marker. + marker++; + // Auth state change notification should also trigger immediately now. + // Simulate Auth event via getIdToken refresh to trigger both listeners. + currentAccessToken = 'newAccessToken'; + user.getIdToken(); + }); + listener1('newAccessToken').$does(function() { + // Marker should confirm listener triggered after notifyAuthListeners_. + assertEquals(2, marker); + asyncTestCase.signal(); + }); + listener2('newAccessToken').$does(function() { + // Marker should confirm listener triggered after notifyAuthListeners_. + assertEquals(2, marker); + asyncTestCase.signal(); + }); + mockControl.$replayAll(); + // Wait for last 2 expected listener calls. + asyncTestCase.waitForSignals(2); + // Test listeners called when state first determined. + auth1.addAuthTokenListener(listener1); +} + + +function testGetUid_userSignedIn() { + // Test getUid() on Auth instance and app instance with user previously + // signed in. + var accountInfo1 = {'uid': '1234'}; + asyncTestCase.waitForSignals(1); + // Get current user storage manager. + var storageKey = fireauth.util.createStorageKey(config1['apiKey'], appId1); + currentUserStorageManager = new fireauth.storage.UserManager(storageKey); + // Create test user instance. + var user = + new fireauth.AuthUser(config1, expectedTokenResponse, accountInfo1); + // Save test user. This will be loaded on Auth init. + currentUserStorageManager.setCurrentUser(user).then(function() { + // Initialize App and Auth. + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + // Initially getUid() should return null; + assertNull(auth1.getUid()); + assertNull(app1.INTERNAL.getUid()); + // Listen to Auth changes. + var unsubscribe = auth1.onIdTokenChanged(function(currentUser) { + // Unsubscribe of Auth state change listener. + unsubscribe(); + // Logged in test user should be detected. + // Confirm getUid() returns expected UID. + assertEquals(accountInfo1['uid'], auth1.getUid()); + assertEquals(accountInfo1['uid'], app1.INTERNAL.getUid()); + goog.Timer.promise(10).then(function() { + // Sign out. + return auth1.signOut(); + }).then(function() { + return goog.Timer.promise(10); + }).then(function() { + // getUid() should return null. + assertNull(auth1.getUid()); + assertNull(app1.INTERNAL.getUid()); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testGetUid_noUserSignedIn() { + // Test getUid() on Auth instance and App instance with no user previously + // signed in and new user signs in. + var accountInfo1 = {'uid': '1234'}; + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + return goog.Promise.resolve(user); + }); + // Simulate successful RpcHandler verifyPassword resolving with expected + // token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', function(email, password) { + // Return tokens for test user. + return goog.Promise.resolve(expectedTokenResponse); + }); + asyncTestCase.waitForSignals(1); + var user = + new fireauth.AuthUser(config1, expectedTokenResponse, accountInfo1); + // Initialize App and Auth. + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + // Listen to Auth changes. + var unsubscribe = auth1.onIdTokenChanged(function(currentUser) { + // Unsubscribe of Auth state change listener. + unsubscribe(); + // Initially getUid() should return null; + assertNull(auth1.getUid()); + assertNull(app1.INTERNAL.getUid()); + // Sign in with email and password. + auth1.signInWithEmailAndPassword('user@example.com', 'password') + .then(function(currentUser) { + // getUid() should return the test user UID. + assertEquals(accountInfo1['uid'], auth1.getUid()); + assertEquals(accountInfo1['uid'], app1.INTERNAL.getUid()); + asyncTestCase.signal(); + }); + }); +} + + +function testNotifyAuthListeners() { + // Simulate available token. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + return goog.Promise.resolve({ + accessToken: currentAccessToken, + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + // User reloaded. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + var currentAccessToken = 'accessToken1'; + var app1AuthTokenListener = goog.testing.recordFunction(); + var app2AuthTokenListener = goog.testing.recordFunction(); + var user = new fireauth.AuthUser( + config1, expectedTokenResponse, accountInfo); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + + asyncTestCase.waitForSignals(2); + // Set current user on auth1. + currentUserStorageManager = new fireauth.storage.UserManager( + config1['apiKey'] + ':' + appId1); + currentUserStorageManager.setCurrentUser(user).then(function() { + app1 = firebase.initializeApp(config1, appId1); + app1.INTERNAL.addAuthTokenListener(app1AuthTokenListener); + auth1 = app1.auth(); + app2 = firebase.initializeApp(config2, appId2); + app2.INTERNAL.addAuthTokenListener(app2AuthTokenListener); + auth2 = app2.auth(); + // Confirm all listeners reset. + assertEquals(0, listener1.getCallCount()); + assertEquals(0, listener2.getCallCount()); + assertEquals(0, listener3.getCallCount()); + assertEquals(0, app1AuthTokenListener.getCallCount()); + assertEquals(0, app2AuthTokenListener.getCallCount()); + auth1.addAuthTokenListener(listener1); + auth1.addAuthTokenListener(listener2); + auth2.addAuthTokenListener(listener3); + // Wait for state to be ready on auth1. + var unsubscribe = auth1.onIdTokenChanged(function(currentUser) { + unsubscribe(); + // Listener 1 and 2 triggered. + assertEquals(1, listener1.getCallCount()); + assertEquals(listener1.getCallCount(), listener2.getCallCount()); + // First trigger on init state. + assertEquals( + listener1.getCallCount(), + app1AuthTokenListener.getCallCount()); + assertEquals( + 'accessToken1', + app1AuthTokenListener.getLastCall().getArgument(0)); + // Remove first listener and reset. + auth1.removeAuthTokenListener(listener1); + listener1.reset(); + listener2.reset(); + app1AuthTokenListener.reset(); + // Force token change. + currentAccessToken = 'accessToken2'; + // Trigger getIdToken to force refresh and Auth token change. + auth1['currentUser'].getIdToken().then(function(token) { + assertEquals('accessToken2', token); + assertEquals(0, listener1.getCallCount()); + // Only listener2 triggered. + assertEquals(1, listener2.getCallCount()); + // Second trigger. + assertEquals( + 1, + app1AuthTokenListener.getCallCount()); + assertEquals( + 'accessToken2', + app1AuthTokenListener.getLastCall().getArgument(0)); + + // Remove remaining listeners and reset. + app1AuthTokenListener.reset(); + listener2.reset(); + auth1.removeAuthTokenListener(listener2); + app1.INTERNAL.removeAuthTokenListener(app1AuthTokenListener); + // Force token change. + currentAccessToken = 'accessToken3'; + auth1['currentUser'].getIdToken().then(function(token) { + assertEquals('accessToken3', token); + // No listeners triggered anymore since they are all unsubscribed. + assertEquals(0, app1AuthTokenListener.getCallCount()); + assertEquals(0, listener1.getCallCount()); + assertEquals(0, listener2.getCallCount()); + asyncTestCase.signal(); + }); + }); + }); + // Wait for state to be ready on auth2. + auth2.onIdTokenChanged(function(currentUser) { + // auth2 listener triggered on init with null state once. + assertEquals(1, listener3.getCallCount()); + assertEquals( + 1, + app2AuthTokenListener.getCallCount()); + assertNull( + app2AuthTokenListener.getLastCall().getArgument(0)); + assertNull(currentUser); + asyncTestCase.signal(); + }); + }); +} + + +/** + * Tests the notifications made to observers defined through the public API, + * when calling the notifyAuthListeners. + */ +function testNotifyAuthStateObservers() { + stubs.reset(); + // Simulate available token. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Generate new token on each call. + counter++; + return goog.Promise.resolve({ + accessToken: 'accessToken' + counter.toString(), + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + // Simulate user logged in. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(user); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Suppress addStateChangeListener. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) {}); + // Simulate available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + asyncTestCase.signal(); + // Token not refreshed, notifyAuthListeners_ should call regardless. + return goog.Promise.resolve(); + }); + var user = new fireauth.AuthUser(config3, expectedTokenResponse); + var observer1 = mockControl.createFunctionMock('observer1'); + var observer2 = mockControl.createFunctionMock('observer2'); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + observer1(user).$does(function(u) { + // Should be triggered after state is resolved. + assertEquals(0, marker++); + auth1.onIdTokenChanged(observer2); + }); + observer2(user).$does(function(u) { + // Should be triggered after listener2 is added. + assertEquals(1, marker++); + // Auth state change notification should also trigger immediately now. + // Simulate Auth event to trigger both listeners. + user.getIdToken(); + }); + observer1(user).$does(function(u) { + // Should be triggered after state is resolved. + assertEquals(2, marker++); + }); + observer2(user).$does(function(u) { + // Should be triggered after listener2 is added. + assertEquals(3, marker++); + // Auth state change notification should also trigger immediately now. + // Simulate Auth event to trigger listeners. + user.getIdToken(); + // Removes the first observer. + unsubscribe1(); + }); + observer2(user).$does(function(u) { + // Marker should confirm listener triggered after notifyAuthListeners_. + assertEquals(4, marker++); + asyncTestCase.signal(); + }); + mockControl.$replayAll(); + // Wait for the final observer call and the 2 intermediate internal callbacks. + asyncTestCase.waitForSignals(2); + // Keep track of what is triggering the events. + var marker = 0; + // Test listeners called when state first determined. + var unsubscribe1 = auth1.onIdTokenChanged(observer1); +} + + +/** + * Tests the notifications made to user state observers defined through the + * public API, when calling the notifyAuthListeners. + */ +function testAuth_onAuthStateChanged() { + stubs.reset(); + // Simulate available token. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Generate new token on each call. + counter++; + return goog.Promise.resolve({ + accessToken: 'accessToken' + counter.toString(), + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + var expectedTokenResponse2 = { + 'idToken': 'ID_TOKEN2', + 'refreshToken': 'REFRESH_TOKEN2', + 'expiresIn': '3600' + }; + // Simulate user initially logged in. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(user); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Suppress addStateChangeListener. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) {}); + // Simulate available token. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Token not refreshed, notifyAuthListeners_ should call regardless. + return goog.Promise.resolve(); + }); + // Simulate new user sign in. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function() { + return goog.Promise.resolve(user2); + }); + var user = new fireauth.AuthUser( + config3, expectedTokenResponse, {'uid': '1234'}); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse2, {'uid': '5678'}); + var observer1 = mockControl.createFunctionMock('observer1'); + var observer2 = mockControl.createFunctionMock('observer2'); + var observer3 = mockControl.createFunctionMock('observer3'); + var unsubscribe1, unsubscribe2, unsubscribe3; + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + observer1(user).$does(function(u) { + // Should be triggered after state is resolved. + assertEquals(0, marker++); + // This should not trigger the listeners. + user.getIdToken().then(function(token) { + assertEquals(1, marker++); + // Add new observer. + unsubscribe2 = auth1.onAuthStateChanged(observer2); + }); + }).$once(); + observer2(user).$does(function(u) { + // Should be triggered after observer2 is added. + assertEquals(2, marker++); + // This should not trigger the listeners. + user.getIdToken().then(function(token) { + assertEquals(3, marker++); + // Add new observer. + unsubscribe3 = auth1.onAuthStateChanged(observer3); + }); + }).$once(); + observer3(user).$does(function(u) { + // Should be triggered after observer3 is added. + assertEquals(4, marker++); + // Unsubscribe first observer. + unsubscribe1(); + // This should trigger the 2 other observers. + auth1.signOut(); + }).$once(); + observer2(null).$does(function(u) { + assertEquals(5, marker++); + }).$once(); + observer3(null).$does(function(u) { + assertEquals(6, marker++); + // Simulate new user sign in. Both observers should trigger with user2. + auth1.signInWithIdTokenResponse(expectedTokenResponse2); + }).$once(); + observer2(user2).$does(function(u) { + assertEquals(7, marker++); + }).$once(); + observer3(user2).$does(function(u) { + assertEquals(8, marker++); + // This should do nothing. + user2.getIdToken().then(function(token) { + assertEquals(9, marker++); + // Unsubscribe second observer. + unsubscribe2(); + // Sign out should trigger observer3. + auth1.signOut(); + }); + }).$once(); + observer3(null).$does(function(u) { + assertEquals(10, marker++); + // Unsubscribe observer3. + unsubscribe3(); + // Add observer1 again. + unsubscribe1 = auth1.onAuthStateChanged(observer1); + }).$once(); + observer1(null).$does(function(u) { + // Observer1 should trigger immediately. + assertEquals(11, marker++); + asyncTestCase.signal(); + }).$once(); + + mockControl.$replayAll(); + asyncTestCase.waitForSignals(1); + // Keep track of what is triggering the events. + var marker = 0; + // Test listeners called when state first determined. + unsubscribe1 = auth1.onAuthStateChanged(observer1); +} + + +function testFetchProvidersForEmail() { + var email = 'foo@bar.com'; + var expectedProviders = ['bar.com', 'google.com']; + + asyncTestCase.waitForSignals(1); + + // Simulate successful RpcHandler fetchProvidersForIdentifier. + stubs.replace( + fireauth.RpcHandler.prototype, + 'fetchProvidersForIdentifier', + function(data) { + assertObjectEquals(email, data); + return goog.Promise.resolve(expectedProviders); + }); + + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.fetchProvidersForEmail(email) + .then(function(providers) { + assertArrayEquals(expectedProviders, providers); + asyncTestCase.signal(); + }); + assertAuthTokenListenerCalledOnce(auth1); +} + + +function testAuth_pendingPromises() { + asyncTestCase.waitForSignals(1); + // Simulate available token. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(appOptions, stsTokenResponse, opt_redirectStorageManager) { + // There should be pending promises before the sign in call below. + assertTrue(auth1.hasPendingPromises()); + return user1; + }); + // verifyCustomToken should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyCustomToken', + function(customToken) { + assertEquals('custom', customToken); + return goog.Promise.resolve(expectedTokenResponse); + }); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.signInWithCustomToken('custom').then(function(user) { + // No longer pending. + assertFalse(auth1.hasPendingPromises()); + asyncTestCase.signal(); + }); +} + + +function testAuth_delete() { + asyncTestCase.waitForSignals(2); + // Simulate available token. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + // Return a promise that does not fulfill to ensure that delete is + // called. + return new goog.Promise(function(resolve, reject) {}); + }); + // verifyCustomToken should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyCustomToken', + function(customToken) { + return goog.Promise.resolve(expectedTokenResponse); + }); + // Listener to removeStateChangeListener. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'removeCurrentUserChangeListener', + goog.testing.recordFunction()); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.signInWithCustomToken('customToken') + .then(function(user) { + fail('This promise should not fulfill after auth.delete!'); + }).thenCatch(function(error) { + // Cancellation error should trigger. + assertEquals(fireauth.authenum.Error.MODULE_DESTROYED, error.message); + asyncTestCase.signal(); + }); + auth1.onIdTokenChanged(function(user) { + auth1.INTERNAL.delete(); + assertFalse(auth1.hasPendingPromises()); + /** @suppress {missingRequire} */ + // confirm removeStateChangeListener called. + assertEquals( + 1, + fireauth.storage.UserManager.prototype.removeCurrentUserChangeListener + .getCallCount()); + // Try to change the language. + auth1.languageCode = 'fr'; + // No change should occur. + assertNull(auth1.languageCode); + asyncTestCase.signal(); + }); +} + + +/** + * Tests sendPasswordResetEmail successful operation with no action code + * settings. + */ +function testSendPasswordResetEmail_success() { + var expectedEmail = 'user@example.com'; + // Simulate successful RpcHandler sendPasswordResetEmail. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendPasswordResetEmail', + function(email, actualActionCodeSettings) { + assertObjectEquals({}, actualActionCodeSettings); + assertEquals(expectedEmail, email); + return goog.Promise.resolve(expectedEmail); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendPasswordResetEmail(expectedEmail).then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests sendPasswordResetEmail failing operation due to backend error. + */ +function testSendPasswordResetEmail_error() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var expectedEmail = 'user@example.com'; + // Simulate unsuccessful RpcHandler sendPasswordResetEmail. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendPasswordResetEmail', + function(email, actualActionCodeSettings) { + assertObjectEquals({}, actualActionCodeSettings); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendPasswordResetEmail(expectedEmail).then(function() { + fail('sendPasswordResetEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests sendPasswordResetEmail successful operation with action code settings. + */ +function testSendPasswordResetEmail_actionCodeSettings_success() { + var expectedEmail = 'user@example.com'; + // Simulate successful RpcHandler sendPasswordResetEmail. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendPasswordResetEmail', + function(email, actualActionCodeSettings) { + assertObjectEquals( + new fireauth.ActionCodeSettings(actionCodeSettings).buildRequest(), + actualActionCodeSettings); + assertEquals(expectedEmail, email); + return goog.Promise.resolve(expectedEmail); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendPasswordResetEmail(expectedEmail, actionCodeSettings) + .then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests sendPasswordResetEmail invalid action code settings. + */ +function testSendPasswordResetEmail_actionCodeSettings_error() { + var settings = { + 'url': '' + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CONTINUE_URI); + var expectedEmail = 'user@example.com'; + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.sendPasswordResetEmail(expectedEmail, settings).then(function() { + fail('sendPasswordResetEmail should not resolve!'); + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyPasswordResetCode successful operation. + */ +function testVerifyPasswordResetCode_success() { + var expectedEmail = 'user@example.com'; + // Valid server response. + var serverResponse = { + kind: 'identitytoolkit#ResetPasswordResponse', + email: expectedEmail, + requestType: 'PASSWORD_RESET' + }; + var expectedCode = 'PASSWORD_RESET_CODE'; + // Simulate successful RpcHandler confirmPasswordReset with no new password. + stubs.replace( + fireauth.RpcHandler.prototype, + 'checkActionCode', + function(code) { + assertEquals(expectedCode, code); + return goog.Promise.resolve(serverResponse); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.verifyPasswordResetCode(expectedCode) + .then(function(email) { + assertEquals(expectedEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests confirmPasswordReset successful operation. + */ +function testConfirmPasswordReset_success() { + var expectedEmail = 'user@example.com'; + var expectedCode = 'PASSWORD_RESET_CODE'; + var expectedNewPassword = 'newPassword'; + // Simulate successful RpcHandler confirmPasswordReset. + stubs.replace( + fireauth.RpcHandler.prototype, + 'confirmPasswordReset', + function(code, newPassword) { + assertEquals(expectedCode, code); + assertEquals(expectedNewPassword, newPassword); + return goog.Promise.resolve(expectedEmail); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.confirmPasswordReset(expectedCode, expectedNewPassword) + .then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests confirmPasswordReset failing operation. + */ +function testConfirmPasswordReset_error() { + var expectedCode = 'PASSWORD_RESET_CODE'; + var expectedNewPassword = 'newPassword'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OOB_CODE); + // Simulate unsuccessful RpcHandler confirmPasswordReset. + stubs.replace( + fireauth.RpcHandler.prototype, + 'confirmPasswordReset', + function(code, newPassword) { + assertEquals(expectedCode, code); + assertEquals(expectedNewPassword, newPassword); + return goog.Promise.reject(expectedError); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.confirmPasswordReset(expectedCode, expectedNewPassword) + .then(function() { + fail('confirmPasswordReset should not resolve!'); + }) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + + +/** + * Tests checkActionCode successful operation. + */ +function testCheckActionCode_success() { + var expectedCode = 'PASSWORD_RESET_CODE'; + var expectedServerResponse = { + kind: 'identitytoolkit#ResetPasswordResponse', + requestType: 'PASSWORD_RESET', + email: 'user@example.com' + }; + var expectedActionCodeInfo = + new fireauth.ActionCodeInfo(expectedServerResponse); + // Simulate successful RpcHandler checkActionCode. + stubs.replace( + fireauth.RpcHandler.prototype, + 'checkActionCode', + function(code) { + assertEquals(expectedCode, code); + return goog.Promise.resolve(expectedServerResponse); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.checkActionCode(expectedCode) + .then(function(info) { + assertObjectEquals(expectedActionCodeInfo, info); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests checkActionCode failing operation. + */ +function testCheckActionCode_error() { + var expectedCode = 'PASSWORD_RESET_CODE'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OOB_CODE); + // Simulate unsuccessful RpcHandler checkActionCode. + stubs.replace( + fireauth.RpcHandler.prototype, + 'checkActionCode', + function(code) { + assertEquals(expectedCode, code); + return goog.Promise.reject(expectedError); + }); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.checkActionCode(expectedCode) + .then(function() { + fail('checkActionCode should not resolve!'); + }) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +/** + * Tests applyActionCode successful operation. + */ +function testApplyActionCode_success() { + var expectedEmail = 'user@example.com'; + var expectedCode = 'EMAIL_VERIFICATION_CODE'; + // Simulate successful RpcHandler applyActionCode. + stubs.replace( + fireauth.RpcHandler.prototype, + 'applyActionCode', + function(code) { + assertEquals(expectedCode, code); + return goog.Promise.resolve(expectedEmail); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + assertAuthTokenListenerCalledOnce(auth1); + auth1.applyActionCode(expectedCode).then(function() { + asyncTestCase.signal(); + }); +} + + +/** + * Tests applyActionCode failing operation. + */ +function testApplyActionCode_error() { + var expectedCode = 'EMAIL_VERIFICATION_CODE'; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OOB_CODE); + // Simulate unsuccessful RpcHandler applyActionCode. + stubs.replace( + fireauth.RpcHandler.prototype, + 'applyActionCode', + function(code) { + assertEquals(expectedCode, code); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config1, appId1); + auth1 = app1.auth(); + auth1.applyActionCode(expectedCode) + .then(function() { + fail('confirmPasswordReset should not resolve!'); + }, function(error) { + asyncTestCase.signal(); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +/** Replace the OAuth Sign in handler with a fake imitation for testing. */ +function fakeOAuthSignInHandler() { + // Helper function to replace instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) {}, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); +} + + +function testAuth_authEventManager() { + // Test Auth event manager. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + var expectedManager = { + 'subscribe': goog.testing.recordFunction(), + 'unsubscribe': goog.testing.recordFunction() + }; + // Return stub manager. + stubs.replace( + fireauth.AuthEventManager, + 'getManager', + function(authDomain, apiKey, appName) { + assertEquals('subdomain.firebaseapp.com', authDomain); + assertEquals('API_KEY', apiKey); + assertEquals(appId1, appName); + return expectedManager; + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Test manager initialized and Auth subscribed. + auth1.onIdTokenChanged(function(user) { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertEquals(expectedManager, manager); + assertEquals(0, expectedManager.unsubscribe.getCallCount()); + assertEquals(1, expectedManager.subscribe.getCallCount()); + assertEquals( + auth1, expectedManager.subscribe.getLastCall().getArgument(0)); + auth1.delete(); + // After destroy, Auth should be unsubscribed. + assertEquals(1, expectedManager.subscribe.getCallCount()); + assertEquals(1, expectedManager.unsubscribe.getCallCount()); + assertEquals( + auth1, expectedManager.unsubscribe.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); +} + + +function testAuth_signout() { + // Test successful sign out. + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Simulate new token on each call. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN' + counter.toString(), + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(3); + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save current user in storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + + var savedUser = null; + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set Auth language. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + var authChangeCalled = 0; + // This should trigger initially and then on sign out. + auth1.addAuthTokenListener(function(token) { + authChangeCalled++; + if (authChangeCalled == 1) { + // Save current user. + savedUser = auth1['currentUser']; + assertUserEquals(user1, auth1['currentUser']); + } else if (authChangeCalled == 2) { + assertNull(auth1['currentUser']); + } else { + fail('Auth state change should not trigger more than twice!'); + } + asyncTestCase.signal(); + }); + auth1.signOut().then(function() { + // User should be deleted from storage. + return currentUserStorageManager.getCurrentUser(); + }) + .then(function(user) { + // No user stored anymore. + assertNull(user); + // Current user should be nullified. + assertNull(auth1['currentUser']); + // Language should be set on signed out user. + assertEquals('fr', savedUser.getLanguageCode()); + // Framework updates should still propagate to signed out user. + assertArrayEquals(['firebaseui'], savedUser.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], savedUser.getFramework()); + // Language updates should still propagate to signed out user. + auth1.languageCode = 'de'; + assertEquals('de', savedUser.getLanguageCode()); + // Refresh token on logged out user. + savedUser.getIdToken().then(function(token) { + // Should not trigger listeners. + assertEquals('ID_TOKEN2', token); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_initState_signedInStatus() { + // Test init state with previously signed in user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // New loaded user should be reloaded before being set as current user. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + // Current user change listener should be added for future changes. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + asyncTestCase.signal(); + }); + // Return new token on each request. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN' + counter.toString(), + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + asyncTestCase.waitForSignals(4); + // Logged in user to be detected in initState. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save signed in user to storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Before init state current user is null. + assertNull(auth1['currentUser']); + // This should run when signed in user is detected. + var tokenChangeCalls = 0; + auth1.addAuthTokenListener(function(token) { + tokenChangeCalls++; + // Signed in user should be detected. + assertUserEquals(user1, auth1['currentUser']); + // Trigger token change on user, it should be detected by Auth listener + // above. Run only on first call. + if (tokenChangeCalls == 1) { + // Framework should propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + auth1['currentUser'].getIdToken().then(function(token) { + // Token listener above should have detected this and incremented + // tokenChangeCalls. + assertEquals(2, tokenChangeCalls); + asyncTestCase.signal(); + }); + } + }); + auth1.onIdTokenChanged(function(user) { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth and its current user should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + asyncTestCase.signal(); + }); + // User state change triggered with user. + auth1.onAuthStateChanged(function(user) { + assertNotNull(user); + asyncTestCase.signal(); + }); + }); +} + + +/** + * Test that external changes on a saved user will apply after loading from + * storage and reload on user is called. Confirm the updated user is saved. + */ +function testAuth_initState_reloadUpdate_previousSignedInUser() { + asyncTestCase.waitForSignals(2); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate reload introduced external changes to user. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + assertFalse(this['emailVerified']); + this.updateProperty('emailVerified', true); + this.updateProperty('displayName', 'New Name'); + // Internally calls Auth user listeners. + return goog.Promise.resolve(); + }); + // Create user with emailVerified set to false. + accountInfo['emailVerified'] = false; + accountInfo['displayName'] = 'Previous Name'; + var user = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Confirm created user has email not verified and old display name. + assertFalse(user['emailVerified']); + assertEquals('Previous Name', user['displayName']); + // Save test user. + currentUserStorageManager.setCurrentUser(user).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This will load user from storage and then call reload on it. + // Confirm that user emailVerified and display name are updated. + auth1.onIdTokenChanged(function(user) { + // These should have updated. + assertTrue(user['emailVerified']); + assertEquals('New Name', user['displayName']); + // Confirm user with verified email and update name is updated in storage + // too. + currentUserStorageManager.getCurrentUser(user).then(function(tempUser) { + assertTrue(tempUser['emailVerified']); + assertEquals('New Name', tempUser['displayName']); + asyncTestCase.signal(); + }); + }); + // User state change triggered with user. + auth1.onAuthStateChanged(function(user) { + assertNotNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_initState_signedInStatus_differentAuthDomain() { + // Test init state with previously signed in user using an authDomain + // different from the current Auth instance authDomain. Its authDomain should + // be overridden with current app authDomain. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // New loaded user should be reloaded before being set as current user. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Simulate the previously logged in user has a different authDomain. + var user1 = new fireauth.AuthUser( + config4, expectedTokenResponse, accountInfo); + // Auth will modify user to use its authDomain. + var modifiedUser1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save previous signed in user to storage with different authDomain. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + currentUserStorageManager.setCurrentUser(user1).then(function() { + // App initialized with other authDomain + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Before init state current user is null. + assertNull(auth1['currentUser']); + // This should run when signed in user is detected. + auth1.addAuthTokenListener(function(token) { + // Framework should propagate to modified signed in user. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Modified signed in user should be detected. + assertUserEquals(modifiedUser1, auth1['currentUser']); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_initState_signedInStatus_withRedirectUser() { + // Test init state with previously signed in user and a pending signed out + // redirect user. The current user in this case does not have the same + // redirect event ID as the redirected user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Assume origin is a valid one. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Return new token on each request. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN' + counter.toString(), + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + // New loaded user should be reloaded before being set as current user. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + goog.testing.recordFunction(function() { + return goog.Promise.resolve(); + })); + // Record enablePopupRedirect on user. + stubs.replace( + fireauth.AuthUser.prototype, + 'enablePopupRedirect', + goog.testing.recordFunction( + fireauth.AuthUser.prototype.enablePopupRedirect)); + asyncTestCase.waitForSignals(2); + // Logged in user to be detected in initState. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Only set the event ID on the redirected user. + user2.setRedirectEventId('12345678'); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Save previous redirect user. + redirectUserStorageManager = new fireauth.storage.RedirectUserManager( + config3['apiKey'] + ':' + appId1); + redirectUserStorageManager.setRedirectUser(user2).then(function() { + // Save signed in user to storage. + return currentUserStorageManager.setCurrentUser(user1); + }).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Before init state current user is null. + assertNull(auth1['currentUser']); + // This should run when signed in user is detected. + var tokenChangeCalls = 0; + auth1.addAuthTokenListener(function(token) { + tokenChangeCalls++; + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth, current user and the redirect user should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + assertEquals( + 2, fireauth.AuthUser.prototype.enablePopupRedirect.getCallCount()); + // Redirect user. + var redirectUser = + fireauth.AuthUser.prototype.enablePopupRedirect.getLastCall() + .getThis(); + // Confirm redirect user. + assertUserEquals(redirectUser, user2); + // Redirect user subscribed. + assertTrue(manager.isSubscribed(redirectUser)); + // Check redirect event ID on redirect user. + assertEquals('12345678', redirectUser.getRedirectEventId()); + // Signed in user should be detected. + assertUserEquals(user1, auth1['currentUser']); + // Trigger all listeners on user, they should be detected by Auth + // listeners above. Trigger only on first call. + if (tokenChangeCalls == 1) { + // Framework should propagate to redirectUser. + assertArrayEquals(['firebaseui'], redirectUser.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], redirectUser.getFramework()); + // Language code should propagate to redirectUser. + assertEquals('fr', redirectUser.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', redirectUser.getLanguageCode()); + auth1['currentUser'].getIdToken().then(function(token) { + // Should trigger a token change, confirming Auth still listening to + // events on this user. + assertEquals(2, tokenChangeCalls); + asyncTestCase.signal(); + }); + // No need to run getRedirectUser below more than once. + return; + } + // Redirect user should not longer be saved in storage. + redirectUserStorageManager.getRedirectUser().then(function(user) { + assertNull(user); + // As no redirect event ID is set on current user, reload should be + // called on the current user loaded from storage. + assertEquals(1, fireauth.AuthUser.prototype.reload.getCallCount()); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_initState_signedInStatus_withRedirectUser_sameEventId() { + // Test init state with a signed in user that is attempting a redirect + // operation ID. The current user and redirect user will have the same + // redirect event ID. + // This test is needed to confirm that user is not reloaded after loading from + // storage. This is important for reauthenticateWithRedirect as this operation + // could be called to recover before token expiration is detected. + // user.reload() could clear the user from storage, ending up with the user no + // longer being current. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Assume origin is a valid one. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Return new token on each request. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN' + counter.toString(), + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + // New loaded user should not be reloaded before being set as current user. + // This is needed to allow the redirect operation to complete. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + goog.testing.recordFunction(function() { + return goog.Promise.resolve(); + })); + // Record enablePopupRedirect on user. + stubs.replace( + fireauth.AuthUser.prototype, + 'enablePopupRedirect', + goog.testing.recordFunction( + fireauth.AuthUser.prototype.enablePopupRedirect)); + asyncTestCase.waitForSignals(2); + // Logged in user to be detected in initState. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Set redirect event ID on user1 to match user2. + user1.setRedirectEventId('12345678'); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Set redirect event ID on the redirect user. + user2.setRedirectEventId('12345678'); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Save previous redirect user. + redirectUserStorageManager = new fireauth.storage.RedirectUserManager( + config3['apiKey'] + ':' + appId1); + redirectUserStorageManager.setRedirectUser(user2).then(function() { + // Save signed in user to storage. + return currentUserStorageManager.setCurrentUser(user1); + }).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Before init state current user is null. + assertNull(auth1['currentUser']); + // This should run when signed in user is detected. + var tokenChangeCalls = 0; + auth1.addAuthTokenListener(function(token) { + tokenChangeCalls++; + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth, current user and the redirect user should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + assertEquals( + 2, fireauth.AuthUser.prototype.enablePopupRedirect.getCallCount()); + // Redirect user. + var redirectUser = + fireauth.AuthUser.prototype.enablePopupRedirect.getLastCall() + .getThis(); + // Confirm redirect user. + assertUserEquals(redirectUser, user2); + // Redirect user subscribed. + assertTrue(manager.isSubscribed(redirectUser)); + // Check redirect event ID on redirect user. + assertEquals('12345678', redirectUser.getRedirectEventId()); + // Signed in user should be detected. + assertUserEquals(user1, auth1['currentUser']); + // Trigger all listeners on user, they should be detected by auth + // listeners above. Trigger only on first call. + if (tokenChangeCalls == 1) { + // Framework should propagate to redirectUser. + assertArrayEquals(['firebaseui'], redirectUser.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], redirectUser.getFramework()); + // Language code should propagate to redirectUser. + assertEquals('fr', redirectUser.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', redirectUser.getLanguageCode()); + auth1['currentUser'].getIdToken().then(function(token) { + // Should trigger a token change, confirming Auth still listening to + // events on this user. + assertEquals(2, tokenChangeCalls); + asyncTestCase.signal(); + }); + // No need to run getRedirectUser below more than once. + return; + } + // Redirect user should not longer be saved in storage. + redirectUserStorageManager.getRedirectUser().then(function(user) { + assertNull(user); + // As redirect event ID is set on the current user to be equal to the + // redirect user event ID, reload should not be called on the current + // user loaded from storage. + assertEquals(0, fireauth.AuthUser.prototype.reload.getCallCount()); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_initState_signedInStatus_deletedUser() { + // Test init state with previously signed in user that was deleted externally. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + // The typical error returned by reload. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // New loaded user should be reloaded. In this case throw an error. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + // Current user change listener should be added. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(6); + // The current stored user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var storageKey = config3['apiKey'] + ':' + appId1; + // Save signed in user. + currentUserStorageManager = new fireauth.storage.UserManager(storageKey); + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Initially current user is null. + assertNull(auth1['currentUser']); + // Listener triggered on state resolution. + auth1.addAuthTokenListener(function(token) { + // Stored user should be retrieved but then on reload error cleared. + assertNull(auth1['currentUser']); + asyncTestCase.signal(); + currentUserStorageManager.getCurrentUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); + auth1.onIdTokenChanged(function(user) { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth only should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + asyncTestCase.signal(); + }); + // User state change triggered with no user. + auth1.onAuthStateChanged(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_initState_signedInStatus_offline() { + // Test init state with previously signed in user in offline mode. The user + // should not be deleted and Auth state listener should trigger. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + // Network error typical in offline mode. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // New loaded user should be reloaded. In this case throw an error. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(4); + // Current user change listener should be added. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + asyncTestCase.signal(); + }); + // The current stored user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var storageKey = config3['apiKey'] + ':' + appId1; + // Save signed in user. + currentUserStorageManager = new fireauth.storage.UserManager(storageKey); + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Initially current user is null. + assertNull(auth1['currentUser']); + // Auth state change listener should trigger too. + auth1.onIdTokenChanged(function(user) { + assertUserEquals(user, auth1['currentUser']); + asyncTestCase.signal(); + }); + // User state change triggered with user. + auth1.onAuthStateChanged(function(user) { + assertNotNull(user); + asyncTestCase.signal(); + }); + // Listener triggered on state resolution. + auth1.onAuthStateChanged(function(token) { + // Stored user should be retrieved and kept. + assertUserEquals(user1, auth1['currentUser']); + // Framework should propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user, auth1['currentUser']); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_initState_signedOutStatus() { + // Test init state with no user signed in. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + // Return a new token on each request. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN' + counter.toString(), + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Current user change listener should be added. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(4); + // Save signed in user. + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This is not realistic but to show that current user is set to null, set it. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + auth1.setCurrentUser_(user1); + // On state resolution this would trigger. + auth1.addAuthTokenListener(function(token) { + // Current user is null. + assertNull(auth1['currentUser']); + // Confirm no listeners on old user. + user1.getIdToken().then(function(token) { + // Should not trigger listeners. + asyncTestCase.signal(); + }); + }); + auth1.onIdTokenChanged(function(user) { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth only should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + asyncTestCase.signal(); + }); + // User state change triggered with no user. + auth1.onAuthStateChanged(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); +} + + +function testAuth_syncAuthChanges_sameUser() { + // Test syncAuthChanges with the same user detected. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Save sync listener. + var syncListener = null; + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + syncListener = listener; + }); + // Get token would get triggered to refresh token on detected user. + // In this case simulate the token changed on the user. + var counter = 0; + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + // Return new token on each call to force Auth token listeners. + counter++; + return goog.Promise.resolve({ + 'accessToken': 'NEW_ACCESS_TOKEN' + counter.toString(), + 'refreshToken': 'NEW_REFRESH_TOKEN', + 'expirationTime': goog.now() + 3600 * 1000 + }); + }); + var accountInfo1 = { + 'uid': '123456', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + var accountInfo2 = { + 'uid': '123456', + 'email': 'user2@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + asyncTestCase.waitForSignals(3); + var userChanges = 0; + // The originally logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + // The same user with external changes. + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo2); + // Save signed in user. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Initially user1 logged in. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + var unsubscribe = auth1.onIdTokenChanged(function(currentUser) { + // Simulate user2 logged in in another tab. + currentUserStorageManager.setCurrentUser(user2).then(function() { + // Simulate syncing to external changes. + syncListener().then(function() { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth and current user should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + // Same user reference remains. + assertEquals(currentUser, auth1['currentUser']); + // User1 should be updated with user2 data. + assertEquals('user2@example.com', auth1['currentUser']['email']); + asyncTestCase.signal(); + }); + }); + var firstCall = true; + // As listeners are still attached and token change triggered, Auth state + // listener should trigger. + auth1.addAuthTokenListener(function(token) { + // Ignore first call when user1 is logged in. + if (firstCall) { + // User1 logged in at this point. + assertUserEquals(user1, auth1['currentUser']); + // Framework set on first user. + assertArrayEquals( + ['firebaseui'], auth1['currentUser'].getFramework()); + // New framework update will be caught by user2. + auth1.logFramework('angularfire'); + // Language should be set on first user. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + // New language code update will be caught by user2. + auth1.languageCode = 'de'; + firstCall = false; + return; + } + // Current user updated with user2 data on next call. + assertUserEquals(user2, auth1['currentUser']); + // Updated framework on new current user. + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Updated language set on new current user. + assertEquals('de', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'ru'; + assertEquals('ru', auth1['currentUser'].getLanguageCode()); + asyncTestCase.signal(); + }); + unsubscribe(); + }); + // Should be called once with the initial user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNotNull(currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_syncAuthChanges_newSignIn() { + // Test syncAuthChanges with a new signed in user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Save sync listener. + var syncListener = null; + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + syncListener = listener; + }); + var accountInfo1 = { + 'uid': '1234', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + var accountInfo2 = { + 'uid': '5678', + 'email': 'user2@example.com', + 'displayName': 'Jane Doe', + 'emailVerified': false + }; + asyncTestCase.waitForSignals(4); + var userChanges = 0; + // The originally logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + // The external new user to be detected with different UID. + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo2); + // Save new signed in user. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Initially user1 signed in. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Wait for state to be ready. + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // User 1 should be initially logged in. + assertUserEquals(user1, auth1['currentUser']); + // Simulate syncing to external changes where user 2 is logged in. + currentUserStorageManager.setCurrentUser(user2).then(function() { + syncListener().then(function() { + // User 2 should be current user now. + assertUserEquals(user2, auth1['currentUser']); + // Confirm expected UID and email. + assertEquals('5678', auth1['currentUser']['uid']); + assertEquals('user2@example.com', user2['email']); + // User1 reference still exists. + assertEquals('REFRESH_TOKEN', user1['refreshToken']); + asyncTestCase.signal(); + }); + }); + var firstCall = true; + // notifyAuthListeners_ would be triggered here. + auth1.addAuthTokenListener(function(token) { + // Ignore first call for initial user. + if (firstCall) { + // Expected framework set on first user. + assertArrayEquals( + ['firebaseui'], auth1['currentUser'].getFramework()); + // Expected language set on first user. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + firstCall = false; + return; + } + // User 2 is the signed in user now on next call. + assertUserEquals(user2, auth1['currentUser']); + // Expected framework set on second user. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Expected language set on new current user. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth and current user should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + asyncTestCase.signal(); + }); + unsubscribe(); + }); + // Should be called twice with the different users. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + if (userChanges == 1) { + assertEquals(user1['uid'], currentUser['uid']); + } else { + assertEquals(user2['uid'], currentUser['uid']); + } + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_syncAuthChanges_newSignIn_differentAuthDomain() { + // Test syncAuthChanges with a new signed in user that has a different + // authDomain than the current app authDomain. The synced user should have its + // authDomain field overridden. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + asyncTestCase.waitForSignals(1); + // The originally logged in user. + // Simulate the new logged in user has a different authDomain. + var user1 = new fireauth.AuthUser( + config4, expectedTokenResponse, accountInfo); + // Auth will modify user to use its authDomain. + var modifiedUser1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Call sign out to wait for init state to resolve and to ensure no user is + // logged in. + auth1.signOut().then(function(result) { + // Save new signed in user with different authDomain to trigger sync later. + currentUserStorageManager.setCurrentUser(user1).then(function() { + // Simulate storage event using a different authDomain. + var storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authUser:' + auth1.getStorageKey(); + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(user1.toPlainObject()); + // Add new listener. + auth1.addAuthTokenListener(function(token) { + // Ignore initial state trigger. + if (!token) { + return; + } + // Modified user with app authDomain should be current user now. + assertUserEquals(modifiedUser1, auth1['currentUser']); + // Framework should propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + asyncTestCase.signal(); + }); + // This should force localstorage sync. + goog.testing.events.fireBrowserEvent(storageEvent); + }); + }); +} + + +function testAuth_syncAuthChanges_newSignOut() { + // Test syncAuthChanges with a sign out event detected. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Save sync listener. + var syncListener = null; + stubs.replace( + fireauth.storage.UserManager.prototype, + 'addCurrentUserChangeListener', + function(listener) { + syncListener = listener; + }); + var accountInfo1 = { + 'uid': '1234', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + asyncTestCase.waitForSignals(4); + // The originally logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + var savedUser; + var userChanges = 0; + // Initially user1 signed in. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Wait for state to be ready. + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // User 1 should be initially logged in. + assertUserEquals(user1, auth1['currentUser']); + // Simulate syncing to external changes where user 1 is logged out. + currentUserStorageManager.removeCurrentUser().then(function() { + // Simulate syncing to external changes. + syncListener().then(function() { + // Current user should be null now. + assertNull(auth1['currentUser']); + // User1 reference still exists. + assertEquals('REFRESH_TOKEN', user1['refreshToken']); + asyncTestCase.signal(); + }); + }); + var firstCall = true; + // notifyAuthListeners_ would be triggered here. + auth1.addAuthTokenListener(function(token) { + // Ignore first call for initial user. + if (firstCall) { + // Save current user. + savedUser = auth1['currentUser']; + firstCall = false; + return; + } + // Null current user on next call. + assertNull(auth1['currentUser']); + // Framework updates should still propagate to saved signed out user. + assertArrayEquals(['firebaseui'], savedUser.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], savedUser.getFramework()); + // Language code should propagate to saved signed out user. + assertEquals('fr', savedUser.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', savedUser.getLanguageCode()); + // Listeners no longer attached to old user. + assertEquals(0, user1.stateChangeListeners_.length); + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth and user1 should be subscribed on init even though user1 is no + // longer current. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(savedUser)); + asyncTestCase.signal(); + }); + unsubscribe(); + }); + // Should be called twice: first with the expected user and then null, the + // second time. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + if (userChanges == 1) { + assertEquals(user1['uid'], currentUser['uid']); + } else { + assertNull(currentUser); + } + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_signInWithIdTokenResponse_newUser() { + // Test signInWithIdTokenResponse returning a new user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Initialize from ID token response should be called and resolved with the + // new signed in user. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse, redirectStorageManager, frameworks) { + // Expected frameworks passed on user initialization. + assertArrayEquals(['firebaseui'], frameworks); + assertObjectEquals(config3, options); + assertObjectEquals(expectedTokenResponse, idTokenResponse); + asyncTestCase.signal(); + return goog.Promise.resolve(user1); + }); + asyncTestCase.waitForSignals(5); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework and test signInWithIdTokenResponse initializes user with the + // correct list of frameworks. + auth1.logFramework('firebaseui'); + var userChanges = 0; + currentUserStorageManager = new fireauth.storage.UserManager( + auth1.getStorageKey()); + // The newly signed in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Run on init only. + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // notifyAuthListeners_ would be triggered here. + auth1.addAuthTokenListener(function(token) { + // Current user set to user1. + assertEquals(user1, auth1['currentUser']); + // Listeners should be attached to new current user. + assertEquals(1, auth1['currentUser'].stateChangeListeners_.length); + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // Auth and user1 should be subscribed. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + asyncTestCase.signal(); + // Confirm new user saved in storage. + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user, user1); + asyncTestCase.signal(); + }); + }); + unsubscribe(); + }); + // User not logged in yet. Run sign in with ID token response. + auth1.signInWithIdTokenResponse(expectedTokenResponse).then(function() { + // Current user should be set to user1. + assertEquals(user1, auth1['currentUser']); + // Framework updates should still propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + asyncTestCase.signal(); + }); + // Should be called with the expected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertEquals(user1['uid'], currentUser['uid']); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithIdTokenResponse_sameUser() { + // Test signInWithIdTokenResponse returning the same user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Initialize from ID token response should be called and resolved with the + // new signed in user. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse, redirectStorageManager, frameworks) { + // Expected frameworks passed on user initialization. + assertArrayEquals(['firebaseui'], frameworks); + assertObjectEquals(config3, options); + assertObjectEquals(expectedTokenResponse, idTokenResponse); + asyncTestCase.signal(); + // Return second user which is the same user but with difference account + // info. + return goog.Promise.resolve(user2); + }); + // Simulate user1 initially logged in. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(user1); + }); + // Stub reload. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + var accountInfo1 = { + 'uid': '1234', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + var accountInfo2 = { + 'uid': '1234', + 'email': 'user2@example.com', + 'displayName': 'Jane Doe', + 'emailVerified': false + }; + asyncTestCase.waitForSignals(5); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework and test signInWithIdTokenResponse initializes user with the + // correct list of frameworks. + auth1.logFramework('firebaseui'); + currentUserStorageManager = new fireauth.storage.UserManager( + auth1.getStorageKey()); + // The existing user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + // The newly signed in user with the same UID as user1. + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo2); + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // Call signInWithIdTokenResponse. This should resolve with same user + // updated. + auth1.signInWithIdTokenResponse( + expectedTokenResponse).then(function() { + // Framework updates should still propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + // Same reference. + assertEquals(user1, auth1['currentUser']); + // Same properties as user2. + assertUserEquals(user2, auth1['currentUser']); + assertEquals('user2@example.com', auth1['currentUser']['email']); + assertEquals('Jane Doe', auth1['currentUser']['displayName']); + asyncTestCase.signal(); + }); + unsubscribe(); + }); + var firstCall = true; + // notifyAuthListeners_ would be triggered here. + auth1.addAuthTokenListener(function(token) { + // Simulate user1 already signed in. + if (firstCall) { + // User1 logged in at this point. + assertEquals(user1, auth1['currentUser']); + firstCall = false; + return; + } + // User1 still set as current user on next call. + assertEquals(user1, auth1['currentUser']); + // Auth and user1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertFalse(manager.isSubscribed(user2)); + assertTrue(manager.isSubscribed(user1)); + asyncTestCase.signal(); + // Confirm current user saved in storage. + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user, auth1['currentUser']); + asyncTestCase.signal(); + }); + }); + var userChanges = 0; + // Should be called with the expected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertEquals(user1['uid'], currentUser['uid']); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithIdTokenResponse_newUserDifferentFromCurrent() { + // Test signInWithIdTokenResponse returning a new user while a previous user + // existed. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Initialize from ID token response should be called and resolved with the + // new signed in user that is different from the current user. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse, redirectStorageManager, frameworks) { + // Expected frameworks passed on user initialization. + assertArrayEquals(['firebaseui'], frameworks); + assertObjectEquals(config3, options); + assertObjectEquals(expectedTokenResponse, idTokenResponse); + asyncTestCase.signal(); + // Return second user which has a different UID. + return goog.Promise.resolve(user2); + }); + // Simulate user1 initially logged in. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + return goog.Promise.resolve(user1); + }); + // Stub reload. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // First user account info. + var accountInfo1 = { + 'uid': '1234', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + // Second user has different UID. + var accountInfo2 = { + 'uid': '5678', + 'email': 'user2@example.com', + 'displayName': 'Jane Doe', + 'emailVerified': false + }; + // Initialize users. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo2); + asyncTestCase.waitForSignals(6); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework and test signInWithIdTokenResponse initializes user with the + // correct list of frameworks. + auth1.logFramework('firebaseui'); + currentUserStorageManager = new fireauth.storage.UserManager( + auth1.getStorageKey()); + // signInWithIdTokenResponse should resolve with user2 as currentUser. + var unsubscribe = auth1.onIdTokenChanged(function(user) { + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + // At this stage auth1 and user1 are only subscribed. This will change after + // new sign in. + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + assertFalse(manager.isSubscribed(user2)); + auth1.signInWithIdTokenResponse( + expectedTokenResponse).then(function() { + // Framework updates should still propagate to new currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], auth1['currentUser'].getFramework()); + // Language code should propagate to new currentUser. + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', auth1['currentUser'].getLanguageCode()); + // User2 is now currentuser. + assertEquals(user2, auth1['currentUser']); + // Same properties as user2. + assertUserEquals(user2, auth1['currentUser']); + assertEquals('5678', auth1['currentUser']['uid']); + assertEquals('user2@example.com', auth1['currentUser']['email']); + assertEquals('Jane Doe', auth1['currentUser']['displayName']); + asyncTestCase.signal(); + }); + unsubscribe(); + }); + var firstCall = true; + // notifyAuthListeners_ would be triggered here. + auth1.addAuthTokenListener(function(token) { + if (firstCall) { + // User1 logged in at this point. + assertEquals(user1, auth1['currentUser']); + firstCall = false; + return; + } + // User2 signed in on next call. + assertEquals(user2, auth1['currentUser']); + // Auth, user1 and user2 should be subscribed even though user1 is no longer + // current. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + assertTrue(manager.isSubscribed(user2)); + asyncTestCase.signal(); + // Confirm new current user saved in storage. Reset getCurrentUser stub + // first. + stubs.reset(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user, auth1['currentUser']); + asyncTestCase.signal(); + }); + }); + var userChanges = 0; + // Should be called twice with the expected user each time. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + if (userChanges == 1) { + assertEquals(user1['uid'], currentUser['uid']); + } else { + assertEquals(user2['uid'], currentUser['uid']); + } + asyncTestCase.signal(); + }); +} + + +function testAuth_getIdToken_signedInUser() { + // Tests getIdToken with a signed in user. + fireauth.AuthEventManager.ENABLED = true; + var expectedToken = 'NEW_ACCESS_TOKEN'; + // Simulate new access token return on a force refresh request to trigger Auth + // state listener. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_refresh) { + // Manual call, force refresh. + if (opt_refresh) { + return goog.Promise.resolve({ + 'accessToken': expectedToken, + 'refreshToken': 'NEW_REFRESH_TOKEN', + 'expirationTime': goog.now() + 3600 * 1000 + }); + } + // Return cached one when called within syncAuthChange. + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': goog.now() + 3600 * 1000 + }); + }); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Initialize user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + asyncTestCase.waitForSignals(5); + var tokenChangeCalls = 0; + // Set user1 as a signed in user. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + auth1.addAuthTokenListener(function(token) { + tokenChangeCalls++; + if (tokenChangeCalls == 1) { + // First time called with original token. + assertEquals(expectedTokenResponse['idToken'], token); + } else if (tokenChangeCalls == 2) { + // Second time called with new token. + assertEquals(expectedToken, token); + } else { + fail('Token listener should not be called more than twice.'); + } + // User should be signed in. + assertUserEquals(user1, auth1['currentUser']); + asyncTestCase.signal(); + // Confirm token changes triggered user related changes to be saved. + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user1, auth1['currentUser']); + asyncTestCase.signal(); + }); + }); + // This should return the new STS token. + auth1.getIdTokenInternal(true).then(function(stsResponse) { + assertEquals(expectedToken, stsResponse['accessToken']); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_getIdToken_signedOutUser() { + // Tests getIdToken with a signed out user. + fireauth.AuthEventManager.ENABLED = true; + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) {}, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should be triggered only on init state. + assertAuthTokenListenerCalledOnce(auth1); + // Since no user is signed in, token should be null. + auth1.getIdTokenInternal(true).then(function(token) { + assertNull(token); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithCustomToken_success() { + // Tests successful signInWithCustomToken. + fireauth.AuthEventManager.ENABLED = true; + var expectedCustomToken = 'CUSTOM_TOKEN'; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithCustomToken should lead to an Auth user being initialized with + // the returned STS token response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponse, tokenResponse); + // Simulate user sign in completed and returned. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return goog.Promise.resolve(user1); + }); + // verifyCustomToken should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyCustomToken', + function(customToken) { + assertEquals(expectedCustomToken, customToken); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponse); + }); + asyncTestCase.waitForSignals(4); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Set to true for testing to make sure this is changed during processing. + user1.updateProperty('isAnonymous', true); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + currentUserStorageManager = new fireauth.storage.UserManager( + auth1.getStorageKey()); + // Sign in with custom token. + auth1.signInWithCustomToken(expectedCustomToken).then(function(user) { + // Anonymous status should be set to false. + assertFalse(user['isAnonymous']); + // Returned user should match expected one. + assertEquals(user1, user); + // Confirm anonymous state saved. + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user1, auth1['currentUser']); + assertFalse(user['isAnonymous']); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithCustomToken_error() { + // Tests unsuccessful signInWithCustomToken. + fireauth.AuthEventManager.ENABLED = true; + // Expected rpc error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedCustomToken = 'CUSTOM_TOKEN'; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Error should not lead to user creation. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called!'); + }); + // verifyCustomToken should be called with expected parameters and throws the + // expected error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyCustomToken', + function(customToken) { + assertEquals(expectedCustomToken, customToken); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with custom token should throw the expected error. + auth1.signInWithCustomToken(expectedCustomToken).thenCatch(function(err) { + fireauth.common.testHelper.assertErrorEquals(expectedError, err); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithEmailAndPassword_success() { + // Tests successful signInWithEmailAndPassword. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and password. + var expectedEmail = 'user@example.com'; + var expectedPass = 'password'; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected + // token response generated by RPC response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponse, tokenResponse); + // Simulate user sign in completed and returned. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return goog.Promise.resolve(user1); + }); + // verifyPassword should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', + function(email, password) { + assertEquals(expectedEmail, email); + assertEquals(expectedPass, password); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponse); + }); + asyncTestCase.waitForSignals(3); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with email and password. + auth1.signInWithEmailAndPassword(expectedEmail, expectedPass) + .then(function(user) { + assertEquals(user1, user); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithEmailAndPassword_error() { + // Tests unsuccessful signInWithEmailAndPassword. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and password. + var expectedEmail = 'user@example.com'; + var expectedPass = 'password'; + // Expected RPC error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should not be called due to RPC error. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called!'); + }); + // verifyPassword should be called with expected parameters and resolved + // with expected error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', + function(email, password) { + assertEquals(expectedEmail, email); + assertEquals(expectedPass, password); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with email and password should throw expected error. + auth1.signInWithEmailAndPassword(expectedEmail, expectedPass) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_createUserWithEmailAndPassword_success() { + // Tests successful createUserWithEmailAndPassword. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and password. + var expectedEmail = 'user@example.com'; + var expectedPass = 'password'; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected + // token response generated by RPC response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponse, tokenResponse); + // Simulate user sign in completed and returned. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return goog.Promise.resolve(user1); + }); + // createAccount should be called with expected parameters and resolved + // with expected token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'createAccount', + function(email, password) { + assertEquals(expectedEmail, email); + assertEquals(expectedPass, password); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponse); + }); + asyncTestCase.waitForSignals(3); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // createUserWithEmailAndPassword should resolve with expected user. + auth1.createUserWithEmailAndPassword(expectedEmail, expectedPass) + .then(function(user) { + assertEquals(user1, user); + asyncTestCase.signal(); + }); +} + + +function testAuth_createUserWithEmailAndPassword_error() { + // Tests unsuccessful createUserWithEmailAndPassword. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and password. + var expectedEmail = 'user@example.com'; + var expectedPass = 'password'; + // Expected rpc error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should not be called due to RPC error. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called!'); + }); + // createAccount should be called with expected parameters and resolved + // with expected error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'createAccount', + function(email, password) { + assertEquals(expectedEmail, email); + assertEquals(expectedPass, password); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // createUserWithEmailAndPassword should throw the expected error. + auth1.createUserWithEmailAndPassword(expectedEmail, expectedPass) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithCredential_success() { + // Stub signInAndRetrieveDataWithCredential and confirm same response is used + // for signInWithCredential without only the user returned. + stubs.replace( + fireauth.Auth.prototype, + 'signInAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.resolve(expectedResponse); + }); + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Expected response. Only the user will be returned. + var expectedResponse = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + // signInWithCredential using Google OAuth credential. + auth1.signInWithCredential(expectedGoogleCredential) + .then(function(user) { + // Confirm expected response. + assertEquals(user1, user); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithCredential_error() { + // Stub signInAndRetrieveDataWithCredential and confirm same error is thrown + // for signInWithCredential. + stubs.replace( + fireauth.Auth.prototype, + 'signInAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.reject(expectedError); + }); + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.NEED_CONFIRMATION); + // signInWithCredential using Google OAuth credential. + auth1.signInWithCredential(expectedGoogleCredential) + .thenCatch(function(error) { + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAndRetrieveDataWithCredential_success() { + // Tests successful signInAndRetrieveDataWithCredential using OAuth + // credential. + fireauth.AuthEventManager.ENABLED = true; + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToke' + + 'n&providerId=' + fireauth.idp.ProviderId.GOOGLE + }, + data); + // Resolve with expected token response. + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected token + // response generated by RPC response. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + // Confirm config options. + assertObjectEquals(config3, options); + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponseWithIdPData, idTokenResponse); + // Return expected user. + return goog.Promise.resolve(user1); + }); + // Record calls to signInWithIdTokenResponse. This will help us confirm + // that the expected user is being initialized, set to currentUser, saved + // to storage and token changes triggered. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + goog.testing.recordFunction( + fireauth.Auth.prototype.signInWithIdTokenResponse)); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // signInAndRetrieveDataWithCredential using Google OAuth credential. + auth1.signInAndRetrieveDataWithCredential(expectedGoogleCredential) + .then(function(result) { + // Confirm fireauth.Auth.prototype.signInWithIdTokenResponse is called + // underneath. This will save the new user in storage and trigger auth + // token changes. + assertEquals( + 1, + fireauth.Auth.prototype.signInWithIdTokenResponse.getCallCount()); + assertObjectEquals( + expectedTokenResponseWithIdPData, + fireauth.Auth.prototype.signInWithIdTokenResponse + .getLastCall().getArgument(0)); + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + auth1.currentUser, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + // Confirm user1 set as currentUser. + assertEquals( + user1, + auth1.currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAndRetrieveDataWithCredential_nonhttp_success() { + // Tests successful signInAndRetrieveDataWithCredential using OAuth credential + // in a non HTTP environment. + fireauth.AuthEventManager.ENABLED = true; + // Non http or https environment. + stubs.replace( + fireauth.util, + 'getCurrentUrl', + function() {return 'chrome-extension://SOME_LONG_ID';}); + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() {return 'chrome-extension:';}); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + // requestUri should be localhost even though the current URL and + // scheme are non http. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToke' + + 'n&providerId=' + fireauth.idp.ProviderId.GOOGLE + }, + data); + // Resolve with expected token response. + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected token + // response generated by RPC response. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + // Confirm config options. + assertObjectEquals(config3, options); + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponseWithIdPData, idTokenResponse); + // Return expected user. + return goog.Promise.resolve(user1); + }); + // Record calls to signInWithIdTokenResponse. This will help us confirm + // that the expected user is being initialized, set to currentUser, saved + // to storage and token changes triggered. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + goog.testing.recordFunction( + fireauth.Auth.prototype.signInWithIdTokenResponse)); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // signInAndRetrieveDataWithCredential using Google OAuth credential. + auth1.signInAndRetrieveDataWithCredential(expectedGoogleCredential) + .then(function(result) { + // Confirm fireauth.Auth.prototype.signInWithIdTokenResponse is called + // underneath. This will save the new user in storage and trigger Auth + // token changes. + assertEquals( + 1, + fireauth.Auth.prototype.signInWithIdTokenResponse.getCallCount()); + assertObjectEquals( + expectedTokenResponseWithIdPData, + fireauth.Auth.prototype.signInWithIdTokenResponse + .getLastCall().getArgument(0)); + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + auth1.currentUser, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + // Confirm user1 set as currentUser. + assertEquals( + user1, + auth1.currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAndRetrieveDataWithCredential_emailPassCredential() { + // Tests successful signInAndRetrieveDataWithCredential using email and + // password credential. + fireauth.AuthEventManager.ENABLED = true; + // Expected email and password. + var expectedEmail = 'user@example.com'; + var expectedPass = 'password'; + // Simulate successful RpcHandler verifyPassword with expected parameters + // passed and expected token response returned. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', + function(email, password) { + assertEquals(expectedEmail, email); + assertEquals(expectedPass, password); + // No credential or additional user info returned. + return goog.Promise.resolve(expectedTokenResponse); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected token + // response generated by RPC response. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + // Confirm config options. + assertObjectEquals(config3, options); + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponse, idTokenResponse); + // Return expected user. + return goog.Promise.resolve(user1); + }); + // Record calls to signInWithIdTokenResponse. This will help us confirm + // that the expected user is being initialized, set to currentUser, saved + // to storage and token changes triggered. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + goog.testing.recordFunction( + fireauth.Auth.prototype.signInWithIdTokenResponse)); + asyncTestCase.waitForSignals(1); + // Initialize expected user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Email/password credential. + var cred = fireauth.EmailAuthProvider.credential(expectedEmail, expectedPass); + // signInAndRetrieveDataWithCredential using email and password credential. + auth1.signInAndRetrieveDataWithCredential(cred) + .then(function(result) { + // Confirm fireauth.Auth.prototype.signInWithIdTokenResponse is called + // underneath. This will save the new user in storage and trigger Auth + // token changes. + assertEquals( + 1, + fireauth.Auth.prototype.signInWithIdTokenResponse.getCallCount()); + assertObjectEquals( + expectedTokenResponse, + fireauth.Auth.prototype.signInWithIdTokenResponse + .getLastCall().getArgument(0)); + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + auth1.currentUser, + // Expected credential returned, null for password credential. + null, + // Expected additional user info, null for password credential. + null, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + // Confirm user1 set as currentUser. + assertEquals( + user1, + auth1.currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAndRetrieveDataWithCredential_error() { + // Tests unsuccessful signInAndRetrieveDataWithCredential. + fireauth.AuthEventManager.ENABLED = true; + // Expected rpc error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.NEED_CONFIRMATION); + // Simulate unsuccessful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + // Confirm expected parameters. + assertObjectEquals( + { + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToke' + + 'n&providerId=' + fireauth.idp.ProviderId.GOOGLE + }, + data); + // Reject with expected error. + return goog.Promise.reject(expectedError); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should not be called due to RPC error. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called'); + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // signInAndRetrieveDataWithCredential with a Google credential should throw + // the expected error. + auth1.signInAndRetrieveDataWithCredential(expectedGoogleCredential) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAnonymously_success() { + // Tests successful signInAnonymously. + fireauth.AuthEventManager.ENABLED = true; + // Simulate successful RpcHandler signInAnonymously resolving with expected + // token response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'signInAnonymously', + function() { + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponse); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should initialize a user using the expected token + // response generated by RPC response. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + // Token response should match rpchandler response. + assertObjectEquals(expectedTokenResponse, tokenResponse); + // Simulate user sign in completed and returned. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return goog.Promise.resolve(user1); + }); + asyncTestCase.waitForSignals(4); + // Initialize expected anonymous user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + currentUserStorageManager = new fireauth.storage.UserManager( + auth1.getStorageKey()); + // signInAnonymousl should be resolved with the expected anonymous user. + auth1.signInAnonymously().then(function(user) { + assertEquals(user1, user); + assertTrue(user1['isAnonymous']); + // Confirm anonymous state saved. + currentUserStorageManager.getCurrentUser().then(function(user) { + assertUserEquals(user1, auth1['currentUser']); + assertTrue(user['isAnonymous']); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAnonymously_anonymousUserAlreadySignedIn() { + // Tests signInAnonymously when an anonymous user is already signed in. + fireauth.AuthEventManager.ENABLED = true; + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'signInAnonymously', + function() { + fail('signInAnonymously on RpcHandler should not be called!'); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Anonymous user is already signed in so there is no need for this to run. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called!'); + }); + asyncTestCase.waitForSignals(3); + // Initialize an anonymous user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + user1.updateProperty('isAnonymous', true); + // Current user reference. + var currentUser = null; + // User state changed counter. + var stateChanged = 0; + // ID token changed counter. + var idTokenChanged = 0; + // Storage key. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Save anonymous user as current in storage. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // All listeners should be called once with the saved anonymous user. + auth1.onAuthStateChanged(function(user) { + stateChanged++; + assertEquals(1, stateChanged); + assertEquals(user1['uid'], user['uid']); + asyncTestCase.signal(); + }); + auth1.onIdTokenChanged(function(user) { + idTokenChanged++; + assertEquals(1, idTokenChanged); + assertEquals(user1['uid'], user['uid']); + asyncTestCase.signal(); + }); + // signInAnonymously should resolve with the already signed in anonymous + // user without calling RPC handler underneath. + return auth1.signInAnonymously(); + }).then(function(user) { + // Expected saved anonymous user. + assertEquals(user1['uid'], user['uid']); + assertEquals(user1['uid'], auth1.currentUser['uid']); + assertTrue(user['isAnonymous']); + // Save reference to current user. + currentUser = auth1.currentUser; + // Sign in anonymously again. + return auth1.signInAnonymously(); + }).then(function(user) { + // Exact same reference should be returned. + assertEquals(currentUser, user); + assertEquals(auth1.currentUser, user); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInAnonymously_error() { + // Tests unsuccessful signInAnonymously. + fireauth.AuthEventManager.ENABLED = true; + // Expected rpc error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + // Simulate unsuccessful RpcHandler signInAnonymously throwing the expected + // error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'signInAnonymously', + function() { + asyncTestCase.signal(); + // Trigger invalid response error. + return goog.Promise.reject(expectedError); + }); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // signInWithIdTokenResponse should not be called due to RPC error. + stubs.replace( + fireauth.Auth.prototype, + 'signInWithIdTokenResponse', + function(tokenResponse) { + fail('signInWithIdTokenResponse should not be called'); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // signInAnonymously should fail with expected error. + auth1.signInAnonymously().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_finishPopupAndRedirectSignIn_success() { + // Test successful finishPopupAndRedirectSignIn with Auth credential. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(3); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID' + }, + data); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + // Simulate Auth user successfully initialized from + // finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + assertObjectEquals(config3, options); + assertObjectEquals(expectedTokenResponseWithIdPData, idTokenResponse); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedUser); + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var expectedUser = new fireauth.AuthUser( + config3, + expectedTokenResponseWithIdPData, + {'uid': 'USER_ID', 'email': 'user@example.com'}); + // This should resolve with expected cred and Auth user. + auth1.finishPopupAndRedirectSignIn('REQUEST_URI', 'SESSION_ID') + .then(function(response) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + expectedUser, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + response); + asyncTestCase.signal(); + }); +} + + +function testAuth_finishPopupAndRedirectSignIn_noCredential() { + // Test successful finishPopupAndRedirectSignIn with no Auth credential. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(3); + // Expected response does not contain Auth credential. + var expectedResponse = { + 'idToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expiresIn': '3600', + 'providerId': 'google.com' + }; + // Add Additional IdP data. + expectedResponse['rawUserInfo'] = + expectedTokenResponseWithIdPData['rawUserInfo']; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID' + }, + data); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedResponse); + }); + // Simulate Auth user successfully initialized from + // finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + assertObjectEquals(config3, options); + assertObjectEquals(expectedResponse, idTokenResponse); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedUser); + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var expectedUser = new fireauth.AuthUser( + config3, + expectedResponse, + {'uid': 'USER_ID', 'email': 'user@example.com'}); + // This should resolve with expected user and credential. + auth1.finishPopupAndRedirectSignIn('REQUEST_URI', 'SESSION_ID') + .then(function(response) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + expectedUser, + // Expected credential returned. + null, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + response); + asyncTestCase.signal(); + }); +} + + +function testAuth_finishPopupAndRedirectSignIn_error() { + // Test finishPopupAndRedirectSignIn verifyAssertion error. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(2); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Simulate RpcHandler verifyAssertion returning an error. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID' + }, + data); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // This should catch the expected error. + auth1.finishPopupAndRedirectSignIn('REQUEST_URI', 'SESSION_ID') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + unsubscribe(); + }); +} + + +function testAuth_signInWithPopup_emailCredentialError() { + // Test when sign in with popup verifyAssertion throws an Auth email + // credential error. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(1); + var expectedPopup = { + 'close': function() {} + }; + // Record handler as it should be triggered after the popup is processed. + var recordedHandler = null; + // Expected Auth email credential error. + var credential = + fireauth.GoogleAuthProvider.credential(null, 'ACCESS_TOKEN'); + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com', + credential: credential + }, + 'Account already exists, please confirm and link.'); + var expectedEventId = '1234'; + // Expected sign in via popup successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config3['authDomain'], + config3['apiKey'], + appId1, + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // Save Auth event handler for later. + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config3['authDomain'], domain); + assertEquals(config3['apiKey'], apiKey); + assertEquals(appId1, name); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Simulate Auth email credential error thrown by verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.reject(expectedError); + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var unsubscribe = auth1.onIdTokenChanged(function(user) { + // signInWithPopup should fail with the expected Auth email credential + // error. + auth1.signInWithPopup(expectedProvider) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + unsubscribe(); + }); +} + + +function testAuth_signInWithPopup_success() { + // Test successful sign in with popup. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + var recordedHandler = null; + var expectedEventId = '1234'; + // Expected sign in via popup successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + // Simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.waitForSignals(5); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Sign in with popup should resolve with expected result. + auth1.signInWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_success_slowIframeEmbed() { + // Test successful sign in with popup with delay in embedding the iframe. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + var recordedHandler = null; + var expectedEventId = '1234'; + // Expected sign in via popup successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // User will be reloaded before sign in success. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function() { + // Additional delay in user reload should not trigger popup closed + // error. + clock.tick(timeoutDelay); + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + // Now that popup timer is cleared, a delay in verify assertion should + // not trigger popup closed error. + clock.tick(timeoutDelay); + // Resolve with expected token response. + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // Simulate popup closed. + expectedPopup.closed = true; + // Simulate the iframe took a while to embed. This should not + // trigger a popup timeout. + clock.tick(timeoutDelay); + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + clock = new goog.testing.MockClock(true); + asyncTestCase.waitForSignals(4); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Sign in with popup should not trigger popup closed error and should resolve + // successfully. + auth1.signInWithPopup(provider).then(function(popupResult) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + auth1.currentUser, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + popupResult); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_error_popupClosed() { + // Test when the popup is closed without completing sign in that the expected + // popup closed error is triggered. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + // Expected popup closed error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + var expectedEventId = '1234'; + // No Auth event detected, typically triggered when the iframe is ready and + // there is no event detected. + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // Simulate popup closed. + expectedPopup.closed = true; + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + onError(expectedError); + return goog.Promise.resolve(); + } + }; + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Sign in with popup should fail with the popup closed error. + auth1.signInWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_error_iframeWebStorageNotSupported() { + // Test when the web storage is not supported in the iframe. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var expectedEventId = '1234'; + // Keep track when the popup is closed. + var isClosed = false; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Record when the popup is closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'isWebStorageSupported': function() { + // Simulate web storage not supported in the iframe. + return goog.Promise.resolve(false); + }, + 'addAuthEventListener': function(handler) { + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + // Web storage not supported error. + onError(expectedError); + return goog.Promise.resolve(); + } + }; + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Sign in with popup should fail with the web storage no supported error. + auth1.signInWithPopup(provider).thenCatch(function(error) { + // Popup should be closed. + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_success_cannotRunInBackground() { + // Test successful sign in with popup when tab cannot run in background. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + var recordedHandler = null; + var expectedEventId = '1234'; + // Expected sign in via popup successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config3['authDomain'], + config3['apiKey'], + appId1, + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config3['authDomain'], domain); + assertEquals(config3['apiKey'], apiKey); + assertEquals(appId1, name); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.waitForSignals(4); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with popup should resolve with expected result. + auth1.signInWithPopup(expectedProvider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_success_iframeCanRunInBackground() { + // Test successful sign in with popup when tab can run in background but is an + // iframe. This should behave the same as the + // testAuth_signInWithPopup_success_cannotRunInBackground test. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + var recordedHandler = null; + var expectedEventId = '1234'; + // Expected sign in via popup successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config3['authDomain'], + config3['apiKey'], + appId1, + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab can run in the background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Simulate app is running in an iframe. This should open the popup with the + // OAuth helper redirect directly. No additional redirect is needed as it + // could be blocked due to iframe sandboxing settings. + stubs.replace( + fireauth.util, + 'isIframe', + function() { + return true; + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Should already be redirected. + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config3['authDomain'], domain); + assertEquals(config3['apiKey'], apiKey); + assertEquals(appId1, name); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.waitForSignals(4); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with popup should resolve with expected result. + auth1.signInWithPopup(expectedProvider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_webStorageUnsupported_cantRunInBackground() { + // Test sign in with popup when the web storage is not supported in the iframe + // and the tab cannot run in background. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + // Keep track when the popup is closed. + var isClosed = false; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var expectedEventId = '1234'; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config3['authDomain'], + config3['apiKey'], + appId1, + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Check when the popup will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'isWebStorageSupported': function() { + // Simulate web storage not supported in the iframe. + return goog.Promise.resolve(false); + }, + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + onError(expectedError); + return goog.Promise.resolve(); + } + }; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config3['authDomain'], domain); + assertEquals(config3['apiKey'], apiKey); + assertEquals(appId1, name); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with popup should reject with the expected error. + auth1.signInWithPopup(expectedProvider).thenCatch(function(error) { + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_invalidEventId() { + // Test that a popup event belonging to a different owner does not resolve + // in the incorrect owner. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + var recordedHandler = null; + // Current owner's event ID. + var expectedEventId = '1234'; + // Sign in via popup triggered by another window with different ID. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + '5678', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + recordedHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return new goog.Promise(function(resolve, reject) {}); + } + }; + }); + // This should not run due to event ID mismatch. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not be called!'); + }); + asyncTestCase.waitForSignals(2); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // This should not resolve as the event is owned by another tab. + auth1.signInWithPopup(provider).then(function(popupResult) { + fail('SignInWithPopup should not resolve due to event ID mismatch!'); + }); +} + + +function testAuth_signInWithPopup_error() { + // Test sign in with popup error. + fireauth.AuthEventManager.ENABLED = true; + var expectedPopup = { + 'close': function() {} + }; + var authEventHandler = null; + var expectedEventId = '1234'; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Sign in via popup with expected error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + expectedEventId, + null, + null, + expectedError); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + authEventHandler = handler; + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + authEventHandler(expectedAuthEvent); + return goog.Promise.resolve(); + } + }; + }); + // This should not resolve due to event error. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not be called due to error!'); + }); + asyncTestCase.waitForSignals(4); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // This should resolve with a null result. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Sign in with popup should resolve with expected error. + auth1.signInWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_blockedPopup() { + // Test sign in with popup with blocked popup error. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.POPUP_BLOCKED); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return null; + }); + // Generate expected event ID for popup. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Stub instantiateOAuthSignInHandler and save event dispatcher. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'processPopup': function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertNull(actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + return goog.Promise.reject(expectedError); + }, + 'startPopupTimeout': function(popupWin, onError, delay) { + return goog.Promise.resolve(); + } + }; + }); + asyncTestCase.waitForSignals(1); + var provider = new fireauth.GoogleAuthProvider(); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should catch the blocked popup error. + auth1.signInWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_missingAuthDomain() { + // Test sign in with popup with missing Auth domain error. + fireauth.AuthEventManager.ENABLED = true; + // Fake popup. + var expectedPopup = { + 'close': function() {} + }; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // Popup may try to close due to error if still open. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(3); + var provider = new fireauth.GoogleAuthProvider(); + // App initialized with no Auth domain. + app1 = firebase.initializeApp({ + 'apiKey': 'API_KEY' + }, appId1); + auth1 = app1.auth(); + // This should catch the blocked popup error + auth1.signInWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPopup_unsupportedEnvironment() { + // Test sign in with popup in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var provider = new fireauth.GoogleAuthProvider(); + // App initialized correctly. + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should catch the expected error. + auth1.signInWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithRedirect_unsupportedEnvironment() { + // Test sign in with redirect in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var provider = new fireauth.GoogleAuthProvider(); + // App initialized correctly. + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should catch the expected error. + auth1.signInWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_getRedirectResult_unsupportedEnvironment() { + // Test getRedirectResult in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(3); + // App initialized correctly. + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should catch the expected error. + auth1.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // State listener should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromSignInWithRedirect_success() { + // Tests the return from a successful sign in with redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedCred = fireauth.GoogleAuthProvider.credential( + null, 'ACCESS_TOKEN'); + // Expected sign in via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + null, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + expectedPopupResult = { + 'user': user1, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + // User 1 should be set here and saved to storage. + auth1.setCurrentUser_(user1); + asyncTestCase.signal(); + return currentUserStorageManager.setCurrentUser(user1).then(function() { + return expectedPopupResult; + }); + }); + var user1, expectedPopupResult; + asyncTestCase.waitForSignals(5); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should resolve with the expected user and credential. + auth1.getRedirectResult().then(function(result) { + // Expected result returned. + assertObjectEquals(expectedPopupResult, result); + asyncTestCase.signal(); + }); + }); + // State listener should fire once only with the final redirected user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with final redirected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertEquals(user1, currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromSignInWithRedirect_withExistingUser() { + // Tests the return from a successful sign in with redirect while having a + // previously signed in user. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + // Expected sign in via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + null, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 and existing user2 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(auth1['currentUser'])); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + accountInfo['uid'] = '5678'; + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // New signed in user. + auth1.setCurrentUser_(user1); + // New user should have popup and redirect enabled. + user1.enablePopupRedirect(); + expectedPopupResult = { + 'user': user1, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.signal(); + // User 1 should be set here and saved to storage. + return currentUserStorageManager.setCurrentUser(user1).then(function() { + return expectedPopupResult; + }); + }); + var user1, expectedPopupResult; + accountInfo['uid'] = '1234'; + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + asyncTestCase.waitForSignals(5); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // When state is ready, currentUser if it exists should be resolved. + // In this we are simulating sign in with redirect when an existing + // user was already signed in. + currentUserStorageManager.setCurrentUser(user2).then(function() { + return pendingRedirectManager.setPendingStatus(); + }).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should return the new user and expected cred. + auth1.getRedirectResult().then(function(result) { + // Newly signed in user. + assertEquals(user1, auth1['currentUser']); + assertObjectEquals(expectedPopupResult, result); + asyncTestCase.signal(); + }); + // State listener should fire once only with the final redirected user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with final redirected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromSignInWithRedirect_error() { + // Test return from sign in via redirect with error generated. + fireauth.AuthEventManager.ENABLED = true; + // The expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Expected sign in via redirect error Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + null, + null, + null, + expectedError); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // As the event has an error this should not be called. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not run on event error!'); + }); + asyncTestCase.waitForSignals(4); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should return the expected error. + auth1.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); + // State listener should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromSignInWithRedirect_error_webStorageNotSupported() { + // Test return from sign in via redirect with web storage not supported error. + fireauth.AuthEventManager.ENABLED = true; + // The expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + // Expected web storage not supported error Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + expectedError); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // As the event has an error this should not be called. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not run on event error!'); + }); + asyncTestCase.waitForSignals(3); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set pending redirect event. + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should return the expected error. + auth1.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); + // State listener should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromSignInWithRedirect_noEvent() { + // Test get redirect result with no previous sign in via redirect attempt. + fireauth.AuthEventManager.ENABLED = true; + // No event detected. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected unknown event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // This should not run as there is no successful event. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not run on unknown event!'); + }); + asyncTestCase.waitForSignals(4); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should return null. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + }); + // State listener should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromLinkWithRedirect_success() { + // Test link with redirect success. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + // When state is ready, currentUser if it exists should be resolved. + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Assume user previously called link via redirect. + user1.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user1); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 and user1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + expectedPopupResult = { + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(5); + var user1, expectedPopupResult; + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should resolve with expected user and credential. + auth1.getRedirectResult().then(function(result) { + assertObjectEquals(expectedPopupResult, result); + asyncTestCase.signal(); + }); + // Should fire once only with the final redirected user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with expected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_redirectedLoggedOutUser_success() { + // Test link with redirect success when a logged out user try to link with + // redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + // When state is ready, currentUser if it exists should be resolved. + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + return goog.Promise.resolve(user1); + }); + // Simulate redirect user loaded from storage. + stubs.replace( + fireauth.storage.RedirectUserManager.prototype, + 'getRedirectUser', + function() { + // Create a logged out user that tried to link with redirect. + user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + user2.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user2); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey) { + return { + 'addAuthEventListener': function(handler) { + // auth1, user1 and redirect user2 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + assertTrue(manager.isSubscribed(user2)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + // This should be called on redirect user only. + assertEquals(user2, this); + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + expectedPopupResult = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(5); + var user1, user2, expectedPopupResult; + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should resolve with redirect user (not current user) + // and expected credential. + auth1.getRedirectResult().then(function(result) { + // User1 logged in. + assertEquals(user1, auth1['currentUser']); + // Framework change should propagate to currentUser. + assertArrayEquals(['firebaseui'], auth1['currentUser'].getFramework()); + assertEquals('fr', auth1['currentUser'].getLanguageCode()); + // User2 expected in getRedirectResult. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user2, + // Expected credential returned. + expectedCred, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + // Framework updates should still propagate to redirect user. + assertArrayEquals(['firebaseui'], result.user.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], result.user.getFramework()); + // Language code should propagate to redirect user. + assertEquals('fr', result.user.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', result.user.getLanguageCode()); + asyncTestCase.signal(); + }); + // Should fire once only with the original user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with original user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_redirectedLoggedOutUser_differentAuthDomain() { + // Test link with redirect success when a logged out user with a different + // authDomain tries to link with redirect. + fireauth.AuthEventManager.ENABLED = true; + stubs.reset(); + // Simulate current origin is whitelisted. + simulateWhitelistedOrigin(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + var expectedEventId = '1234'; + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey) { + return { + 'addAuthEventListener': function(handler) { + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + return goog.Promise.resolve({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + }); + asyncTestCase.waitForSignals(3); + // The logged out user with the different authDomain. + var user1 = new fireauth.AuthUser( + config4, expectedTokenResponse, accountInfo); + user1.setRedirectEventId(expectedEventId); + // Auth will modify user to use its authDomain. + var modifiedUser1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + modifiedUser1.setRedirectEventId(expectedEventId); + // Set saved logged out redirect user using different authDomain. + redirectUserStorageManager = new fireauth.storage.RedirectUserManager( + config3['apiKey'] + ':' + appId1); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + redirectUserStorageManager.setRedirectUser(user1).then(function() { + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + // Get redirect result should resolve with redirect user and its + // authDomain overridden with the current app authDomain. + auth1.getRedirectResult().then(function(result) { + // No logged in user. + assertNull(auth1.currentUser); + // Redirect logged out user should have authDomain matching with + // app's. + assertUserEquals(modifiedUser1, result.user); + // Framework updates should still propagate to redirected user. + assertArrayEquals(['firebaseui'], result.user.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], result.user.getFramework()); + // Language code should propagate to redirected user. + assertEquals('fr', result.user.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', result.user.getLanguageCode()); + asyncTestCase.signal(); + }); + // Should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_noCurrentUser_redirectUser() { + // Test link with redirect success when a logged out user try to link with + // redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate redirect user loaded from storage. + stubs.replace( + fireauth.storage.RedirectUserManager.prototype, + 'getRedirectUser', + function() { + // Create a logged out user that tried to link with redirect. + user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + user2.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user2); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey) { + return { + 'addAuthEventListener': function(handler) { + // auth1, redirect user2 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user2)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + // This should be called on redirect user only. + assertEquals(user2, this); + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + expectedPopupResult = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(5); + var user2, expectedPopupResult; + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Set language code. + auth1.languageCode = 'fr'; + // Log framework. + auth1.logFramework('firebaseui'); + pendingRedirectManager.setPendingStatus().then(function() { + // Get redirect result should resolve with redirect user (not current user) + // and expected credential. + auth1.getRedirectResult().then(function(result) { + // No current user. + assertNull(auth1['currentUser']); + // User2 expected in getRedirectResult. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user2, + // Expected credential returned. + expectedCred, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + // Framework updates should still propagate to redirected non current + // user. + assertArrayEquals(['firebaseui'], result.user.getFramework()); + auth1.logFramework('angularfire'); + assertArrayEquals( + ['firebaseui', 'angularfire'], result.user.getFramework()); + // Language code should propagate to redirected non current user. + assertEquals('fr', result.user.getLanguageCode()); + auth1.languageCode = 'de'; + assertEquals('de', result.user.getLanguageCode()); + asyncTestCase.signal(); + }); + // Should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_noUsers() { + // Test link with redirect success when a logged out user try to link with + // redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectLink should not call!'); + }); + asyncTestCase.waitForSignals(4); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should resolve with redirect user (not current user) + // and expected credential. + auth1.getRedirectResult().then(function(result) { + // No current user. + assertNull(auth1['currentUser']); + // No results. + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_redirectedLoggedInUser_success() { + // Test link with redirect success when a logged in user tries to redirect. + // The same user will typically be also retrieved from storage but only the + // current user will be handled. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + var expectedCred = fireauth.GoogleAuthProvider.credential(null, + 'ACCESS_TOKEN'); + // Expected link via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + // When state is ready, currentUser if it exists should be resolved. + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Current user user1 tried to link via redirect. + user1.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user1); + }); + // Simulate redirect user loaded from storage. + stubs.replace( + fireauth.storage.RedirectUserManager.prototype, + 'getRedirectUser', + function() { + // The same user should be save as rediret user too. + user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + user2.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user2); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey) { + return { + 'addAuthEventListener': function(handler) { + // auth1, user1 and redirect user2 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + assertTrue(manager.isSubscribed(user2)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectLink. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + // This should be called on current user only as he is subscribed first. + assertEquals(user1, this); + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + expectedPopupResult = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }); + asyncTestCase.signal(); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(5); + var user1, user2, expectedPopupResult; + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should resolve with redirect user (not current user) + // and expected credential. + auth1.getRedirectResult().then(function(result) { + // User1 logged in. + assertEquals(user1, auth1['currentUser']); + // User1 expected in getRedirectResult. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user1, + // Expected credential returned. + expectedCred, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.SIGN_IN, + result); + asyncTestCase.signal(); + }); + // Should fire once only with redirected user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with redirected user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_invalidUser() { + // Test link with redirect event belonging to a diffent user. + // This could happen if the same user started the redirect event in a + // different tab. The event should resolve in the same tab. + fireauth.AuthEventManager.ENABLED = true; + var expectedEventId = '1234'; + // Successful link via redirect belonging to another user. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + 'OTHER_ID', + 'http://www.example.com/#response', + 'SESSION_ID'); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + // When state is ready, currentUser if it exists should be resolved. + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Set redirect event ID. + user1.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user1); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 and user1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + // At this stage user and Auth are subscribed. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // This should not run as the logged in user's event ID does not match with + // detected event ID. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectLink should not call due to UID mismatch!'); + }); + asyncTestCase.waitForSignals(4); + var user1; + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Due to event ID mismatch, this should return null. + auth1.getRedirectResult().then(function(result) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, undefined, result); + asyncTestCase.signal(); + }); + // Should fire once only with original user1. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with original user1. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_returnFromLinkWithRedirect_error() { + // Test return from link with redirect event having an error. + fireauth.AuthEventManager.ENABLED = true; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedEventId = '1234'; + // Expected link via redirect event with error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + expectedEventId, + null, + null, + expectedError); + stubs.reset(); + // Simulate local storage change synchronized. + simulateLocalStorageSynchronized(); + // Simulate user loaded from storage. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.storage.UserManager.prototype, + 'getCurrentUser', + function() { + // When state is ready, currentUser if it exists should be resolved. + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Set user as owner of this event. + user1.setRedirectEventId(expectedEventId); + return goog.Promise.resolve(user1); + }); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 and user1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + assertTrue(manager.isSubscribed(user1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // As the event contains an error, this should not run. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectLink should not call due to event error!'); + }); + asyncTestCase.waitForSignals(4); + var user1; + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should contain an error. + auth1.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Should fire once only with original user1. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with original user1. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertUserEquals(user1, currentUser); + asyncTestCase.signal(); + }); + }); +} + + +function testAuth_signInWithRedirect_success_unloadsOnRedirect() { + // Test successful request for sign in via redirect when page unloads on + // redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedMode = fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT; + asyncTestCase.waitForSignals(1); + // Track calls to savePersistenceForRedirect. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'savePersistenceForRedirect', + goog.testing.recordFunction()); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'unloadsOnRedirect': function() { return true; }, + 'processRedirect': function( + actualMode, actualProvider, actualEventId) { + assertEquals(expectedMode, actualMode); + assertEquals(expectedProvider, actualProvider); + assertUndefined(actualEventId); + // Confirm current persistence is saved before redirect. + assertEquals( + 1, + fireauth.storage.UserManager.prototype + .savePersistenceForRedirect.getCallCount()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + } + }; + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // sign in with redirect should call processRedirect underneath and remain + // pending. + auth1.signInWithRedirect(expectedProvider).then(function() { + fail('SignInWithRedirect should remain pending in environment where ' + + 'OAuthSignInHandler unloads the page.'); + }); +} + + +function testAuth_signInWithRedirect_success_doesNotUnloadOnRedirect() { + // Test successful request for sign in via redirect when page does not unload + // on redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedMode = fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT; + asyncTestCase.waitForSignals(1); + // Track calls to savePersistenceForRedirect. + stubs.replace( + fireauth.storage.UserManager.prototype, + 'savePersistenceForRedirect', + goog.testing.recordFunction()); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'unloadsOnRedirect': function() { return false; }, + 'processRedirect': function( + actualMode, actualProvider, actualEventId) { + assertEquals(expectedMode, actualMode); + assertEquals(expectedProvider, actualProvider); + assertUndefined(actualEventId); + // Confirm current persistence is saved before redirect. + assertEquals( + 1, + fireauth.storage.UserManager.prototype + .savePersistenceForRedirect.getCallCount()); + return goog.Promise.resolve(); + } + }; + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with redirect should resolve in this case as the page does not + // necessarily unload. + auth1.signInWithRedirect(expectedProvider).then(function() { + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithRedirect_missingAuthDomain() { + // Test failing request for sign in via redirect due to missing Auth domain. + fireauth.AuthEventManager.ENABLED = true; + // No Auth domain supplied. + var config = { + 'apiKey': 'API_KEY' + }; + // Expected missing Auth domain error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN); + asyncTestCase.waitForSignals(1); + app1 = firebase.initializeApp(config, appId1); + auth1 = app1.auth(); + var provider = new fireauth.GoogleAuthProvider(); + // Sign in with redirect should fail with missing Auth domain. + auth1.signInWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithRedirect_invalidProvider() { + // Test sign in with redirect failing with invalid provider. + fireauth.AuthEventManager.ENABLED = true; + // Expected invalid OAuth provider error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + asyncTestCase.waitForSignals(1); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; }, + 'unloadsOnRedirect': function() { return true; }, + 'processRedirect': function( + actualMode, actualProvider, actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, actualMode); + assertEquals(provider, actualProvider); + assertUndefined(actualEventId); + return goog.Promise.reject(expectedError); + } + }; + }); + var provider = new fireauth.EmailAuthProvider(); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Sign in with redirect should fail with invalid OAuth provider error. + auth1.signInWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_returnFromSignInWithRedirect_timeout() { + // Test return from sign in with redirect getRedirectResult timing out. + fireauth.AuthEventManager.ENABLED = true; + // Expected timeout error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TIMEOUT); + clock = new goog.testing.MockClock(true); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 and user1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // This should not run due to timeout error. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectSignIn should not call due to timeout!'); + }); + asyncTestCase.waitForSignals(4); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should fail with timeout error. + auth1.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Should fire once only with null user. + var idTokenChangeCounter = 0; + auth1.onIdTokenChanged(function(currentUser) { + idTokenChangeCounter++; + assertEquals(1, idTokenChangeCounter); + assertNull(currentUser); + asyncTestCase.signal(); + }); + var userChanges = 0; + // Should be called with null user. + auth1.onAuthStateChanged(function(currentUser) { + userChanges++; + assertEquals(1, userChanges); + assertNull(currentUser); + asyncTestCase.signal(); + }); + // Speed up timeout. + clock.tick(timeoutDelay); + }); +} + + +function testAuth_invalidateSession_tokenExpired() { + // Test when a token expired error is triggered on a current user that the + // user is signed out. + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Expected token error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + // Whether to trigger the token error or not. + var triggerTokenError = false; + // Stub token manager to either throw the token error or the valid tokens. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + if (triggerTokenError) { + return goog.Promise.reject(expectedError); + } + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save current user in storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + + // Track token change events. + var tokenChangeCounter = 0; + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should trigger initially and then on sign out. + auth1.addAuthTokenListener(function(token) { + // Keep track. + tokenChangeCounter++; + if (token) { + // Initial sign in. + assertEquals(1, tokenChangeCounter); + // Confirm ID token. + assertEquals('ID_TOKEN', token); + // Force token error on next call. + triggerTokenError = true; + // Token error should be detected. + auth1.currentUser.getIdToken(true).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + } else { + // Should be triggered again on user invalidation. + assertEquals(2, tokenChangeCounter); + // User signed out. + assertNull(auth1.currentUser); + // User cleared from storage. + currentUserStorageManager.getCurrentUser().then(function(user) { + // No user stored anymore. + assertNull(user); + asyncTestCase.signal(); + }); + } + }); + }); +} + + +function testAuth_invalidateSession_userDisabled() { + // Test when a user disabled error is triggered on a current user that the + // user is signed out. + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Expected token error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + // Whether to trigger the token error or not. + var triggerTokenError = false; + // Stub token manager to either throw the token error or the valid tokens. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + if (triggerTokenError) { + return goog.Promise.reject(expectedError); + } + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save current user in storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + + // Track token change events. + var tokenChangeCounter = 0; + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should trigger initially and then on sign out. + auth1.addAuthTokenListener(function(token) { + // Keep track. + tokenChangeCounter++; + if (token) { + // Initial sign in. + assertEquals(1, tokenChangeCounter); + // Confirm ID token. + assertEquals('ID_TOKEN', token); + // Force token error on next call. + triggerTokenError = true; + // Token error should be detected. + auth1.currentUser.getIdToken(true).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + } else { + // Should be triggered again on user invalidation. + assertEquals(2, tokenChangeCounter); + // User signed out. + assertNull(auth1.currentUser); + // User cleared from storage. + currentUserStorageManager.getCurrentUser().then(function(user) { + // No user stored anymore. + assertNull(user); + asyncTestCase.signal(); + }); + } + }); + }); +} + + +function testAuth_invalidateSession_dispatchUserInvalidatedEvent() { + // Test when a user invalidate event is dispatched on a current user that the + // user is signed out. + fireauth.AuthEventManager.ENABLED = true; + // Stub OAuth sign in handler. + fakeOAuthSignInHandler(); + // Stub token manager to return valid tokens. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function() { + return goog.Promise.resolve({ + 'accessToken': 'ID_TOKEN', + 'refreshToken': 'REFRESH_TOKEN', + 'expirationTime': now + 3600 * 1000 + }); + }); + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + // Access token unchanged, should trigger notifyAuthListeners_. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save current user in storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + + // Track token change events. + var tokenChangeCounter = 0; + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // This should trigger initially and then on sign out. + auth1.addAuthTokenListener(function(token) { + // Keep track. + tokenChangeCounter++; + if (token) { + // Initial sign in. + assertEquals(1, tokenChangeCounter); + // Confirm ID token. + assertEquals('ID_TOKEN', token); + // Dispatch user invalidation event on current user. + auth1.currentUser.dispatchEvent( + fireauth.UserEventType.USER_INVALIDATED); + } else { + // Should be triggered again on user invalidation. + assertEquals(2, tokenChangeCounter); + // User signed out. + assertNull(auth1.currentUser); + // User cleared from storage. + currentUserStorageManager.getCurrentUser().then(function(user) { + // No user stored anymore. + assertNull(user); + asyncTestCase.signal(); + }); + } + }); + }); +} + + +function testAuth_proactiveTokenRefresh_multipleUsers() { + // Test proactive refresh when a Firebase service is added before sign in + // and multiple users are signed in successively. + // Record startProactiveRefresh and stopProactiveRefresh calls. + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.AuthUser.prototype, + 'startProactiveRefresh', + goog.testing.recordFunction()); + stubs.replace( + fireauth.AuthUser.prototype, + 'stopProactiveRefresh', + goog.testing.recordFunction()); + // Logged in users. + var accountInfo1 = { + 'uid': '1234', + 'email': 'user1@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + var accountInfo2 = { + 'uid': '5678', + 'email': 'user2@example.com', + 'displayName': 'John Smith', + 'emailVerified': false + }; + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo1); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo2); + var calls = 0; + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + calls++; + // Return user1 on first call and user2 on second. + if (calls == 1) { + return goog.Promise.resolve(user1); + } else { + return goog.Promise.resolve(user2); + } + }); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var subscriber = function(token) {}; + // Simulate Firebase service added. + app1.INTERNAL.addAuthTokenListener(subscriber); + // Simulate user1 signed in. + auth1.signInWithIdTokenResponse(expectedTokenResponse).then(function() { + // Current user should be set to user1. + assertEquals(user1, auth1['currentUser']); + // Confirm proactive refresh started on user1. + assertEquals( + 1, fireauth.AuthUser.prototype.startProactiveRefresh.getCallCount()); + assertEquals( + user1, + fireauth.AuthUser.prototype.startProactiveRefresh.getLastCall() + .getThis()); + // Stop not yet called. + assertEquals( + 0, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + // Sign in another user. + return auth1.signInWithIdTokenResponse(expectedTokenResponse); + }).then(function() { + // Current user should be set to user2. + assertEquals(user2, auth1['currentUser']); + // Confirm proactive refresh started on user2. + assertEquals( + 2, fireauth.AuthUser.prototype.startProactiveRefresh.getCallCount()); + assertEquals( + user2, + fireauth.AuthUser.prototype.startProactiveRefresh.getLastCall() + .getThis()); + // Stop proactive refresh on user1. + assertEquals( + 1, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + assertEquals( + user1, + fireauth.AuthUser.prototype.stopProactiveRefresh.getLastCall() + .getThis()); + // Sign out the user2. + return auth1.signOut(); + }).then(function() { + // Confirm proactive refresh stopped on user2. + assertEquals( + 2, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + assertEquals( + user2, + fireauth.AuthUser.prototype.stopProactiveRefresh.getLastCall() + .getThis()); + asyncTestCase.signal(); + }); +} + + +function testAuth_proactiveTokenRefresh_firebaseServiceAddedAfterSignIn() { + // Test proactive refresh when a Firebase service is added after sign in. + // Record startProactiveRefresh and stopProactiveRefresh calls. + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.AuthUser.prototype, + 'startProactiveRefresh', + goog.testing.recordFunction()); + stubs.replace( + fireauth.AuthUser.prototype, + 'stopProactiveRefresh', + goog.testing.recordFunction()); + var subscriber = function(token) {}; + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + // Save current user in storage. + currentUserStorageManager = new fireauth.storage.UserManager( + config3['apiKey'] + ':' + appId1); + // Simulate user signed in. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var unsubscribe = auth1.onIdTokenChanged(function(user) { + unsubscribe(); + // Confirm proactive refresh not started on that user. + assertEquals( + 0, fireauth.AuthUser.prototype.startProactiveRefresh.getCallCount()); + // Simulate Firebase service added. + app1.INTERNAL.addAuthTokenListener(subscriber); + // Confirm proactive refresh started on that user. + assertEquals( + 1, fireauth.AuthUser.prototype.startProactiveRefresh.getCallCount()); + // Confirm proactive refresh not stopped yet on that user. + assertEquals( + 0, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + // Sign out the user. + auth1.signOut().then(function() { + // Confirm proactive refresh stopped on that user. + assertEquals( + 1, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAuth_proactiveTokenRefresh_firebaseServiceRemovedAfterSignIn() { + // Test proactive refresh stopped when a Firebase service is removed. + // Record startProactiveRefresh and stopProactiveRefresh calls. + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.AuthUser.prototype, + 'startProactiveRefresh', + goog.testing.recordFunction()); + stubs.replace( + fireauth.AuthUser.prototype, + 'stopProactiveRefresh', + goog.testing.recordFunction()); + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + return goog.Promise.resolve(user1); + }); + // Logged in user. + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var subscriber = function(token) {}; + // Simulate Firebase service added. + app1.INTERNAL.addAuthTokenListener(subscriber); + // Add same listener again to check that removing it once will ensure the + // proactive refresh is stopped. + app1.INTERNAL.addAuthTokenListener(subscriber); + // Simulate user signed in. + auth1.signInWithIdTokenResponse(expectedTokenResponse).then(function() { + // Current user should be set to user1. + assertEquals(user1, auth1['currentUser']); + // Confirm proactive refresh started on that user. + assertEquals( + 1, fireauth.AuthUser.prototype.startProactiveRefresh.getCallCount()); + // Stop not yet called. + assertEquals( + 0, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + // Simulate Firebase service removed. + app1.INTERNAL.removeAuthTokenListener(subscriber); + // Confirm proactive refresh stopped on that user. + assertEquals( + 1, fireauth.AuthUser.prototype.stopProactiveRefresh.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPhoneNumber_success() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedCode = '123456'; + var expectedPhoneNumber = '+15551234567'; + var expectedRecaptchaToken = 'RECAPTCHA_TOKEN'; + var expectedCredential = fireauth.PhoneAuthProvider.credential( + expectedVerificationId, expectedCode); + var appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(expectedRecaptchaToken); + } + }; + // Expected promise to be returned by signInAndRetrieveDataWithCredential. + var expectedPromise = new goog.Promise(function(resolve, reject) {}); + // Phone Auth provider instance. + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + // Phone Auth provider constructor mock. + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + // Provider instance should be initialized with the expected Auth instance + // and return the expected phone Auth provider instance. + phoneAuthProviderConstructor(auth1) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would resolve with the expected verification + // ID. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.resolve(expectedVerificationId)).$once(); + // Code confirmation should call signInAndRetrieveDataWithCredential with the + // expected credential. + stubs.replace( + fireauth.Auth.prototype, + 'signInAndRetrieveDataWithCredential', + goog.testing.recordFunction(function(cred) { + // Confirm expected credential passed. + assertObjectEquals( + expectedCredential.toPlainObject(), + cred.toPlainObject()); + // Return expected promise. + return expectedPromise; + })); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + // Call signInWithPhoneNumber. + auth1.signInWithPhoneNumber(expectedPhoneNumber, appVerifier) + .then(function(confirmationResult) { + // Confirmation result returned should contain expected verification ID. + assertEquals( + expectedVerificationId, confirmationResult['verificationId']); + // Code confirmation should return the same response as the underlying + // signInAndRetrieveDataWithCredential. + assertEquals(expectedPromise, confirmationResult.confirm(expectedCode)); + // Confirm signInAndRetrieveDataWithCredential called once. + assertEquals( + 1, + fireauth.Auth.prototype.signInAndRetrieveDataWithCredential + .getCallCount()); + // Confirm signInAndRetrieveDataWithCredential is bound to auth1. + assertEquals( + auth1, + fireauth.Auth.prototype.signInAndRetrieveDataWithCredential + .getLastCall().getThis()); + asyncTestCase.signal(); + }); +} + + +function testAuth_signInWithPhoneNumber_error() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var expectedPhoneNumber = '+15551234567'; + var expectedRecaptchaToken = 'RECAPTCHA_TOKEN'; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(expectedRecaptchaToken); + } + }; + // Phone Auth provider instance. + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + // Phone Auth provider constructor mock. + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + // Mock signInAndRetrieveDataWithCredential. + var signInAndRetrieveDataWithCredential = mockControl.createMethodMock( + fireauth.Auth.prototype, 'signInAndRetrieveDataWithCredential'); + // Provider instance should be initialized with the expected Auth instance + // and return the expected phone Auth provider instance. + phoneAuthProviderConstructor(auth1) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would reject with the expected error. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.reject(expectedError)).$once(); + // signInAndRetrieveDataWithCredential should not be called. + signInAndRetrieveDataWithCredential(ignoreArgument).$times(0); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + // Call signInWithPhoneNumber. + auth1.signInWithPhoneNumber(expectedPhoneNumber, appVerifier) + .thenCatch(function(error) { + // This should throw the same error thrown by verifyPhoneNumber. + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testAuth_setPersistence_invalid() { + var unsupportedTypeError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_PERSISTENCE); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Session should throw an unsupported persistence type error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + auth1.setPersistence('bla'); + })); +} + + +function testAuth_setPersistence_noExistingAuthState() { + // Test when persistence is set that future sign-in attempts are stored + // using specified persistence. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + calls++; + // Return user1 on first call and user2 on second. + if (calls == 1) { + assertObjectEquals(expectedTokenResponse, idTokenResponse); + return goog.Promise.resolve(user1); + } else { + assertObjectEquals(expectedTokenResponse2, idTokenResponse); + return goog.Promise.resolve(user2); + } + }); + // Stub verifyPassword to return tokens for first user. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', + function(email, password) { + return goog.Promise.resolve(expectedTokenResponse); + }); + // Stub verifyCustomToken to return tokens for second user. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyCustomToken', + function(customToken) { + return goog.Promise.resolve(expectedTokenResponse2); + }); + // Fixes weird IE flakiness. + clock = new goog.testing.MockClock(true); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, {'uid': '1234'}); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse2, {'uid': '5678'}); + var calls = 0; + asyncTestCase.waitForSignals(1); + // Switch to session persistence. + auth1.setPersistence('session'); + clock.tick(1); + // Sign in with email/password. + auth1.signInWithEmailAndPassword( + 'user@example.com', 'password').then(function(user) { + clock.tick(1); + // Confirm first user saved in session storage. + assertUserEquals(user1, user); + return fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'session', user1); + }).then(function() { + // Sign in with custom token. + return auth1.signInWithCustomToken('CUSTOM_TOKEN'); + }).then(function() { + // Confirm second user saved in session storage. + clock.tick(1); + return fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'session', user2); + }).then(function() { + asyncTestCase.signal(); + }); +} + + +function testAuth_setPersistence_existingAuthState() { + // Test when persistence is set after initialization, a stored Auth state + // will be switched to the new type of storage. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + return goog.Promise.resolve(user2); + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'signInAnonymously', + function() { + // Return tokens for second test user. + return goog.Promise.resolve(expectedTokenResponse2); + }); + asyncTestCase.waitForSignals(2); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, {'uid': '1234'}); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse2, {'uid': '5678'}); + currentUserStorageManager = + new fireauth.storage.UserManager(config3['apiKey'] + ':' + appId1); + // Simulate logged in user, save to storage, it will be picked up on init + // Auth state. This will use the default local persistence. + currentUserStorageManager.setCurrentUser(user1).then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Switch to session persistence. + auth1.setPersistence('session'); + var unsubscribe = auth1.onAuthStateChanged(function(user) { + unsubscribe(); + // When this is first triggered, the previously signed in user should be + // switched to session storage. + fireauth.common.testHelper.assertUserStorage( + config3['apiKey'] + ':' + appId1, 'session', user1).then(function() { + // Sign in a new user. + return auth1.signInAnonymously(); + }).then(function() { + // Second user should be also persistence in session storage. + return fireauth.common.testHelper.assertUserStorage( + config3['apiKey'] + ':' + appId1, 'session', user2); + }).then(function() { + asyncTestCase.signal(); + }); + }); + // Track all token changes. Confirm persistence changes do not trigger + // unexpected calls. + var uidsDetected = []; + auth1.onIdTokenChanged(function(user) { + // Keep track of UIDs each time this is called. + uidsDetected.push(user && user.uid); + if (uidsDetected.length == 2) { + assertArrayEquals(['1234', '5678'], uidsDetected); + asyncTestCase.signal(); + } else if (uidsDetected.length > 2) { + fail('Unexpected token change!'); + } + }); + }); +} + + +function testAuth_temporaryPersistence_externalChange() { + // Test when temporary persistence is set and an external change is detected + // local persistence is set after synchronization. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + return goog.Promise.resolve(user3); + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'signInAnonymously', + function() { + // Return tokens for third test user. + return goog.Promise.resolve(expectedTokenResponse3); + }); + var storageKey = 'firebase:authUser:' + config3['apiKey'] + ':' + appId1; + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, {'uid': '1234'}); + var user2 = new fireauth.AuthUser( + config3, expectedTokenResponse2, {'uid': '5678'}); + var user3 = new fireauth.AuthUser( + config3, expectedTokenResponse3, {'uid': '9012'}); + var storageEvent = new goog.testing.events.Event( + goog.events.EventType.STORAGE, window); + // Simulate existing user stored in session storage. + window.sessionStorage.setItem( + storageKey, JSON.stringify(user1.toPlainObject())); + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + storageEvent.key = 'firebase:authUser:' + auth1.getStorageKey(); + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(user2.toPlainObject()); + asyncTestCase.waitForSignals(1); + var calls = 0; + auth1.onIdTokenChanged(function(user) { + calls++; + if (calls == 1) { + // On first call, the first user should be stored in session storage. + assertUserEquals(user1, user); + fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'session', user1).then(function() { + // Simulate external user signed in on another tab. + window.localStorage.setItem( + storageKey, JSON.stringify(user2.toPlainObject())); + goog.testing.events.fireBrowserEvent(storageEvent); + }); + } else if (calls == 2) { + // On second call, the second user detected from external event should + // be detected and stored in local storage. + assertUserEquals(user2, user); + fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'local', user2).then(function() { + // Sign in anonymously. + auth1.signInAnonymously(); + }); + } else if (calls == 3) { + // Third anonymous user detected and should be stored in local storage. + assertUserEquals(user3, user); + fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'local', user3).then(function() { + asyncTestCase.signal(); + }); + } + }); +} + + +function testAuth_storedPersistence_returnFromRedirect() { + // getRedirectResult will resolve with the user stored with expected + // persistence from the previous page. + // Tests the return from a successful sign in with redirect. + fireauth.AuthEventManager.ENABLED = true; + var expectedCred = fireauth.GoogleAuthProvider.credential( + null, 'ACCESS_TOKEN'); + // Expected sign in via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + null, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.Auth.prototype, + 'finishPopupAndRedirectSignIn', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + expectedPopupResult = { + 'user': user1, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + // User 1 should be set here and saved to storage. + auth1.setCurrentUser_(user1); + return currentUserStorageManager.setCurrentUser(user1).then(function() { + return expectedPopupResult; + }); + }); + var user1, expectedPopupResult; + asyncTestCase.waitForSignals(2); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + var currentUserStorageManager = + new fireauth.storage.UserManager(config3['apiKey'] + ':' + appId1); + // Simulate persistence set to session on previous page. + currentUserStorageManager.setPersistence('session'); + currentUserStorageManager.savePersistenceForRedirect().then(function() { + // Set pending redirect. + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Get redirect result should resolve with the expected user and + // credential. + auth1.getRedirectResult().then(function(result) { + // Expected result returned. + assertObjectEquals(expectedPopupResult, result); + // User should be stored in session storage since + // savePersistenceForRedirect was previously called with session + // persistence. + return fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'session', user1); + }).then(function() { + asyncTestCase.signal(); + }); + // Track all token changes. Confirm persistence changes do not trigger + // unexpected calls. + var uidsDetected = []; + auth1.onIdTokenChanged(function(user) { + // Keep track of UIDs detected on each call. + uidsDetected.push(user && user.uid); + if (uidsDetected.length == 1) { + assertArrayEquals([user1.uid], uidsDetected); + asyncTestCase.signal(); + } else if (uidsDetected.length > 1) { + fail('Unexpected token change!'); + } + }); + }); + }); +} + + +function testAuth_changedPersistence_returnFromRedirect() { + // If persistence is specified after initialization, getRedirectResult will + // resolve with the user stored with expected persistence and not the saved + // one. + fireauth.AuthEventManager.ENABLED = true; + // Expected sign in via redirect successful Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + null, + 'http://www.example.com/#response', + 'SESSION_ID'); + // Stub instantiateOAuthSignInHandler. + stubs.replace( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName) { + return { + 'addAuthEventListener': function(handler) { + // auth1 should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config3['authDomain'], config3['apiKey'], app1.name); + assertTrue(manager.isSubscribed(auth1)); + // In this case run immediately with expected redirect event. + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { return false; }, + 'hasVolatileStorage': function() { return false; } + }; + }); + // Simulate successful verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + // Simulate Auth user successfully initialized from + // finishPopupAndRedirectSignIn. + stubs.replace( + fireauth.AuthUser, + 'initializeFromIdTokenResponse', + function(options, idTokenResponse) { + assertObjectEquals(config3, options); + assertObjectEquals(expectedTokenResponseWithIdPData, idTokenResponse); + return goog.Promise.resolve(user1); + }); + var user1 = new fireauth.AuthUser( + config3, expectedTokenResponse, accountInfo); + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.SIGN_IN + }; + asyncTestCase.waitForSignals(2); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config3['apiKey'] + ':' + appId1); + var currentUserStorageManager = + new fireauth.storage.UserManager(config3['apiKey'] + ':' + appId1); + // Simulate persistence set to session on previous page. + currentUserStorageManager.setPersistence('session'); + currentUserStorageManager.savePersistenceForRedirect().then(function() { + // Set pending redirect. + pendingRedirectManager.setPendingStatus().then(function() { + app1 = firebase.initializeApp(config3, appId1); + auth1 = app1.auth(); + // Switch persistence to local. + auth1.setPersistence('local'); + // Get redirect result should resolve with the expected user and + // credential. + auth1.getRedirectResult().then(function(result) { + // Expected result returned. + assertObjectEquals(expectedPopupResult, result); + // Even though previous persistence set via savePersistenceForRedirect + // was session, it will be overriddent by the local persistence + // explicitly called after Auth instance is initialized. + return fireauth.common.testHelper.assertUserStorage( + auth1.getStorageKey(), 'local', user1); + }).then(function() { + asyncTestCase.signal(); + }); + // Track all token changes. Confirm persistence changes are not triggered + // unexpected calls. + var uidsDetected = []; + auth1.onIdTokenChanged(function(user) { + // Keep track of all UIDs on each call. + uidsDetected.push(user && user.uid); + if (uidsDetected.length == 1) { + assertArrayEquals([user1.uid], uidsDetected); + asyncTestCase.signal(); + } else if (uidsDetected.length > 1) { + fail('Unexpected token change!'); + } + }); + }); + }); +} diff --git a/packages/auth/test/authcredential_test.js b/packages/auth/test/authcredential_test.js new file mode 100644 index 00000000000..abdc18de6b4 --- /dev/null +++ b/packages/auth/test/authcredential_test.js @@ -0,0 +1,2033 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for authcredential.js + */ + +goog.provide('fireauth.AuthCredentialTest'); + +goog.require('fireauth.Auth'); +goog.require('fireauth.AuthCredential'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.FacebookAuthProvider'); +goog.require('fireauth.GithubAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.IdToken'); +goog.require('fireauth.OAuthCredential'); +goog.require('fireauth.OAuthProvider'); +goog.require('fireauth.PhoneAuthCredential'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.TwitterAuthProvider'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.deprecation'); +goog.require('fireauth.idp.ProviderId'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.AuthCredentialTest'); + + +var mockControl; +var stubs = new goog.testing.PropertyReplacer(); +var rpcHandler = new fireauth.RpcHandler('apiKey'); +var responseForIdToken; + +var app = firebase.initializeApp({ + apiKey: 'myApiKey' +}); +var auth = new fireauth.Auth(app); + + +function setUp() { + responseForIdToken = { + 'idToken': 'ID_TOKEN' + }; + stubs.replace( + fireauth.util, + 'getCurrentUrl', + function() { + // Simulates a non http://localhost current URL. + return 'http://www.example.com'; + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertion', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyPassword', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + goog.testing.recordFunction(function(request) { + return goog.Promise.resolve(responseForIdToken); + })); + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateEmailAndPassword', + goog.testing.recordFunction(goog.Promise.resolve)); + + // Internally we should not be using any deprecated methods. + stubs.replace(fireauth.deprecation, 'log', function(message) { + fail('Deprecation message unexpectedly displayed: ' + message); + }); + mockControl = new goog.testing.MockControl(); +} + + +function tearDown() { + mockControl.$verifyAll(); + mockControl.$tearDown(); + stubs.reset(); +} + + +/** + * Initialize the IdToken mocks for parsing an expected ID token and returning + * the expected UID string. + * @param {?string|undefined} expectedIdToken The expected ID token string. + * @param {string} expectedUid The expected UID to be returned if the token is + * valid. + */ +function initializeIdTokenMocks(expectedIdToken, expectedUid) { + // Mock idToken parsing. + var tokenInstance = mockControl.createStrictMock(fireauth.IdToken); + var idTokenParse = mockControl.createMethodMock(fireauth.IdToken, 'parse'); + if (!!expectedIdToken) { + // Valid expected ID token string. + idTokenParse(expectedIdToken).$returns(tokenInstance).$once(); + // Return expected token UID when getLocalId() called. + tokenInstance.getLocalId().$returns(expectedUid); + } else { + // No ID token string provided. + idTokenParse(expectedIdToken).$returns(null).$once(); + } + mockControl.$replayAll(); +} + + +/** + * Assert that the correct request is sent to RPC handler + * verifyAssertionFor. + * @param {?Object} request The verifyAssertion request. + */ +function assertRpcHandlerVerifyAssertion(request) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.verifyAssertion.getCallCount()); + assertObjectEquals( + request, + fireauth.RpcHandler.prototype.verifyAssertion + .getLastCall() + .getArgument(0)); +} + + +/** + * Assert that the correct request is sent to RPC handler verifyPassword. + * @param {!string} email The email in verifyPassword request. + * @param {!string} password The password in verifyPassword request. + */ +function assertRpcHandlerVerifyPassword(email, password) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.verifyPassword.getCallCount()); + assertObjectEquals( + email, + fireauth.RpcHandler.prototype.verifyPassword + .getLastCall() + .getArgument(0)); + assertObjectEquals( + password, + fireauth.RpcHandler.prototype.verifyPassword + .getLastCall() + .getArgument(1)); +} + + +/** + * Assert that the correct request is sent to RPC handler + * verifyAssertionForLinking. + * @param {?Object} request The verifyAssertionForLinking request. + */ +function assertRpcHandlerVerifyAssertionForLinking(request) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.verifyAssertionForLinking.getCallCount()); + assertObjectEquals( + request, + fireauth.RpcHandler.prototype.verifyAssertionForLinking + .getLastCall() + .getArgument(0)); +} + + +/** + * Assert that the correct request is sent to RPC handler + * verifyAssertionForExisting. + * @param {?Object} request The verifyAssertionForLinking request. + */ +function assertRpcHandlerVerifyAssertionForExisting(request) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.verifyAssertionForExisting.getCallCount()); + assertObjectEquals( + request, + fireauth.RpcHandler.prototype.verifyAssertionForExisting + .getLastCall() + .getArgument(0)); +} + + +/** + * Assert that the correct request is sent to RPC handler + * updateEmailAndPassword. + * @param {!string} idToken The ID token in updateEmailAndPassword request. + * @param {!string} email The email in updateEmailAndPassword request. + * @param {!string} password The password in updateEmailAndPassword request. + */ +function assertRpcHandlerUpdateEmailAndPassword(idToken, email, password) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.updateEmailAndPassword.getCallCount()); + assertObjectEquals( + idToken, + fireauth.RpcHandler.prototype.updateEmailAndPassword + .getLastCall() + .getArgument(0)); + assertObjectEquals( + email, + fireauth.RpcHandler.prototype.updateEmailAndPassword + .getLastCall() + .getArgument(1)); + assertObjectEquals( + password, + fireauth.RpcHandler.prototype.updateEmailAndPassword + .getLastCall() + .getArgument(2)); +} + + +/** + * Test invalid Auth credential. + */ +function testInvalidCredential() { + var errorOAuth1 = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: expected 2 arguments ' + + '(the OAuth access token and secret).'); + var errorOAuth2 = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: expected 1 argument ' + + '(the OAuth access token).'); + var errorOidc = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'credential failed: must provide the ID token and/or the access ' + + 'token.'); + + try { + fireauth.FacebookAuthProvider.credential(''); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(errorOAuth2, e); + } + try { + fireauth.GithubAuthProvider.credential(''); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(errorOAuth2, e); + } + try { + fireauth.GoogleAuthProvider.credential('', ''); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(errorOidc, e); + } + try { + fireauth.TwitterAuthProvider.credential('twitterAccessToken', ''); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(errorOAuth1, e); + } + try { + new fireauth.OAuthProvider('example.com').credential('', ''); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + fireauth.common.testHelper.assertErrorEquals(errorOidc, e); + } + // Test invalid credentials from response. + // Empty response. + assertNull(fireauth.AuthProvider.getCredentialFromResponse({})); + // Missing OAuth response. + assertNull( + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'facebook.com' + })); +} + + +function testOAuthCredential() { + var provider = new fireauth.OAuthProvider('example.com'); + var authCredential = provider.credential( + 'exampleIdToken', 'exampleAccessToken'); + assertEquals('exampleIdToken', authCredential['idToken']); + assertEquals('exampleAccessToken', authCredential['accessToken']); + assertEquals('example.com', authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertObjectEquals( + { + 'oauthAccessToken': 'exampleAccessToken', + 'oauthIdToken': 'exampleIdToken', + 'providerId': 'example.com' + }, + authCredential.toPlainObject()); + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=exampleIdToken&access_token=exampleAccessToken' + + '&providerId=example.com' + }); +} + + +function testInvalidOAuthCredential() { + // Test the case where invalid arguments were passed to the OAuthCredential + // constructor. The constructor is only called internally, so the errors are + // internal errors. + try { + new fireauth.OAuthCredential('twitter.com', { + 'oauthToken': 'token' + // OAuth1 secret missing. + }); + fail('Should have triggered an invalid Auth credential error!'); + } catch (e) { + assertEquals('auth/internal-error', e.code); + } +} + + +function testOAuthProvider_constructor() { + var provider = new fireauth.OAuthProvider('example.com'); + assertTrue(provider['isOAuthProvider']); + assertEquals('example.com', provider['providerId']); + // Should not throw an error. + assertNotThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); +} + + +function testOAuthProvider_scopes() { + var provider = new fireauth.OAuthProvider('example.com'); + provider.addScope('scope1'); + assertArrayEquals(['scope1'], provider.getScopes()); + provider.addScope('scope2').addScope('scope3'); + assertArrayEquals(['scope1', 'scope2', 'scope3'], provider.getScopes()); +} + + +function testOAuthProvider_customParameters() { + var provider = new fireauth.OAuthProvider('example.com'); + // Set OAuth custom parameters. + provider.setCustomParameters({ + // Valid OAuth2/OIDC parameters. + 'login_hint': 'user@example.com', + 'prompt': 'consent', + 'include_granted_scopes': true, + // Reserved parameters below should be filtered out. + 'client_id': 'CLIENT_ID', + 'response_type': 'token', + 'scope': 'scope1', + 'redirect_uri': 'https://www.evil.com', + 'state': 'STATE' + }); + // Get custom parameters should only return the valid parameters. + assertObjectEquals({ + 'login_hint': 'user@example.com', + 'prompt': 'consent', + 'include_granted_scopes': 'true', + }, provider.getCustomParameters()); + + // Modify custom parameters. + provider.setCustomParameters({ + 'login_hint': 'user2@example.com' + }).setCustomParameters({ + 'login_hint': 'user3@example.com' + }); + // Parameters should be updated. + assertObjectEquals({ + 'login_hint': 'user3@example.com' + }, provider.getCustomParameters()); +} + + +function testOAuthProvider_chainedMethods() { + // Test that method chaining works. + var provider = new fireauth.OAuthProvider('example.com') + .addScope('scope1') + .addScope('scope2') + .setCustomParameters({ + 'login_hint': 'user@example.com' + }) + .addScope('scope3'); + assertArrayEquals(['scope1', 'scope2', 'scope3'], provider.getScopes()); + assertObjectEquals({ + 'login_hint': 'user@example.com' + }, provider.getCustomParameters()); +} + + +function testOAuthProvider_getCredentialFromResponse() { + var provider = new fireauth.OAuthProvider('example.com'); + var authCredential = provider.credential( + 'exampleIdToken', 'exampleAccessToken'); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'oauthAccessToken': 'exampleAccessToken', + 'oauthIdToken': 'exampleIdToken', + 'providerId': 'example.com' + }).toPlainObject()); +} + + +function testOAuthProvider_getCredentialFromResponse_accessTokenOnly() { + // Test Auth credential from response with access token only. + var provider = new fireauth.OAuthProvider('example.com'); + var authCredential = provider.credential( + null, 'exampleAccessToken'); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'oauthAccessToken': 'exampleAccessToken', + 'providerId': 'example.com' + }).toPlainObject()); +} + + +function testOAuthProvider_getCredentialFromResponse_idTokenOnly() { + // Test Auth credential from response with ID token only. + var provider = new fireauth.OAuthProvider('example.com'); + authCredential = provider.credential('exampleIdToken'); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'oauthIdToken': 'exampleIdToken', + 'providerId': 'example.com' + }).toPlainObject()); +} + + +function testOAuthCredential_linkToIdToken() { + var provider = new fireauth.OAuthProvider('example.com'); + var authCredential = provider.credential( + 'exampleIdToken', 'exampleAccessToken'); + authCredential.linkToIdToken(rpcHandler, 'myIdToken'); + assertRpcHandlerVerifyAssertionForLinking({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=exampleIdToken&access_token=exampleAccessToken' + + '&providerId=example.com', + 'idToken': 'myIdToken' + }); +} + + +function testOAuthCredential_matchIdTokenWithUid() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + + var provider = new fireauth.OAuthProvider('example.com'); + var authCredential = provider.credential( + 'exampleIdToken', 'exampleAccessToken'); + var p = authCredential.matchIdTokenWithUid(rpcHandler, '1234'); + assertRpcHandlerVerifyAssertionForExisting({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=exampleIdToken&access_token=exampleAccessToken' + + '&providerId=example.com' + }); + return p; +} + + +/** + * Test Facebook Auth credential. + */ +function testFacebookAuthCredential() { + assertEquals( + fireauth.idp.ProviderId.FACEBOOK, + fireauth.FacebookAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.FacebookAuthProvider.credential( + 'facebookAccessToken'); + assertEquals('facebookAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertObjectEquals( + { + 'oauthAccessToken': 'facebookAccessToken', + 'providerId': fireauth.idp.ProviderId.FACEBOOK + }, + authCredential.toPlainObject()); + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=facebookAccessToken&providerId=' + + fireauth.idp.ProviderId.FACEBOOK + }); + var provider = new fireauth.FacebookAuthProvider(); + // Should not throw an error. + assertNotThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + assertArrayEquals([], provider.getScopes()); + provider.addScope('scope1'); + assertArrayEquals(['scope1'], provider.getScopes()); + provider.addScope('scope2').addScope('scope3'); + // Add duplicate scope. + provider.addScope('scope1'); + assertArrayEquals(['scope1', 'scope2', 'scope3'], provider.getScopes()); + assertEquals(fireauth.idp.ProviderId.FACEBOOK, provider['providerId']); + assertTrue(provider['isOAuthProvider']); + // Set OAuth custom parameters. + provider.setCustomParameters({ + // Valid Facebook OAuth 2.0 parameters. + 'display': 'popup', + 'auth_type': 'rerequest', + 'locale': 'pt_BR', + // Reserved parameters below should be filtered out. + 'client_id': 'CLIENT_ID', + 'response_type': 'token', + 'scope': 'scope1', + 'redirect_uri': 'https://www.evil.com', + 'state': 'STATE' + }); + // Get custom parameters should only return the valid parameters. + assertObjectEquals({ + 'display': 'popup', + 'auth_type': 'rerequest', + 'locale': 'pt_BR' + }, provider.getCustomParameters()); + // Modify custom parameters. + provider.setCustomParameters({ + // Valid Facebook OAuth 2.0 parameters. + 'auth_type': 'rerequest' + }); + // Parameters should be updated. + assertObjectEquals({ + 'auth_type': 'rerequest' + }, provider.getCustomParameters()); + // Test Auth credential from response. + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'facebook.com', + 'oauthAccessToken': 'facebookAccessToken' + }).toPlainObject()); +} + + +function testFacebookAuthProvider_localization() { + var provider = new fireauth.FacebookAuthProvider(); + // Set default language on provider. + provider.setDefaultLanguage('fr_FR'); + // Default language should be set as custom param. + assertObjectEquals({ + 'locale': 'fr_FR' + }, provider.getCustomParameters()); + // Set some other parameters without the provider's language. + provider.setCustomParameters({ + 'display': 'popup', + 'client_id': 'CLIENT_ID', + 'lang': 'de', + 'hl': 'de' + }); + // The expected parameters include the provider's default language. + assertObjectEquals({ + 'display': 'popup', + 'lang': 'de', + 'hl': 'de', + 'locale': 'fr_FR' + }, provider.getCustomParameters()); + // Set custom parameters with the provider's language. + provider.setCustomParameters({ + 'locale': 'pt_BR', + }); + // Default language should be overwritten. + assertObjectEquals({ + 'locale': 'pt_BR' + }, provider.getCustomParameters()); + // Even after setting the default language, the non-default should still + // apply. + provider.setDefaultLanguage('fr_FR'); + assertObjectEquals({ + 'locale': 'pt_BR' + }, provider.getCustomParameters()); + // Update custom parameters to not include a language field. + provider.setCustomParameters({}); + // Default should apply again. + assertObjectEquals({ + 'locale': 'fr_FR' + }, provider.getCustomParameters()); + // Set default language to null. + provider.setDefaultLanguage(null); + // No language should be returned anymore. + assertObjectEquals({}, provider.getCustomParameters()); +} + + +function testFacebookAuthCredential_alternateConstructor() { + var authCredential = fireauth.FacebookAuthProvider.credential( + {'accessToken': 'facebookAccessToken'}); + assertEquals('facebookAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + assertObjectEquals( + { + 'oauthAccessToken': 'facebookAccessToken', + 'providerId': fireauth.idp.ProviderId.FACEBOOK + }, + authCredential.toPlainObject()); + + // Missing token. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + var error = assertThrows(function() { + fireauth.FacebookAuthProvider.credential({}); + }); + assertEquals(expectedError.code, error.code); +} + + +function testFacebookAuthProvider_chainedMethods() { + // Test that method chaining works. + var provider = new fireauth.FacebookAuthProvider() + .addScope('scope1') + .addScope('scope2') + .setCustomParameters({ + 'locale': 'pt_BR' + }) + .addScope('scope3'); + assertArrayEquals(['scope1', 'scope2', 'scope3'], provider.getScopes()); + assertObjectEquals({ + 'locale': 'pt_BR' + }, provider.getCustomParameters()); +} + + +/** + * Test Facebook Auth credential with non HTTP request. + */ +function testFacebookAuthCredential_nonHttp() { + // Non http or https environment. + stubs.replace( + fireauth.util, + 'getCurrentUrl', + function() {return 'chrome-extension://SOME_LONG_ID';}); + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() {return 'chrome-extension:';}); + assertEquals( + fireauth.idp.ProviderId.FACEBOOK, + fireauth.FacebookAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.FacebookAuthProvider.credential( + 'facebookAccessToken'); + assertEquals('facebookAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.FACEBOOK, authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertObjectEquals( + { + 'oauthAccessToken': 'facebookAccessToken', + 'providerId': fireauth.idp.ProviderId.FACEBOOK + }, + authCredential.toPlainObject()); + // http://localhost should be used instead of the real current URL. + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=facebookAccessToken&providerId=' + + fireauth.idp.ProviderId.FACEBOOK + }); +} + + +/** + * Test GitHub Auth credential. + */ +function testGithubAuthCredential() { + assertEquals( + fireauth.idp.ProviderId.GITHUB, + fireauth.GithubAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.GithubAuthProvider.credential( + 'githubAccessToken'); + assertEquals('githubAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.GITHUB, authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertObjectEquals( + { + 'oauthAccessToken': 'githubAccessToken', + 'providerId': fireauth.idp.ProviderId.GITHUB + }, + authCredential.toPlainObject()); + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=githubAccessToken&providerId=' + + fireauth.idp.ProviderId.GITHUB + }); + var provider = new fireauth.GithubAuthProvider(); + // Should not throw an error. + assertNotThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + assertArrayEquals([], provider.getScopes()); + provider.addScope('scope1'); + assertArrayEquals(['scope1'], provider.getScopes()); + provider.addScope('scope2'); + assertArrayEquals(['scope1', 'scope2'], provider.getScopes()); + assertEquals(fireauth.idp.ProviderId.GITHUB, provider['providerId']); + assertTrue(provider['isOAuthProvider']); + // Set OAuth custom parameters. + provider.setCustomParameters({ + // Valid GitHub OAuth 2.0 parameters. + 'allow_signup': false, + // Reserved parameters below should be filtered out. + 'client_id': 'CLIENT_ID', + 'response_type': 'token', + 'scope': 'scope1', + 'redirect_uri': 'https://www.evil.com', + 'state': 'STATE' + }); + // Get custom parameters should only return the valid parameters. + assertObjectEquals({ + 'allow_signup': 'false' + }, provider.getCustomParameters()); + // Modify custom parameters. + provider.setCustomParameters({ + // Valid GitHub OAuth 2.0 parameters. + 'allow_signup': true + }); + // Parameters should be updated. + assertObjectEquals({ + 'allow_signup': 'true' + }, provider.getCustomParameters()); + // Test Auth credential from response. + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'github.com', + 'oauthAccessToken': 'githubAccessToken' + }).toPlainObject()); +} + + +function testGithubAuthProvider_localization() { + var provider = new fireauth.GithubAuthProvider(); + // Set default language on provider. + provider.setDefaultLanguage('fr'); + // Default language should be ignored as Github doesn't support localization. + assertObjectEquals({}, provider.getCustomParameters()); + // Set all possible language parameters. + provider.setCustomParameters({ + 'locale': 'ar', + 'hl': 'ar', + 'lang': 'ar' + }); + // All language parameters just piped through without the default. + assertObjectEquals({ + 'locale': 'ar', + 'hl': 'ar', + 'lang': 'ar' + }, provider.getCustomParameters()); +} + + +function testOAuthProvider_localization() { + var provider = new fireauth.OAuthProvider('yahoo.com'); + // Set default language on provider. + provider.setDefaultLanguage('fr'); + // Default language should be ignored as generic providers don't support + // default localization. + assertObjectEquals({}, provider.getCustomParameters()); + // Set all possible language parameters. + provider.setCustomParameters({ + 'locale': 'ar', + 'hl': 'ar', + 'lang': 'ar' + }); + // All language parameters just piped through without the default. + assertObjectEquals({ + 'locale': 'ar', + 'hl': 'ar', + 'lang': 'ar' + }, provider.getCustomParameters()); +} + + +function testGithubAuthCredential_alternateConstructor() { + var authCredential = fireauth.GithubAuthProvider.credential( + {'accessToken': 'githubAccessToken'}); + assertEquals('githubAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.GITHUB, authCredential['providerId']); + assertObjectEquals( + { + 'oauthAccessToken': 'githubAccessToken', + 'providerId': fireauth.idp.ProviderId.GITHUB + }, + authCredential.toPlainObject()); + + // Missing token. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + var error = assertThrows(function() { + fireauth.GithubAuthProvider.credential({}); + }); + assertEquals(expectedError.code, error.code); +} + + +function testGithubAuthProvider_chainedMethods() { + // Test that method chaining works. + var provider = new fireauth.GithubAuthProvider() + .addScope('scope1') + .addScope('scope2') + .setCustomParameters({ + 'allow_signup': false + }) + .addScope('scope3'); + assertArrayEquals(['scope1', 'scope2', 'scope3'], provider.getScopes()); + assertObjectEquals({ + 'allow_signup': 'false' + }, provider.getCustomParameters()); +} + + +function testGithubAuthCredential_linkToIdToken() { + var authCredential = fireauth.GithubAuthProvider.credential( + 'githubAccessToken'); + authCredential.linkToIdToken(rpcHandler, 'myIdToken'); + assertRpcHandlerVerifyAssertionForLinking({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=githubAccessToken&providerId=' + + fireauth.idp.ProviderId.GITHUB, + 'idToken': 'myIdToken' + }); +} + + +function testGithubAuthCredential_matchIdTokenWithUid() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + + var authCredential = fireauth.GithubAuthProvider.credential( + 'githubAccessToken'); + var p = authCredential.matchIdTokenWithUid(rpcHandler, '1234'); + assertRpcHandlerVerifyAssertionForExisting({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=githubAccessToken&providerId=' + + fireauth.idp.ProviderId.GITHUB, + }); + return p; +} + + +/** + * Test Google Auth credential. + */ +function testGoogleAuthCredential() { + assertEquals( + fireauth.idp.ProviderId.GOOGLE, + fireauth.GoogleAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + assertEquals('googleIdToken', authCredential['idToken']); + assertEquals('googleAccessToken', authCredential['accessToken']); + assertEquals(fireauth.idp.ProviderId.GOOGLE, authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertObjectEquals( + { + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE + }, + authCredential.toPlainObject()); + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToken&provi' + + 'derId=' + fireauth.idp.ProviderId.GOOGLE + }); + var provider = new fireauth.GoogleAuthProvider(); + // Should not throw an error. + assertNotThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + assertArrayEquals(['profile'], provider.getScopes()); + provider.addScope('scope1'); + assertArrayEquals(['profile', 'scope1'], provider.getScopes()); + provider.addScope('scope2'); + assertArrayEquals(['profile', 'scope1', 'scope2'], provider.getScopes()); + assertEquals(fireauth.idp.ProviderId.GOOGLE, provider['providerId']); + assertTrue(provider['isOAuthProvider']); + // Set OAuth custom parameters. + provider.setCustomParameters({ + // Valid Google OAuth 2.0 parameters. + 'login_hint': 'user@example.com', + 'hd': 'example.com', + 'hl': 'fr', + 'prompt': 'consent', + 'include_granted_scopes': true, + // Reserved parameters below should be filtered out. + 'client_id': 'CLIENT_ID', + 'response_type': 'token', + 'scope': 'scope1', + 'redirect_uri': 'https://www.evil.com', + 'state': 'STATE' + }); + // Get custom parameters should only return the valid parameters. + assertObjectEquals({ + 'login_hint': 'user@example.com', + 'hd': 'example.com', + 'hl': 'fr', + 'prompt': 'consent', + 'include_granted_scopes': 'true' + }, provider.getCustomParameters()); + // Modify custom parameters. + provider.setCustomParameters({ + // Valid Google OAuth 2.0 parameters. + 'login_hint': 'user2@example.com' + }); + // Parameters should be updated. + assertObjectEquals({ + 'login_hint': 'user2@example.com' + }, provider.getCustomParameters()); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'google.com', + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken' + }).toPlainObject()); + // Test Auth credential from response with access token only. + authCredential = fireauth.GoogleAuthProvider.credential(null, + 'googleAccessToken'); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'google.com', + 'oauthAccessToken': 'googleAccessToken' + }).toPlainObject()); + // Test Auth credential from response with ID token only. + authCredential = fireauth.GoogleAuthProvider.credential('googleIdToken'); + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'google.com', + 'oauthIdToken': 'googleIdToken' + }).toPlainObject()); +} + + +function testGoogleAuthProvider_localization() { + var provider = new fireauth.GoogleAuthProvider(); + // Set default language on provider. + provider.setDefaultLanguage('fr'); + // Default language should be set as custom param. + assertObjectEquals({ + 'hl': 'fr' + }, provider.getCustomParameters()); + // Set some other parameters without the provider's language. + provider.setCustomParameters({ + 'prompt': 'consent', + 'client_id': 'CLIENT_ID', + 'lang': 'ar', + 'locale': 'ar' + }); + // The expected parameters include the provider's default language. + assertObjectEquals({ + 'prompt': 'consent', + 'hl': 'fr', + 'lang': 'ar', + 'locale': 'ar' + }, provider.getCustomParameters()); + // Set custom parameters with the provider's language. + provider.setCustomParameters({ + 'hl': 'de' + }); + // Default language should be overwritten. + assertObjectEquals({ + 'hl': 'de' + }, provider.getCustomParameters()); + // Even after setting the default language, the non-default should still + // apply. + provider.setDefaultLanguage('fr'); + assertObjectEquals({ + 'hl': 'de' + }, provider.getCustomParameters()); + // Update custom parameters to not include a language field. + provider.setCustomParameters({}); + // Default should apply again. + assertObjectEquals({ + 'hl': 'fr' + }, provider.getCustomParameters()); + // Set default language to null. + provider.setDefaultLanguage(null); + // No language should be returned anymore. + assertObjectEquals({}, provider.getCustomParameters()); +} + + +function testGoogleAuthProvider_chainedMethods() { + // Test that method chaining works. + var provider = new fireauth.GoogleAuthProvider() + .addScope('scope1') + .addScope('scope2') + .setCustomParameters({ + 'login_hint': 'user@example.com' + }) + .addScope('scope3'); + assertArrayEquals(['profile', 'scope1', 'scope2', 'scope3'], + provider.getScopes()); + assertObjectEquals({ + 'login_hint': 'user@example.com' + }, provider.getCustomParameters()); +} + + +function testGoogleAuthCredential_idTokenConstructor() { + var authCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken'); + assertEquals('googleIdToken', authCredential['idToken']); + assertUndefined(authCredential['accessToken']); +} + + +function testGoogleAuthCredential_accessTokenConstructor() { + var authCredential = fireauth.GoogleAuthProvider.credential( + null, 'googleAccessToken'); + assertEquals('googleAccessToken', authCredential['accessToken']); + assertUndefined(authCredential['idToken']); +} + + +function testGoogleAuthCredential_idAndAccessTokenConstructor() { + var authCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + assertEquals('googleIdToken', authCredential['idToken']); + assertEquals('googleAccessToken', authCredential['accessToken']); +} + + +function testGoogleAuthCredential_alternateConstructor() { + // Only ID token. + var authCredentialIdToken = fireauth.GoogleAuthProvider.credential( + {'idToken': 'googleIdToken'}); + assertEquals('googleIdToken', authCredentialIdToken['idToken']); + assertUndefined(authCredentialIdToken['accessToken']); + assertObjectEquals({ + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE + }, authCredentialIdToken.toPlainObject()); + + // Only access token. + var authCredentialAccessToken = fireauth.GoogleAuthProvider.credential( + {'accessToken': 'googleAccessToken'}); + assertEquals('googleAccessToken', authCredentialAccessToken['accessToken']); + assertUndefined(authCredentialAccessToken['idToken']); + assertObjectEquals({ + 'oauthAccessToken': 'googleAccessToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE + }, authCredentialAccessToken.toPlainObject()); + + // Both tokens. + var authCredentialBoth = fireauth.GoogleAuthProvider.credential( + {'idToken': 'googleIdToken', 'accessToken': 'googleAccessToken'}); + assertEquals('googleAccessToken', authCredentialBoth['accessToken']); + assertEquals('googleIdToken', authCredentialBoth['idToken']); + assertObjectEquals({ + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken', + 'providerId': fireauth.idp.ProviderId.GOOGLE + }, authCredentialBoth.toPlainObject()); + + // Neither token. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + var error = assertThrows(function() { + fireauth.GoogleAuthProvider.credential({}); + }); + assertEquals(expectedError.code, error.code); +} + + +function testGoogleAuthCredential_linkToIdToken() { + var authCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + authCredential.linkToIdToken(rpcHandler, 'myIdToken'); + assertRpcHandlerVerifyAssertionForLinking({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToken&provi' + + 'derId=' + fireauth.idp.ProviderId.GOOGLE, + 'idToken': 'myIdToken' + }); +} + + +function testGoogleAuthCredential_matchIdTokenWithUid() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + + var authCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + var p = authCredential.matchIdTokenWithUid(rpcHandler, '1234'); + assertRpcHandlerVerifyAssertionForExisting({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'id_token=googleIdToken&access_token=googleAccessToken&provi' + + 'derId=' + fireauth.idp.ProviderId.GOOGLE + }); + return p; +} + + +/** + * Test Twitter Auth credential. + */ +function testTwitterAuthCredential() { + assertEquals( + fireauth.idp.ProviderId.TWITTER, + fireauth.TwitterAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.TwitterAuthProvider.credential( + 'twitterOauthToken', 'twitterOauthTokenSecret'); + assertEquals('twitterOauthToken', authCredential['accessToken']); + assertEquals('twitterOauthTokenSecret', authCredential['secret']); + assertEquals(fireauth.idp.ProviderId.TWITTER, authCredential['providerId']); + assertObjectEquals( + { + 'oauthAccessToken': 'twitterOauthToken', + 'oauthTokenSecret': 'twitterOauthTokenSecret', + 'providerId': fireauth.idp.ProviderId.TWITTER + }, + authCredential.toPlainObject()); + authCredential.getIdTokenProvider(rpcHandler); + assertRpcHandlerVerifyAssertion({ + // requestUri should be http://localhost regardless of current URL. + 'requestUri': 'http://localhost', + 'postBody': 'access_token=twitterOauthToken&oauth_token_secret=twitter' + + 'OauthTokenSecret&providerId=' + + fireauth.idp.ProviderId.TWITTER + }); + var provider = new fireauth.TwitterAuthProvider(); + // Should not throw an error. + assertNotThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + assertEquals(fireauth.idp.ProviderId.TWITTER, provider['providerId']); + assertTrue(provider['isOAuthProvider']); + // Set OAuth custom parameters. + provider.setCustomParameters({ + 'lang': 'es', + // Reserved parameters below should be filtered out. + 'oauth_consumer_key': 'OAUTH_CONSUMER_KEY', + 'oauth_nonce': 'OAUTH_NONCE', + 'oauth_signature': 'OAUTH_SIGNATURE', + 'oauth_signature_method': 'HMAC-SHA1', + 'oauth_timestamp': '1318622958', + 'oauth_token': 'OAUTH_TOKEN', + 'oauth_version': '1.0' + }); + // Get custom parameters should only return the valid parameters. + assertObjectEquals({ + 'lang': 'es' + }, provider.getCustomParameters()); + // Modify custom parameters. + provider.setCustomParameters({ + 'lang': 'en' + }); + // Parameters should be updated. + assertObjectEquals({ + 'lang': 'en' + }, provider.getCustomParameters()); + // Test Auth credential from response. + assertObjectEquals( + authCredential.toPlainObject(), + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'twitter.com', + 'oauthAccessToken': 'twitterOauthToken', + 'oauthTokenSecret': 'twitterOauthTokenSecret' + }).toPlainObject()); +} + + +function testTwitterAuthProvider_localization() { + var provider = new fireauth.TwitterAuthProvider(); + // Set default language on provider. + provider.setDefaultLanguage('fr'); + // Default language should be set as custom param. + assertObjectEquals({ + 'lang': 'fr' + }, provider.getCustomParameters()); + // Set some other parameters without the provider's language. + provider.setCustomParameters({ + 'foo': 'bar', + 'oauth_consumer_key': 'OAUTH_CONSUMER_KEY', + 'locale': 'ar', + 'hl': 'ar' + }); + // The expected parameters include the provider's default language. + assertObjectEquals({ + 'foo': 'bar', + 'lang': 'fr', + 'locale': 'ar', + 'hl': 'ar' + }, provider.getCustomParameters()); + // Set custom parameters with the provider's language. + provider.setCustomParameters({ + 'lang': 'de', + }); + // Default language should be overwritten. + assertObjectEquals({ + 'lang': 'de' + }, provider.getCustomParameters()); + // Even after setting the default language, the non-default should still + // apply. + provider.setDefaultLanguage('fr'); + assertObjectEquals({ + 'lang': 'de' + }, provider.getCustomParameters()); + // Update custom parameters to not include a language field. + provider.setCustomParameters({}); + // Default should apply again. + assertObjectEquals({ + 'lang': 'fr' + }, provider.getCustomParameters()); + // Set default language to null. + provider.setDefaultLanguage(null); + // No language should be returned anymore. + assertObjectEquals({}, provider.getCustomParameters()); +} + + +function testTwitterAuthProvider_chainedMethods() { + // Test that method chaining works. + var provider = new fireauth.TwitterAuthProvider() + .setCustomParameters({ + 'lang': 'en' + }) + .setCustomParameters({ + 'lang': 'es' + }); + assertObjectEquals({ + 'lang': 'es' + }, provider.getCustomParameters()); +} + + +function testTwitterAuthCredential_tokenSecretConstructor() { + var authCredential = fireauth.TwitterAuthProvider.credential( + 'twitterOauthToken', 'twitterOauthTokenSecret'); + assertEquals('twitterOauthToken', authCredential['accessToken']); + assertEquals('twitterOauthTokenSecret', authCredential['secret']); +} + + +function testTwitterAuthCredential_alternateConstructor() { + var authCredential = fireauth.TwitterAuthProvider.credential({ + 'oauthToken': 'twitterOauthToken', + 'oauthTokenSecret': 'twitterOauthTokenSecret' + }); + assertEquals('twitterOauthToken', authCredential['accessToken']); + assertEquals('twitterOauthTokenSecret', authCredential['secret']); + assertEquals(fireauth.idp.ProviderId.TWITTER, authCredential['providerId']); + assertObjectEquals({ + 'oauthAccessToken': 'twitterOauthToken', + 'oauthTokenSecret': 'twitterOauthTokenSecret', + 'providerId': fireauth.idp.ProviderId.TWITTER + }, + authCredential.toPlainObject()); + + // Missing token or secret should be an error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + var error = assertThrows(function() { + fireauth.TwitterAuthProvider.credential({ + 'oauthToken': 'twitterOauthToken' + }); + }); + assertEquals(expectedError.code, error.code); + + error = assertThrows(function() { + fireauth.TwitterAuthProvider.credential({ + 'oauthTokenSecret': 'twitterOauthTokenSecret' + }); + }); + assertEquals(expectedError.code, error.code); +} + + +/** + * Test Email Password Auth credential. + */ +function testEmailAuthCredential() { + assertEquals( + fireauth.idp.ProviderId.PASSWORD, + fireauth.EmailAuthProvider['PROVIDER_ID']); + var authCredential = fireauth.EmailAuthProvider.credential( + 'user@example.com', 'password'); + assertObjectEquals( + { + 'email': 'user@example.com', + 'password': 'password' + }, + authCredential.toPlainObject()); + assertEquals(fireauth.idp.ProviderId.PASSWORD, authCredential['providerId']); + authCredential.getIdTokenProvider(rpcHandler); + assertRpcHandlerVerifyPassword('user@example.com', 'password'); + var provider = new fireauth.EmailAuthProvider(); + // Should throw an invalid OAuth provider error. + var error = assertThrows(function() { + fireauth.AuthProvider.checkIfOAuthSupported(provider); + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + assertEquals(fireauth.idp.ProviderId.PASSWORD, provider['providerId']); + assertFalse(provider['isOAuthProvider']); +} + + +function testEmailAuthCredential_linkToIdToken() { + var authCredential = fireauth.EmailAuthProvider.credential( + 'foo@bar.com', '123123'); + authCredential.linkToIdToken(rpcHandler, 'myIdToken'); + assertRpcHandlerUpdateEmailAndPassword( + 'myIdToken', 'foo@bar.com', '123123'); +} + + +function testEmailAuthCredential_matchIdTokenWithUid() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + var authCredential = fireauth.EmailAuthProvider.credential( + 'user@example.com', 'password'); + var p = authCredential.matchIdTokenWithUid(rpcHandler, '1234'); + assertRpcHandlerVerifyPassword('user@example.com', 'password'); + return p; +} + + +function testPhoneAuthProvider() { + assertEquals(fireauth.PhoneAuthProvider['PROVIDER_ID'], + fireauth.idp.ProviderId.PHONE); + var provider = new fireauth.PhoneAuthProvider(auth); + assertEquals(provider['providerId'], fireauth.idp.ProviderId.PHONE); +} + + +function testPhoneAuthProvider_noAuth() { + stubs.set(firebase, 'auth', function() { + throw new Error('app not initialized'); + }); + var error = assertThrows(function() { + new fireauth.PhoneAuthProvider(); + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + assertEquals(expectedError.code, error.code); +} + + +function testVerifyPhoneNumber() { + var phoneNumber = '+16505550101'; + var recaptchaToken = 'theRecaptchaToken'; + var verificationId = 'theVerificationId'; + + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(recaptchaToken); + } + }; + var expectedSendVerificationCodeRequest = { + 'phoneNumber': phoneNumber, + 'recaptchaToken': recaptchaToken + }; + var auth = mockControl.createStrictMock(fireauth.Auth); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + auth.getRpcHandler().$once().$returns(rpcHandler); + rpcHandler.sendVerificationCode(expectedSendVerificationCodeRequest) + .$once() + .$returns(goog.Promise.resolve(verificationId)); + + mockControl.$replayAll(); + + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber(phoneNumber, applicationVerifier) + .then(function(actualVerificationId) { + assertEquals(verificationId, actualVerificationId); + }); +} + + +function testVerifyPhoneNumber_reset_sendVerificationCodeTwice() { + var phoneNumber = '+16505550101'; + var recaptchaToken1 = 'theRecaptchaToken1'; + var recaptchaToken2 = 'theRecaptchaToken2'; + var verificationId1 = 'theVerificationId1'; + var verificationId2 = 'theVerificationId2'; + var verify = mockControl.createFunctionMock('verify'); + var reset = mockControl.createFunctionMock('reset'); + verify().$once().$returns(goog.Promise.resolve(recaptchaToken1)); + reset().$once(); + verify().$once().$returns(goog.Promise.resolve(recaptchaToken2)); + reset().$once(); + + // Everytime after reset being called, verifier returns a new token. + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': verify, + 'reset': reset + }; + var expectedSendVerificationCodeRequest1 = { + 'phoneNumber': phoneNumber, + 'recaptchaToken': recaptchaToken1 + }; + var expectedSendVerificationCodeRequest2 = { + 'phoneNumber': phoneNumber, + 'recaptchaToken': recaptchaToken2 + }; + + var auth = mockControl.createStrictMock(fireauth.Auth); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + auth.getRpcHandler().$times(2).$returns(rpcHandler); + rpcHandler.sendVerificationCode(expectedSendVerificationCodeRequest1) + .$once() + .$returns(goog.Promise.resolve(verificationId1)); + rpcHandler.sendVerificationCode(expectedSendVerificationCodeRequest2) + .$once() + .$returns(goog.Promise.resolve(verificationId2)); + + mockControl.$replayAll(); + + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber(phoneNumber, applicationVerifier) + .then(function(actualVerificationId1) { + assertEquals(verificationId1, actualVerificationId1); + return provider.verifyPhoneNumber(phoneNumber, applicationVerifier) + .then(function(actualVerificationId2) { + assertEquals(verificationId2, actualVerificationId2); + }); + }); +} + + +function testVerifyPhoneNumber_reset_sendVerificationCodeError() { + var phoneNumber = '+16505550101'; + var recaptchaToken = 'theRecaptchaToken'; + var expectedError = 'something bad happened!!!'; + + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(recaptchaToken); + }, + 'reset': goog.testing.recordFunction(function() {}) + }; + var expectedSendVerificationCodeRequest = { + 'phoneNumber': phoneNumber, + 'recaptchaToken': recaptchaToken + }; + var auth = mockControl.createStrictMock(fireauth.Auth); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + auth.getRpcHandler().$once().$returns(rpcHandler); + rpcHandler.sendVerificationCode(expectedSendVerificationCodeRequest) + .$once() + .$does(function() { + return goog.Promise.reject(expectedError); + }); + + mockControl.$replayAll(); + + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber(phoneNumber, applicationVerifier) + .then(fail, function(error) { + assertEquals(1, applicationVerifier.reset.getCallCount()); + assertEquals(expectedError, error); + }); +} + + +function testVerifyPhoneNumber_defaultAuthInstance() { + // Tests that verifyPhoneNumber works when using the default Auth instance. + var phoneNumber = '+16505550101'; + var recaptchaToken = 'theRecaptchaToken'; + var verificationId = 'theVerificationId'; + + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(recaptchaToken); + } + }; + var expectedSendVerificationCodeRequest = { + 'phoneNumber': phoneNumber, + 'recaptchaToken': recaptchaToken + }; + var auth = mockControl.createStrictMock(fireauth.Auth); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + stubs.set(firebase, 'auth', function() { + return auth; + }); + auth.getRpcHandler().$once().$returns(rpcHandler); + rpcHandler.sendVerificationCode(expectedSendVerificationCodeRequest) + .$once() + .$returns(goog.Promise.resolve(verificationId)); + + mockControl.$replayAll(); + + var provider = new fireauth.PhoneAuthProvider(); + return provider.verifyPhoneNumber(phoneNumber, applicationVerifier) + .then(function(actualVerificationId) { + assertEquals(verificationId, actualVerificationId); + }); +} + + +function testVerifyPhoneNumber_notRecaptcha() { + var applicationVerifier = { + // The ApplicationVerifier type is not supported. + 'type': 'some-unsupported-type', + 'verify': function() { + return goog.Promise.resolve('some assertion'); + } + }; + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber('+16505550101', applicationVerifier) + .then(fail, function(error) { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + assertEquals(expectedError.code, error.code); + }); +} + + +function testVerifyPhoneNumber_verifierReturnsUnexpectedType() { + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': function() { + // The assertion is not a string. + return goog.Promise.resolve(12345); + } + }; + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber('+16505550101', applicationVerifier) + .then(fail, function(error) { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR); + assertEquals(expectedError.code, error.code); + }); +} + + +function testVerifyPhoneNumber_verifierThrowsError() { + var expectedError = 'something bad happened!!!'; + var applicationVerifier = { + 'type': 'recaptcha', + 'verify': function() { + // The verifier throws its own error. + return goog.Promise.reject(expectedError); + } + }; + var provider = new fireauth.PhoneAuthProvider(auth); + return provider.verifyPhoneNumber('+16505550101', applicationVerifier) + .then(fail, function(error) { + assertEquals(expectedError, error); + }); +} + + +function testPhoneAuthCredential_validateArguments() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var error; + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({verificationId: 'foo'}); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({verificationCode: 'foo'}); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({temporaryProof: 'foo'}); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({phoneNumber: 'foo'}); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({ + verificationCode: 'foo', + phoneNumber: 'bar' + }); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + + error = assertThrows(function() { + fireauth.PhoneAuthCredential({ + verificationCode: 'foo', + temporaryProof: 'bar' + }); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} + + +function testPhoneAuthCredential() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + + var credential = fireauth.PhoneAuthProvider.credential( + verificationId, verificationCode); + assertEquals(fireauth.idp.ProviderId.PHONE, credential['providerId']); + assertObjectEquals({ + 'providerId': fireauth.idp.ProviderId.PHONE, + 'verificationId': verificationId, + 'verificationCode': verificationCode + }, credential.toPlainObject()); + + assertNull( + fireauth.AuthProvider.getCredentialFromResponse({ + 'providerId': 'phone' + })); +} + + +function testPhoneAuthCredential_missingFieldsErrors() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + + var error = assertThrows(function() { + fireauth.PhoneAuthProvider.credential('', verificationCode); + }); + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_SESSION_INFO), + error); + + error = assertThrows(function() { + fireauth.PhoneAuthProvider.credential(verificationId, ''); + }); + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE), + error); + + error = assertThrows(function() { + fireauth.PhoneAuthProvider.credential('', ''); + }); + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_SESSION_INFO), + error); +} + + +function testPhoneAuthCredential_getIdTokenProvider() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + + var expectedVerifyPhoneNumberRequest = { + 'sessionInfo': verificationId, + 'code': verificationCode + }; + var verifyPhoneNumberResponse = { + 'idToken': 'myIdToken', + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': 'myLocalId', + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumber(expectedVerifyPhoneNumberRequest).$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.PhoneAuthProvider.credential( + verificationId, verificationCode); + return credential.getIdTokenProvider(rpcHandler) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_linkToIdToken() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + var idToken = 'myIdToken'; + + var expectedVerifyPhoneNumberRequest = { + 'idToken': idToken, + 'sessionInfo': verificationId, + 'code': verificationCode + }; + var verifyPhoneNumberResponse = { + 'idToken': 'myNewIdToken', + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': 'myLocalId', + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForLinking(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.PhoneAuthProvider.credential( + verificationId, verificationCode); + return credential.linkToIdToken(rpcHandler, idToken) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_matchIdTokenWithUid() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + var idToken = 'myIdToken'; + var uid = '1234'; + initializeIdTokenMocks(idToken, uid); + + var expectedVerifyPhoneNumberRequest = { + 'sessionInfo': verificationId, + 'code': verificationCode + }; + var verifyPhoneNumberResponse = { + 'idToken': idToken, + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': uid, + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForExisting(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.PhoneAuthProvider.credential( + verificationId, verificationCode); + return credential.matchIdTokenWithUid(rpcHandler, uid) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_matchIdTokenWithUid_mismatch() { + var verificationId = 'theVerificationId'; + var verificationCode = 'theVerificationCode'; + var idToken = 'myIdToken'; + var passedUid = '5678'; + var uid = '1234'; + initializeIdTokenMocks(idToken, uid); + + var expectedVerifyPhoneNumberRequest = { + 'sessionInfo': verificationId, + 'code': verificationCode + }; + var verifyPhoneNumberResponse = { + 'idToken': idToken, + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': uid, + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForExisting(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.PhoneAuthProvider.credential( + verificationId, verificationCode); + return credential.matchIdTokenWithUid(rpcHandler, passedUid) + .then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH), + error); + }); +} + + +function testPhoneAuthCredential_temporaryProof() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + + assertEquals(fireauth.idp.ProviderId.PHONE, credential['providerId']); + assertObjectEquals({ + 'providerId': fireauth.idp.ProviderId.PHONE, + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }, credential.toPlainObject()); +} + + +function testPhoneAuthCredential_temporaryProof_getIdTokenProvider() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + + var expectedVerifyPhoneNumberRequest = { + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }; + var verifyPhoneNumberResponse = { + 'idToken': 'myIdToken', + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': 'myLocalId', + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumber(expectedVerifyPhoneNumberRequest).$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + return credential.getIdTokenProvider(rpcHandler) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_temporaryProof_linkToIdToken() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var idToken = 'myIdToken'; + + var expectedVerifyPhoneNumberRequest = { + 'idToken': idToken, + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }; + var verifyPhoneNumberResponse = { + 'idToken': 'myNewIdToken', + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': 'myLocalId', + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForLinking(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + return credential.linkToIdToken(rpcHandler, idToken) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_temporaryProof_matchIdTokenWithUid() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var idToken = 'myIdToken'; + var uid = '1234'; + initializeIdTokenMocks(idToken, uid); + + var expectedVerifyPhoneNumberRequest = { + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }; + var verifyPhoneNumberResponse = { + 'idToken': idToken, + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': uid, + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForExisting(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + return credential.matchIdTokenWithUid(rpcHandler, uid) + .then(function(response) { + assertObjectEquals(verifyPhoneNumberResponse, response); + }); +} + + +function testPhoneAuthCredential_temporaryProof_matchIdTokenWithUid_mismatch() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var idToken = 'myIdToken'; + var passedUid = '5678'; + var uid = '1234'; + initializeIdTokenMocks(idToken, uid); + + var expectedVerifyPhoneNumberRequest = { + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }; + var verifyPhoneNumberResponse = { + 'idToken': idToken, + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': uid, + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + rpcHandler.verifyPhoneNumberForExisting(expectedVerifyPhoneNumberRequest) + .$once() + .$returns(goog.Promise.resolve(verifyPhoneNumberResponse)); + + mockControl.$replayAll(); + + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + return credential.matchIdTokenWithUid(rpcHandler, passedUid) + .then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH), + error); + }); +} + + +function testVerifyTokenResponseUid_match() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + return fireauth.AuthCredential.verifyTokenResponseUid( + goog.Promise.resolve(responseForIdToken), '1234'); +} + + +function testVerifyTokenResponseUid_idTokenNotFound_mismatch() { + // No ID token returned. + var noIdTokenResponse = {}; + return fireauth.AuthCredential.verifyTokenResponseUid( + goog.Promise.resolve(noIdTokenResponse), '1234') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH), + error); + }); +} + + +function testVerifyTokenResponseUid_userFound_mismatch() { + // Mock idToken parsing. + initializeIdTokenMocks('ID_TOKEN', '1234'); + return fireauth.AuthCredential.verifyTokenResponseUid( + // The UID does not match the ID token UID. + goog.Promise.resolve(responseForIdToken), '5678') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH), + error); + }); +} + + +function testVerifyTokenResponseUid_passThroughError() { + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + return fireauth.AuthCredential.verifyTokenResponseUid( + goog.Promise.reject(expectedError), '1234') + .thenCatch(function(error) { + assertEquals(expectedError, error); + }); +} + + +function testVerifyTokenResponseUid_userNotFound() { + // Confirm USER_DELETED error is translated to USER_MISMATCH. + var error = + new fireauth.AuthError(fireauth.authenum.Error.USER_DELETED); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH); + return fireauth.AuthCredential.verifyTokenResponseUid( + goog.Promise.reject(error), '1234') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} diff --git a/packages/auth/test/authevent_test.js b/packages/auth/test/authevent_test.js new file mode 100644 index 00000000000..5f26d5d26dc --- /dev/null +++ b/packages/auth/test/authevent_test.js @@ -0,0 +1,252 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for authevent.js + */ + +goog.provide('fireauth.AuthEventTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.authenum.Error'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.AuthEventTest'); + + +var authEvent; +var authEvent2; +var authEventObject; +var authEventObject2; +var popupType = [ + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP +]; +var redirectType = [ + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT +]; + + +function setUp() { + authEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + authEvent2 = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '12345678', + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + authEventObject = { + 'type': 'signInViaPopup', + 'eventId': null, + 'urlResponse': 'http://www.example.com/#oauthResponse', + 'sessionId': 'SESSION_ID', + 'error': null + }; + authEventObject2 = { + 'type': 'signInViaRedirect', + 'eventId': '12345678', + 'urlResponse': null, + 'sessionId': null, + 'error': { + 'code': fireauth.AuthError.ERROR_CODE_PREFIX + + fireauth.authenum.Error.INTERNAL_ERROR, + 'message': 'An internal error has occurred.' + } + }; +} + + +function tearDown() { + authEvent = null; + authEvent2 = null; + authEventObject = null; + authEventObject2 = null; +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!Error} expected + * @param {!Error} actual + */ +function assertErrorEquals(expected, actual) { + assertEquals(expected.code, actual.code); + assertEquals(expected.message, actual.message); +} + + +function testAuthEvent_isRedirect() { + // Popup types should return false. + for (var i = 0; i < popupType.length; i++) { + assertFalse(fireauth.AuthEvent.isRedirect( + new fireauth.AuthEvent( + popupType[i], + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'))); + } + // Unknown event type should return false. + assertFalse(fireauth.AuthEvent.isRedirect( + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)))); + // verifyApp event type should return false. + assertFalse(fireauth.AuthEvent.isRedirect( + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.VERIFY_APP, + null, + 'http://www.example.com/#oauthResponse', + 'blank'))); + // Redirect types should return true. + for (var i = 0; i < redirectType.length; i++) { + assertTrue(fireauth.AuthEvent.isRedirect( + new fireauth.AuthEvent( + redirectType[i], + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'))); + } +} + + +function testAuthEvent_isPopup() { + // Popup types should return true. + for (var i = 0; i < popupType.length; i++) { + assertTrue(fireauth.AuthEvent.isPopup( + new fireauth.AuthEvent( + popupType[i], + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'))); + } + // Unknown event type should return false. + assertFalse(fireauth.AuthEvent.isPopup( + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)))); + // verifyApp event type should return false. + assertFalse(fireauth.AuthEvent.isPopup( + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.VERIFY_APP, + null, + 'http://www.example.com/#oauthResponse', + 'blank'))); + // Redirect types should return false. + for (var i = 0; i < redirectType.length; i++) { + assertFalse(fireauth.AuthEvent.isPopup( + new fireauth.AuthEvent( + redirectType[i], + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'))); + } +} + + +function testAuthEvent_error() { + try { + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP); + fail('Auth event requires either an error or a URL response.'); + } catch(error) { + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT), + error); + } + try { + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + '12345678', + 'http://www.example.com/#oauthResponse', + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + fail('Auth event cannot have a URL response and an error.'); + } catch(error) { + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT), + error); + } + try { + new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + null, + 'http://www.example.com/#oauthResponse'); + fail('Auth event cannot have a URL response without a session ID.'); + } catch(error) { + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT), + error); + } +} + + +function testAuthEvent() { + assertEquals( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + authEvent.getType()); + assertEquals( + 'http://www.example.com/#oauthResponse', authEvent.getUrlResponse()); + assertEquals( + 'SESSION_ID', authEvent.getSessionId()); + assertNull(authEvent.getEventId()); + assertNull(authEvent.getError()); + assertFalse(authEvent.hasError()); + + assertEquals( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + authEvent2.getType()); + assertEquals('12345678', authEvent2.getEventId()); + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + authEvent2.getError()); + assertTrue(authEvent2.hasError()); +} + + +function testAuthEvent_toPlainObject() { + assertObjectEquals( + authEventObject, + authEvent.toPlainObject()); + assertObjectEquals( + authEventObject2, + authEvent2.toPlainObject()); +} + + +function testAuthEvent_fromPlainObject() { + assertObjectEquals( + authEvent, + fireauth.AuthEvent.fromPlainObject(authEventObject)); + assertObjectEquals( + authEvent2, + fireauth.AuthEvent.fromPlainObject(authEventObject2)); + assertNull(fireauth.AuthEvent.fromPlainObject({})); +} diff --git a/packages/auth/test/autheventmanager_test.js b/packages/auth/test/autheventmanager_test.js new file mode 100644 index 00000000000..5380ccc5f1b --- /dev/null +++ b/packages/auth/test/autheventmanager_test.js @@ -0,0 +1,2613 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for autheventmanager.js + */ + +goog.provide('fireauth.AuthEventManagerTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthEventManager'); +goog.require('fireauth.CordovaHandler'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.InvalidOriginError'); +goog.require('fireauth.PopupAuthEventProcessor'); +goog.require('fireauth.RedirectAuthEventProcessor'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.storage.OAuthHandlerManager'); +goog.require('fireauth.storage.PendingRedirectManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.crypt'); +goog.require('goog.crypt.Sha256'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.AuthEventManagerTest'); + + +var handler; +var stubs = new goog.testing.PropertyReplacer(); +var appName1 = 'APP1'; +var apiKey1 = 'API_KEY1'; +var authDomain1 = 'subdomain1.firebaseapp.com'; +var appName2 = 'APP2'; +var apiKey2 = 'API_KEY2'; +var authDomain2 = 'subdomain2.firebaseapp.com'; +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +// Firebase SDK version in case not available. +firebase.SDK_VERSION = firebase.SDK_VERSION || '3.0.0'; +var clock; +var expectedVersion; +var universalLinks; +var BuildInfo; +var cordova; +var OAuthSignInHandler; +var androidUA = 'Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Buil' + + 'd/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Sa' + + 'fari/534.30'; +var savePartialEventManager; +var timeoutDelay = 30000; +var mockControl; +var ignoreArgument; + +function setUp() { + mockControl = new goog.testing.MockControl(); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl.$resetAll(); + handler = { + 'canHandleAuthEvent': goog.testing.recordFunction(), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + simulateLocalStorageSynchronized(); + // Default OAuth sign in handler is IfcHandler. + setOAuthSignInHandlerEnvironment(false); +} + + +function tearDown() { + fireauth.AuthEventManager.manager_ = {}; + window.localStorage.clear(); + window.sessionStorage.clear(); + stubs.reset(); + goog.dispose(clock); + // Clear plugins. + universalLinks = {}; + BuildInfo = {}; + cordova = {}; + cordovaHandler = null; + OAuthSignInHandler = null; + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } +} + + +/** + * @param {string} str The string to hash. + * @return {string} The hashed string. + */ +function sha256(str) { + var sha256 = new goog.crypt.Sha256(); + sha256.update(str); + return goog.crypt.byteArrayToHex(sha256.digest()); +} + + +/** + * Utility function to initialize the Cordova mock plugins. + * @param {?function(?string, function(!Object))} subscribe The universal link + * subscriber. + * @param {?string} packageName The package name. + * @param {?string} displayName The app display name. + * @param {boolean} isAvailable Whether browsertab is supported. + * @param {?function(string, ?function(), ?function())} openUrl The URL opener. + * @param {?function()} close The browsertab closer if applicable. + * @param {?function()} open The inappbrowser opener if available. + */ +function initializePlugins( + subscribe, packageName, displayName, isAvailable, openUrl, close, open) { + // Initializes all mock plugins. + universalLinks = { + subscribe: subscribe + }; + BuildInfo = { + packageName: packageName, + displayName: displayName + }; + cordova = { + plugins: { + browsertab: { + isAvailable: function(cb) { + cb(isAvailable); + }, + openUrl: openUrl, + close: close + } + }, + InAppBrowser: { + open: open + } + }; +} + + +/** Simulates that local storage synchronizes across tabs. */ +function simulateLocalStorageSynchronized() { + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return false;}); +} + + +/** + * Helper function to set the current OAuth sign in handler. + * @param {boolean} isCordova Whether to simulate a Cordova environment. + */ +function setOAuthSignInHandlerEnvironment(isCordova) { + stubs.replace( + fireauth.util, + 'isAndroidOrIosFileEnvironment', + function() { + return isCordova; + }); + stubs.replace( + fireauth.util, + 'checkIfCordova', + function() { + if (isCordova) { + return goog.Promise.resolve(); + } else { + return goog.Promise.reject(); + } + }); + if (isCordova) { + OAuthSignInHandler = fireauth.CordovaHandler; + // Storage manager helpers. + savePartialEventManager = new fireauth.storage.OAuthHandlerManager(); + // Simulate Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Initialize plugins. + initializePlugins( + goog.testing.recordFunction(), + 'com.example.app', + 'Test App', + true, + goog.testing.recordFunction(), + goog.testing.recordFunction(), + goog.testing.recordFunction()); + } else { + OAuthSignInHandler = fireauth.iframeclient.IfcHandler; + // Override iframewrapper to prevent iframe from being embedded in tests. + stubs.replace(fireauth.iframeclient, 'IframeWrapper', function(url) { + return { + registerEvent: function(eventName, callback) {}, + onReady: function() { return goog.Promise.resolve(); } + }; + }); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + } +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +function assertErrorEquals(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +} + + +function testGetManager() { + var manager1 = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + assertEquals( + fireauth.AuthEventManager.manager_[ + fireauth.AuthEventManager.getKey_(apiKey1, appName1)], + manager1); + assertEquals( + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1), + manager1); + var manager2 = fireauth.AuthEventManager.getManager( + authDomain2, apiKey2, appName2); + assertEquals( + fireauth.AuthEventManager.manager_[ + fireauth.AuthEventManager.getKey_(apiKey2, appName2)], + manager2); + assertEquals( + fireauth.AuthEventManager.getManager(authDomain2, apiKey2, appName2), + manager2); +} + + +function testInstantiateOAuthSignInHandler_ifcHandler() { + // Simulate browser environment. + setOAuthSignInHandlerEnvironment(false); + // IfcHandler should be instantiated. + var ifcHandler = mockControl.createStrictMock( + fireauth.iframeclient.IfcHandler); + var ifcHandlerConstructor = mockControl.createConstructorMock( + fireauth.iframeclient, 'IfcHandler'); + // Confirm expected endpoint used. + ifcHandlerConstructor( + authDomain1, apiKey1, appName1, firebase.SDK_VERSION, + fireauth.constants.Endpoint.STAGING.id).$returns(ifcHandler); + mockControl.$replayAll(); + + fireauth.AuthEventManager.instantiateOAuthSignInHandler( + authDomain1, apiKey1, appName1, firebase.SDK_VERSION, + fireauth.constants.Endpoint.STAGING.id); +} + + +function testInstantiateOAuthSignInHandler_cordovaHandler() { + // Simulate Cordova environment + setOAuthSignInHandlerEnvironment(true); + // CordovaHandler should be instantiated. + var cordovaHandler = mockControl.createStrictMock( + fireauth.CordovaHandler); + var cordovaHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'CordovaHandler'); + // Confirm expected endpoint used. + cordovaHandlerConstructor( + authDomain1, apiKey1, appName1, firebase.SDK_VERSION, undefined, + undefined, fireauth.constants.Endpoint.STAGING.id) + .$returns(cordovaHandler); + mockControl.$replayAll(); + + fireauth.AuthEventManager.instantiateOAuthSignInHandler( + authDomain1, apiKey1, appName1, firebase.SDK_VERSION, + fireauth.constants.Endpoint.STAGING.id); +} + + + +function testAuthEventManager_initialize_manually_withSubscriber() { + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', '1234', 'http://www.example.com/#response', 'SESSION_ID'); + // This test is not environment specific. + stubs.replace( + fireauth.AuthEventManager, + 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName, version) { + assertEquals('subdomain1.firebaseapp.com', authDomain); + assertEquals('API_KEY1', apiKey); + assertEquals('APP1', appName); + assertEquals(firebase.SDK_VERSION, version); + return { + 'addAuthEventListener': function(handler) { + asyncTestCase.signal(); + // Trigger immediately to test that handleAuthEvent_ + // is triggered with expected event. + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { + return false; + }, + 'hasVolatileStorage': function() { + return false; + } + }; + }); + var handler1 = { + 'canHandleAuthEvent': function(type, id) { + // Auth event should be passed to handler to check if it can handle it. + assertEquals('linkViaPopup', type); + assertEquals('1234', id); + asyncTestCase.signal(); + return false; + }, + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + asyncTestCase.waitForSignals(2); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.subscribe(handler1); + manager.initialize(); +} + + +function testAuthEventManager_initialize_manually_withNoSubscriber() { + // Test manual initialization with no subscriber. + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', '1234', 'http://www.example.com/#response', 'SESSION_ID'); + var isReady = false; + // This test is not environment specific. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // This should not be called twice on initialization. + assertFalse(isReady); + // Trigger expected event. + isReady = true; + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(2); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.initialize().then(function() { + assertTrue(isReady); + asyncTestCase.signal(); + }); + // This will return the above cached response and should not try to + // initialize a new handler. + manager.initialize().then(function() { + assertTrue(isReady); + asyncTestCase.signal(); + }); +} + + +function testAuthEventManager_initialize_automatically_pendingRedirect() { + // Test automatic initialization when pending redirect available on + // subscription. + // This test is relevant to a browser environment. + setOAuthSignInHandlerEnvironment(false); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var isInitialized = false; + // Used to trigger the Auth event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + isInitialized = true; + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(1); + var expectedResult = { + 'user': {}, + 'credential': {} + }; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var handler1 = { + 'canHandleAuthEvent': function(type, id) { + // Auth event should be passed to handler to check if it can handle it. + assertEquals('signInViaRedirect', type); + assertEquals('1234', id); + return true; + }, + 'resolvePendingPopupEvent': function( + mode, popupRedirectResult, error, opt_eventId) { + }, + 'getAuthEventHandlerFinisher': function(mode, opt_eventId) { + return function(requestUri, sessionId) { + return goog.Promise.resolve(expectedResult); + }; + } + }; + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + // Simulate pending result. + pendingRedirectManager.setPendingStatus().then(function() { + // This will trigger initialize due to pending redirect. + manager.subscribe(handler1); + // This will resolve with the expected result. + manager.getRedirectResult().then(function(result) { + assertTrue(isInitialized); + assertObjectEquals(expectedResult, result); + // Pending result should be cleared. + pendingRedirectManager.getPendingStatus().then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); + }); + + }); +} + + +function testAuthEventManager_initialize_automatically_volatileStorage() { + // If interface has volatile storage, handler should be automatically + // initialized even when no pending redirect is detected. + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var isInitialized = false; + // Used to trigger the Auth event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + isInitialized = true; + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(1); + var expectedResult = { + 'user': {}, + 'credential': {} + }; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var handler1 = { + 'canHandleAuthEvent': function(type, id) { + // auth event should be passed to handler to check if it can handle it. + assertEquals('signInViaRedirect', type); + assertEquals('1234', id); + return true; + }, + 'resolvePendingPopupEvent': function( + mode, popupRedirectResult, error, opt_eventId) { + }, + 'getAuthEventHandlerFinisher': function(mode, opt_eventId) { + return function(requestUri, sessionId) { + return goog.Promise.resolve(expectedResult); + }; + } + }; + // This will trigger initialize even though there is no pending redirect in + // session storage. + manager.subscribe(handler1); + // This will resolve with the expected result. + manager.getRedirectResult().then(function(result) { + assertTrue(isInitialized); + assertEquals(expectedResult, result); + asyncTestCase.signal(); + }); +} + + +function testAuthEventManager_initialize_automatically_safariMobile() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test automatic initialization on subscription for Safari mobile. + var ua = 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 ' + + '(KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25'; + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return ua; + }); + // OAuth handler should be initialized automatically on subscription. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var handler = { + 'canHandleAuthEvent': goog.testing.recordFunction(), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + manager.subscribe(handler); +} + + +function testAuthEventManager_getRedirectResult_noRedirect() { + // Browser environment where sessionStorage is not volatile. + // Test getRedirect when no pending redirect is found. + setOAuthSignInHandlerEnvironment(false); + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.AuthEventManager.prototype, + 'initialize', + function() { + fail('This should not initialize automatically.'); + }); + var expectedResult = { + 'user': null + }; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var handler1 = { + 'canHandleAuthEvent': goog.testing.recordFunction(), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + // This will resolve redirect result to default null result. + manager.subscribe(handler1); + // This should resolve quickly since there is no pending redirect without the + // need to initialize the OAuth sign-in handler. + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedResult, result); + asyncTestCase.signal(); + }); +} + + +function testAuthEventManager_subscribeAndUnsubscribe() { + var recordedHandler = null; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', '1234', 'http://www.example.com/#response', 'SESSION_ID'); + stubs.replace( + fireauth.PopupAuthEventProcessor.prototype, + 'processAuthEvent', + goog.testing.recordFunction()); + // This test is not environment specific. + stubs.replace( + fireauth.AuthEventManager, + 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName, version) { + assertEquals('subdomain1.firebaseapp.com', authDomain); + assertEquals('API_KEY1', apiKey); + assertEquals('APP1', appName); + assertEquals(firebase.SDK_VERSION, version); + return { + 'addAuthEventListener': function(handler) { + recordedHandler = handler; + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { + return false; + }, + 'hasVolatileStorage': function() { + return false; + } + }; + }); + // Create a handler that can't handle the provided event. + var handler1 = { + 'canHandleAuthEvent': goog.testing.recordFunction(function(type, eventId) { + assertEquals('linkViaPopup', type); + assertEquals('1234', eventId); + return false; + }), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + // Create another handler that can't handle the provided event. + var handler2 = { + 'canHandleAuthEvent': goog.testing.recordFunction(function(type, eventId) { + assertEquals('linkViaPopup', type); + assertEquals('1234', eventId); + return false; + }), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + // Create a handler that can handle a specified Auth event. + var handler3 = { + 'canHandleAuthEvent': goog.testing.recordFunction(function(type, eventId) { + assertEquals('linkViaPopup', type); + assertEquals('1234', eventId); + return true; + }), + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + asyncTestCase.waitForSignals(1); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.initialize(); + assertFalse(manager.isSubscribed(handler1)); + // Subscribe first handler and trigger event. + manager.subscribe(handler1); + assertTrue(manager.isSubscribed(handler1)); + assertFalse(recordedHandler(expectedAuthEvent)); + assertEquals(1, handler1.canHandleAuthEvent.getCallCount()); + assertEquals(0, handler2.canHandleAuthEvent.getCallCount()); + assertEquals( + 0, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Subscribe second handler and trigger event. + assertFalse(manager.isSubscribed(handler2)); + manager.subscribe(handler2); + assertTrue(manager.isSubscribed(handler2)); + assertFalse(recordedHandler(expectedAuthEvent)); + assertEquals(2, handler1.canHandleAuthEvent.getCallCount()); + assertEquals(1, handler2.canHandleAuthEvent.getCallCount()); + assertEquals( + 0, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Unsubscribe first handler and trigger event. + manager.unsubscribe(handler1); + assertFalse(manager.isSubscribed(handler1)); + assertFalse(recordedHandler(expectedAuthEvent)); + assertEquals(2, handler1.canHandleAuthEvent.getCallCount()); + assertEquals(2, handler2.canHandleAuthEvent.getCallCount()); + assertEquals( + 0, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Unsubscribe second handler and trigger event. + manager.unsubscribe(handler2); + assertFalse(manager.isSubscribed(handler2)); + assertFalse(recordedHandler(expectedAuthEvent)); + assertEquals(2, handler1.canHandleAuthEvent.getCallCount()); + assertEquals(2, handler2.canHandleAuthEvent.getCallCount()); + assertEquals( + 0, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Reset handler. + handler1.canHandleAuthEvent.reset(); + handler2.canHandleAuthEvent.reset(); + handler3.canHandleAuthEvent.reset(); + // Subscribe all handlers and add handler3. + manager.subscribe(handler1); + manager.subscribe(handler3); + manager.subscribe(handler2); + // Trigger event, once it reaches the correct handler, it will stop checking + // other handlers and return true. + assertTrue(recordedHandler(expectedAuthEvent)); + assertEquals(1, handler1.canHandleAuthEvent.getCallCount()); + assertEquals(0, handler2.canHandleAuthEvent.getCallCount()); + assertEquals(1, handler3.canHandleAuthEvent.getCallCount()); + assertEquals( + 1, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + // processAuthEvent should be called with the expected event and handler3 as + // owner of that event. + assertEquals( + expectedAuthEvent, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.getLastCall() + .getArgument(0)); + assertEquals( + handler3, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent.getLastCall() + .getArgument(1)); +} + + +function testAuthEventManager_testEventToProcessor() { + var recordedHandler; + asyncTestCase.waitForSignals(1); + // This test is not environment specific. + stubs.replace( + fireauth.AuthEventManager, + 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName, version) { + assertEquals('subdomain1.firebaseapp.com', authDomain); + assertEquals('API_KEY1', apiKey); + assertEquals('APP1', appName); + assertEquals(firebase.SDK_VERSION, version); + return { + 'addAuthEventListener': function(handler) { + recordedHandler = handler; + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { + return false; + }, + 'hasVolatileStorage': function() { + return false; + } + }; + }); + stubs.replace( + fireauth.PopupAuthEventProcessor.prototype, + 'processAuthEvent', + goog.testing.recordFunction()); + stubs.replace( + fireauth.RedirectAuthEventProcessor.prototype, + 'processAuthEvent', + goog.testing.recordFunction()); + // For testing all cases, use a handler that can handler everything. + var handler = { + 'canHandleAuthEvent': function(type, eventId) {return true;}, + 'resolvePendingPopupEvent': goog.testing.recordFunction(), + 'getAuthEventHandlerFinisher': goog.testing.recordFunction() + }; + var unknownEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var signInViaPopupEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var signInViaRedirectEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var linkViaPopupEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var linkViaRedirectEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var reauthViaPopupEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var reauthViaRedirectEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.initialize(); + manager.subscribe(handler); + // Test with unknown event. + recordedHandler(unknownEvent); + assertEquals( + 1, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [unknownEvent, handler], + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + assertEquals( + 0, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Test with sign up via popup event. + recordedHandler(signInViaPopupEvent); + assertEquals( + 1, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertEquals( + 1, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [signInViaPopupEvent, handler], + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + + // Test with sign in via redirect event. + recordedHandler(signInViaRedirectEvent); + assertEquals( + 2, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [signInViaRedirectEvent, handler], + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + assertEquals( + 1, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Test with link via popup event. + recordedHandler(linkViaPopupEvent); + assertEquals( + 2, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertEquals( + 2, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [linkViaPopupEvent, handler], + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + + // Test with link via redirect event. + recordedHandler(linkViaRedirectEvent); + assertEquals( + 3, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [linkViaRedirectEvent, handler], + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + assertEquals( + 2, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + + // Test with reauth via popup event. + recordedHandler(reauthViaPopupEvent); + assertEquals( + 3, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertEquals( + 3, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [reauthViaPopupEvent, handler], + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + + // Test with reauth via redirect event. + recordedHandler(reauthViaRedirectEvent); + assertEquals( + 4, + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); + assertArrayEquals( + [reauthViaRedirectEvent, handler], + fireauth.RedirectAuthEventProcessor.prototype.processAuthEvent. + getLastCall().getArguments()); + assertEquals( + 3, + fireauth.PopupAuthEventProcessor.prototype.processAuthEvent. + getCallCount()); +} + + +function testProcessPopup_success() { + // This is only relevant to OAuth handlers that support popups. + setOAuthSignInHandlerEnvironment(false); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, + apiKey1, + appName1, + 'linkViaPopup', + provider, + null, + '1234', + firebase.SDK_VERSION); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Fake popup window. + var popupWin = {}; + // Keep track of when the popup is redirected. + var popupRedirected = false; + // Catch popup window redirection. + stubs.replace( + fireauth.util, + 'goTo', + function(url, win) { + popupRedirected = true; + assertEquals(expectedUrl, url); + assertEquals(popupWin, win); + asyncTestCase.signal(); + }); + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(3); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .then(function() { + assertTrue(popupRedirected); + // This should resolve now as it is already initialized. + asyncTestCase.signal(); + }); + // Confirm OAuth handler initialized before redirect. + manager.initialize().then(function() { + // Should not be redirected yet. + assertFalse(popupRedirected); + asyncTestCase.signal(); + }); +} + + +function testProcessPopup_popupNotSupported() { + // Test for environments where popup sign in is not supported. + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + // Fake popup window. + var popupWin = {}; + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(1); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testReset_ifcHandler() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + var oauthHandlerInstance = null; + var calls = 0; + // Listener to initializations of ifchandler. + stubs.replace( + OAuthSignInHandler.prototype, + 'initializeAndWait', + goog.testing.recordFunction( + OAuthSignInHandler.prototype.initializeAndWait)); + stubs.replace( + OAuthSignInHandler.prototype, 'addAuthEventListener', + function(handler) { + // Each call should be run on a new instance as reset will force a new + // instance to be created. + assertNotEquals(oauthHandlerInstance, this); + // Save current instance. + oauthHandlerInstance = this; + calls++; + }); + var manager = + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1); + // This should be cancelled by reset. + manager.initialize(); + // Note first initialized ifchandler instance. + var initializedInstance1 = OAuthSignInHandler.prototype + .initializeAndWait.getLastCall().getThis(); + manager.reset(); + // This call should succeed. + manager.initialize(); + // Note second initialized ifchandler instance. + var initializedInstance2 = OAuthSignInHandler.prototype + .initializeAndWait.getLastCall().getThis(); + // Confirm both instances are not the same. + assertNotEquals(initializedInstance1, initializedInstance2); + assertEquals(2, calls); +} + + +function testReset_cordovaHandler() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var oauthHandlerInstance = null; + var calls = 0; + // Listener to initializations of the current OAuth sign in handler. + stubs.replace( + OAuthSignInHandler.prototype, + 'initializeAndWait', + goog.testing.recordFunction( + OAuthSignInHandler.prototype.initializeAndWait)); + stubs.replace( + OAuthSignInHandler.prototype, 'addAuthEventListener', + function(handler) { + // Each call should be run on a new instance as reset will force a new + // instance to be created. + assertNotEquals(oauthHandlerInstance, this); + // Save current instance. + oauthHandlerInstance = this; + calls++; + }); + var manager = + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1); + // This should be cancelled by reset. + manager.initialize(); + // Note first initialized ifchandler instance. + var initializedInstance1 = OAuthSignInHandler.prototype + .initializeAndWait.getLastCall().getThis(); + manager.reset(); + // This call should succeed. + manager.initialize(); + // Note second initialized ifchandler instance. + var initializedInstance2 = OAuthSignInHandler.prototype + .initializeAndWait.getLastCall().getThis(); + // Confirm both instances are not the same. + assertNotEquals(initializedInstance1, initializedInstance2); + assertEquals(2, calls); +} + + +function testInitialize_errorLoadingOAuthHandler() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test manager initialization when the OAuth handler fails to load. + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', null, null, null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Listen to reset calls. + stubs.replace( + fireauth.AuthEventManager.prototype, 'reset', + goog.testing.recordFunction(fireauth.AuthEventManager.prototype.reset)); + // If the OAuth handler is to be initialized, trigger no redirect event to + // notify event manager that it is ready. + stubs.replace( + OAuthSignInHandler.prototype, 'addAuthEventListener', + function(handler) { + // Only when handler is not failing, handle event. + if (!willFail) { + handler(expectedAuthEvent); + } + }); + stubs.replace( + OAuthSignInHandler.prototype, + 'initializeAndWait', + function() { + if (willFail) { + // When handler fails to load. + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + } + // Handler succeeds to load. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + var manager = + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1); + // Simulate the handler first fails to load. + var willFail = true; + // Reset not called yet. + assertEquals(0, fireauth.AuthEventManager.prototype.reset.getCallCount()); + manager.initialize().thenCatch(function(error) { + // OAuth handler should be reset. + assertEquals(1, fireauth.AuthEventManager.prototype.reset.getCallCount()); + // Network error triggered. + assertErrorEquals(expectedError, error); + // Simulate on next trial the handler will load correctly. + willFail = false; + // Try to initialize again. This time it should work. + manager.initialize().then(function() { + // No additional call to reset. + assertEquals(1, fireauth.AuthEventManager.prototype.reset.getCallCount()); + // This should resolve now as it is already initialized. + asyncTestCase.signal(); + }); + }); +} + + +function testProcessPopup_errorLoadingIframe() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test when the handler fails to load. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, apiKey1, appName1, 'linkViaPopup', provider, null, '1234', + firebase.SDK_VERSION); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', null, null, null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Fake popup window. + var popupWin = {}; + var popupRedirected = false; + // Listen to reset calls. + stubs.replace( + fireauth.AuthEventManager.prototype, 'reset', + goog.testing.recordFunction(fireauth.AuthEventManager.prototype.reset)); + stubs.replace( + fireauth.RpcHandler.prototype, 'getAuthorizedDomains', function() { + // Assume connection works for this call and only fails on handler load. + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + // Catch popup window redirection. + stubs.replace(fireauth.util, 'goTo', function(url, win) { + if (willFail) { + // When handler fails, no redirect should happen. + fail('OAuth handler loading failure should not lead to a popup ' + + 'redirect.'); + } else { + // When OAuth handler succeeds, it should redirect the popup window. + popupRedirected = true; + assertEquals(expectedUrl, url); + assertEquals(popupWin, win); + } + }); + // If the OAuth handler is to be initialized, trigger no redirect event to + // notify event manager that it is ready. + stubs.replace( + OAuthSignInHandler.prototype, 'addAuthEventListener', + function(handler) { + // Only when handler is not failing, handle event. + if (!willFail) { + handler(expectedAuthEvent); + } + }); + stubs.replace( + OAuthSignInHandler.prototype, + 'initializeAndWait', + function() { + if (willFail) { + // When handler fails to load. + return goog.Promise.reject(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + } + // Handler succeeds to load. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + var manager = + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1); + // Simulate the OAuth handler first fails to load. + var willFail = true; + // Reset not called yet. + assertEquals(0, fireauth.AuthEventManager.prototype.reset.getCallCount()); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + // OAuth handler should be reset. + assertEquals( + 1, fireauth.AuthEventManager.prototype.reset.getCallCount()); + // No redirect. + assertFalse(popupRedirected); + // Network error triggered. + assertErrorEquals(expectedError, error); + // Simulate on next trial the OAuth handler will load correctly. + willFail = false; + // This will succeed now. + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .then(function() { + // No additional call to reset. + assertEquals( + 1, fireauth.AuthEventManager.prototype.reset.getCallCount()); + assertTrue(popupRedirected); + // This should resolve now as it is already initialized. + asyncTestCase.signal(); + }); + }); +} + + +function testProcessPopup_getAuthorizedDomainsNetworkError() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test when getAuthorizedDomains throws a network error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, apiKey1, appName1, 'linkViaPopup', provider, null, '1234', + firebase.SDK_VERSION); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', null, null, null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Fake popup window. + var popupWin = {}; + var popupRedirected = false; + // Listen to reset calls. + stubs.replace( + fireauth.AuthEventManager.prototype, 'reset', + goog.testing.recordFunction(fireauth.AuthEventManager.prototype.reset)); + stubs.replace( + fireauth.RpcHandler.prototype, 'getAuthorizedDomains', function() { + // Throw network error when this is supposed to fail. + if (willFail) { + return goog.Promise.reject(expectedError); + } + // Assume connection works for this call and only fails on handler load. + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + // Catch popup window redirection. + stubs.replace(fireauth.util, 'goTo', function(url, win) { + if (willFail) { + // When OAuth handler fails, no redirect should happen. + fail('OAuth handler loading failure should not lead to a popup ' + + 'redirect.'); + } else { + // When OAuth handler succeeds, it should redirect the popup window. + popupRedirected = true; + assertEquals(expectedUrl, url); + assertEquals(popupWin, win); + } + }); + // If the OAuth handler is to be initialized, trigger no redirect event to + // notify event manager that it is ready. + stubs.replace( + OAuthSignInHandler.prototype, 'addAuthEventListener', + function(handler) { + // This should not be called since the handler should not be initialized + // in this case. + if (willFail) { + fail('getAuthorizedDomains error should not trigger handler init.'); + } + // Only when OAuth handler is not failing, handle event. + handler(expectedAuthEvent); + }); + stubs.replace( + OAuthSignInHandler.prototype, + 'initializeAndWait', + function() { + // This should not be called since the OAuth handler should not be + // initialized in this case. + if (willFail) { + fail('getAuthorizedDomains error should not trigger handler init.'); + } + // OAuth handler succeeds to load. + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + var manager = + fireauth.AuthEventManager.getManager(authDomain1, apiKey1, appName1); + // Simulate getAuthorizedDomains network error. + var willFail = true; + // Reset not called. + assertEquals(0, fireauth.AuthEventManager.prototype.reset.getCallCount()); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + // No reset needed as the OAuth handler was not initialized to begin + // with. + assertEquals( + 0, fireauth.AuthEventManager.prototype.reset.getCallCount()); + // No redirect. + assertFalse(popupRedirected); + // Network error triggered. + assertErrorEquals(expectedError, error); + // Simulate on next trial the OAuth handler will load correctly and + // getAuthorizedDomains would resolve correctly. + willFail = false; + // This will succeed now. + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .then(function() { + // No call to reset. + assertEquals( + 0, fireauth.AuthEventManager.prototype.reset.getCallCount()); + // Popup redirected successfully + assertTrue(popupRedirected); + // This should resolve now as it is already initialized. + asyncTestCase.signal(); + }); + }); +} + + +function testProcessPopup_alreadyRedirected() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test processPopup when it's already redirected. This is used in mobile + // environments. + // Fake popup window. + var popupWin = {}; + // OAuth handler should be initialized automatically if not already + // intialized. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(2); + var provider = new fireauth.GoogleAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234', true) + .then(function() { + // This should resolve immediately. + asyncTestCase.signal(); + }); +} + + +function testProcessPopup_success_confirmAutoInitialized() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Test initialize called automatically when processPopup called. + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, + apiKey1, + appName1, + 'linkViaPopup', + provider, + null, + '1234', + firebase.SDK_VERSION); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Fake popup window. + var popupWin = {}; + // Catch popup window redirection. + stubs.replace( + fireauth.util, + 'goTo', + function(url, win) { + assertEquals(expectedUrl, url); + assertEquals(popupWin, win); + asyncTestCase.signal(); + }); + // If the OAuth handler is to be initialized, trigger no redirect event to + // notify event manager that it is ready. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(2); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .then(function() { + // This should resolve immediately as it is already initialized. + manager.initialize().then(function() { + asyncTestCase.signal(); + }); + }); +} + + +function testProcessPopup_error_invalidOrigin() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Simulate when popup is requested with invalid origin. + // Expected invalid origin error. + var expectedError = + new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // If the OAuth handler is to be initialized, trigger no redirect event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + // Assume origin is an invalid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + // No authorized domains. + return goog.Promise.resolve([]); + }); + // Fake popup window. + var popupWin = {}; + asyncTestCase.waitForSignals(2); + var provider = new fireauth.GoogleAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + // This should fail with invalid origin error. + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // processPopup should initialize manager regardless of error. + manager.initialize().then(function() { + asyncTestCase.signal(); + }); +} + + +function testProcessPopup_error_blockedPopup() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // If the OAuth handler is to be initialized, trigger no redirect event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(2); + var provider = new fireauth.GoogleAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(null, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.POPUP_BLOCKED), + error); + asyncTestCase.signal(); + }); + // processPopup should initialize manager regardless of error. + manager.initialize().then(function() { + asyncTestCase.signal(); + }); +} + + +function testProcessPopup_error_unsupportedProvider() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // If the OAuth handler is to be initialized, trigger no redirect event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + asyncTestCase.waitForSignals(2); + // Fake popup window. + var popupWin = {}; + var provider = new fireauth.EmailAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals( + new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER), + error); + asyncTestCase.signal(); + }); + // processPopup should initialize manager regardless of error. + manager.initialize().then(function() { + asyncTestCase.signal(); + }); +} + + +function testProcessRedirect_success_ifchandler() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, + apiKey1, + appName1, + 'linkViaRedirect', + provider, + window.location.href, + '1234', + firebase.SDK_VERSION); + stubs.replace( + fireauth.util, + 'goTo', + function(url) { + assertEquals(expectedUrl, url); + // Pending redirect should be saved. + pendingRedirectManager.getPendingStatus().then(function(status) { + assertTrue(status); + asyncTestCase.signal(); + }); + }); + asyncTestCase.waitForSignals(1); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + manager.processRedirect('linkViaRedirect', provider, '1234'); +} + + +function testAuthEventManager_nonCordovaIosOrAndroidFileEnvironment() { + // Simulate Android file browser environment. + setOAuthSignInHandlerEnvironment(false); + stubs.replace( + fireauth.util, + 'isAndroidOrIosFileEnvironment', + function() { + return true; + }); + stubs.replace( + fireauth.util, + 'checkIfCordova', + function() { + return goog.Promise.reject(); + }); + var popupWin = { + closed: false + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + var provider = new fireauth.GoogleAuthProvider(); + handler.canHandleAuthEvent = function(mode, opt_eventId) { + return true; + }; + asyncTestCase.waitForSignals(4); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.subscribe(handler); + // All popup/redirect methods should fail with operation not supported errors. + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + manager.processPopup(popupWin, 'linkViaPopup', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + manager.startPopupTimeout(handler, 'linkViaPopup', popupWin, '1234') + .then(function() { + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + asyncTestCase.signal(); + }); +} + + +function testProcessRedirect_success_cordovahandler() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var rawSessionId = '11111111111111111111'; + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, + apiKey1, + appName1, + 'linkViaRedirect', + provider, + null, + '1234', + firebase.SDK_VERSION, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256(rawSessionId) + }); + var pendingRedirectError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING); + var savedCb = null; + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + cordova.plugins.browsertab.openUrl = function(url) { + assertEquals(expectedUrl, url); + savedCb({url: incomingUrl}); + }; + universalLinks.subscribe = function(eventName, cb) { + // Trigger initial no event. + cb({url: null}); + savedCb = cb; + }; + // Simulate handler can handle the event. + handler.canHandleAuthEvent = function(mode, opt_eventId) { + return true; + }; + handler.getAuthEventHandlerFinisher = function(mode, opt_eventId) { + return function(requestUri, sessionId) { + assertEquals(incomingUrl, requestUri); + assertEquals(sessionId, rawSessionId); + return goog.Promise.resolve(expectedResult); + }; + }; + asyncTestCase.waitForSignals(3); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + var expectedResult = { + 'user': {}, + 'credential': {} + }; + manager.subscribe(handler); + // Initial result is null. + manager.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + manager.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Pending redirect should be cleared on redirect back to app. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + manager.getRedirectResult().then(function(result) { + assertEquals(expectedResult, result); + // Call processRedirect again. This should resolve as there is no + // pending operation. + return manager.processRedirect('linkViaRedirect', provider, '1234'); + }).then(function() { + asyncTestCase.signal(); + }); + }); + // This should fail as the above is still pending. + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(pendingRedirectError, error); + asyncTestCase.signal(); + }); +} + + +function testProcessRedirect_error_cordovahandler() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + var rawSessionId = '11111111111111111111'; + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain1, + apiKey1, + appName1, + 'linkViaRedirect', + provider, + null, + '1234', + firebase.SDK_VERSION, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256(rawSessionId) + }); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var savedCb = null; + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback?firebaseError=' + + JSON.stringify(expectedError.toPlainObject()); + cordova.plugins.browsertab.openUrl = function(url) { + assertEquals(expectedUrl, url); + savedCb({url: incomingUrl}); + }; + universalLinks.subscribe = function(eventName, cb) { + // Trigger initial no event. + cb({url: null}); + savedCb = cb; + }; + // Simulate handler can handle the event. + handler.canHandleAuthEvent = function(mode, opt_eventId) { + return true; + }; + asyncTestCase.waitForSignals(2); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + manager.subscribe(handler); + // Initial result is null. + manager.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + manager.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Pending redirect should be cleared on redirect back to app. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testProcessRedirect_error_unsupportedProvider_cordovaHandler() { + asyncTestCase.waitForSignals(2); + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var invalidProviderError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + var pendingRedirectError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING); + var provider = new fireauth.EmailAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(invalidProviderError, error); + // Pending redirect should not be saved. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + // Call again, this should be allowed to complete. + return manager.processRedirect('linkViaRedirect', provider, '1234'); + }).thenCatch(function(error) { + // The expected invalid provider error is thrown. + assertErrorEquals(invalidProviderError, error); + asyncTestCase.signal(); + }); + // This should fail as there is a pending redirect operation. + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(pendingRedirectError, error); + asyncTestCase.signal(); + }); +} + + +function testProcessRedirect_error_redirectCancelled_cordovaHandler() { + asyncTestCase.waitForSignals(1); + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_CANCELLED_BY_USER); + // Simulate the OAuth sign in handler throws the expected error. + stubs.replace( + OAuthSignInHandler.prototype, + 'processRedirect', + function(handler) { + // Trigger expected error. + return goog.Promise.reject(expectedError); + }); + var provider = new fireauth.GoogleAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + // The underlying OAuth handler processRedirect error should be thrown. + assertErrorEquals(expectedError, error); + // Pending redirect should not be saved. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); +} + + +function testGetRedirectResult_success_cordovahandler() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var rawSessionId = '11111111111111111111'; + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + rawSessionId, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + universalLinks.subscribe = function(eventName, cb) { + // Trigger initial event. + cb({url: incomingUrl}); + }; + // Simulate handler can handle the event. + handler.canHandleAuthEvent = function(mode, opt_eventId) { + assertEquals('linkViaRedirect', mode); + assertEquals('1234', opt_eventId); + return true; + }; + handler.getAuthEventHandlerFinisher = function(mode, opt_eventId) { + return function(requestUri, sessionId) { + assertEquals(incomingUrl, requestUri); + assertEquals(sessionId, rawSessionId); + return goog.Promise.resolve(expectedResult); + }; + }; + var storageKey = apiKey1 + ':' + appName1; + var expectedResult = { + 'user': {}, + 'credential': {} + }; + asyncTestCase.waitForSignals(1); + // Assume pending redirect event. + savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.subscribe(handler); + // Initial expected result should resolve. + manager.getRedirectResult().then(function(result) { + assertEquals(expectedResult, result); + asyncTestCase.signal(); + }); + }); +} + + +function testGetRedirectResult_error_cordovahandler() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + var rawSessionId = '11111111111111111111'; + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + rawSessionId, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback?firebaseError=' + + JSON.stringify(expectedError.toPlainObject()); + universalLinks.subscribe = function(eventName, cb) { + // Trigger initial event. + cb({url: incomingUrl}); + }; + // Simulate handler can handle the event. + handler.canHandleAuthEvent = function(mode, opt_eventId) { + assertEquals('linkViaRedirect', mode); + assertEquals('1234', opt_eventId); + return true; + }; + var storageKey = apiKey1 + ':' + appName1; + asyncTestCase.waitForSignals(1); + // Assume pending redirect event. + savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.subscribe(handler); + // Initial expected result should resolve. + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testProcessRedirect_error_invalidOrigin() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + // Simulate when redirect is requested with invalid origin. + // Expected invalid origin error. + var expectedError = + new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + // Assume origin is an invalid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + // No authorized domains. + return goog.Promise.resolve([]); + }); + asyncTestCase.waitForSignals(1); + var provider = new fireauth.GoogleAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + // This should fail with invalid origin error. + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + // Pending redirect should not be saved. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); +} + + +function testProcessRedirect_error_unsupportedProvider_ifcHandler() { + asyncTestCase.waitForSignals(1); + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + var provider = new fireauth.EmailAuthProvider(); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + manager.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals( + new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER), + error); + // Pending redirect should not be saved. + return pendingRedirectManager.getPendingStatus(); + }).then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); +} + + +function testStartPopupTimeout_webStorageSupported() { + // Browser only environment. + clock = new goog.testing.MockClock(true); + setOAuthSignInHandlerEnvironment(false); + asyncTestCase.waitForSignals(1); + // No auth event used to resolve redirect result promise. + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // If the OAuth handler is to be initialized, trigger no redirect event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + // On OAuth handler ready, simulate web storage supported. + stubs.replace( + fireauth.iframeclient.IfcHandler.prototype, + 'isWebStorageSupported', + function() { + return goog.Promise.resolve(true); + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + var popupWin = {'closed': true}; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + // Manager must be initialized for the OAuth handler to be ready before + // listening to the popup being closed. + manager.initialize(); + manager.startPopupTimeout( + handler, 'linkViaPopup', popupWin, '1234').then(function() { + // On timeout, resolve pending popup event should reject with a timeout + // error. + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertEquals( + null, + handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', + handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); + // Timeout popup quickly. + clock.tick(5000); +} + + +function testStartPopupTimeout_webStorageNotSupported() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + asyncTestCase.waitForSignals(1); + // No Auth event used to resolve redirect result promise. + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // If the OAuth handler is to be initialized, trigger no redirect event. + stubs.replace( + OAuthSignInHandler.prototype, + 'addAuthEventListener', + function(handler) { + // Trigger expected event. + handler(expectedAuthEvent); + }); + // On handler ready, simulate web storage not supported in iframe. + stubs.replace( + fireauth.iframeclient.IfcHandler.prototype, + 'isWebStorageSupported', + function() { + return goog.Promise.resolve(false); + }); + // Web storage not supported in iframe error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var popupWin = { + 'closed': false, + 'close': function() { + popupWin.closed = true; + } + }; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + // Manager must be initialized before listening to the popup being closed. + manager.initialize(); + manager.startPopupTimeout( + handler, 'linkViaPopup', popupWin, '1234').then(function() { + // On timeout, resolve pending popup event should reject with a web storage + // not supported error. + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertEquals( + null, + handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', + handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); +} + + +function testStartPopupTimeout_popupSupported_cancel() { + // Browser only environment. + clock = new goog.testing.MockClock(true); + setOAuthSignInHandlerEnvironment(false); + var popupWin = {'closed': true}; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.startPopupTimeout( + handler, 'linkViaPopup', popupWin, '1234').then(function() { + fail('Popup timeout should be cancelled before timing out.'); + }).cancel(); +} + + +function testStartPopupTimeout_popupNotSupported() { + // Cordova environment. + setOAuthSignInHandlerEnvironment(true); + asyncTestCase.waitForSignals(1); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + var popupWin = {'closed': false}; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.startPopupTimeout( + handler, 'linkViaPopup', popupWin, '1234').then(function() { + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertNull(handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_invalidAuthEvent() { + asyncTestCase.waitForSignals(1); + var expectedAuthEvent = null; + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).thenCatch(function(error) { + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT), + error); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_unknownAuthEvent() { + asyncTestCase.waitForSignals(2); + var expectedResult = { + 'user': null + }; + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedResult, result); + asyncTestCase.signal(); + }); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_unknownAuthEvent_webStorageNotSupported() { + // Browser only environment. + setOAuthSignInHandlerEnvironment(false); + asyncTestCase.waitForSignals(1); + // Web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var expectedAuthEvent = new fireauth.AuthEvent( + 'unknown', + null, + null, + null, + expectedError); + // If the OAuth handler is to be initialized, trigger unknown event with web + // storage not supported error. + stubs.replace( + fireauth.AuthEventManager, + 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName, version) { + assertEquals('subdomain1.firebaseapp.com', authDomain); + assertEquals('API_KEY1', apiKey); + assertEquals('APP1', appName); + assertEquals(firebase.SDK_VERSION, version); + return { + 'addAuthEventListener': function(handler) { + handler(expectedAuthEvent); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { + return false; + }, + 'hasVolatileStorage': function() { + return false; + } + }; + }); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + var storageKey = apiKey1 + ':' + appName1; + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(storageKey); + // Simulate handler can handle the event. + handler.canHandleAuthEvent = function(mode, opt_eventId) { + return true; + }; + // Simulate pending result. + pendingRedirectManager.setPendingStatus().then(function() { + manager.subscribe(handler); + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testProcessAuthEvent_popupErrorAuthEvent() { + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', + '1234', + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.popupAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Resolve popup with error. + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertEquals( + null, + handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', + handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_redirectErrorAuthEvent() { + asyncTestCase.waitForSignals(2); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Should not be called as event is not a popup type. + assertEquals(0, handler.resolvePendingPopupEvent.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_finisher_successfulPopupAuthEvent() { + // Browser only environment. + asyncTestCase.waitForSignals(1); + var expectedPopupResponse = { + 'user': {}, + 'credential': {} + }; + handler.getAuthEventHandlerFinisher = function(mode, eventId) { + assertEquals('linkViaPopup', mode); + assertEquals('1234', eventId); + return function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + return goog.Promise.resolve(expectedPopupResponse); + }; + }; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.popupAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Resolve popup with success. + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertObjectEquals( + expectedPopupResponse, + handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertNull( + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', + handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_finisher_errorPopupAuthEvent() { + // Browser only environment. + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + handler.getAuthEventHandlerFinisher = function(mode, eventId) { + assertEquals('linkViaPopup', mode); + assertEquals('1234', eventId); + return function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + return goog.Promise.reject(expectedError); + }; + }; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.popupAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Resolve popup with error. + assertEquals(1, handler.resolvePendingPopupEvent.getCallCount()); + assertEquals( + 'linkViaPopup', + handler.resolvePendingPopupEvent.getLastCall().getArgument(0)); + assertObjectEquals( + null, + handler.resolvePendingPopupEvent.getLastCall().getArgument(1)); + assertErrorEquals( + expectedError, + handler.resolvePendingPopupEvent.getLastCall().getArgument(2)); + assertEquals( + '1234', + handler.resolvePendingPopupEvent.getLastCall().getArgument(3)); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_finisher_successfulRedirectAuthEvent() { + clock = new goog.testing.MockClock(true); + asyncTestCase.waitForSignals(2); + handler.getAuthEventHandlerFinisher = function(mode, eventId) { + assertEquals('linkViaRedirect', mode); + assertEquals('1234', eventId); + return function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // Once the handler is called, the redirect event cannot timeout. + clock.tick(timeoutDelay); + return goog.Promise.resolve(expectedRedirectResult); + }; + }; + var expectedRedirectResult = { + 'user': {}, + 'credential': {} + }; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedRedirectResult, result); + asyncTestCase.signal(); + }); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Popup resolve should not be called as this is not a popup event. + assertEquals(0, handler.resolvePendingPopupEvent.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_finisher_errorRedirectAuthEvent() { + asyncTestCase.waitForSignals(2); + handler.getAuthEventHandlerFinisher = function(mode, eventId) { + assertEquals('linkViaRedirect', mode); + assertEquals('1234', eventId); + return function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + return goog.Promise.reject(expectedError); + }; + }; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Popup resolve should not be called as this is not a popup event. + assertEquals(0, handler.resolvePendingPopupEvent.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testProcessAuthEvent_noHandler() { + // No handler available for whatever reason. + handler.getAuthEventHandlerFinisher = function() {return null;}; + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH_EVENT); + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testGetRedirect_noHandlerCanProcessEvent() { + asyncTestCase.waitForSignals(2); + var expectedRedirectResult = { + 'user': null + }; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaPopup', '1234', 'http://www.example.com/#response', 'SESSION_ID'); + // This test is not environment specific. + stubs.replace( + fireauth.AuthEventManager, + 'instantiateOAuthSignInHandler', + function(authDomain, apiKey, appName, version) { + assertEquals('subdomain1.firebaseapp.com', authDomain); + assertEquals('API_KEY1', apiKey); + assertEquals('APP1', appName); + assertEquals(firebase.SDK_VERSION, version); + return { + 'addAuthEventListener': function(handler) { + // handleAuthEvent_ should not resolve. + assertFalse(handler(expectedAuthEvent)); + asyncTestCase.signal(); + }, + 'initializeAndWait': function() { return goog.Promise.resolve(); }, + 'shouldBeInitializedEarly': function() { + return false; + }, + 'hasVolatileStorage': function() { + return false; + } + }; + }); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + handler.canHandleAuthEvent = function() {return false;}; + manager.initialize(); + manager.subscribe(handler); + // When on load no handler can process event, getRedirectResult should still + // resolve. + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedRedirectResult, result); + asyncTestCase.signal(); + }); +} + + +function testRedirectResult_timeout() { + clock = new goog.testing.MockClock(true); + asyncTestCase.waitForSignals(1); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.TIMEOUT); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Speed up timeout. + clock.tick(timeoutDelay); +} + + +function testRedirectResult_overwritePreviousRedirectResult() { + // Once redirect result is determined, it can still be overwritten, though + // in some cases like ifchandler which gets redirect result from + // sessionStorage, this should not happen. + asyncTestCase.waitForSignals(4); + handler.getAuthEventHandlerFinisher = function(mode, eventId) { + return function(requestUri, sessionId) { + // Iterate through results array each call. + return goog.Promise.resolve(results[index++]); + }; + }; + var index = 0; + var expectedRedirectResult = { + 'user': {'uid': '1234'}, + 'credential': {} + }; + var expectedRedirectResult2 = { + 'user': {'uid': '5678'}, + 'credential': {} + }; + var results = [expectedRedirectResult, expectedRedirectResult2]; + var expectedAuthEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + var manager = fireauth.AuthEventManager.getManager( + authDomain1, apiKey1, appName1); + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedRedirectResult, result); + asyncTestCase.signal(); + }); + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + // Popup resolve should not be called as this is not a popup event. + assertEquals(0, handler.resolvePendingPopupEvent.getCallCount()); + asyncTestCase.signal(); + // Call Second time. + manager.redirectAuthEventProcessor_.processAuthEvent( + expectedAuthEvent, handler).then(function() { + assertEquals(0, handler.resolvePendingPopupEvent.getCallCount()); + asyncTestCase.signal(); + // getRedirectResult should resolve with the second expected result. + manager.getRedirectResult().then(function(result) { + assertObjectEquals(expectedRedirectResult2, result); + asyncTestCase.signal(); + }); + }); + }); +} diff --git a/packages/auth/test/authstorage_test.js b/packages/auth/test/authstorage_test.js new file mode 100644 index 00000000000..78f52e0e360 --- /dev/null +++ b/packages/auth/test/authstorage_test.js @@ -0,0 +1,1023 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for authstorage.js + */ + +goog.provide('fireauth.authStorageTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +/** @suppress {extraRequire} Needed for firebase.app().auth() */ +goog.require('fireauth.exports'); +goog.require('fireauth.storage.IndexedDB'); +goog.require('fireauth.storage.LocalStorage'); +goog.require('fireauth.storage.SessionStorage'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.authStorageTest'); + + +var config = { + apiKey: 'apiKey1' +}; +var stubs = new goog.testing.PropertyReplacer(); +var appId = 'appId1'; +var clock; + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + clock = new goog.testing.MockClock(true); + window.localStorage.clear(); + window.sessionStorage.clear(); +} + + +function tearDown() { + stubs.reset(); + goog.dispose(clock); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testValidatePersistenceArgument_validAndSupported() { + assertNotThrows(function() { + fireauth.authStorage.validatePersistenceArgument('local'); + fireauth.authStorage.validatePersistenceArgument('session'); + fireauth.authStorage.validatePersistenceArgument('none'); + }); +} + + +function testValidatePersistenceArgument_invalid() { + var invalidTypeError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_PERSISTENCE); + var invalidOptions = ['LOCAL', 'bla', null, {}, true, ['none']]; + for (var i = 0; i < invalidOptions.length; i++) { + fireauth.common.testHelper.assertErrorEquals( + invalidTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument(invalidOptions[i]); + })); + } +} + + +function testValidatePersistenceArgument_node() { + // Simulate Node.js. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() { + return fireauth.util.Env.NODE; + }); + var unsupportedTypeError = new fireauth.AuthError( + fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE); + // Local should throw an error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument('local'); + })); + // Session should throw an error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument('session'); + })); + // None should be supported. + assertNotThrows(function() { + fireauth.authStorage.validatePersistenceArgument('none'); + }); +} + + +function testValidatePersistenceArgument_reactNative() { + // Simulate React-Native. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() { + return fireauth.util.Env.REACT_NATIVE; + }); + var unsupportedTypeError = new fireauth.AuthError( + fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE); + // Only session should throw an error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument('session'); + })); + // Local and none should be supported. + assertNotThrows(function() { + fireauth.authStorage.validatePersistenceArgument('local'); + fireauth.authStorage.validatePersistenceArgument('none'); + }); +} + + +function testValidatePersistenceArgument_browser() { + // Browser or other. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() { + return fireauth.util.Env.BROWSER; + }); + // Simulate web storage supported. + stubs.replace( + fireauth.util, + 'isWebStorageSupported', + function() { + return true; + }); + var unsupportedTypeError = new fireauth.AuthError( + fireauth.authenum.Error.UNSUPPORTED_PERSISTENCE); + // Should be supported when web storage is. + assertNotThrows(function() { + fireauth.authStorage.validatePersistenceArgument('local'); + fireauth.authStorage.validatePersistenceArgument('session'); + fireauth.authStorage.validatePersistenceArgument('none'); + }); + // Simulate web storage not supported. + stubs.replace( + fireauth.util, + 'isWebStorageSupported', + function() { + return false; + }); + // Only none should work. + assertNotThrows(function() { + fireauth.authStorage.validatePersistenceArgument('none'); + }); + // Local should throw an error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument('local'); + })); + // Session should throw an error. + fireauth.common.testHelper.assertErrorEquals( + unsupportedTypeError, + assertThrows(function() { + fireauth.authStorage.validatePersistenceArgument('session'); + })); +} + + +function testWebStorageNotSupported() { + // Test when web storage not supported. In memory storage should be used + // instead. + stubs.replace( + fireauth.storage.LocalStorage, + 'isAvailable', + function() { + return false; + }); + stubs.replace( + fireauth.storage.SessionStorage, + 'isAvailable', + function() { + return false; + }); + // The following should not fail and should create in memory storage + // instances. + var manager = getDefaultManagerInstance(); + var tempKey = {name: 'temporary', persistent: 'session'}; + var tempStorageKey = 'firebase:temporary:appId1'; + var persistentKey = {name: 'persistent', persistent: 'local'}; + var persistentStorageKey = 'firebase:persistent:appId1'; + var expectedValue = 'something'; + // Get, set and remove should work as expected on both types of storage. + return goog.Promise.resolve() + .then(function() { + // Test temporary storage. + return manager.set(tempKey, expectedValue, appId); + }) + .then(function() { + return manager.get(tempKey, appId); + }) + .then(function(value) { + // Nothing should be stored in sessionStorage. + assertNull(window.sessionStorage.getItem(tempStorageKey)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(tempKey, appId); + }) + .then(function() { + return manager.get(tempKey, appId); + }) + .then(function(value) { + assertUndefined(value); + // Test persistent storage. + return manager.set(persistentKey, expectedValue, appId); + }) + .then(function() { + return manager.get(persistentKey, appId); + }) + .then(function(value) { + // Nothing should be stored in localStorage. + assertNull(window.localStorage.getItem(persistentStorageKey)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(persistentKey, appId); + }) + .then(function() { + return manager.get(persistentKey, appId); + }) + .then(function(value) { + assertUndefined(value); + }); + +} + + +function testGetSet_temporaryStorage() { + var manager = getDefaultManagerInstance(); + var key = {name: 'temporary', persistent: 'session'}; + var expectedValue = 'something'; + var storageKey = 'firebase:temporary:appId1'; + return goog.Promise.resolve() + .then(function() { + return manager.set(key, expectedValue, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertNull(window.localStorage.getItem(storageKey)); + assertEquals( + window.sessionStorage.getItem(storageKey), + JSON.stringify(expectedValue)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(key, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertNull(window.sessionStorage.getItem(storageKey)); + assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + }); +} + + +function testGetSet_inMemoryStorage() { + var manager = getDefaultManagerInstance(); + var key = {name: 'temporary', persistent: 'none'}; + var expectedValue = 'something'; + var storageKey = 'firebase:none:appId1'; + return goog.Promise.resolve() + .then(function() { + return manager.set(key, expectedValue, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertNull(window.sessionStorage.getItem(storageKey)); + assertNull(window.localStorage.getItem(storageKey)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(key, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertNull(window.sessionStorage.getItem(storageKey)); + assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + }); +} + + +function testGetSet_persistentStorage() { + var manager = getDefaultManagerInstance(); + var key = {name: 'persistent', persistent: 'local'}; + var expectedValue = 'something'; + var storageKey = 'firebase:persistent:appId1'; + return goog.Promise.resolve() + .then(function() { + return manager.set(key, expectedValue, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertEquals( + window.localStorage.getItem(storageKey), + JSON.stringify(expectedValue)); + assertNull(window.sessionStorage.getItem(storageKey)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(key, appId); + }) + .then(function() { + return manager.get(key, appId); + }) + .then(function(value) { + assertNull(window.localStorage.getItem(storageKey)); + assertNull(window.sessionStorage.getItem(storageKey)); + assertUndefined(value); + }); +} + + +function testGetSet_persistentStorage_noId() { + var manager = getDefaultManagerInstance(); + var key = {name: 'persistent', persistent: 'local'}; + var expectedValue = 'something'; + var storageKey = 'firebase:persistent'; + return goog.Promise.resolve() + .then(function() { + return manager.set(key, expectedValue); + }) + .then(function() { + return manager.get(key); + }) + .then(function(value) { + assertEquals( + window.localStorage.getItem(storageKey), + JSON.stringify(expectedValue)); + assertObjectEquals(expectedValue, value); + }) + .then(function() { + return manager.remove(key); + }) + .then(function() { + return manager.get(key); + }) + .then(function(value) { + assertNull(window.localStorage.getItem(storageKey)); + assertUndefined(value); + }); +} + + +function testAddRemoveListeners_localStorage() { + var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + window.localStorage.setItem( + 'name:authUser:appId1', JSON.stringify({'foo': 'bar'})); + window.localStorage.setItem( + 'name:authEvent:appId1', JSON.stringify({'foo': 'bar'})); + // Add listeners for 2 events. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Trigger user event. + storageEvent.key = 'name:authUser:appId1'; + storageEvent.newValue = null; + window.localStorage.removeItem('name:authUser:appId1'); + goog.testing.events.fireBrowserEvent(storageEvent); + // Listener 1 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Trigger second event. + storageEvent.key = 'name:authEvent:appId1'; + storageEvent.newValue = null; + window.localStorage.removeItem('name:authEvent:appId1'); + goog.testing.events.fireBrowserEvent(storageEvent); + // Only second listener should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Some unknown event. + storageEvent.key = 'key3'; + storageEvent.newValue = null; + goog.testing.events.fireBrowserEvent(storageEvent); + // No listeners should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Remove all listeners. + manager.removeListener(key1, 'appId1', listener1); + manager.removeListener(key2, 'appId1', listener2); + manager.removeListener(key1, 'appId1', listener3); + // Trigger first event. + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'name:authUser:appId1'; + window.localStorage.setItem( + 'name:authUser:appId1', JSON.stringify({'foo': 'bar'})); + storageEvent.newValue = JSON.stringify({'foo': 'bar'}); + goog.testing.events.fireBrowserEvent(storageEvent); + // No change. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); +} + + +function testAddRemoveListeners_localStorage_nullKey() { + var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var listener4 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + var key3 = {'name': 'other', 'persistent': true}; + // Save existing data for key1 and key2. + var storageKey1 = 'name:authUser:appId1'; + window.localStorage.setItem(storageKey1, JSON.stringify({'foo': 'bar'})); + var storageKey2 = 'name:authEvent:appId1'; + window.localStorage.setItem(storageKey2, JSON.stringify({'foo2': 'bar2'})); + // Add listeners for 3 keys. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + manager.addListener(key3, 'appId1', listener4); + // No listener should trigger initially. + assertEquals(0, listener1.getCallCount()); + assertEquals(0, listener2.getCallCount()); + assertEquals(0, listener3.getCallCount()); + assertEquals(0, listener4.getCallCount()); + // Simulate a storage even will null key. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Trigger event with null key (localStorage completely cleared via developer + // tools). + storageEvent.key = null; + storageEvent.newValue = null; + window.localStorage.removeItem(storageKey1); + window.localStorage.removeItem(storageKey2); + goog.testing.events.fireBrowserEvent(storageEvent); + // Listener 1, 2 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener2.getCallCount()); + assertEquals(1, listener3.getCallCount()); + // No change in key3 value. + assertEquals(0, listener4.getCallCount()); +} + + +function testAddRemoveListeners_localStorage_ie10() { + // Simulate IE 10 with localStorage events not synchronized. + // event.newValue will not be immediately equal to + // localStorage.getItem(event.key). + stubs.replace( + fireauth.util, + 'isIe10', + function() { + return true; + }); + var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + window.localStorage.setItem( + 'name:authUser:appId1', JSON.stringify({'foo': 'bar'})); + window.localStorage.setItem( + 'name:authEvent:appId1', JSON.stringify({'foo': 'bar'})); + window.localStorage.setItem('key3', JSON.stringify({'foo': 'bar'})); + // Add listeners for 2 events. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Trigger user event. + storageEvent.key = 'name:authUser:appId1'; + storageEvent.oldValue = JSON.stringify({'foo': 'bar'}); + storageEvent.newValue = null; + goog.testing.events.fireBrowserEvent(storageEvent); + window.localStorage.removeItem('name:authUser:appId1'); + // Simulate some delay for localStorage event newValue to be available in + // localStorage.getItem(event.key). + clock.tick(fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY); + // Listener 1 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Trigger second event. + storageEvent.key = 'name:authEvent:appId1'; + storageEvent.oldValue = JSON.stringify({'foo': 'bar'}); + storageEvent.newValue = null; + goog.testing.events.fireBrowserEvent(storageEvent); + window.localStorage.removeItem('name:authEvent:appId1'); + // Simulate some delay for localStorage event newValue to be available in + // localStorage.getItem(event.key). + clock.tick(fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY); + // Only second listener should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + // Some unknown event. + storageEvent.key = 'key3'; + storageEvent.oldValue = JSON.stringify({'foo': 'bar'}); + storageEvent.newValue = null; + goog.testing.events.fireBrowserEvent(storageEvent); + window.localStorage.removeItem('key3'); + // Simulate some delay for localStorage event newValue to be available in + // localStorage.getItem(event.key). + clock.tick(fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY); + // No listeners should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Remove all listeners. + manager.removeListener(key1, 'appId1', listener1); + manager.removeListener(key2, 'appId1', listener2); + manager.removeListener(key1, 'appId1', listener3); + // Trigger first event. + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'name:authUser:appId1'; + storageEvent.newValue = JSON.stringify({'foo': 'bar'}); + storageEvent.oldValue = null; + goog.testing.events.fireBrowserEvent(storageEvent); + window.localStorage.setItem( + 'name:authUser:appId1', JSON.stringify({'foo': 'bar'})); + // Simulate some delay for localStorage event newValue to be available in + // localStorage.getItem(event.key). + clock.tick(fireauth.authStorage.IE10_LOCAL_STORAGE_SYNC_DELAY); + // No change. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); +} + + +function testAddRemoveListeners_indexeddb() { + // Mock indexedDB local storage manager. + var mockIndexeddb = { + handlers: [], + addStorageListener: function(indexeddbHandler) { + mockIndexeddb.handlers.push(indexeddbHandler); + }, + removeStorageListener: function(indexeddbHandler) { + goog.array.remove(mockIndexeddb.handlers, indexeddbHandler); + }, + trigger: function(key) { + // Trigger all listeners on key. + for (var i = 0; i < mockIndexeddb.handlers.length; i++) { + // Trigger key change. + mockIndexeddb.handlers[i]([key]); + } + } + }; + // Simulate browser that does not synchronize between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return true; + }); + // Use mock indexedDB manager. + stubs.replace( + fireauth.storage.IndexedDB, + 'getFireauthManager', + function() { + return mockIndexeddb; + }); + var manager = new fireauth.authStorage.Manager('name', ':', false, true); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + // Add listeners for 2 events. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + // Trigger user event. + mockIndexeddb.trigger('name:authUser:appId1'); + // Listener 1 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + // Trigger second event. + mockIndexeddb.trigger('name:authEvent:appId1'); + // Only second listener should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Some unknown event. + mockIndexeddb.trigger('key3'); + // No listeners should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Remove all listeners. + manager.removeListener(key1, 'appId1', listener1); + manager.removeListener(key2, 'appId1', listener2); + manager.removeListener(key1, 'appId1', listener3); + // Trigger first event. + mockIndexeddb.trigger('name:authUser:appId1'); + // No change. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); +} + + +function testAddRemoveListeners_indexeddb_cannotRunInBackground() { + // Even when app cannot run in the background and localStorage is not synced + // between iframe and popup, polling web storage function should not be used. + // indexedDB should be used. + // Mock indexedDB local storage manager. + var mockIndexeddb = { + handlers: [], + addStorageListener: function(indexeddbHandler) { + mockIndexeddb.handlers.push(indexeddbHandler); + }, + removeStorageListener: function(indexeddbHandler) { + goog.array.remove(mockIndexeddb.handlers, indexeddbHandler); + }, + trigger: function(key) { + // Trigger all listeners on key. + for (var i = 0; i < mockIndexeddb.handlers.length; i++) { + // Trigger key change. + mockIndexeddb.handlers[i]([key]); + } + } + }; + // Simulate browser that does not synchronize between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return true; + }); + // Use mock indexedDB manager. + stubs.replace( + fireauth.storage.IndexedDB, + 'getFireauthManager', + function() { + return mockIndexeddb; + }); + // Cannot run in the background. + var manager = new fireauth.authStorage.Manager('name', ':', false, false); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + var key1 = {'name': 'authUser', 'persistent': true}; + var key2 = {'name': 'authEvent', 'persistent': true}; + // Add listeners for 2 events. + manager.addListener(key1, 'appId1', listener1); + manager.addListener(key2, 'appId1', listener2); + manager.addListener(key1, 'appId1', listener3); + // Trigger user event. + mockIndexeddb.trigger('name:authUser:appId1'); + // Listener 1 and 3 should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + // Trigger second event. + mockIndexeddb.trigger('name:authEvent:appId1'); + // Only second listener should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Some unknown event. + mockIndexeddb.trigger('key3'); + // No listeners should trigger. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); + // Remove all listeners. + manager.removeListener(key1, 'appId1', listener1); + manager.removeListener(key2, 'appId1', listener2); + manager.removeListener(key1, 'appId1', listener3); + // Trigger first event. + mockIndexeddb.trigger('name:authUser:appId1'); + // No change. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(1, listener2.getCallCount()); +} + + +function testSafariLocalStorageSync_newEvent() { + var manager = + new fireauth.authStorage.Manager('firebase', ':', true, true); + // Simulate Safari bug. + stubs.replace( + fireauth.util, + 'isSafariLocalStorageNotSynced', + function() {return true;}); + var key1 = {'name': 'authEvent', 'persistent': true}; + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + // New Auth event. + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(expectedEvent); + manager.addListener(key1, 'appId1', listener1); + // This should force localStorage sync. + goog.testing.events.fireBrowserEvent(storageEvent); + // Auth event should be synchronized. + assertEquals( + storageEvent.newValue, + window.localStorage.getItem(storageEvent.key)); + assertEquals(1, listener1.getCallCount()); + manager.removeListener(key1, 'appId1', listener1); + goog.testing.events.fireBrowserEvent(storageEvent); + // No further call. + assertEquals(1, listener1.getCallCount()); +} + + +function testSafariLocalStorageSync_cannotRunInBackground() { + // This simulates iframe embedded in a cross origin domain. + // Realistically only storage event should trigger here. + // Test when new data is added to storage. + var manager = + new fireauth.authStorage.Manager('firebase', ':', true, false); + // Simulate Safari bug. + stubs.replace( + fireauth.util, + 'isSafariLocalStorageNotSynced', + function() {return true;}); + var key1 = {'name': 'authEvent', 'persistent': true}; + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + // New Auth event. + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(expectedEvent); + manager.addListener(key1, 'appId1', listener1); + // This should force localStorage sync. + goog.testing.events.fireBrowserEvent(storageEvent); + // Auth event should be synchronized. + assertEquals( + storageEvent.newValue, + window.localStorage.getItem(storageEvent.key)); + assertEquals(1, listener1.getCallCount()); + manager.removeListener(key1, 'appId1', listener1); + goog.testing.events.fireBrowserEvent(storageEvent); + // No further call. + assertEquals(1, listener1.getCallCount()); +} + + +function testSafariLocalStorageSync_deletedEvent() { + // This simulates iframe embedded in a cross origin domain. + // Realistically only storage event should trigger here. + // Test when old data is deleted from storage. + var manager = + new fireauth.authStorage.Manager('firebase', ':', true, true); + var key1 = {'name': 'authEvent', 'persistent': true}; + // Simulate Safari bug. + stubs.replace( + fireauth.util, + 'isSafariLocalStorageNotSynced', + function() {return true;}); + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + // Deleted Auth event. + storageEvent.oldValue = JSON.stringify(expectedEvent); + storageEvent.newValue = null; + window.localStorage.setItem(storageEvent.key, storageEvent.oldValue); + manager.addListener(key1, 'appId1', listener1); + // This should force localStorage sync. + goog.testing.events.fireBrowserEvent(storageEvent); + // Auth event should be synchronized. + assertNull(window.localStorage.getItem(storageEvent.key)); + assertEquals(1, listener1.getCallCount()); + manager.removeListener(key1, 'appId1', listener1); + goog.testing.events.fireBrowserEvent(storageEvent); + // No further call. + assertEquals(1, listener1.getCallCount()); +} + + +function testRunsInBackground_storageEventMode() { + // Test when browser does not run in the background while another tab is in + // foreground. + // Test when storage event is first detected. Polling should be disabled to + // prevent duplicate storage detection. + var key = {name: 'authEvent', persistent: 'local'}; + var storageKey = 'firebase:authEvent:appId1'; + var manager = new fireauth.authStorage.Manager( + 'firebase', ':', false, false); + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + // Simulate storage event. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + // New Auth event. + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(expectedEvent); + + // Add listener. + manager.addListener(key, appId, listener1); + // Test that storage event listener is not set. + // This should not be detected. + window.localStorage.setItem( + storageKey, JSON.stringify(expectedEvent)); + goog.testing.events.fireBrowserEvent(storageEvent); + // Listener should trigger. + assertEquals(1, listener1.getCallCount()); + // Clear storage. + window.localStorage.clear(); + // Save Auth event and confirm listener not triggered. + // This simulates polling. + window.localStorage.setItem( + storageKey, JSON.stringify(expectedEvent)); + // Run clock. + clock.tick(1000); + // Duplicate polling event should not trigger. + assertEquals(1, listener1.getCallCount()); + // Remove listener. + manager.removeListener(key, appId, listener1); + // Simulate another storage to same item. + goog.testing.events.fireBrowserEvent(storageEvent); + // Listener should not be triggered as it has been removed. + assertEquals(1, listener1.getCallCount()); +} + + +function testRunsInBackground_pollingMode() { + // Test when browser does not run in the background while another tab is in + // foreground. + // Test when storage polling first detects a storage notification. + // Storage event listener should be disabled to prevent duplicate storage + // detection. + var key = {name: 'authEvent', persistent: 'local'}; + var storageKey = 'firebase:authEvent:appId1'; + var manager = new fireauth.authStorage.Manager( + 'firebase', ':', false, false); + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + // Simulate storage event. Don't trigger yet. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + // New auth event. + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(expectedEvent); + + // Add listener. + manager.addListener(key, appId, listener1); + // Simulate storage update in some other tab and storage event not + // triggered. + window.localStorage.setItem( + storageKey, JSON.stringify(expectedEvent)); + // Run clock. + clock.tick(1000); + // Listener should be triggered. + assertEquals(1, listener1.getCallCount()); + // Test that duplicate storage event is ignored. + // This should not be detected. + goog.testing.events.fireBrowserEvent(storageEvent); + window.localStorage.setItem( + storageKey, JSON.stringify(expectedEvent)); + // Listener should not trigger. + assertEquals(1, listener1.getCallCount()); + // Remove listener. + manager.removeListener(key, appId, listener1); + // Simulate another storage to same item. + window.localStorage.removeItem(storageKey); + // Run clock. + clock.tick(1000); + // Listener should not be triggered as it has been removed. + assertEquals(1, listener1.getCallCount()); +} + + +function testRunsInBackground_currentTabChangesIgnored() { + // Test when browser does not run in the background while another tab is in + // foreground. + // This tests that only other tab changes are detected and current tab changes + // are ignored. + var key = {name: 'authEvent', persistent: 'local'}; + var storageKey = 'firebase:authEvent:appId1'; + var manager = new fireauth.authStorage.Manager( + 'firebase', ':', false, false); + var listener1 = goog.testing.recordFunction(); + var expectedEvent = { + type: 'signInViaPopup', + eventId: '1234', + callbackUrl: 'http://www.example.com/#oauthResponse', + sessionId: 'SESSION_ID' + }; + // Add listener. + manager.addListener(key, appId, listener1); + // Save Auth event and confirm listener not triggered. + return manager.set(key, expectedEvent, appId).then(function() { + // Changes in same tab should not trigger listener. + assertEquals(0, listener1.getCallCount()); + // Delete Auth event and confirm listener not triggered. + return manager.remove(key, appId); + }).then(function() { + // Changes in same tab should not trigger listener. + assertEquals(0, listener1.getCallCount()); + // Simulate storage update in some other tab and storage event not + // triggered. + window.localStorage.setItem( + storageKey, JSON.stringify(expectedEvent)); + // Run clock. + clock.tick(1000); + // Listener should be triggered. + assertEquals(1, listener1.getCallCount()); + // Remove listener. + manager.removeListener(key, appId, listener1); + // Simulate another storage to same item. + window.localStorage.removeItem(storageKey); + // Run clock. + clock.tick(1000); + // Listener should not be triggered as it has been removed. + assertEquals(1, listener1.getCallCount()); + }); +} diff --git a/packages/auth/test/authuser_test.js b/packages/auth/test/authuser_test.js new file mode 100644 index 00000000000..11006b9f2ca --- /dev/null +++ b/packages/auth/test/authuser_test.js @@ -0,0 +1,10283 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for authuser.js + */ + +goog.provide('fireauth.AuthUserTest'); + +goog.require('fireauth.ActionCodeSettings'); +goog.require('fireauth.Auth'); +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.AuthEventManager'); +goog.require('fireauth.AuthUser'); +goog.require('fireauth.AuthUserInfo'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.OAuthSignInHandler'); +goog.require('fireauth.PhoneAuthCredential'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.ProactiveRefresh'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.StsTokenManager'); +goog.require('fireauth.TokenRefreshTime'); +goog.require('fireauth.UserEventType'); +goog.require('fireauth.UserMetadata'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.constants'); +goog.require('fireauth.deprecation'); +goog.require('fireauth.idp'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.object'); +goog.require('fireauth.storage.PendingRedirectManager'); +goog.require('fireauth.storage.RedirectUserManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.events'); +goog.require('goog.events.EventTarget'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.AuthUserTest'); + +var config = { + apiKey: 'apiKey1' +}; +var user = null; +var accountInfo = null; +var accountInfoWithPhone = null; +var providerData1 = null; +var providerData2 = null; +var providerDataPhone = null; +var config1 = null; +var config2 = null; +var rpcHandler = null; +var token = null; +var tokenResponse = null; +var accountInfo2 = null; +var getAccountInfoResponse = null; +var getAccountInfoResponseProviderData1 = null; +var getAccountInfoResponseProviderData2 = null; +// A sample JWT, along with its decoded contents. +var idTokenGmail = { + jwt: 'HEADER.ew0KICAiaXNzIjogIkdJVGtpdCIsDQogICJleHAiOiAxMzI2NDM5' + + 'MDQ0LA0KICAic3ViIjogIjY3OSIsDQogICJhdWQiOiAiMjA0MjQxNjMxNjg2IiwNCiAgImZl' + + 'ZGVyYXRlZF9pZCI6ICJodHRwczovL3d3dy5nb29nbGUuY29tL2FjY291bnRzLzEyMzQ1Njc4' + + 'OSIsDQogICJwcm92aWRlcl9pZCI6ICJnbWFpbC5jb20iLA0KICAiZW1haWwiOiAidGVzdDEy' + + 'MzQ1NkBnbWFpbC5jb20iDQp9.SIGNATURE', + data: { + exp: 1326439044, + sub: '679', + aud: '204241631686', + provider_id: 'gmail.com', + email: 'test123456@gmail.com', + federated_id: 'https://www.google.com/accounts/123456789' + } +}; +var stsTokenResponse = { + 'idToken': 'myIdToken', + 'refreshToken': 'myRefreshToken' +}; +var expectedTokenResponseWithIdPData; +var expectedAdditionalUserInfo; +var expectedGoogleCredential; +var expectedReauthenticateTokenResponse; +var now = goog.now(); + +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); + +var stubs = new goog.testing.PropertyReplacer(); +var ignoreArgument; +var mockControl; + +var app; +var auth; +var getAccountInfoResponseGoogleProviderData; +var getAccountInfoResponsePhoneAuthProviderData; +var expectedPhoneNumber; +var appVerifier; +var expectedRecaptchaToken; +var actionCodeSettings = { + 'url': 'https://www.example.com/?state=abc', + 'iOS': { + 'bundleId': 'com.example.ios' + }, + 'android': { + 'packageName': 'com.example.android', + 'installApp': true, + 'minimumVersion': '12' + }, + 'handleCodeInApp': true +}; +var lastLoginAt = '1506050282000'; +var createdAt = '1506044998000'; +var lastLoginAt2 = '1506053999000'; +var createdAt2 = '1505980145000'; + + +function setUp() { + // Disable Auth event manager by default unless needed for a specific test. + fireauth.AuthEventManager.ENABLED = false; + config1 = { + apiKey: 'apiKey1', + appName: 'appId1' + }; + config2 = { + apiKey: 'apiKey2', + appName: 'appId2' + }; + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + // Simulate tab can run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // In case the tests are run from an iframe. + stubs.replace( + fireauth.util, + 'isIframe', + function() { + return false; + }); + stubs.replace( + goog, + 'now', + function() { + return now; + }); + accountInfo = { + 'uid': 'defaultUserId', + 'email': 'user@default.com', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'emailVerified': true, + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }; + accountInfoWithPhone = { + 'uid': 'defaultUserId', + 'email': 'user@default.com', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }; + accountInfo2 = { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + // common_typos_disable. + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true, + 'lastLoginAt': lastLoginAt2, + 'createdAt': createdAt2 + }; + + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + 'user1', + 'https://www.example.com/user1/photo.png'); + providerData2 = new fireauth.AuthUserInfo( + 'providerUserId2', + 'providerId2', + 'user2@example.com', + 'user2', + 'https://www.example.com/user2/photo.png'); + providerDataPhone = new fireauth.AuthUserInfo( + '+16505550101', 'phone', undefined, undefined, undefined, '+16505550101'); + rpcHandler = new fireauth.RpcHandler('apiKey1'); + token = new fireauth.StsTokenManager(rpcHandler); + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', now + 3600 * 1000); + tokenResponse = { + 'idToken': 'accessToken', + 'refreshToken': 'refreshToken', + 'expiresIn': 3600 + }; + + // accountInfo in the format of a getAccountInfo response. + getAccountInfoResponse = { + 'users': [{ + 'localId': 'defaultUserId', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'displayName': 'defaultDisplayName', + 'providerUserInfo': [], + 'photoUrl': 'https://www.default.com/default/default.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false, + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }] + }; + // providerData1 and providerData2 in the format of a getAccountInfo response. + getAccountInfoResponseProviderData1 = { + 'providerId': 'providerId1', + 'displayName': 'user1', + 'email': 'user1@example.com', + 'photoUrl': 'https://www.example.com/user1/photo.png', + 'rawId': 'providerUserId1' + }; + getAccountInfoResponseProviderData2 = { + 'providerId': 'providerId2', + 'displayName': 'user2', + 'email': 'user2@example.com', + 'photoUrl': 'https://www.example.com/user2/photo.png', + 'rawId': 'providerUserId2' + }; + getAccountInfoResponseGoogleProviderData = { + 'providerId': 'google.com', + 'displayName': 'My Google Name', + 'email': 'me@gmail.com', + 'photoUrl': 'https://www.google.com/me.png', + 'rawId': 'myGoogleId' + }; + getAccountInfoResponsePhoneAuthProviderData = { + 'providerId': 'phone', + 'rawId': '+16505550101', + 'phoneNumber': '+16505550101' + }; + expectedTokenResponseWithIdPData = { + 'idToken': 'newStsToken', + 'refreshToken': 'newRefreshToken', + 'expiresIn': '3600', + // Credential returned. + 'providerId': 'google.com', + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken', + 'oauthExpireIn': 3600, + // Additional user info data. + 'rawUserInfo': '{"kind":"plus#person","displayName":"John Doe","na' + + 'me":{"givenName":"John","familyName":"Doe"}}' + }; + expectedReauthenticateTokenResponse = { + 'idToken': idTokenGmail.jwt, + 'refreshToken': 'myRefreshToken', + 'expiresIn': 3600, + // Credential returned. + 'providerId': 'google.com', + 'oauthAccessToken': 'googleAccessToken', + 'oauthIdToken': 'googleIdToken', + 'oauthExpireIn': 3600, + // Additional user info data. + 'rawUserInfo': '{"kind":"plus#person","displayName":"John Doe","na' + + 'me":{"givenName":"John","familyName":"Doe"}}' + }; + expectedAdditionalUserInfo = { + 'profile': { + 'kind': 'plus#person', + 'displayName': 'John Doe', + 'name': { + 'givenName': 'John', + 'familyName': 'Doe' + } + }, + 'providerId': 'google.com', + 'isNewUser': false + }; + expectedGoogleCredential = fireauth.GoogleAuthProvider.credential( + 'googleIdToken', 'googleAccessToken'); + expectedPhoneNumber = '+16505550101'; + expectedRecaptchaToken = 'RECAPTCHA_TOKEN'; + appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(expectedRecaptchaToken); + } + }; + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +function tearDown() { + for (var i = 0; i < firebase.apps.length; i++) { + asyncTestCase.waitForSignals(1); + firebase.apps[i].delete().then(function() { + asyncTestCase.signal(); + }); + } + if (auth) { + auth.delete(); + } + // Reset already initialized Auth event managers. + fireauth.AuthEventManager.manager_ = {}; + user = null; + accountInfo = null; + accountInfoWithPhone = null; + accountInfo2 = null; + getAccountInfoResponse = null; + getAccountInfoResponseProviderData1 = null; + getAccountInfoResponseProviderData2 = null; + providerData1 = null; + providerData2 = null; + providerDataPhone = null; + rpcHandler = null; + token = null; + tokenResponse = null; + config1 = null; + config2 = null; + window.localStorage.clear(); + window.sessionStorage.clear(); + stubs.reset(); + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } +} + + +/** @return {!goog.events.EventTarget} The event dispatcher test object. */ +function createEventDispatcher() { + return new goog.events.EventTarget(); +} + + +/** + * Asserts that token events do not trigger. + * @param {!fireauth.AuthUser} user + */ +function assertNoTokenEvents(user) { + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + fail('Token change should not trigger due to token being unchanged!'); + }); +} + + +/** + * Asserts that user invalidated events do not trigger. + * @param {!fireauth.AuthUser} user + */ +function assertNoUserInvalidatedEvents(user) { + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + fail('User invalidate event should not trigger!'); + }); +} + + +/** + * Asserts that state events do not trigger. + * @param {!fireauth.AuthUser} user + */ +function assertNoStateEvents(user) { + user.addStateChangeListener(function(userTemp) { + fail('State change listener should not trigger!'); + }); +} + + +/** + * Asserts that delete events do not trigger. + * @param {!fireauth.AuthUser} user + */ +function assertNoDeleteEvents(user) { + goog.events.listen( + user, fireauth.UserEventType.USER_DELETED, function(event) { + fail('User deleted listener should not trigger!'); + }); +} + + +/** + * Asserts that a method should fail when user is destroyed and no listeners + * are triggered. + * @param {!string} methodName The name of the method of AuthUser that should + * fail if the user is destroyed. + * @param {!Array} parameters The arguments to pass to the method. + */ +function assertFailsWhenUserIsDestroyed(methodName, parameters) { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoDeleteEvents(user); + assertNoUserInvalidatedEvents(user); + + user.destroy(); + user[methodName].apply(user, parameters).then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MODULE_DESTROYED), + error); + asyncTestCase.signal(1); + }); +} + + +function testProviderData() { + assertEquals('providerUserId1', providerData1['uid']); + assertEquals('providerId1', providerData1['providerId']); + assertEquals('user1@example.com', providerData1['email']); + assertEquals('user1', providerData1['displayName']); + assertEquals( + 'https://www.example.com/user1/photo.png', providerData1['photoURL']); +} + + +function testUser() { + accountInfo['email'] = null; + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png'); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + + assertObjectEquals( + new fireauth.UserMetadata(createdAt, lastLoginAt), user.metadata); + assertEquals('accessToken', user.lastAccessToken_); + assertEquals('defaultUserId', user['uid']); + assertEquals('defaultDisplayName', user['displayName']); + assertNull(user['email']); + assertEquals('https://www.default.com/default/default.png', user['photoURL']); + assertEquals('firebase', user['providerId']); + assertEquals(false, user['isAnonymous']); + assertArrayEquals(['providerId1', 'providerId2'], user.getProviderIds()); + assertObjectEquals( + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': null + }, + user['providerData'][0]); + assertObjectEquals( + { + 'uid': 'providerUserId2', + 'displayName': 'user2', + 'photoURL': 'https://www.example.com/user2/photo.png', + 'email': 'user2@example.com', + 'providerId': 'providerId2', + 'phoneNumber': null + }, + user['providerData'][1]); + + // Test popup event ID setters and getters. + assertNull(user.getPopupEventId()); + user.setPopupEventId('1234'); + assertEquals('1234', user.getPopupEventId()); + user.setPopupEventId('5678'); + assertEquals('5678', user.getPopupEventId()); + + // Test redirect event ID setters and getters. + assertNull(user.getRedirectEventId()); + user.setRedirectEventId('1234'); + assertEquals('1234', user.getRedirectEventId()); + user.setRedirectEventId('5678'); + assertEquals('5678', user.getRedirectEventId()); +} + + +function testUser_rpcHandlerEndpoints() { + // Confirm expected endpoint config passed to underlying RPC handler. + var endpoint = fireauth.constants.Endpoint.STAGING; + var endpointConfig = { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + stubs.replace( + fireauth.constants, + 'getEndpointConfig', + function(opt_id) { + return endpointConfig; + }); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor(config1['apiKey'], endpointConfig, ignoreArgument) + .$returns(rpcHandler); + mockControl.$replayAll(); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); +} + + +function testUser_stateChangeListeners() { + // Test user state change listeners: adding, removing and their execution. + asyncTestCase.waitForSignals(3); + var listener1 = goog.testing.recordFunction(function(userTemp) { + assertEquals(user, userTemp); + // Whether it resolves or rejects, it shouldn't affect the outcome. + return goog.Promise.resolve(); + }); + var listener2 = goog.testing.recordFunction(function(userTemp) { + assertEquals(user, userTemp); + // Whether it resolves or rejects, it shouldn't affect the outcome. + return goog.Promise.reject(); + }); + // Listener that does not return a promise. + var listener3 = goog.testing.recordFunction(); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Add all listeners. + user.addStateChangeListener(listener1); + user.addStateChangeListener(listener2); + user.addStateChangeListener(listener3); + // Notify listeners. + user.notifyStateChangeListeners_().then(function(userTemp) { + assertEquals(user, userTemp); + // All should run. + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener2.getCallCount()); + assertEquals(1, listener3.getCallCount()); + // Remove second and third listener. + user.removeStateChangeListener(listener2); + user.removeStateChangeListener(listener3); + asyncTestCase.signal(); + // Notify listeners. + user.notifyStateChangeListeners_().then(function(userTemp) { + assertEquals(user, userTemp); + // Only first listener should run. + assertEquals(2, listener1.getCallCount()); + assertEquals(1, listener2.getCallCount()); + assertEquals(1, listener3.getCallCount()); + // Remove remaining listener. + user.removeStateChangeListener(listener1); + asyncTestCase.signal(); + // Notify listeners. + user.notifyStateChangeListeners_().then(function(userTemp) { + assertEquals(user, userTemp); + // No listener should run. + assertEquals(2, listener1.getCallCount()); + assertEquals(1, listener2.getCallCount()); + assertEquals(1, listener3.getCallCount()); + asyncTestCase.signal(); + }); + }); + }); +} + + +function testAddProviderData_sameProviderId() { + var providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'theProviderId', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png'); + var providerData2 = new fireauth.AuthUserInfo( + 'providerUserId2', + 'theProviderId', + 'user2@example.com', + null, + 'https://www.example.com/user2/photo.png'); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + + assertArrayEquals(['theProviderId'], user.getProviderIds()); + assertArrayEquals([{ + 'uid': 'providerUserId2', + 'displayName': null, + 'photoURL': 'https://www.example.com/user2/photo.png', + 'email': 'user2@example.com', + 'providerId': 'theProviderId', + 'phoneNumber': null + }], user['providerData']); +} + + + +function testUser_removeProviderData() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + + assertArrayEquals(['providerId1', 'providerId2'], user.getProviderIds()); + user.removeProviderData('providerId1'); + assertArrayEquals(['providerId2'], user.getProviderIds()); +} + + +function testUser_setUserAccountInfoFromToken_success() { + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [ + { + 'email': 'user@gmail.com', + 'providerId': 'google.com', + 'displayName': 'John G. Doe', + 'photoUrl': 'https://lh5.googleusercontent.com/123456789/photo.jpg', + 'federatedId': 'https://accounts.google.com/123456789', + 'rawId': '123456789' + }, + { + 'providerId': 'twitter.com', + 'displayName': 'John Gammell Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/' + + 'default_profile_3_normal.png', + 'federatedId': 'http://twitter.com/987654321', + 'rawId': '987654321' + } + ], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/' + + 'default_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var expectedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true + }); + expectedUser.addProviderData(new fireauth.AuthUserInfo( + '123456789', + 'google.com', + 'user@gmail.com', + 'John G. Doe', + 'https://lh5.googleusercontent.com/123456789/photo.jpg')); + expectedUser.addProviderData(new fireauth.AuthUserInfo( + '987654321', + 'twitter.com', + null, + 'John Gammell Doe', + 'http://abs.twimg.com/sticky/default_profile_images/default_profile_' + + '3_normal.png')); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + resolve(response); + }); + }); + asyncTestCase.waitForSignals(1); + // Initialize user with no account info or provider data. + user = new fireauth.AuthUser(config1, tokenResponse); + var stateChangedCounter = 0; + user.addStateChangeListener(function(user) { + stateChangedCounter++; + return goog.Promise.resolve(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.reload().then(function() { + assertEquals(1, stateChangedCounter); + assertObjectEquals(expectedUser.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); +} + + +function testSetUserAccountInfoFromToken_success_emailAndPassword() { + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'passwordHash': 'PASSWORD_HASH', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/' + + 'default_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var expectedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + resolve(response); + }); + }); + user = new fireauth.AuthUser(config1, tokenResponse); + asyncTestCase.waitForSignals(1); + user.reload().then(function() { + assertObjectEquals(expectedUser, user); + asyncTestCase.signal(); + }); +} + + +function testSetUserAccountInfoFromToken_success_emailNoPassword() { + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var expectedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true, + 'isAnonymous': false + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + resolve(response); + }); + }); + user = new fireauth.AuthUser(config1, tokenResponse); + asyncTestCase.waitForSignals(1); + user.reload().then(function() { + assertObjectEquals(expectedUser, user); + asyncTestCase.signal(); + }); +} + + +function testSetUserAccountInfoFromToken_success_passwordNoEmail() { + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'passwordHash': 'PASSWORD_HASH', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var expectedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'isAnonymous': false + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + resolve(response); + }); + }); + + user = new fireauth.AuthUser(config1, tokenResponse); + asyncTestCase.waitForSignals(1); + user.reload().then(function() { + assertObjectEquals(expectedUser, user); + asyncTestCase.signal(); + }); +} + + +function testUser_setUserAccountInfoFromToken_error() { + var error = { + 'error': fireauth.authenum.Error.INTERNAL_ERROR + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return goog.Promise.reject(error); + }); + asyncTestCase.waitForSignals(1); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.reload().thenCatch(function(e) { + // User data unchanged. + for (var key in accountInfo) { + // Metadata is structured differently in user compared to accountInfo. + if (key == 'lastLoginAt') { + assertEquals( + fireauth.util.utcTimestampToDateString(accountInfo[key]), + user['metadata']['lastSignInTime']); + } else if (key == 'createdAt') { + assertEquals( + fireauth.util.utcTimestampToDateString(accountInfo[key]), + user['metadata']['creationTime']); + } else { + assertEquals(accountInfo[key], user[key]); + } + } + assertObjectEquals(error, e); + asyncTestCase.signal(); + }); +} + + +function testUser_setUserAccountInfoFromToken_invalidResponse() { + // Test with invalid server response. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + // Resolve getAccountInfo with invalid server response. + return goog.Promise.resolve({}); + }); + asyncTestCase.waitForSignals(1); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.reload().thenCatch(function(error) { + var expected = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + assertEquals(expected.code, error.code); + // User data unchanged. + for (var key in accountInfo) { + // Metadata is structured differently in user compared to accountInfo. + if (key == 'lastLoginAt') { + assertEquals( + fireauth.util.utcTimestampToDateString(accountInfo[key]), + user['metadata']['lastSignInTime']); + } else if (key == 'createdAt') { + assertEquals( + fireauth.util.utcTimestampToDateString(accountInfo[key]), + user['metadata']['creationTime']); + } else { + assertEquals(accountInfo[key], user[key]); + } + } + asyncTestCase.signal(); + }); +} + + +function testUser_reload_success() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.addStateChangeListener(function(user) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + assertEquals('accessToken', idToken); + user.copy(updatedUser); + asyncTestCase.signal(); + return goog.Promise.resolve(myAccountInfo); + }); + var myAccountInfo = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'new_uid123@fake.com', + 'emailVerified': true, + 'displayName': 'Fabrice', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var updatedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': 'new_uid123@fake.com', + 'displayName': 'Fabrice', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true + }); + asyncTestCase.waitForSignals(3); + user.reload().then(function() { + assertObjectEquals(updatedUser.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); +} + + +/** + * Tests the case where a user currently stored in local storage as not + * anonymous reloads its data and has no more credential (for instance, it has + * unlinked all its providers). The isAnonymous flag should remain false. + */ +function testUser_reload_success_noCredentialUserLocallyNotAnonymous() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.addStateChangeListener(function(user) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'disabled': false + }] + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + asyncTestCase.signal(); + resolve(response); + }); + }); + var updatedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'isAnonymous': false + }); + asyncTestCase.waitForSignals(3); + user.reload().then(function() { + assertObjectEquals(updatedUser.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); +} + + +/** + * Tests that an anonymous user remains anonymous when no credential in the + * GetAccountInfo response. + */ +function testUser_reload_success_noCredentialUserLocallyAnonymous() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.updateProperty('isAnonymous', true); + user.addStateChangeListener(function(user) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'providerUserInfo': [], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'disabled': false + }] + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + assertEquals('accessToken', data); + asyncTestCase.signal(); + resolve(response); + }); + }); + var updatedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': '', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'isAnonymous': true + }); + asyncTestCase.waitForSignals(3); + user.reload().then(function() { + assertObjectEquals(updatedUser.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); +} + + +function testUser_reload_general_error() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + assertNoDeleteEvents(user); + asyncTestCase.waitForSignals(2); + user.reload().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reload_userNotFound_error() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(3); + user.reload().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testExtractLinkedAccounts() { + var resp = { + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [ + { + 'email': 'user@gmail.com', + 'providerId': 'google.com', + 'displayName': 'John Doe', + 'photoUrl': 'https://lh5.googleusercontent.com/123456789/photo.jpg', + 'federatedId': 'https://accounts.google.com/123456789', + 'rawId': '123456789' + }, + { + 'providerId': 'twitter.com', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defa' + + 'ult_profile_3_normal.png', + 'federatedId': 'http://twitter.com/987654321', + 'rawId': '987654321' + } + ], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }; + var expectedProviders = [ + new fireauth.AuthUserInfo( + '123456789', + 'google.com', + 'user@gmail.com', + 'John Doe', + 'https://lh5.googleusercontent.com/123456789/photo.jpg'), + new fireauth.AuthUserInfo( + '987654321', + 'twitter.com', + null, + 'John Doe', + 'http://abs.twimg.com/sticky/default_profile_images/default_profile_' + + '3_normal.png')]; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertObjectEquals( + expectedProviders, + user.extractLinkedAccounts_(resp)); +} + + +function testExtractLinkedAccounts_withoutEmail() { + var resp = { + 'localId': '14584746072031976743', + 'email': '', + 'emailVerified': false, + 'displayName': 'John Doe', + 'providerUserInfo': [ + { + 'providerId': 'twitter.com', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defa' + + 'ult_profile_3_normal.png', + 'federatedId': 'http://twitter.com/987654321', + 'rawId': '987654321' + } + ], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }; + var expectedProviders = [ + new fireauth.AuthUserInfo( + '987654321', + 'twitter.com', + null, + 'John Doe', + 'http://abs.twimg.com/sticky/default_profile_images/default_profile_' + + '3_normal.png')]; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertObjectEquals( + expectedProviders, + user.extractLinkedAccounts_(resp)); +} + + +function testUser_getIdToken() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + asyncTestCase.signal(); + }); + assertNoStateEvents(user); + assertNoUserInvalidatedEvents(user); + asyncTestCase.waitForSignals(2); + // Test with available token. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + return goog.Promise.resolve({ + accessToken: 'accessToken1', + refreshToken: 'refreshToken1', + expirationTime: now + 3600 * 1000 + }); + }); + user.getIdToken().then(function(stsAccessToken) { + assertEquals('accessToken1', stsAccessToken); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdToken_expiredToken_reauthAfterInvalidation() { + // Test when token is expired and user is reauthenticated. + // User should be validated after, even though user invalidation event is + // triggered. + var validTokenResponse = { + 'idToken': expectedReauthenticateTokenResponse['idToken'], + 'accessToken': expectedReauthenticateTokenResponse['idToken'], + 'refreshToken': expectedReauthenticateTokenResponse['refreshToken'], + 'expiresIn': 3600 + }; + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + } + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Track token changes. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation events. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Stub token manager. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Resolve if new refresh token provided. + if (this.getRefreshToken() == validTokenResponse['refreshToken']) { + return goog.Promise.resolve(validTokenResponse); + } + // Reject otherwise. + return goog.Promise.reject(expectedError); + }); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Call getIdToken, it should trigger the expected error and a state change + // event. + user.getIdToken().thenCatch(function(error) { + // Refresh token nullified. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No state change. + assertEquals(0, stateChangeCounter); + // No Auth change. + assertEquals(0, authChangeCounter); + // User invalidated change. + assertEquals(1, userInvalidateCounter); + // Call again, it should not trigger another state change or any other + // event. + user.getIdToken().thenCatch(function(error) { + // Resolves with same error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No additional change. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(1, userInvalidateCounter); + // Assume user reauthenticated. + // This should resolve. + user.reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Set via reauthentication. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + // Auth token change triggered. + assertEquals(1, authChangeCounter); + // State change triggers, after reauthentication. + assertEquals(1, stateChangeCounter); + // Shouldn't trigger again. + assertEquals(1, userInvalidateCounter); + // This should return cached token set via reauthentication. + user.getIdToken().then(function(idToken) { + // Shouldn't trigger again. + assertEquals(1, authChangeCounter); + assertEquals(1, stateChangeCounter); + assertEquals(1, userInvalidateCounter); + // Refresh token should be updated along with ID token. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + assertEquals(idTokenGmail.jwt, idToken); + asyncTestCase.signal(); + }); + }); + }); + }); +} + + +function testUser_getIdToken_expiredToken_reauthWithPopupAfterInvalidation() { + // Test when token is expired and user is reauthenticated with popup. + // User should be validated after, even though user invalidation event is + // triggered. + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + var validTokenResponse = { + 'idToken': expectedReauthenticateTokenResponse['idToken'], + 'accessToken': expectedReauthenticateTokenResponse['idToken'], + 'refreshToken': expectedReauthenticateTokenResponse['refreshToken'], + 'expiresIn': 3600 + }; + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate Auth email credential error thrown by verifyAssertionForExisting. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + // Match the token UID. + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Track token changes. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation events. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Stub token manager. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Resolve if new refresh token provided. + if (this.getRefreshToken() == validTokenResponse['refreshToken']) { + return goog.Promise.resolve(validTokenResponse); + } + // Reject otherwise. + return goog.Promise.reject(expectedError); + }); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + var provider = new fireauth.GoogleAuthProvider(); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user.enablePopupRedirect(); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Call getIdToken, it should trigger the expected error and a state change + // event. + user.getIdToken().thenCatch(function(error) { + // Refresh token remains the same. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No state change. + assertEquals(0, stateChangeCounter); + // No Auth change. + assertEquals(0, authChangeCounter); + // User invalidated change. + assertEquals(1, userInvalidateCounter); + // Call again, it should not trigger another state change or any other + // event. + user.getIdToken().thenCatch(function(error) { + // Resolves with same error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No additional change. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(1, userInvalidateCounter); + // Assume user reauthenticated. + // This should resolve. + user.reauthenticateWithPopup(provider).then(function(result) { + // Set via reauthentication. + assertEquals(validTokenResponse['refreshToken'], user['refreshToken']); + // Auth token change triggered. + assertEquals(1, authChangeCounter); + // State change triggers, after reauthentication. + assertEquals(1, stateChangeCounter); + // Shouldn't trigger again. + assertEquals(1, userInvalidateCounter); + // This should return cached token set via reauthentication. + user.getIdToken().then(function(idToken) { + // Shouldn't trigger again. + assertEquals(1, authChangeCounter); + assertEquals(1, stateChangeCounter); + assertEquals(1, userInvalidateCounter); + // Refresh token should be updated along with ID token. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + assertEquals(idTokenGmail.jwt, idToken); + asyncTestCase.signal(); + }); + }); + }); + }); +} + + +function testUser_getIdToken_expiredToken_reauthBeforeInvalidation() { + // Test when token is expired and user is reauthenticated before invalidation + // is detected. + var validTokenResponse = { + 'idToken': expectedReauthenticateTokenResponse['idToken'], + 'accessToken': expectedReauthenticateTokenResponse['idToken'], + 'refreshToken': expectedReauthenticateTokenResponse['refreshToken'], + 'expiresIn': 3600 + }; + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + } + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + // Match token UID. + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Track token changes. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation events. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Stub token manager. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Resolve if new refresh token provided. + if (this.getRefreshToken() == validTokenResponse['refreshToken']) { + return goog.Promise.resolve(validTokenResponse); + } + // Reject otherwise. + return goog.Promise.reject(expectedError); + }); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + // Call reauthenticate, this should succeed and not trigger user invalidation + // event. + user.reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Set via reauthentication. + assertEquals(validTokenResponse['refreshToken'], user['refreshToken']); + // Auth token change triggered. + assertEquals(1, authChangeCounter); + // State change triggered. + assertEquals(1, stateChangeCounter); + // Externally user should not be invalidated. + assertEquals(0, userInvalidateCounter); + // This should return cached token set via reauthentication. + user.getIdToken().then(function(idToken) { + // No additional listeners should retrigger. + assertEquals(1, authChangeCounter); + assertEquals(1, stateChangeCounter); + assertEquals(0, userInvalidateCounter); + // Refresh token should be updated along with ID token. + assertEquals( + validTokenResponse['refreshToken'], user['refreshToken']); + assertEquals(idTokenGmail.jwt, idToken); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_getIdToken_expiredToken_reauthWithPopupBeforeInvalidation() { + // Test when token is expired and user is reauthenticated with popup before + // invalidation is detected. + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + var validTokenResponse = { + 'idToken': expectedReauthenticateTokenResponse['idToken'], + 'accessToken': expectedReauthenticateTokenResponse['idToken'], + 'refreshToken': expectedReauthenticateTokenResponse['refreshToken'], + 'expiresIn': 3600 + }; + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate verifyAssertionForExisting returns a token with the same UID. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Track token changes. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation events. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + asyncTestCase.waitForSignals(1); + // Stub token manager. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Resolve if new refresh token provided. + if (this.getRefreshToken() == validTokenResponse['refreshToken']) { + return goog.Promise.resolve(validTokenResponse); + } + // Reject otherwise. + return goog.Promise.reject(expectedError); + }); + // Confirm expected initial refresh token set on user. + assertEquals(tokenResponse['refreshToken'], user['refreshToken']); + var provider = new fireauth.GoogleAuthProvider(); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user.enablePopupRedirect(); + // Call reauthenticate, this should succeed and not trigger user invalidation + // event. + user.reauthenticateWithPopup(provider).then(function(result) { + // Set via reauthentication. + assertEquals(validTokenResponse['refreshToken'], user['refreshToken']); + // Auth token change triggered. + assertEquals(1, authChangeCounter); + // State change triggered. + assertEquals(1, stateChangeCounter); + // Externally user should not be invalidated. + assertEquals(0, userInvalidateCounter); + // This should return cached token set via reauthentication. + user.getIdToken().then(function(idToken) { + // No additional listeners should retrigger. + assertEquals(1, authChangeCounter); + assertEquals(1, stateChangeCounter); + assertEquals(0, userInvalidateCounter); + // Refresh token should be updated along with ID token. + assertEquals(validTokenResponse['refreshToken'], user['refreshToken']); + assertEquals(idTokenGmail.jwt, idToken); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_getIdToken_otherError() { + // Test when any error other than expired token is returned that no state + // change or token change is triggered. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // No token change. + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + // No state change. + assertNoStateEvents(user); + asyncTestCase.waitForSignals(1); + // Test with some unexpected error. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + return goog.Promise.reject(expectedError); + }); + user.getIdToken().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_getIdToken_tokenManagerReturnsNull() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + asyncTestCase.waitForSignals(1); + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + return goog.Promise.resolve(null); + }); + user.getIdToken().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), error); + asyncTestCase.signal(); + }); +} + + + +function testUser_getIdToken_unchanged() { + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + assertNoStateEvents(user); + asyncTestCase.waitForSignals(1); + // Test with available token. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + // Token unchanged. + return goog.Promise.resolve({ + accessToken: 'accessToken', + refreshToken: 'refreshToken', + expirationTime: now + 3600 * 1000 + }); + }); + user.getIdToken().then(function(stsAccessToken) { + // Token unchanged. + assertEquals('accessToken', stsAccessToken); + asyncTestCase.signal(); + }); +} + + +function testUser_refreshToken() { + asyncTestCase.waitForSignals(1); + var refreshToken = 'myRefreshToken'; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + assertEquals('refreshToken', user['refreshToken']); + // Test available token with refresh token to expected one above. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + this.setRefreshToken(refreshToken); + this.setAccessToken('accessToken1', now + 3600 * 1000); + return goog.Promise.resolve({ + accessToken: 'accessToken1', + refreshToken: refreshToken, + expirationTime: now + 3600 * 1000 + }); + }); + user.getIdToken().then(function(stsAccessToken) { + assertEquals('accessToken1', stsAccessToken); + assertEquals(refreshToken, user['refreshToken']); + asyncTestCase.signal(); + }); +} + + +function testUpdateTokensIfPresent_newTokens() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoUserInvalidatedEvents(user); + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + // Checks the tokens stored. + user.getIdToken().then(function(idToken) { + assertEquals('newToken', idToken); + assertEquals('newToken', user.lastAccessToken_); + asyncTestCase.signal(); + }); + }); + + user.updateTokensIfPresent_({ + 'idToken': 'newToken', + 'refreshToken': 'newRefreshToken' + }); +} + + +function testUpdateTokensIfPresent_identicalTokens() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.updateTokensIfPresent_(tokenResponse); + user.getIdToken().then(function(idToken) { + assertEquals(tokenResponse['idToken'], idToken); + asyncTestCase.signal(); + }); +} + + +function testUpdateTokensIfPresent_noTokens() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.updateTokensIfPresent_({ + 'email': 'user@default.com' + }); + user.getIdToken().then(function(idToken) { + assertEquals(tokenResponse['idToken'], idToken); + asyncTestCase.signal(); + }); +} + + +function testUpdateProperty() { + user = new fireauth.AuthUser(config1, tokenResponse, {'uid': 'userId1'}); + user.updateProperty('uid', '12345678'); + assertEquals(user['uid'], '12345678'); + user.updateProperty('uid', null); + assertEquals(user['uid'], '12345678'); + user.updateProperty('displayName', 'Jack Smith'); + assertEquals(user['displayName'], 'Jack Smith'); + user.updateProperty('photoURL', 'http://www.example.com/photo/photo.png'); + assertEquals(user['photoURL'], 'http://www.example.com/photo/photo.png'); + user.updateProperty('email', 'user@example.com'); + assertEquals(user['email'], 'user@example.com'); + user.updateProperty('isAnonymous', true); + assertEquals(true, user['isAnonymous']); + user.updateProperty('invalid', 'something'); + assertUndefined(user['invalid']); +} + + +function testUpdateEmail_success() { + asyncTestCase.waitForSignals(2); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + + // Simulates successful rpcHandler updateEmail. + var expectedResponse = { + 'email': 'newuser@example.com' + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateEmail', + function(idToken, newEmail) { + assertEquals('accessToken', idToken); + assertEquals('newuser@example.com', newEmail); + return goog.Promise.resolve(expectedResponse); + }); + // Simulates the update by the server of the email and emailVerified + // properties. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + assertEquals('accessToken', idToken); + return goog.Promise.resolve({ + 'users': [{ + 'email': 'newuser@example.com', + 'emailVerified': false + }] + }); + }); + + user.updateEmail('newuser@example.com').then(function() { + assertEquals('newuser@example.com', user['email']); + assertFalse(user['emailVerified']); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmail_error() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + + // Simulates rpcHandler updateEmail error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_SIGNED_OUT); + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateEmail', + function(idToken, newEmail) { + assertEquals('accessToken', idToken); + assertEquals('newuser@example.com', newEmail); + return goog.Promise.reject(expectedError); + }); + // User should not be reloaded. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + fail('The user should not be reloaded!'); + }); + + user.updateEmail('newuser@example.com').then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // No update. + assertEquals('user@default.com', user['email']); + assertTrue(user['emailVerified']); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmail_userDestroyed() { + assertFailsWhenUserIsDestroyed('updateEmail', ['newuser@example.com']); +} + + +function testUpdatePhoneNumber_success() { + var expectedResponse = { + 'idToken': 'myNewIdToken', + 'refreshToken': 'myRefreshToken', + 'expiresIn': '3600', + 'localId': 'myLocalId', + 'isNewUser': false, + 'phoneNumber': '+16505550101' + }; + var phoneAuthCredential = mockControl.createStrictMock( + fireauth.PhoneAuthCredential); + phoneAuthCredential.linkToIdToken(ignoreArgument, tokenResponse['idToken']) + .$once() + .$returns(goog.Promise.resolve(expectedResponse)); + + var getAccountInfoResponse = { + 'users': [{ + 'localId': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'phoneNumber': '+16505550101', + 'providerUserInfo': [{ + 'providerId': 'phone', + 'rawId': '+16505550101', + 'phoneNumber': '+16505550101' + }], + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var getAccountInfoByIdToken = mockControl.createMethodMock( + fireauth.RpcHandler.prototype, 'getAccountInfoByIdToken'); + // New STS token should be used. + getAccountInfoByIdToken('myNewIdToken').$returns( + goog.Promise.resolve(getAccountInfoResponse)).$once(); + + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.updatePhoneNumber(phoneAuthCredential) + .then(function() { + // User properties should be updated. + assertEquals('+16505550101', user['phoneNumber']); + assertEquals(1, user['providerData'].length); + assertObjectEquals({ + 'uid': '+16505550101', + 'displayName': null, + 'photoURL': null, + 'email': null, + 'phoneNumber': '+16505550101', + 'providerId': 'phone' + }, user['providerData'][0]); + asyncTestCase.signal(); + }); +} + + +function testUpdatePhoneNumber_error() { + asyncTestCase.waitForSignals(1); + + // Simulates error from backend. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var credential = fireauth.PhoneAuthProvider.credential('id', 'code'); + stubs.replace(credential, 'linkToIdToken', function(rpcHandler, idToken) { + assertEquals('accessToken', idToken); + return goog.Promise.reject(expectedError); + }); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.updatePhoneNumber(credential).then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUpdatePhoneNumber_userDestroyed() { + var credential = fireauth.PhoneAuthProvider.credential('id', 'code'); + assertFailsWhenUserIsDestroyed('updatePhoneNumber', [credential]); +} + + +function testUser_sessionInvalidation_updatePhoneNumber_tokenExpired() { + // Test user invalidation with token expired error on + // user.updatePhoneNumber. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'updatePhoneNumber', + [ + fireauth.PhoneAuthProvider.credential('foo', 'bar') + ], + invalidationError); +} + + +function testUser_sessionInvalidation_reload_userDisabled() { + // Test user invalidation with user disabled error on user.updatePhoneNumber. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation('updatePhoneNumber', + [fireauth.PhoneAuthProvider.credential('foo', 'bar')], + invalidationError); +} + + +function testUpdatePassword_success() { + asyncTestCase.waitForSignals(2); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + + // Simulates successful rpcHandler updatePassword. + var expectedResponse = { + 'email': 'newuser@example.com' + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'updatePassword', + function(idToken, newPassword) { + assertEquals('accessToken', idToken); + assertEquals('newPassword', newPassword); + return goog.Promise.resolve(expectedResponse); + }); + // Simulates the reload. + var reloaded = 0; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + reloaded++; + assertEquals('accessToken', idToken); + return goog.Promise.resolve({ + 'users': [{ + 'email': 'newuser@example.com' + }] + }); + }); + + user.updatePassword('newPassword').then(function() { + assertEquals(1, reloaded); + asyncTestCase.signal(); + }); +} + + +function testUpdatePassword_error() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + + // Simulates rpcHandler updatePassword error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + stubs.replace( + fireauth.RpcHandler.prototype, + 'updatePassword', + function(idToken, newPassword) { + assertEquals('accessToken', idToken); + assertEquals('newPassword', newPassword); + return goog.Promise.reject(expectedError); + }); + + user.updatePassword('newPassword').then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUpdatePassword_userDestroyed() { + assertFailsWhenUserIsDestroyed('updatePassword', ['newPassword']); +} + + +function testUpdateProfile_success() { + asyncTestCase.waitForSignals(2); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + + var expectedResponse = { + 'email': 'uid123@fake.com', + 'displayName': 'Jack Smith', + 'photoUrl': 'http://www.example.com/photo/photo.png' + }; + // Ignore reload for this test. + stubs.replace( + fireauth.AuthUser.prototype, + 'reload', + function() { + return goog.Promise.resolve(); + }); + // Simulates updateProfile. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateProfile', + function(idToken, profileData) { + assertEquals('accessToken', idToken); + assertObjectEquals({ + 'displayName': 'Jack Smith', + 'photoUrl': 'http://www.example.com/photo/photo.png' + }, profileData); + return goog.Promise.resolve(expectedResponse); + }); + // Records calls to updateTokensIfPresent_. + stubs.replace( + user, + 'updateTokensIfPresent_', + goog.testing.recordFunction()); + + user.updateProfile({ + 'displayName': 'Jack Smith', + 'photoURL': 'http://www.example.com/photo/photo.png' + }).then(function() { + assertEquals('Jack Smith', user['displayName']); + assertEquals('http://www.example.com/photo/photo.png', user['photoURL']); + assertEquals(1, user.updateTokensIfPresent_.getCallCount()); + assertEquals( + expectedResponse, + user.updateTokensIfPresent_.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_success_withPasswordProvider() { + asyncTestCase.waitForSignals(1); + var expectedDisplayName = 'Test User'; + var expectedPhotoUrl = 'http://www.example.com/photo/photo.png'; + var expectedResponse = { + 'displayName': expectedDisplayName, + 'photoUrl': expectedPhotoUrl + }; + // Mocks updateProfile. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateProfile', + function(idToken, profileData) { + assertEquals('accessToken', idToken); + assertObjectEquals({ + 'displayName': expectedDisplayName, + 'photoUrl': expectedPhotoUrl + }, profileData); + return goog.Promise.resolve(expectedResponse); + }); + var providerData1 = new fireauth.AuthUserInfo( + 'UID1', + 'google.com', + 'uid1@example.com', + null, + null); + var providerData2 = new fireauth.AuthUserInfo( + 'UID2', + 'password', + 'uid2@example.com', + null, + null); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + user.updateProfile({ + 'displayName': expectedDisplayName, + 'photoURL': expectedPhotoUrl + }).then(function() { + // Confirm top level changes. + assertEquals(user['displayName'], expectedDisplayName); + assertEquals(user['photoURL'], expectedPhotoUrl); + // Confirm update on password provider display name and photo URL. + assertEquals(2, user['providerData'].length); + assertObjectEquals( + user['providerData'][0], + { + 'uid': 'UID1', + 'displayName': null, + 'photoURL': null, + 'email': 'uid1@example.com', + 'providerId': 'google.com', + 'phoneNumber': null + }); + assertObjectEquals( + user['providerData'][1], + { + 'uid': 'UID2', + 'displayName': expectedDisplayName, + 'photoURL': expectedPhotoUrl, + 'email': 'uid2@example.com', + 'providerId': 'password', + 'phoneNumber': null + }); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_success_withoutPasswordProvider() { + asyncTestCase.waitForSignals(1); + var expectedDisplayName = 'Test User'; + var expectedPhotoUrl = 'http://www.example.com/photo/photo.png'; + var expectedResponse = { + 'displayName': expectedDisplayName, + 'photoUrl': expectedPhotoUrl + }; + // Mocks updateProfile. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateProfile', + function(idToken, profileData) { + assertEquals('accessToken', idToken); + assertObjectEquals({ + 'displayName': expectedDisplayName, + 'photoUrl': expectedPhotoUrl + }, profileData); + return goog.Promise.resolve(expectedResponse); + }); + var providerData1 = new fireauth.AuthUserInfo( + 'UID1', + 'google.com', + 'uid1@example.com', + null, + null); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.updateProfile({ + 'displayName': expectedDisplayName, + 'photoURL': expectedPhotoUrl + }).then(function() { + // Confirm top level changes. + assertEquals(user['displayName'], expectedDisplayName); + assertEquals(user['photoURL'], expectedPhotoUrl); + // Confirm no changes on providerData array as no password provider is + // found. + assertEquals(1, user['providerData'].length); + assertObjectEquals( + user['providerData'][0], + { + 'uid': 'UID1', + 'displayName': null, + 'photoURL': null, + 'email': 'uid1@example.com', + 'providerId': 'google.com', + 'phoneNumber': null + }); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_emptyChange() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + // Ensures updateProfile isn't called. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateProfile', + function(idToken, profileData) { + fail('updateProfile should not be called!'); + }); + + user.updateProfile({ + 'wrongKey': 'whatever' + }).then(function() { + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_error() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + // Simulates updateProfile. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateProfile', + function(idToken, profileData) { + assertEquals('accessToken', idToken); + assertObjectEquals({ + 'displayName': 'Jack Smith', + 'photoUrl': 'http://www.example.com/photo/photo.png' + }, profileData); + return goog.Promise.reject(expectedError); + }); + + user.updateProfile({ + 'displayName': 'Jack Smith', + 'photoURL': 'http://www.example.com/photo/photo.png' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + assertEquals(accountInfo['displayName'], user['displayName']); + assertEquals(accountInfo['photoURL'], user['photoURL']); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_userDestroyed() { + assertFailsWhenUserIsDestroyed('updateProfile', [{ + 'displayName': 'Jack Smith', + 'photoURL': 'http://www.example.com/photo/photo.png' + }]); +} + + +function testReauthenticateWithCredential_success() { + // Test that reauthenticateWithCredential calls + // reauthenticateAndRetrieveDataWithCredential underneath and returns void, on + // success. + // Stub reauthenticateAndRetrieveDataWithCredential and confirm promise + // resolves with void. + stubs.replace( + fireauth.AuthUser.prototype, + 'reauthenticateAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.resolve(expectedResponse); + }); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Expected response. Only the user will be returned. + var expectedResponse = { + 'user': user, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + // reauthenticateWithCredential using Google OAuth credential. + user.reauthenticateWithCredential(expectedGoogleCredential) + .then(function(response) { + // Confirm no response returned. + assertUndefined(response); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateWithCredential_error() { + // Test that reauthenticateWithCredential calls + // reauthenticateAndRetrieveDataWithCredential underneath and funnels any + // underlying error thrown. + stubs.replace( + fireauth.AuthUser.prototype, + 'reauthenticateAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // reauthenticateWithCredential using Google OAuth credential. + user.reauthenticateWithCredential(expectedGoogleCredential) + .thenCatch(function(error) { + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateAndRetrieveDataWithCredential() { + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + } + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + assertEquals(idTokenGmail.jwt, idToken); + return goog.Promise.resolve({ + 'users': [{ + 'localId': '679' + }] + }); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '679' + }); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + goog.events.listen(user, fireauth.UserEventType.TOKEN_CHANGED, + function(event) { + asyncTestCase.signal(); + }); + assertNoUserInvalidatedEvents(user); + user.reauthenticateAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.REAUTHENTICATE, + result); + + assertEquals('679', user['uid']); + return user.getIdToken(); + }) + .then(function(idToken) { + assertEquals(idTokenGmail.jwt, idToken); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(3); +} + + +function testReauthenticateAndRetrieveDataWithCredential_userMismatch() { + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH); + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + return goog.Promise.reject(expectedError); + } + }); + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '679' + }); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.reauthenticateAndRetrieveDataWithCredential(credential) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testReauthenticateAndRetrieveDataWithCredential_fail() { + var error = new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var credential = /** @type {!fireauth.AuthCredential} */ ({ + matchIdTokenWithUid: function() { + return goog.Promise.reject(error); + } + }); + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '679' + }); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.reauthenticateAndRetrieveDataWithCredential(credential) + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(error, actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testLinkWithCredential_success() { + // Test that linkWithCredential calls linkAndRetrieveDataWithCredential + // underneath and returns the user only, on success. + // Stub linkAndRetrieveDataWithCredential and confirm same response is used + // for linkWithCredential with only the user returned. + stubs.replace( + fireauth.AuthUser.prototype, + 'linkAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.resolve(expectedResponse); + }); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Expected response. Only the user will be returned. + var expectedResponse = { + 'user': user, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + // linkWithCredential using Google OAuth credential. + user.linkWithCredential(expectedGoogleCredential).then(function(response) { + // Confirm expected user response. + assertEquals(user, response); + asyncTestCase.signal(); + }); +} + + +function testLinkWithCredential_error() { + // Test that linkWithCredential calls linkAndRetrieveDataWithCredential + // underneath and funnels any underlying error thrown. + stubs.replace( + fireauth.AuthUser.prototype, + 'linkAndRetrieveDataWithCredential', + function(cred) { + assertEquals(expectedGoogleCredential, cred); + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // linkWithCredential using Google OAuth credential. + user.linkWithCredential(expectedGoogleCredential).thenCatch(function(error) { + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testLinkAndRetrieveDataWithCredential_emailAndPassword() { + var email = 'me@foo.com'; + var password = 'myPassword'; + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateEmailAndPassword', + function(actualIdToken, actualEmail, actualPassword) { + asyncTestCase.signal(); + assertEquals('accessToken', actualIdToken); + assertEquals(email, actualEmail); + assertEquals(password, actualPassword); + // No credential or additional user info returned when linking an email + // and password credential. + return goog.Promise.resolve({ + 'email': email, + 'idToken': 'newStsToken', + 'refreshToken': 'newRefreshToken', + 'expiresIn': 3600 + }); + }); + + // The updated information from the backend after linking. + var updatedUserResponse = { + 'users': [{ + 'localId': 'defaultUserId', + 'email': email, + 'emailVerified': false, + 'providerUserInfo': [], + 'photoUrl': '', + 'passwordHash': 'myPasswordHash', + 'passwordUpdatedAt': now, + 'disabled': false + }] + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return goog.Promise.resolve(updatedUserResponse); + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 'defaultUserId', + 'isAnonymous': true + }); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + goog.events.listen(user, fireauth.UserEventType.TOKEN_CHANGED, + function(event) { + asyncTestCase.signal(); + }); + assertNoUserInvalidatedEvents(user); + var credential = fireauth.EmailAuthProvider.credential(email, + password); + user.linkAndRetrieveDataWithCredential(credential) + .then(function(result) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user, + // Expected null credential returned. + null, + // Expected null additional user info. + null, + // operationType not implemented yet. + fireauth.constants.OperationType.LINK, + result); + + // Email should be updated. + assertEquals(email, user['email']); + + // Should not be anonymous + assertFalse(user['isAnonymous']); + + // Tokens should be updated. + assertEquals('newRefreshToken', user['refreshToken']); + return user.getIdToken(); + }) + .then(function(returnedToken) { + assertEquals('newStsToken', returnedToken); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(4); +} + + +function testLinkAndRetrieveDataWithCredential_federatedIdP() { + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(request) { + asyncTestCase.signal(); + assertEquals( + 'id_token=googleIdToken&access_token=googleAccessToken&' + + 'providerId=google.com', + request['postBody']); + assertEquals('accessToken', request['idToken']); + + // Update the backend user data. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseGoogleProviderData); + // Return token response with credential and additional IdP data. + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 'defaultUserId' + }); + user.addStateChangeListener(function(userTemp) { + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + goog.events.listen(user, fireauth.UserEventType.TOKEN_CHANGED, + function(event) { + asyncTestCase.signal(); + }); + assertNoUserInvalidatedEvents(user); + user.linkAndRetrieveDataWithCredential(expectedGoogleCredential) + .then(function(result) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.LINK, + result); + + // Should have Google as a provider. + assertEquals('google.com', user['providerData'][0]['providerId']); + + // Tokens should be updated. + assertEquals('newRefreshToken', user['refreshToken']); + return user.getIdToken(); + }) + .then(function(returnedToken) { + assertEquals('newStsToken', returnedToken); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(4); +} + + +function testLinkAndRetrieveDataWithCredential_alreadyLinked() { + // User on server has the federated provider linked already. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseGoogleProviderData); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + var error = new fireauth.AuthError( + fireauth.authenum.Error.PROVIDER_ALREADY_LINKED); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(request) { + fail('verifyAssertionForLinking RPC should not be called.'); + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + var credential = fireauth.GoogleAuthProvider.credential(null, + 'googleAccessToken'); + user.linkAndRetrieveDataWithCredential(credential) + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(error, actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testLinkAndRetrieveDataWithCredential_fail() { + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + var error = new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(request) { + return goog.Promise.reject(error); + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 'defaultUserId' + }); + var credential = fireauth.GoogleAuthProvider.credential({ + 'idToken': 'googleIdToken' + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.linkAndRetrieveDataWithCredential(credential) + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(error, actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testUnlink_success() { + // User on server has two federated providers and one phone provider linked. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseProviderData1); + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseProviderData2); + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponsePhoneAuthProviderData); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // Simulate update linked account. + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteLinkedAccounts', + function(idToken, providersToDelete) { + assertEquals('accessToken', idToken); + // providerId2 should be removed from user's original providers. + assertArrayEquals(providersToDelete, ['providerId2']); + var response = { + 'email': 'user@default.com', + 'providerUserInfo': [ + {'providerId': 'providerId1'}, + {'providerId': 'phone'} + ] + }; + return goog.Promise.resolve(response); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + user.addProviderData(providerDataPhone); + + var userWithoutProvider2 = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + userWithoutProvider2.addProviderData(providerData1); + userWithoutProvider2.addProviderData(providerDataPhone); + + user.addStateChangeListener(function(event) { + asyncTestCase.signal(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.unlink('providerId2') + .then(function(passedUser) { + // Update user in Auth state. + assertObjectEquals(userWithoutProvider2.toPlainObject(), + user.toPlainObject()); + // Should be same instance. + assertEquals(user, passedUser); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(2); +} + + +function testUnlink_alreadyDeleted() { + // User on server has only one federated provider linked despite the local + // copy having two. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseProviderData1); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteLinkedAccounts', + function(idToken, providersToDelete) { + fail('deleteLinkedAccounts RPC should not be called when the linked ' + + 'account is already deleted.'); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + user.addProviderData(providerData2); + + var userWithoutProvider2 = new fireauth.AuthUser(config1, tokenResponse, + accountInfo); + userWithoutProvider2.addProviderData(providerData1); + + user.addStateChangeListener(function(event) { + asyncTestCase.signal(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.unlink('providerId2') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.NO_SUCH_PROVIDER), + error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(2); +} + + +function testUnlink_failure() { + var error = new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + + // User on server has one federated provider linked. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseProviderData1); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteLinkedAccounts', + function(idToken, providersToDelete) { + return goog.Promise.reject(error); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.addProviderData(providerData1); + + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.unlink('providerId1') + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(error, actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testUnlink_phone() { + // User on server has a federated provider and a phone number linked. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseProviderData1); + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponsePhoneAuthProviderData); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // Simulate update linked account. + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteLinkedAccounts', + function(idToken, providersToDelete) { + assertEquals('accessToken', idToken); + // phone should be removed from user's original providers. + assertArrayEquals(providersToDelete, ['phone']); + var response = { + 'email': 'user@default.com', + 'providerUserInfo': [ + {'providerId': 'providerId1'} + ] + }; + return goog.Promise.resolve(response); + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user.addProviderData(providerData1); + user.addProviderData(providerDataPhone); + + var userWithoutPhone = new fireauth.AuthUser(config1, tokenResponse, + accountInfo); + userWithoutPhone.addProviderData(providerData1); + + user.addStateChangeListener(function(event) { + asyncTestCase.signal(); + }); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + + user.unlink(fireauth.PhoneAuthProvider['PROVIDER_ID']) + .then(function(passedUser) { + // Update user in Auth state. + assertObjectEquals(userWithoutPhone.toPlainObject(), + user.toPlainObject()); + + // Explicitly test that phone number is null. + assertNull(user['phoneNumber']); + + // Should be same instance. + assertEquals(user, passedUser); + + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(2); +} + + +function testDelete_success() { + asyncTestCase.waitForSignals(2); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + goog.events.listen( + user, fireauth.UserEventType.USER_DELETED, function(event) { + asyncTestCase.signal(); + }); + + // Simulate rpcHandler deleteAccount. + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteAccount', + function(idToken) { + assertEquals('accessToken', idToken); + return goog.Promise.resolve(); + }); + // Checks that destroy is called. + stubs.replace( + user, + 'destroy', + goog.testing.recordFunction(goog.bind(user.destroy, user))); + user['delete']().then(function() { + assertEquals(1, user.destroy.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testDelete_error() { + asyncTestCase.waitForSignals(1); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + goog.events.listen( + user, fireauth.UserEventType.USER_DELETED, function(event) { + fail('Auth change listener should not trigger!'); + }); + + // Simulate rpcHandler deleteAccount. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_AUTH); + stubs.replace( + fireauth.RpcHandler.prototype, + 'deleteAccount', + function(idToken) { + assertEquals('accessToken', idToken); + return goog.Promise.reject(expectedError); + }); + // Checks that destroy is not called. + stubs.replace( + user, + 'destroy', + function() { + fail('User destroy should not be called!'); + }); + user['delete']().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testDelete_userDestroyed() { + assertFailsWhenUserIsDestroyed('delete', []); +} + + +function testSendEmailVerification_success() { + // Simulate successful RpcHandler sendEmailVerification. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendEmailVerification', + function(idToken, actualActionCodeSettings) { + assertEquals('accessToken', idToken); + assertObjectEquals({}, actualActionCodeSettings); + return goog.Promise.resolve('user@default.com'); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.sendEmailVerification().then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testSendEmailVerification_localCopyWrongEmail() { + var expectedEmail = 'user@email.com'; + + // The backend has the email user@email.com associated with the account. + var updatedUserResponse = { + 'users': [{ + 'localId': 'userId1', + 'email': expectedEmail, + 'emailVerified': true, + 'providerUserInfo': [], + 'photoUrl': '', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return goog.Promise.resolve(updatedUserResponse); + }); + + // The RPC should still succeed and return user@email.com. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendEmailVerification', + function(idToken, actualActionCodeSettings) { + assertEquals('accessToken', idToken); + assertObjectEquals({}, actualActionCodeSettings); + return goog.Promise.resolve(expectedEmail); + }); + + // This user does not have an email. + var user = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 'userId1' + }); + assertNull(user['email']); + user.sendEmailVerification().then(function() { + assertEquals(expectedEmail, user['email']); + asyncTestCase.signal(); + }); + + // This user has the wrong email. + var user2 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 'userId1', + 'email': 'wrong@email.com' + }); + user2.sendEmailVerification().then(function() { + assertEquals(expectedEmail, user2['email']); + asyncTestCase.signal(); + }); + + asyncTestCase.waitForSignals(2); +} + + +function testSendEmailVerification_error() { + var expectedError = { + 'error': fireauth.authenum.Error.INVALID_RESPONSE + }; + // Simulate unsuccessful RpcHandler sendEmailVerification. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendEmailVerification', + function(idToken, actualActionCodeSettings) { + assertObjectEquals({}, actualActionCodeSettings); + return goog.Promise.reject(expectedError); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.sendEmailVerification() + .then(function() { + fail('sendEmailVerification should not resolve!'); + }) + .thenCatch(function(error) { + assertObjectEquals(expectedError, error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testSendEmailVerification_actionCodeSettings_success() { + // Simulate successful RpcHandler sendEmailVerification with action code + // settings. + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendEmailVerification', + function(idToken, actualActionCodeSettings) { + assertObjectEquals( + new fireauth.ActionCodeSettings(actionCodeSettings).buildRequest(), + actualActionCodeSettings); + assertEquals('accessToken', idToken); + return goog.Promise.resolve('user@default.com'); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.sendEmailVerification(actionCodeSettings).then(function() { + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testSendEmailVerification_actionCodeSettings_error() { + // Simulate sendEmailVerification with invalid action code settings. + var settings = { + 'url': '' + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CONTINUE_URI); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + assertNoStateEvents(user); + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + user.sendEmailVerification(settings).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testDestroy() { + var config = { + 'apiKey': 'apiKey1', + 'appName': 'appId1', + 'authDomain': 'subdomain.firebaseapp.com' + }; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + mockControl.$replayAll(); + // Confirm a user is subscribed to Auth event manager. + stubs.replace( + fireauth.AuthEventManager.prototype, + 'subscribe', + goog.testing.recordFunction()); + // Confirm a user is unsubscribed from Auth event manager. + stubs.replace( + fireauth.AuthEventManager.prototype, + 'unsubscribe', + goog.testing.recordFunction()); + fireauth.AuthEventManager.ENABLED = true; + var user = new fireauth.AuthUser(config, tokenResponse, accountInfo); + user.enablePopupRedirect(); + // User should be subscribed after enabling popup and redirect. + assertEquals(1, fireauth.AuthEventManager.prototype.subscribe.getCallCount()); + assertEquals( + user, + fireauth.AuthEventManager.prototype.subscribe.getLastCall(). + getArgument(0)); + assertEquals( + 0, fireauth.AuthEventManager.prototype.unsubscribe.getCallCount()); + // Destroy user. + user.destroy(); + // User should be unsubscribed from Auth event manager. + assertEquals(1, fireauth.AuthEventManager.prototype.subscribe.getCallCount()); + assertEquals( + 1, fireauth.AuthEventManager.prototype.unsubscribe.getCallCount()); + assertEquals( + user, + fireauth.AuthEventManager.prototype.unsubscribe.getLastCall(). + getArgument(0)); + assertNull(user['refreshToken']); + return user.reload() + .then(fail, function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MODULE_DESTROYED), + error); + }); +} + + +function testHasSameUserIdAs() { + var user1 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '07478372139011515641', + 'displayName': 'John Doe' + }); + var user2 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid' : '07478372139011515641', + 'displayName' : 'Definitely Not John Doe' + }); + assertTrue(user1.hasSameUserIdAs(user2)); +} + + +function testHasSameUserIdAs_differentUser() { + var user1 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '07478372139011515641', + 'displayName': 'John Doe' + }); + var user2 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid' : '1231231223123112313', + 'displayName' : 'John Doe' + }); + assertFalse(user1.hasSameUserIdAs(user2)); +} + + +function testHasSameUserIdAs_noUserId() { + var user1 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': undefined + }); + var user2 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid' : '07478372139011515641' + }); + assertFalse(user1.hasSameUserIdAs(user2)); + assertFalse(user2.hasSameUserIdAs(user1)); +} + + +function testHasSameUserIdAs_falsyId() { + var user1 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': 0, + 'displayName': 'John Doe' + }); + var user2 = new fireauth.AuthUser(config1, tokenResponse, { + 'uid' : 0, + 'displayName': 'Dohn Joe' + }); + assertTrue(user1.hasSameUserIdAs(user2)); +} + + +function testUser_copy() { + var accountInfo1 = { + 'uid': 'accountUserId1', + 'email': null, + 'displayName': 'DefaultUser1', + 'photoURL': 'https://www.example.com/default1/photo.png', + 'emailVerified': true, + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }; + var providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + null); + var accountInfo2 = { + 'uid': 'accountUserId2', + 'email': 'user2@default.com', + 'displayName': null, + 'photoURL': null, + 'emailVerified': false, + 'isAnonymous': true, + 'lastLoginAt': lastLoginAt2, + 'createdAt': createdAt2 + }; + var providerData2 = new fireauth.AuthUserInfo( + 'providerUserId2', + 'providerId2', + 'user2@example.com', + null, + null); + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo1); + user1.addProviderData(providerData1); + var user2 = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user1.addProviderData(providerData2); + assertObjectNotEquals(user1, user2); + user1.copy(user2); + // user1 should now be equal to user2. + assertObjectEquals(user1, user2); +} + + +function testUser_toPlainObject() { + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png', + '+11234567890'); + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user1.addProviderData(providerData1); + // Confirm redirect event ID is added to plain object. + user1.setRedirectEventId('5678'); + assertObjectEquals( + { + 'uid': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'isAnonymous': false, + 'providerData': [ + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': '+11234567890' + } + ], + 'apiKey': 'apiKey1', + 'authDomain': 'www.example.com', + 'appName': 'appId1', + 'stsTokenManager': { + 'apiKey': 'apiKey1', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': now + 3600 * 1000 + }, + 'redirectEventId': '5678', + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }, + user1.toPlainObject()); +} + + +function testUser_toPlainObject_noMetadata() { + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png', + '+11234567890'); + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + // Remove metadata from account info. + delete accountInfoWithPhone['lastLoginAt']; + delete accountInfoWithPhone['createdAt']; + var user1 = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user1.addProviderData(providerData1); + // Confirm redirect event ID is added to plain object. + user1.setRedirectEventId('5678'); + assertObjectEquals( + { + 'uid': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'isAnonymous': false, + 'providerData': [ + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': '+11234567890' + } + ], + 'apiKey': 'apiKey1', + 'authDomain': 'www.example.com', + 'appName': 'appId1', + 'stsTokenManager': { + 'apiKey': 'apiKey1', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': now + 3600 * 1000 + }, + 'redirectEventId': '5678', + // Metadata should be null. + 'lastLoginAt': null, + 'createdAt': null + }, + user1.toPlainObject()); +} + + +function testToJson() { + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user1.addProviderData(providerData1); + assertObjectEquals(user1.toPlainObject(), user1.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(user1), JSON.stringify(user1.toPlainObject())); +} + + +function testUser_fromPlainObject() { + accountInfoWithPhone['isAnonymous'] = true; + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png'); + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user1.addProviderData(providerData1); + user1.addProviderData(providerDataPhone); + // Confirm redirect event ID is populated from plain object. + user1.setRedirectEventId('5678'); + // Missing API key. + assertNull(fireauth.AuthUser.fromPlainObject(accountInfo)); + accountInfo['apiKey'] = 'apiKey1'; + // Missing STS token response. + assertNull(fireauth.AuthUser.fromPlainObject(accountInfo)); + assertObjectEquals( + user1, + fireauth.AuthUser.fromPlainObject({ + 'uid': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'isAnonymous': true, + 'providerData': [ + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': null + }, + { + 'uid': '+16505550101', + 'displayName': null, + 'photoURL': null, + 'email': null, + 'providerId': 'phone', + 'phoneNumber': '+16505550101' + } + ], + 'apiKey': 'apiKey1', + 'authDomain': 'www.example.com', + 'appName': 'appId1', + 'stsTokenManager': { + 'apiKey': 'apiKey1', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': now + 3600 * 1000 + }, + 'redirectEventId': '5678', + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + })); +} + + +function testUser_fromPlainObject_noMetadata() { + // User previously saved with older version without metadata. + accountInfoWithPhone['isAnonymous'] = true; + delete accountInfoWithPhone['lastLoginAt']; + delete accountInfoWithPhone['createdAt']; + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png'); + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, + accountInfoWithPhone); + user1.addProviderData(providerData1); + user1.addProviderData(providerDataPhone); + // Confirm redirect event ID is populated from plain object. + user1.setRedirectEventId('5678'); + // Missing API key. + assertNull(fireauth.AuthUser.fromPlainObject(accountInfo)); + accountInfo['apiKey'] = 'apiKey1'; + // Missing STS token response. + assertNull(fireauth.AuthUser.fromPlainObject(accountInfo)); + assertObjectEquals( + user1, + fireauth.AuthUser.fromPlainObject({ + 'uid': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': '+16505550101', + 'isAnonymous': true, + 'providerData': [ + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': null + }, + { + 'uid': '+16505550101', + 'displayName': null, + 'photoURL': null, + 'email': null, + 'providerId': 'phone', + 'phoneNumber': '+16505550101' + } + ], + 'apiKey': 'apiKey1', + 'authDomain': 'www.example.com', + 'appName': 'appId1', + 'stsTokenManager': { + 'apiKey': 'apiKey1', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': now + 3600 * 1000 + }, + 'redirectEventId': '5678' + })); +} + + +function testUser_fromPlainObject_tokenExpired() { + // This will simulate a user with an expired refresh token being loaded from + // storage. getIdToken should reject with the expected token expired error and + // should not trigger state or Auth change event. + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'providerId1', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png', + '+11234567890'); + config1['authDomain'] = 'www.example.com'; + config1['appName'] = 'appId1'; + // Simulate the user has an expired token. + tokenResponse['refreshToken'] = null; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user1.addProviderData(providerData1); + accountInfo['apiKey'] = 'apiKey1'; + var parsedUser = fireauth.AuthUser.fromPlainObject({ + 'uid': 'defaultUserId', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'email': 'user@default.com', + 'emailVerified': true, + 'phoneNumber': null, + 'isAnonymous': false, + 'providerData': [ + { + 'uid': 'providerUserId1', + 'displayName': null, + 'photoURL': 'https://www.example.com/user1/photo.png', + 'email': 'user1@example.com', + 'providerId': 'providerId1', + 'phoneNumber': '+11234567890' + } + ], + 'apiKey': 'apiKey1', + 'authDomain': 'www.example.com', + 'appName': 'appId1', + 'stsTokenManager': { + 'apiKey': 'apiKey1', + // Expired refresh token. + 'refreshToken': null, + 'accessToken': 'accessToken', + 'expirationTime': now + 3600 * 1000 + }, + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }); + // Confirm matching users with expired refresh token. + assertObjectEquals(user1, parsedUser); + assertNull(parsedUser['refreshToken']); + // No state or token changes should be triggered. + assertNoStateEvents(parsedUser); + assertNoTokenEvents(parsedUser); + assertNoUserInvalidatedEvents(user1); + // Get token on parsed user should triggered expired token error with no state + // change. + parsedUser.getIdToken().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_initializeFromIdTokenResponse() { + var response = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [ + { + 'email': 'user@gmail.com', + 'providerId': 'google.com', + 'displayName': 'John G. Doe', + 'photoUrl': 'https://lh5.googleusercontent.com/123456789/photo.jpg', + 'federatedId': 'https://accounts.google.com/123456789', + 'rawId': '123456789' + }, + { + 'providerId': 'twitter.com', + 'displayName': 'John Gammell Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/def' + + 'ault_profile_3_normal.png', + 'federatedId': 'http://twitter.com/987654321', + 'rawId': '987654321' + } + ], + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + var expectedUser = new fireauth.AuthUser(config1, tokenResponse, { + 'uid': '14584746072031976743', + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoURL': 'http://abs.twimg.com/sticky/default_profile_images/defaul' + + 't_profile_3_normal.png', + 'emailVerified': true + }); + expectedUser.addProviderData(new fireauth.AuthUserInfo( + '123456789', + 'google.com', + 'user@gmail.com', + 'John G. Doe', + 'https://lh5.googleusercontent.com/123456789/photo.jpg')); + expectedUser.addProviderData(new fireauth.AuthUserInfo( + '987654321', + 'twitter.com', + null, + 'John Gammell Doe', + 'http://abs.twimg.com/sticky/default_profile_images/default_profile_' + + '3_normal.png')); + var frameworks = ['firebaseui', 'angularfire']; + // Listen to all calls on setFramework. + stubs.replace( + fireauth.AuthUser.prototype, + 'setFramework', + goog.testing.recordFunction(fireauth.AuthUser.prototype.setFramework)); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + // Confirm setFramework called with expected parameters before + // getAccountInfo call. + assertEquals( + 1, + fireauth.AuthUser.prototype.setFramework.getCallCount()); + assertArrayEquals( + frameworks, + fireauth.AuthUser.prototype.setFramework.getLastCall() + .getArgument(0)); + assertEquals('accessToken', data); + resolve(response); + }); + }); + asyncTestCase.waitForSignals(1); + assertEquals(0, fireauth.AuthUser.prototype.setFramework.getCallCount()); + fireauth.AuthUser.initializeFromIdTokenResponse( + config1, tokenResponse, null, frameworks).then(function(createdUser) { + // Confirm no additional calls on setFramework. + assertEquals( + 1, + fireauth.AuthUser.prototype.setFramework.getCallCount()); + // Confirm frameworks set on created user. + assertArrayEquals(frameworks, createdUser.getFramework()); + assertObjectEquals( + expectedUser.toPlainObject(), createdUser.toPlainObject()); + assertEquals('refreshToken', createdUser['refreshToken']); + // Confirm STS token manager instance properly created. + assertTrue( + createdUser.stsTokenManager_ instanceof fireauth.StsTokenManager); + assertEquals('accessToken', createdUser.stsTokenManager_.accessToken_); + assertEquals( + 'refreshToken', createdUser.stsTokenManager_.refreshToken_); + assertEquals( + now + 3600 * 1000, createdUser.stsTokenManager_.expirationTime_); + asyncTestCase.signal(); + }); +} + + +function testUser_authEventManager_noAuthDomain() { + // When no Auth domain provided, all popup and redirect operations should + // throw the relevant error. + fireauth.AuthEventManager.ENABLED = true; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN); + // Test no Auth domain and call getAuthEventManager. + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user1.enablePopupRedirect(); + try { + user1.getAuthEventManager(); + fail('getAuthEventManager should throw missing Auth domain error.'); + } catch (error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + } +} + + +function testUser_authEventManager_authDomainProvided() { + // Confirm getAuthEventManager when authDomain provided. + fireauth.AuthEventManager.ENABLED = true; + var expectedManager = { + 'subscribe': goog.testing.recordFunction(), + 'unsubscribe': goog.testing.recordFunction() + }; + // Return a manager with recorded subscribe and unsubscribe operations. + stubs.replace( + fireauth.AuthEventManager, + 'getManager', + function(authDomain, apiKey, appName) { + assertEquals('subdomain.firebaseapp.com', authDomain); + assertEquals('apiKey1', apiKey); + assertEquals('appId1', appName); + return expectedManager; + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect operaitons. + user1.enablePopupRedirect(); + // This should resolve with the manager instance. + assertEquals(expectedManager, user1.getAuthEventManager()); + // User should be subscribed. + assertEquals(0, expectedManager.unsubscribe.getCallCount()); + assertEquals(1, expectedManager.subscribe.getCallCount()); + assertEquals(user1, expectedManager.subscribe.getLastCall().getArgument(0)); + // Destroy user. + user1.destroy(); + // User should be now unsubscribed. + assertEquals(1, expectedManager.subscribe.getCallCount()); + assertEquals(1, expectedManager.unsubscribe.getCallCount()); + assertEquals(user1, expectedManager.unsubscribe.getLastCall().getArgument(0)); +} + + +function testUser_authEventManager_unsubscribed() { + // Test getAuthEventManager on an unsubscribed user. + fireauth.AuthEventManager.ENABLED = true; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var expectedManager = { + 'subscribe': goog.testing.recordFunction(), + 'unsubscribe': goog.testing.recordFunction() + }; + stubs.replace( + fireauth.AuthEventManager, + 'getManager', + function(authDomain, apiKey, appName) { + assertEquals('subdomain.firebaseapp.com', authDomain); + assertEquals('apiKey1', apiKey); + assertEquals('appId1', appName); + return expectedManager; + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // User not subscribed yet. This should throw an error. + try { + user1.getAuthEventManager(); + fail('getAuthEventManager should throw an error.'); + } catch (error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + } +} + + +function testUser_finishPopupAndRedirectLink_success() { + asyncTestCase.waitForSignals(5); + // This should be populated from verifyAssertionForLinking response. + var expectedCred = fireauth.GoogleAuthProvider.credential( + null, 'ACCESS_TOKEN'); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID', + 'idToken': 'accessToken' + }, + data); + asyncTestCase.signal(); + return goog.Promise.resolve({ + 'idToken': 'newIdToken', + 'refreshToken': 'newRefreshToken', + 'expiresIn': '3600', + 'providerId': 'google.com', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'rawUserInfo': expectedTokenResponseWithIdPData['rawUserInfo'] + }); + }); + // Reload should be called. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + asyncTestCase.signal(); + return goog.Promise.resolve(getAccountInfoResponse); + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Token change should be triggered. + goog.events.listen( + user1, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + asyncTestCase.signal(); + }); + assertNoUserInvalidatedEvents(user1); + // Finish popup and redirect linking. + user1.finishPopupAndRedirectLink('REQUEST_URI', 'SESSION_ID') + .then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.LINK, response); + // It should have updated the tokens. + assertEquals('newIdToken', user1['_lat']); + assertEquals('newRefreshToken', user1.refreshToken); + asyncTestCase.signal(); + }); +} + + +function testUser_finishPopupAndRedirectReauth_success() { + asyncTestCase.waitForSignals(5); + // This should be populated from verifyAssertion response. + var expectedCred = fireauth.GoogleAuthProvider.credential( + null, 'ACCESS_TOKEN'); + // Simulate successful RpcHandler verifyAssertionForExisting. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID' + }, + data); + asyncTestCase.signal(); + return goog.Promise.resolve({ + 'idToken': idTokenGmail.jwt, + 'accessToken': idTokenGmail.jwt, + 'refreshToken': 'newRefreshToken', + 'expiresIn': '3600', + 'providerId': 'google.com', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'rawUserInfo': expectedTokenResponseWithIdPData['rawUserInfo'] + }); + }); + // Reload should be called. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + asyncTestCase.signal(); + return goog.Promise.resolve(getAccountInfoResponse); + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + // Modify accountInfo UID to match the token UID. + accountInfo['uid'] = 679; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Token change should be triggered. + goog.events.listen( + user1, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + asyncTestCase.signal(); + }); + assertNoUserInvalidatedEvents(user1); + // Finish popup and redirect reauth. + user1.finishPopupAndRedirectReauth('REQUEST_URI', 'SESSION_ID') + .then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.REAUTHENTICATE, response); + // It should have updated the tokens. + assertEquals(idTokenGmail.jwt, user1['_lat']); + assertEquals('newRefreshToken', user1.refreshToken); + asyncTestCase.signal(); + }); +} + + +function testUser_finishPopupAndRedirectLink_error() { + asyncTestCase.waitForSignals(2); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Simulate error in RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID', + 'idToken': 'accessToken' + }, + data); + asyncTestCase.signal(); + return goog.Promise.reject(expectedError); + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // No state or token changes should be triggered. + assertNoStateEvents(user1); + assertNoTokenEvents(user1); + assertNoUserInvalidatedEvents(user1); + // Finish popup and redirect linking. This should throw the same error above. + user1.finishPopupAndRedirectLink('REQUEST_URI', 'SESSION_ID') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_finishPopupAndRedirectReauth_error() { + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Simulate error in RpcHandler verifyAssertionForExisting. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + goog.testing.recordFunction(function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.reject(expectedError); + })); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // No state or token changes should be triggered. + assertNoStateEvents(user1); + assertNoTokenEvents(user1); + assertNoUserInvalidatedEvents(user1); + // Finish popup and redirect reauth. This should throw the same error above. + user1.finishPopupAndRedirectReauth('REQUEST_URI', 'SESSION_ID') + .thenCatch(function(error) { + assertEquals( + 1, + fireauth.RpcHandler.prototype.verifyAssertionForExisting + .getCallCount()); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_finishPopupAndRedirectLink_noCredential() { + asyncTestCase.waitForSignals(2); + // Test when for some reason OAuth response is not returned. + var expectedCred = null; + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(data) { + assertObjectEquals( + { + 'requestUri': 'REQUEST_URI', + 'sessionId': 'SESSION_ID', + 'idToken': 'accessToken' + }, + data); + asyncTestCase.signal(); + return goog.Promise.resolve({ + 'idToken': 'newIdToken', + 'refreshToken': 'newRefreshToken', + 'expiresIn': '3600', + 'providerId': 'google.com', + 'rawUserInfo': expectedTokenResponseWithIdPData['rawUserInfo'] + }); + }); + // Successful linking should trigger reload. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + asyncTestCase.signal(); + return goog.Promise.resolve(getAccountInfoResponse); + }); + config1['authDomain'] = 'subdomain.firebaseapp.com'; + var user1 = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Finish popup and redirect linking. No credential should be returned. + user1.finishPopupAndRedirectLink('REQUEST_URI', 'SESSION_ID') + .then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.LINK, response); + // It should have updated the tokens. + assertEquals( + 'newIdToken', user1.getStsTokenManager().accessToken_); + assertEquals( + 'newRefreshToken', + user1.getStsTokenManager().refreshToken_); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithRedirect_success_unloadsOnRedirect() { + // Test successful request for link with redirect when page unloads on + // redirect. + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect + // event ID. + storageManager.getRedirectUser().then(function(user) { + assertEquals(expectedEventId, user.getRedirectEventId()); + assertObjectEquals(user1.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(true); + mockControl.$replayAll(); + + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Link with redirect should succeed and remain pending. + user1.linkWithRedirect(expectedProvider).then(function() { + fail('LinkWithRedirect should remain pending in environment where ' + + 'OAuthSignInHandler unloads the page.'); + }); +} + + +function testUser_reauthenticateWithRedirect_success_unloadsOnRedirect() { + // Test successful request for reauth with redirect when page unloads on + // redirect. + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect + // event ID. + storageManager.getRedirectUser().then(function(user) { + assertEquals(expectedEventId, user.getRedirectEventId()); + assertObjectEquals(user1.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(true); + mockControl.$replayAll(); + + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Reauth with redirect should succeed and remain pending. + user1.reauthenticateWithRedirect(expectedProvider).then(function() { + fail('ReauthenticateWithRedirect should remain pending in environment ' + + 'where OAuthSignInHandler unloads the page.'); + }); +} + + +function testUser_linkWithRedirect_success_doesNotUnloadOnRedirect() { + // Test successful request for link with redirect when page does not unload + // on redirect. + fireauth.AuthEventManager.ENABLED = true; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(false); + mockControl.$replayAll(); + + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Link with redirect should succeed and resolve in this case. + user1.linkWithRedirect(expectedProvider).then(function() { + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect event ID. + storageManager.getRedirectUser().then(function(user) { + assertEquals(expectedEventId, user.getRedirectEventId()); + assertObjectEquals(user1.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_reauthenticateWithRedirect_success_doesNotUnloadOnRedirect() { + // Test successful request for reauth with redirect when page does not unload + // on redirect. + fireauth.AuthEventManager.ENABLED = true; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(false); + mockControl.$replayAll(); + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Reauth with redirect should succeed and resolve in this case. + user1.reauthenticateWithRedirect(expectedProvider).then(function() { + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect event ID. + storageManager.getRedirectUser().then(function(user) { + assertEquals(expectedEventId, user.getRedirectEventId()); + assertObjectEquals(user1.toPlainObject(), user.toPlainObject()); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithRedirect_success_noStorageManager() { + // Test when no storage manager supplied. + fireauth.AuthEventManager.ENABLED = true; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect + // event ID. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(true); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Link with redirect should never resolve. + user1.linkWithRedirect(expectedProvider).then(function() { + fail('LinkWithRedirect should remain pending in environment where ' + + 'OAuthSignInHandler unloads the page.'); + }); +} + + +function testUser_reauthenticateWithRedirect_success_noStorageManager() { + // Test when no storage manager supplied. + fireauth.AuthEventManager.ENABLED = true; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Redirect event ID should be saved. + assertEquals(expectedEventId, user1.getRedirectEventId()); + // Redirect user should be saved in storage with correct redirect + // event ID. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.unloadsOnRedirect().$returns(true); + mockControl.$replayAll(); + var expectedEventId = '1234'; + asyncTestCase.waitForSignals(2); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // An event ID should be generated. + return expectedEventId; + }); + var expectedProvider = new fireauth.GoogleAuthProvider(); + expectedProvider.addScope('scope1'); + expectedProvider.addScope('scope2'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + user1.addStateChangeListener(function(user) { + // User state change should be triggered. + // Redirect event ID should be saved. + assertEquals(expectedEventId, user.getRedirectEventId()); + asyncTestCase.signal(); + return goog.Promise.resolve(); + }); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Reauth with redirect should never resolve. + user1.reauthenticateWithRedirect(expectedProvider).then(function() { + fail('ReauthenticateWithRedirect should remain pending in environment ' + + 'where OAuthSignInHandler unloads the page.'); + }); +} + + +function testLinkWithRedirect_missingAuthDomain() { + // Link with redirect should fail when Auth domain is missing. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(2); + + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + var provider = new fireauth.GoogleAuthProvider(); + var config = { + 'apiKey': 'apiKey1', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.enablePopupRedirect(); + // linkWithRedirect should fail with missing Auth domain error. + user1.linkWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN), + error); + // Redirect user should not be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateWithRedirect_missingAuthDomain() { + // Reauth with redirect should fail when Auth domain is missing. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(1); + + var provider = new fireauth.GoogleAuthProvider(); + var config = { + 'apiKey': 'apiKey1', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + user1.enablePopupRedirect(); + // reauthenticateWithRedirect should fail with missing Auth domain error. + user1.reauthenticateWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_AUTH_DOMAIN), + error); + // Redirect user should not be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithRedirect_invalidProvider() { + // Link with redirect should fail when provider is an OAuth provider. + fireauth.AuthEventManager.ENABLED = true; + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processRedirect( + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualMode, + actualProvider, + actualEventId) { + assertEquals( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, actualMode); + assertEquals(provider, actualProvider); + return goog.Promise.reject(expectedError); + }); + mockControl.$replayAll(); + asyncTestCase.waitForSignals(2); + + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Email and password Auth provider. + var provider = new fireauth.EmailAuthProvider(); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enabled popup and redirect. + user1.enablePopupRedirect(); + // linkWithRedirect should fail with an invalid OAuth provider error. + user1.linkWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Redirect user should not be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithRedirect_invalidProvider() { + // Reauth with redirect should fail when provider is not an OAuth provider. + fireauth.AuthEventManager.ENABLED = true; + asyncTestCase.waitForSignals(1); + + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + + // Email and password Auth provider. + var provider = new fireauth.EmailAuthProvider(); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enabled popup and redirect. + user1.enablePopupRedirect(); + // reauthenticateWithRedirect should fail with an invalid OAuth provider + // error. + user1.reauthenticateWithRedirect(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Redirect user should not be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithRedirect_alreadyLinked() { + // User on server has the federated provider linked already. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseGoogleProviderData); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.linkWithRedirect(new fireauth.GoogleAuthProvider()) + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(new fireauth.AuthError( + fireauth.authenum.Error.PROVIDER_ALREADY_LINKED), actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testUser_linkWithPopup_success_slowIframeEmbed() { + // Test successful link with popup with delay in embedding the iframe. + asyncTestCase.waitForSignals(3); + var clock = new goog.testing.MockClock(true); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Simulate popup closed. + expectedPopup.closed = true; + // Simulate the iframe took a while to embed. This should not + // trigger a popup timeout. + clock.tick(10000); + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful link via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate successful RpcHandler verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(data) { + // Now that popup timer is cleared, a delay in verify assertion should + // not trigger popup closed error. + clock.tick(10000); + // Resolve with expected token response. + return goog.Promise.resolve(expectedTokenResponseWithIdPData); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should not trigger popup closed error and should resolve + // successfully. + user1.linkWithPopup(provider).then(function(popupResult) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user1, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.LINK, + popupResult); + goog.dispose(clock); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_success_slowIframeEmbed() { + // Test successful reauth with popup with delay in embedding the iframe. + asyncTestCase.waitForSignals(1); + var clock = new goog.testing.MockClock(true); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Simulate popup closed. + expectedPopup.closed = true; + // Simulate the iframe took a while to embed. This should not + // trigger a popup timeout. + clock.tick(10000); + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Stub getAccountInfoByIdToken which is called on reload. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate successful RpcHandler verifyAssertionForExisting. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + function(data) { + // Now that popup timer is cleared, a delay in verify assertion should + // not trigger popup closed error. + clock.tick(10000); + // Resolve with expected token response. + return goog.Promise.resolve(expectedReauthenticateTokenResponse); + }); + accountInfo['uid'] = '679'; + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should not trigger popup closed error and should + // resolve successfully. + user1.reauthenticateWithPopup(provider).then(function(popupResult) { + // Expected result returned. + fireauth.common.testHelper.assertUserCredentialResponse( + // Expected current user returned. + user1, + // Expected credential returned. + expectedGoogleCredential, + // Expected additional user info. + expectedAdditionalUserInfo, + // operationType not implemented yet. + fireauth.constants.OperationType.REAUTHENTICATE, + popupResult); + goog.dispose(clock); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_error_popupClosed() { + // Test when the popup is closed without completing sign in that the expected + // popup closed error is triggered. + asyncTestCase.waitForSignals(3); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Trigger popup closed by user error. + onError(expectedError); + // This should be ignored. + recordedHandler(delayedPopupAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Expected popup closed error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + // The expected popup event ID. + var expectedEventId = '1234'; + // Delayed expected popup Auth event. + var delayedPopupAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should fail with the popup closed error. + user1.linkWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_error_popupClosed() { + // Test when the popup is closed without completing sign in that the expected + // popup closed error is triggered. + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Trigger popup closed by user error. + onError(expectedError); + // This should be ignored. + recordedHandler(delayedPopupAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Expected popup closed error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + // The expected popup event ID. + var expectedEventId = '1234'; + // Delayed expected popup Auth event. + var delayedPopupAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + goog.testing.recordFunction(function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + })); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + goog.testing.recordFunction(function(win) { + assertEquals(expectedPopup, win); + })); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should fail with the popup closed error. + user1.reauthenticateWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_error_iframeWebStorageNotSupported() { + // Test when the web storage is not supported in the iframe. + asyncTestCase.waitForSignals(1); + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.iframeclient.IfcHandler); + mockControl.createConstructorMock(fireauth.iframeclient, 'IfcHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Trigger web storage not supported error. + onError(expectedError); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + // The expected popup event ID. + var expectedEventId = '1234'; + // Keep track when the popup is closed. + var isClosed = false; + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Record when the popup is closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should fail with the web storage no supported error. + user1.linkWithPopup(provider).thenCatch(function(error) { + // Popup should be closed. + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthWithPopup_error_iframeWebStorageNotSupported() { + // Test when the web storage is not supported in the iframe. + asyncTestCase.waitForSignals(1); + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.iframeclient.IfcHandler); + mockControl.createConstructorMock(fireauth.iframeclient, 'IfcHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + // Trigger web storage not supported error. + onError(expectedError); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + // The expected popup event ID. + var expectedEventId = '1234'; + // Keep track when the popup is closed. + var isClosed = false; + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Record when the popup is closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should fail with the web storage no supported + // error. + user1.reauthenticateWithPopup(provider).thenCatch(function(error) { + // Popup should be closed. + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_success() { + asyncTestCase.waitForSignals(3); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful link via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should succeed with the expected popup result. + user1.linkWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_success() { + asyncTestCase.waitForSignals(1); + + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + goog.testing.recordFunction(function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + })); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + goog.testing.recordFunction(function(win) { + assertEquals(expectedPopup, win); + })); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + // Add Google as linked provider to confirm that reauth does not fail like + // linking does when called with an already linked provider. + providerData1 = new fireauth.AuthUserInfo( + 'providerUserId1', + 'google.com', + 'user1@example.com', + null, + 'https://www.example.com/user1/photo.png'); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + user1.addProviderData(providerData1); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should succeed with the expected popup result. + user1.reauthenticateWithPopup(provider).then(function(popupResult) { + // Confirm popup and closeWindow called in the process. + /** @suppress {missingRequire} */ + assertEquals(1, fireauth.util.popup.getCallCount()); + /** @suppress {missingRequire} */ + assertEquals(1, fireauth.util.closeWindow.getCallCount()); + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithPopup_emailCredentialError() { + // Test when link with popup verifyAssertion throws an Auth email credential + // error. + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + var credential = + fireauth.GoogleAuthProvider.credential({'accessToken': 'ACCESS_TOKEN'}); + // Expected Auth email credential error. + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + email: 'user@example.com', + credential: credential + }); + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful link via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate Auth email credential error thrown by verifyAssertion. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForLinking', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID', + 'idToken': 'accessToken' + }, + data); + return goog.Promise.reject(expectedError); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should fail with the expected Auth email credential error. + user1.linkWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_userMismatchError() { + // Test when reauth with popup verifyAssertion returns an ID token with a + // different user ID. + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Expected Auth email credential error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.USER_MISMATCH); + // This response will contain an ID token with a UID that does not match the + // current user's UID. + var expectedUserMismatchResponse = { + 'idToken': idTokenGmail.jwt, + 'accessToken': idTokenGmail.jwt, + 'refreshToken': 'REFRESH_TOKEN', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE' + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Simulate verifyAssertionForExisting returns a token with a different UID. + stubs.replace( + fireauth.RpcHandler.prototype, + 'verifyAssertionForExisting', + function(data) { + assertObjectEquals( + { + 'requestUri': 'http://www.example.com/#response', + 'sessionId': 'SESSION_ID' + }, + data); + return goog.Promise.resolve(expectedUserMismatchResponse); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + // Set redirect storage manager. + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should fail with the user mismatch error. + user1.reauthenticateWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_unsupportedEnvironment() { + // Test linkWithPopup in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.linkWithPopup(new fireauth.GoogleAuthProvider()) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_unsupportedEnvironment() { + // Test reauthenticateWithPopup in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.reauthenticateWithPopup(new fireauth.GoogleAuthProvider()) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithRedirect_unsupportedEnvironment() { + // Test linkWithRedirect in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.linkWithRedirect(new fireauth.GoogleAuthProvider()) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithRedirect_unsupportedEnvironment() { + // Test reauthenticateWithRedirect in unsupported environment. + // Simulate popup and redirect not supported in current environment. + stubs.replace( + fireauth.util, + 'isPopupRedirectSupported', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.reauthenticateWithRedirect(new fireauth.GoogleAuthProvider()) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_success_cannotRunInBackground() { + asyncTestCase.waitForSignals(4); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful link via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + // linkWithPopup should succeed with the expected popup result. + user1.linkWithPopup(expectedProvider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_success_cannotRunInBackground() { + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + // reauthenticateWithPopup should succeed with the expected popup result. + user1.reauthenticateWithPopup(expectedProvider) + .then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithPopup_success_iframeCanRunInBackground() { + // Test successful link with popup when tab can run in background but is an + // iframe. This should behave the same as the + // testUser_linkWithPopup_success_cannotRunInBackground test. + asyncTestCase.waitForSignals(4); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Should already be redirected. + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful link via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab can run in the background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Simulate app is running in an iframe. This should open the popup with the + // OAuth helper redirect directly. No additional redirect is needed as it + // could be blocked due to iframe sandboxing settings. + stubs.replace( + fireauth.util, + 'isIframe', + function() { + return true; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + // linkWithPopup should succeed with the expected popup result. + user1.linkWithPopup(expectedProvider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_success_iframeCanRunInBackground() { + // Test successful reauth with popup when tab can run in background but is an + // iframe. This should behave the same as the + // testUser_reauthenticateWithPopup_success_cannotRunInBackground test. + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + // Should already be redirected. + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected successful reauth via popup Auth event. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedEventId, + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab can run in the background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Simulate app is running in an iframe. This should open the popup with the + // OAuth helper redirect directly. No additional redirect is needed as it + // could be blocked due to iframe sandboxing settings. + stubs.replace( + fireauth.util, + 'isIframe', + function() { + return true; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // The expected popup result should be returned. + return goog.Promise.resolve(expectedPopupResult); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + // reauthenticateWithPopup should succeed with the expected popup result. + user1.reauthenticateWithPopup(expectedProvider) + .then(function(popupResult) { + assertObjectEquals(expectedPopupResult, popupResult); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithPopup_webStorageUnsupported_cannotRunInBackground() { + // Test link with popup when the web storage is not supported in the iframe + // and the tab cannot run in background. + asyncTestCase.waitForSignals(1); + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + onError(expectedError); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Keep track when the popup is closed. + var isClosed = false; + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Check when the popup will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // linkWithPopup should reject with the expected error. + user1.linkWithPopup(expectedProvider).thenCatch(function(error) { + // Popup should be closed. + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_reauthWithPopup_webStorageUnsupported_cantRunInBackground() { + // Test reauth with popup when the web storage is not supported in the iframe + // and the tab cannot run in background. + asyncTestCase.waitForSignals(1); + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.iframeclient.IfcHandler); + mockControl.createConstructorMock(fireauth.iframeclient, 'IfcHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(expectedProvider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertTrue(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + onError(expectedError); + return goog.Promise.resolve(); + }); + mockControl.$replayAll(); + // Keep track when the popup is closed. + var isClosed = false; + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // Expected web storage not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var expectedProvider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + config['authDomain'], + config['apiKey'], + config['appName'], + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + expectedProvider, + null, + expectedEventId, + firebase.SDK_VERSION); + // Simulate tab cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Destination URL popped directly without the second redirect. + assertEquals(expectedUrl, url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Check when the popup will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + isClosed = true; + assertEquals(expectedPopup, win); + }); + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + // A popup event ID should be generated. + return expectedEventId; + }); + // Reset static getOAuthHelperWidgetUrl method on IfcHandler. + stubs.set( + fireauth.iframeclient.IfcHandler, + 'getOAuthHelperWidgetUrl', + function(domain, apiKey, name, mode, provider, url, eventId) { + assertEquals(config['authDomain'], domain); + assertEquals(config['apiKey'], apiKey); + assertEquals(config['appName'], name); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, mode); + assertEquals(expectedProvider, provider); + assertNull(url); + assertEquals(expectedEventId, eventId); + return expectedUrl; + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Set redirect storage manager. + storageManager = new fireauth.storage.RedirectUserManager( + fireauth.util.createStorageKey(config['apiKey'], config['appName'])); + user1.setRedirectStorageManager(storageManager); + // Enable popup and redirect. + user1.enablePopupRedirect(); + // reauthenticateWithPopup should reject with the expected error. + user1.reauthenticateWithPopup(expectedProvider) + .thenCatch(function(error) { + // Popup should be closed. + assertTrue(isClosed); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Popup user should never be saved in storage. + storageManager.getRedirectUser().then(function(user) { + assertNull(user); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_linkWithPopup_multipleUsers_success() { + // Test link with popup on multiple users. + asyncTestCase.waitForSignals(6); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertObjectEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertObjectEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + startPopupTimeoutCalls++; + // Both users already ready, handle events for both. + if (startPopupTimeoutCalls == 2) { + recordedHandler(expectedAuthEvent1); + recordedHandler(expectedAuthEvent2); + // User one can only handle first event. + assertTrue(user1.canHandleAuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, '1234')); + assertFalse(user1.canHandleAuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, '5678')); + // User two can only handle second event. + assertTrue(user2.canHandleAuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, '5678')); + assertFalse(user2.canHandleAuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, '1234')); + } + return new goog.Promise(function(resolve, reject) {}); + }).$times(2); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // The expected successful link via popup Auth event for first user. + var expectedAuthEvent1 = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // The expected successful link via popup Auth event for second user. + var expectedAuthEvent2 = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + '5678', + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + // Number of popup timeout calls. + var startPopupTimeoutCalls = 0; + fireauth.AuthEventManager.ENABLED = true; + var firstCall = true; + // Generate event ID depending on user calling it. + stubs.replace( + fireauth.util, + 'generateEventId', + function(prefix) { + // Skip other calls for generateEventId. + // This is used for testing that popup and redirects are supported. + if (!prefix) { + return 'random'; + } + if (firstCall) { + firstCall = false; + return '1234'; + } else { + return '5678'; + } + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // Finish popup and redirect link should be called for each user. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + if (this == user1) { + // Resolve with first expected result for first user. + return goog.Promise.resolve(expectedPopupResult1); + } else { + // Resolve with second expected result for second user. + return goog.Promise.resolve(expectedPopupResult2); + } + }); + // Create 2 users, it doesn't matter if they have same parameters. + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + var user2 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // The expected popup results. + var expectedPopupResult1 = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + var expectedPopupResult2 = { + 'user': user2, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + var provider = new fireauth.GoogleAuthProvider(); + // Enable popup and redirect on the first user. + user1.enablePopupRedirect(); + // linkWithPopup should succeed with the first expected popup result. + user1.linkWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult1, popupResult); + asyncTestCase.signal(); + }); + var provider = new fireauth.GoogleAuthProvider(); + // Enable popup and redirect on the first user. + user2.enablePopupRedirect(); + // linkWithPopup should succeed with the second expected popup result. + user2.linkWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult2, popupResult); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_multipleUsers_success() { + // Test reauth with popup on multiple users. + asyncTestCase.waitForSignals(2); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertObjectEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertObjectEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + startPopupTimeoutCalls++; + if (startPopupTimeoutCalls == 2) { + recordedHandler(expectedAuthEvent1); + recordedHandler(expectedAuthEvent2); + // User one can only handle first event. + assertTrue(user1.canHandleAuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, '1234')); + assertFalse(user1.canHandleAuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, '5678')); + // User two can only handle second event. + assertTrue(user2.canHandleAuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, '5678')); + assertFalse(user2.canHandleAuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, '1234')); + } + return new goog.Promise(function(resolve, reject) {}); + }).$times(2); + mockControl.$replayAll(); + var recordedHandler = null; + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // On success if popup is still opened, it will be closed. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + }); + // The expected successful reauth via popup Auth event for first user. + var expectedAuthEvent1 = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // The expected successful reauth via popup Auth event for second user. + var expectedAuthEvent2 = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + '5678', + 'http://www.example.com/#response', + 'SESSION_ID'); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + // Number of popup timeout calls. + var startPopupTimeoutCalls = 0; + fireauth.AuthEventManager.ENABLED = true; + var firstCall = true; + // Generate event ID depending on user calling it. + stubs.replace( + fireauth.util, + 'generateEventId', + function(prefix) { + // Skip other calls for generateEventId. + // This is used for testing that popup and redirects are supported. + if (!prefix) { + return 'random'; + } + if (firstCall) { + firstCall = false; + return '1234'; + } else { + return '5678'; + } + }); + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Finish popup and redirect reauth should be called for each user. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + if (this == user1) { + // Resolve with first expected result for first user. + return goog.Promise.resolve(expectedPopupResult1); + } else { + // Resolve with second expected result for second user. + return goog.Promise.resolve(expectedPopupResult2); + } + }); + // Create 2 users, it doesn't matter if they have same parameters. + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + var user2 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // The expected popup results. + var expectedPopupResult1 = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + var expectedPopupResult2 = { + 'user': user2, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + var provider = new fireauth.GoogleAuthProvider(); + // Enable popup and redirect on the first user. + user1.enablePopupRedirect(); + // reauthenticateWithPopup should succeed with the first expected popup + // result. + user1.reauthenticateWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult1, popupResult); + asyncTestCase.signal(); + }); + var provider = new fireauth.GoogleAuthProvider(); + // Enable popup and redirect on the first user. + user2.enablePopupRedirect(); + // reauthenticateWithPopup should succeed with the second expected popup + // result. + user2.reauthenticateWithPopup(provider).then(function(popupResult) { + assertObjectEquals(expectedPopupResult2, popupResult); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_timeout() { + fireauth.AuthEventManager.ENABLED = true; + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return new goog.Promise(function(resolve, reject) {}); + }).$times(2); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected expire error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }; + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + // This will resolve only for the second link with popup operation. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + '5678', + 'http://www.example.com/#response', + 'SESSION_ID'); + // The expected event IDs for each operation. + var expectedEventId = ['1234', '5678']; + // expectedEventId current index. + var index = 0; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Called twice. + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // Popup may try to close on resolution if still open (called twice). + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + // Called twice. + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // A popup event ID should be generated. + stubs.replace( + fireauth.util, + 'generateEventId', + function(prefix) { + // Skip other calls for generateEventId. + // This is used for testing that popup and redirects are supported. + if (!prefix) { + return 'random'; + } + // Return the next available event ID. + return expectedEventId[index++]; + }); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(6); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Enable popup and redirect on user. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // Call link with popup first. This will be expired after the second call. + user1.linkWithPopup(provider).thenCatch(function(error) { + // The second call should force this call to expire. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Call link with popup second. + user1.linkWithPopup(provider).then(function(popupResult) { + // This will cancel the previous popup operation and eventually resolve. + assertObjectEquals(expectedPopupResult, popupResult); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_timeout() { + fireauth.AuthEventManager.ENABLED = true; + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return new goog.Promise(function(resolve, reject) {}); + }).$times(2); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected expire error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.EXPIRED_POPUP_REQUEST); + // The expected popup result. + var expectedPopupResult = { + 'user': user1, + 'credential': expectedGoogleCredential, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }; + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + // This will resolve only for the second reauth with popup operation. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + '5678', + 'http://www.example.com/#response', + 'SESSION_ID'); + // The expected event IDs for each operation. + var expectedEventId = ['1234', '5678']; + // expectedEventId current index. + var index = 0; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + // Called twice. + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + }); + // Popup may try to close on resolution if still open (called twice). + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + // Called twice. + assertEquals(expectedPopup, win); + }); + // A popup event ID should be generated. + stubs.replace( + fireauth.util, + 'generateEventId', + function(prefix) { + // Skip other calls for generateEventId. + // This is used for testing that popup and redirects are supported. + if (!prefix) { + return 'random'; + } + // Return the next available event ID. + return expectedEventId[index++]; + }); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + return goog.Promise.resolve(expectedPopupResult); + }); + asyncTestCase.waitForSignals(2); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Enable popup and redirect on user. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // Call reauth with popup first. This will be expired after the second call. + user1.reauthenticateWithPopup(provider).thenCatch(function(error) { + // The second call should force this call to expire. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Call reauth with popup second. + user1.reauthenticateWithPopup(provider).then(function(popupResult) { + // This will cancel the previous popup operation and eventually resolve. + assertObjectEquals(expectedPopupResult, popupResult); + asyncTestCase.signal(); + }); +} + + +function testUser_linkWithPopup_error() { + asyncTestCase.waitForSignals(3); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.LINK_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return new goog.Promise(function(resolve, reject) {}); + }); + mockControl.$replayAll(); + // Set the backend user info with no linked providers. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected error reported in expected Auth event. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // The expected Auth event with the error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_POPUP, + '1234', + null, + null, + expectedError); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + asyncTestCase.signal(); + return expectedPopup; + }); + // Popup may try to close due to error if still open. + stubs.replace( + fireauth.util, + 'closeWindow', + function(win) { + assertEquals(expectedPopup, win); + asyncTestCase.signal(); + }); + // A popup event ID should be generated. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('Auth error should not trigger finishPopupAndRedirectLink!'); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // linkWithPopup should throw the expected error. + user1.linkWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUser_reauthenticateWithPopup_error() { + asyncTestCase.waitForSignals(1); + var recordedHandler = null; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.processPopup( + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument, + ignoreArgument).$does(function( + actualPopupWin, + actualMode, + actualProvider, + actualOnInit, + actualOnError, + actualEventId, + actualAlreadyRedirected) { + assertEquals(expectedPopup, actualPopupWin); + assertEquals(fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, actualMode); + assertEquals(provider, actualProvider); + assertEquals(expectedEventId, actualEventId); + assertFalse(actualAlreadyRedirected); + actualOnInit(); + return goog.Promise.resolve(); + }); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + recordedHandler = handler; + }); + oAuthSignInHandlerInstance.shouldBeInitializedEarly().$returns(false); + oAuthSignInHandlerInstance.hasVolatileStorage().$returns(false); + oAuthSignInHandlerInstance.startPopupTimeout( + ignoreArgument, ignoreArgument, ignoreArgument) + .$does(function(popupWin, onError, delay) { + recordedHandler(expectedAuthEvent); + return new goog.Promise(function(resolve, reject) {}); + }); + mockControl.$replayAll(); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // The expected popup event ID. + var expectedEventId = '1234'; + // The expected error reported in expected Auth event. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // The expected Auth event with the error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_POPUP, + '1234', + null, + null, + expectedError); + var config = { + 'apiKey': 'apiKey1', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + fireauth.AuthEventManager.ENABLED = true; + // Replace random number generator. + stubs.replace( + fireauth.util, + 'generateRandomString', + function() { + return '87654321'; + }); + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + goog.testing.recordFunction(function(url, name, width, height) { + assertNull(url); + assertEquals('87654321', name); + assertEquals(fireauth.idp.Settings.GOOGLE.popupWidth, width); + assertEquals(fireauth.idp.Settings.GOOGLE.popupHeight, height); + return expectedPopup; + })); + // Popup may try to close due to error if still open. + stubs.replace( + fireauth.util, + 'closeWindow', + goog.testing.recordFunction(function(win) { + assertEquals(expectedPopup, win); + })); + // A popup event ID should be generated. + stubs.replace( + fireauth.util, + 'generateEventId', + function() { + return expectedEventId; + }); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + fail('Auth error should not trigger finishPopupAndRedirectReauth!'); + }); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Enable popup and redirect. + user1.enablePopupRedirect(); + var provider = new fireauth.GoogleAuthProvider(); + // reauthenticateWithPopup should throw the expected error. + user1.reauthenticateWithPopup(provider).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + + +function testUser_linkWithPopup_alreadyLinked() { + // User on server has the federated provider linked already. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponseGoogleProviderData); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + return goog.Promise.resolve(getAccountInfoResponse); + }); + // Don't actually open the popup. + stubs.replace(fireauth.util, 'popup', function(url, name) { + return {'close': function() {}}; + }); + + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.linkWithPopup(new fireauth.GoogleAuthProvider()) + .thenCatch(function(actualError) { + fireauth.common.testHelper.assertErrorEquals(new fireauth.AuthError( + fireauth.authenum.Error.PROVIDER_ALREADY_LINKED), actualError); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testUser_returnFromLinkWithRedirect_success() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // The expected credential. + var expectedCred = fireauth.GoogleAuthProvider.credential( + {'accessToken': 'ACCESS_TOKEN'}); + // The expected link via redirect Auth event for the current user. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // Return the expected result. + var result = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }); + return goog.Promise.resolve(result); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID matching the dispatched one. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should return expected result with current user and + // the expected credential. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.LINK, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromReauthenticateWithRedirect_success() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // The expected credential. + var expectedCred = fireauth.GoogleAuthProvider.credential( + {'accessToken': 'ACCESS_TOKEN'}); + // The expected reauth via redirect Auth event for the current user. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // Return the expected result. + var result = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }); + return goog.Promise.resolve(result); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID matching the dispatched one. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should return expected result with current user and + // the expected credential. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.REAUTHENTICATE, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromLinkWithRedirect_success_multipleUsers() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Both users should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + assertTrue(manager.isSubscribed(user1)); + assertTrue(manager.isSubscribed(user2)); + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()).$times(2); + mockControl.$replayAll(); + // The expected credential. + var expectedCred = fireauth.GoogleAuthProvider.credential( + {'accessToken': 'ACCESS_TOKEN'}); + // The expected link via redirect Auth event for the first user only. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Finish popup and redirect link should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + // User1 will handle this only. + assertEquals(user1, this); + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // Return the expected result. + var result = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.LINK + }); + asyncTestCase.signal(); + return goog.Promise.resolve(result); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(3); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID matching the dispatched one. + user1.setRedirectEventId('1234'); + var user2 = new fireauth.AuthUser(config, tokenResponse, accountInfo2); + user2.setRedirectEventId('5678'); + // Enable popup and redirect on both. + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + user1.enablePopupRedirect(); + user2.enablePopupRedirect(); + // Get redirect result should return expected result with first user and + // the expected credential. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.LINK, response); + asyncTestCase.signal(); + }); + // This should also resolve to the same result. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.LINK, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromReauthenticateWithRedirect_success_multipleUsers() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Both users should be subscribed. + var manager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + assertTrue(manager.isSubscribed(user1)); + assertTrue(manager.isSubscribed(user2)); + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()).$times(2); + mockControl.$replayAll(); + // The expected credential. + var expectedCred = fireauth.GoogleAuthProvider.credential( + {'accessToken': 'ACCESS_TOKEN'}); + // The expected reauth via redirect Auth event for the first user only. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + '1234', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Finish popup and redirect reauth should be called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + goog.testing.recordFunction(function(requestUri, sessionId) { + // User1 will handle this only. + assertEquals(user1, this); + assertEquals('http://www.example.com/#response', requestUri); + assertEquals('SESSION_ID', sessionId); + // Return the expected result. + var result = fireauth.object.makeReadonlyCopy({ + 'user': this, + 'credential': expectedCred, + 'additionalUserInfo': expectedAdditionalUserInfo, + 'operationType': fireauth.constants.OperationType.REAUTHENTICATE + }); + return goog.Promise.resolve(result); + })); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID matching the dispatched one. + user1.setRedirectEventId('1234'); + var user2 = new fireauth.AuthUser(config, tokenResponse, accountInfo2); + user2.setRedirectEventId('5678'); + // Enable popup and redirect on both. + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + user1.enablePopupRedirect(); + user2.enablePopupRedirect(); + // Get redirect result should return expected result with first user and + // the expected credential. + authEventManager.getRedirectResult().then(function(response) { + assertEquals( + 1, + fireauth.AuthUser.prototype.finishPopupAndRedirectReauth + .getCallCount()); + fireauth.common.testHelper.assertUserCredentialResponse( + user1, expectedCred, expectedAdditionalUserInfo, + fireauth.constants.OperationType.REAUTHENTICATE, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromLinkWithRedirect_invalidUser() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // Successful link via redirect for a different user. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + 'OTHER_EVENT_ID', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectLink should not call due to UID mismatch!'); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID that does not match the event's. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should resolve to null as the user does not match the + // redirect Auth event's. Keep in mind if the Auth event's user exists in + // the current window, this should return that user in the response. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, null, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromReauthenticateWithRedirect_invalidUser() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // Successful reauth via redirect for a different user. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + 'OTHER_EVENT_ID', + 'http://www.example.com/#response', + 'SESSION_ID'); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectReauth should not call due to UID ' + + 'mismatch!'); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + var authEventManager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID that does not match the event's. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should resolve to null as the user does not match the + // redirect Auth event's. Keep in mind if the Auth event's user exists in + // the current window, this should return that user in the response. + authEventManager.getRedirectResult().then(function(response) { + fireauth.common.testHelper.assertUserCredentialResponse( + null, null, null, null, response); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromLinkWithRedirect_error() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // The link with redirect expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // The expected Auth event with the redirect error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.LINK_VIA_REDIRECT, + '1234', + null, + null, + expectedError); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectLink', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectLink should not call due to event error!'); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(2); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID that matches Auth event's. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should throw the expected error. + var manager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + manager.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_returnFromReauthenticateWithRedirect_error() { + fireauth.AuthEventManager.ENABLED = true; + // Mock OAuth sign in handler. + var oAuthSignInHandlerInstance = + mockControl.createStrictMock(fireauth.OAuthSignInHandler); + mockControl.createConstructorMock(fireauth, 'OAuthSignInHandler'); + var instantiateOAuthSignInHandler = mockControl.createMethodMock( + fireauth.AuthEventManager, 'instantiateOAuthSignInHandler'); + instantiateOAuthSignInHandler( + ignoreArgument, ignoreArgument, ignoreArgument, ignoreArgument, + ignoreArgument).$returns(oAuthSignInHandlerInstance); + oAuthSignInHandlerInstance.addAuthEventListener(ignoreArgument) + .$does(function(handler) { + // Dispatch expected Auth event immediately to simulate return from + // redirect operation. + handler(expectedAuthEvent); + asyncTestCase.signal(); + }); + oAuthSignInHandlerInstance.initializeAndWait() + .$returns(goog.Promise.resolve()); + mockControl.$replayAll(); + // The reauth with redirect expected error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // The expected Auth event with the redirect error. + var expectedAuthEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.REAUTH_VIA_REDIRECT, + '1234', + null, + null, + expectedError); + // Since the expected Auth event already has an error, this should not be + // called. + stubs.replace( + fireauth.AuthUser.prototype, + 'finishPopupAndRedirectReauth', + function(requestUri, sessionId) { + fail('finishPopupAndRedirectReauth should not call due to event ' + + 'error!'); + }); + var config = { + 'apiKey': 'API_KEY', + 'authDomain': 'subdomain.firebaseapp.com', + 'appName': 'appId1' + }; + asyncTestCase.waitForSignals(1); + var user1 = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Assume pending redirect event ID that matches Auth event's. + user1.setRedirectEventId('1234'); + var pendingRedirectManager = new fireauth.storage.PendingRedirectManager( + config['apiKey'] + ':' + config['appName']); + pendingRedirectManager.setPendingStatus().then(function() { + // Enable popup and redirect. + user1.enablePopupRedirect(); + // Get redirect result should throw the expected error. + var manager = fireauth.AuthEventManager.getManager( + config['authDomain'], config['apiKey'], config['appName']); + manager.getRedirectResult().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +/** + * Helper function to simulate session invalidation for a specific user public + * operation. + * @param {string} fn The user specific function name to test. + * @param {!Array<*>} args The array of arguments to pass to apply on the user + * function. + * @param {!fireauth.AuthError} invalidationError The specific invalidation + * error to simulate. + */ +function simulateSessionInvalidation(fn, args, invalidationError) { + asyncTestCase.waitForSignals(1); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + var userDeletedCounter = 0; + accountInfo['uid'] = '679'; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // Track token change. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // Track user deletion. + goog.events.listen( + user, fireauth.UserEventType.USER_DELETED, function(event) { + userDeletedCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + // Simulate invalidation error via getIdToken. + // This error could be triggered via other RPC when old token is still cached, + // but should behave the same as calls are chained to getIdToken and in this + // case, it is easier to test with getIdToken error as all APIs call that + // before calling other backend APIs. + stubs.replace( + fireauth.StsTokenManager.prototype, + 'getToken', + function(opt_forceRefresh) { + return goog.Promise.reject(invalidationError); + }); + // Apply the user function with the provided arguments. + user[fn].apply(user, args).thenCatch(function(error) { + // Only user invalidation event should be triggered. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(0, userDeletedCounter); + assertEquals(1, userInvalidateCounter); + // Expected error should be thrown. + fireauth.common.testHelper.assertErrorEquals(invalidationError, error); + // Retry. The cached error should be thrown. + user[fn].apply(user, args).thenCatch(function(error) { + // Should be cached and no new events triggered. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(0, userDeletedCounter); + assertEquals(1, userInvalidateCounter); + // Expected error should be thrown. + fireauth.common.testHelper.assertErrorEquals(invalidationError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_sessionInvalidation_reload_tokenExpired() { + // Test user invalidation with token expired error on user.reload. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation('reload', [], invalidationError); +} + + +function testUser_sessionInvalidation_reload_userDisabled() { + // Test user invalidation with user disabled error on user.reload. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation('reload', [], invalidationError); +} + + +function testUser_sessionInvalidation_getIdToken_tokenExpired() { + // Test user invalidation with token expired error on user.getIdToken. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation('getIdToken', [], invalidationError); +} + + +function testUser_sessionInvalidation_getIdToken_userDisabled() { + // Test user invalidation with user disabled error on user.getIdToken. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation('getIdToken', [], invalidationError); +} + + +function testUser_sessionInvalidation_link_tokenExpired() { + // Test user invalidation with token expired error on user.link. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'link', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessionInvalidation_link_tokenExpired() { + // Test user invalidation with token expired error on user.linkWithCredential. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'linkWithCredential', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessionInvalidation_link_userDisabled() { + // Test user invalidation with user disabled error on user.link. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'link', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessionInvalidation_link_userDisabled() { + // Test user invalidation with user disabled error on user.linkWithCredential. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'linkWithCredential', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessInvalid_linkAndRetrieveDataWithCredential_tokenExpired() { + // Test user invalidation with token expired error on + // user.linkAndRetrieveDataWithCredential. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'linkAndRetrieveDataWithCredential', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessInvalid_linkAndRetrieveDataWithCredential_userDisabled() { + // Test user invalidation with user disabled error on + // user.linkAndRetrieveDataWithCredential. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'linkAndRetrieveDataWithCredential', + [ + fireauth.GoogleAuthProvider.credential(null, 'googleAccessToken') + ], + invalidationError); +} + + +function testUser_sessionInvalidation_updateEmail_tokenExpired() { + // Test user invalidation with token expired error on user.updateEmail. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'updateEmail', ['user@example.com'], invalidationError); +} + + +function testUser_sessionInvalidation_updateEmail_userDisabled() { + // Test user invalidation with user disabled error on user.updateEmail. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'updateEmail', ['user@example.com'], invalidationError); +} + + +function testUser_sessionInvalidation_updatePassword_tokenExpired() { + // Test user invalidation with token expired error on user.updatePassword. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'updatePassword', ['password'], invalidationError); +} + + +function testUser_sessionInvalidation_updatePassword_userDisabled() { + // Test user invalidation with user disabled error on user.updatePassword. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'updatePassword', ['password'], invalidationError); +} + + +function testUser_sessionInvalidation_updateProfile_tokenExpired() { + // Test user invalidation with token expired error on user.updateProfile. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'updateProfile', + [ + { + displayName: 'John Doe' + } + ], + invalidationError); +} + + +function testUser_sessionInvalidation_updateProfile_userDisabled() { + // Test user invalidation with user disabled error on user.updateProfile. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'updateProfile', + [ + { + displayName: 'John Doe' + } + ], + invalidationError); +} + + +function testUser_sessionInvalidation_unlink_tokenExpired() { + // Test user invalidation with token expired error on user.unlink. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation('unlink', ['password'], invalidationError); +} + + +function testUser_sessionInvalidation_unlink_userDisabled() { + // Test user invalidation with user disabled error on user.unlink. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation('unlink', ['password'], invalidationError); +} + + +function testUser_sessionInvalidation_delete_tokenExpired() { + // Test user invalidation with token expired error on user.delete. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation('delete', [], invalidationError); +} + + +function testUser_sessionInvalidation_delete_userDisabled() { + // Test user invalidation with user disabled error on user.delete. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation('delete', [], invalidationError); +} + + +function testUser_sessionInvalidation_linkWithPopup_tokenExpired() { + // Test user invalidation with token expired error on user.linkWithPopup. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + var popupCalls = 0; + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + popupCalls++; + if (popupCalls > 1) { + fail('The second call to linkWithPopup should fail without openin' + + 'g the popup!'); + } + return expectedPopup; + }); + simulateSessionInvalidation( + 'linkWithPopup', [new fireauth.GoogleAuthProvider()], invalidationError); +} + + +function testUser_sessionInvalidation_linkWithPopup_userDisabled() { + // Test user invalidation with user disabled error on user.linkWithPopup. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + // The expected popup window object. + var expectedPopup = { + 'close': function() {} + }; + // Simulate popup. + stubs.replace( + fireauth.util, + 'popup', + function(url, name, width, height) { + return expectedPopup; + }); + simulateSessionInvalidation( + 'linkWithPopup', [new fireauth.GoogleAuthProvider()], invalidationError); +} + + +function testUser_sessionInvalidation_linkWithRedirect_tokenExpired() { + // Test user invalidation with token expired error on user.linkWithRedirect. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'linkWithRedirect', + [new fireauth.GoogleAuthProvider()], + invalidationError); +} + + +function testUser_sessionInvalidation_linkWithRedirect_userDisabled() { + // Test user invalidation with user disabled error on user.linkWithRedirect. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'linkWithRedirect', + [new fireauth.GoogleAuthProvider()], + invalidationError); +} + + +function testUser_sessionInvalidation_sendEmailVerification_tokenExpired() { + // Test user invalidation with token expired error on + // user.sendEmailVerification. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'sendEmailVerification', [], invalidationError); +} + + +function testUser_sessionInvalidation_sendEmailVerification_userDisabled() { + // Test user invalidation with user disabled error on + // user.sendEmailVerification. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'sendEmailVerification', [], invalidationError); +} + + +function testUser_sessionInvalidation_linkWithPhoneNumber_tokenExpired() { + // Test user invalidation with token expired error on + // user.linkWithPhoneNumber. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED); + simulateSessionInvalidation( + 'linkWithPhoneNumber', + [expectedPhoneNumber, appVerifier], + invalidationError); +} + + +function testUser_sessionInvalidation_linkWithPhoneNumber_userDisabled() { + // Test user invalidation with user disabled error on + // user.linkWithPhoneNumber. + var invalidationError = new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED); + simulateSessionInvalidation( + 'linkWithPhoneNumber', + [expectedPhoneNumber, appVerifier], + invalidationError); +} + + +function testUser_sessionInvalidation_otherRpc() { + // Confirm if session invalidation thrown in other RPC, it is caught and + // cached. + // Expected session invalidation error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + // Event trackers. + var stateChangeCounter = 0; + var authChangeCounter = 0; + var userInvalidateCounter = 0; + var userDeletedCounter = 0; + // Track RPC call. + rpcTriggered = 0; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + // Track token change. + goog.events.listen( + user, fireauth.UserEventType.TOKEN_CHANGED, function(event) { + authChangeCounter++; + }); + // Track user invalidation. + goog.events.listen( + user, fireauth.UserEventType.USER_INVALIDATED, function(event) { + userInvalidateCounter++; + }); + // Track user deletion. + goog.events.listen( + user, fireauth.UserEventType.USER_DELETED, function(event) { + userDeletedCounter++; + }); + // State change should be triggered. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + // Simulate error in this RPC. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + rpcTriggered++; + return goog.Promise.reject(expectedError); + }); + asyncTestCase.waitForSignals(1); + // This should throw expected error. No event should trigger. + user.reload().thenCatch(function(error) { + // RPC called. + assertEquals(1, rpcTriggered); + // Only user invalidation event should be triggered. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(0, userDeletedCounter); + assertEquals(1, userInvalidateCounter); + // Expected error should be thrown. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // This will succeed with only state change triggering. + user.reload().thenCatch(function() { + // Should be cached and no new events triggered. + assertEquals(0, stateChangeCounter); + assertEquals(0, authChangeCounter); + assertEquals(0, userDeletedCounter); + assertEquals(1, userInvalidateCounter); + // RPC should not be called again. + assertEquals(1, rpcTriggered); + // Expected error should be thrown. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_reload_nonSessionInvalidationErrors() { + // Confirm non session invalidation errors are ignored and no error caching + // happens in that case. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Whether to trigger the error. + var triggerError = true; + // Track state changed + var stateChangeCounter = 0; + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + // Listen to state changes. + user.addStateChangeListener(function(userTemp) { + stateChangeCounter++; + return goog.Promise.resolve(); + }); + // No other events should be triggered. + assertNoTokenEvents(user); + assertNoUserInvalidatedEvents(user); + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAccountInfoByIdToken', + function(idToken) { + // Throw error initially. + if (triggerError) { + return goog.Promise.reject(expectedError); + } + // Resolve on next call. + return goog.Promise.resolve(getAccountInfoResponse); + }); + asyncTestCase.waitForSignals(1); + // This should throw expected error. No event should trigger. + user.reload().thenCatch(function(error) { + assertEquals(0, stateChangeCounter); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Do not trigger error on next call. + triggerError = false; + // This will succeed with only state change triggering. + user.reload().then(function() { + assertEquals(1, stateChangeCounter); + asyncTestCase.signal(); + }); + }); +} + + +function testUser_proactiveRefresh_startAndStop() { + // Test proactive token refresh called with expected configurations. + // Record getIdToken calls. + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + goog.testing.recordFunction()); + var proactiveRefreshInstance = mockControl.createStrictMock( + fireauth.ProactiveRefresh); + var proactiveRefreshConstructor = mockControl.createConstructorMock( + fireauth, 'ProactiveRefresh'); + // Listen to proactive refresh initialization and confirm arguments passed. + proactiveRefreshConstructor( + ignoreArgument, + ignoreArgument, + ignoreArgument, + fireauth.TokenRefreshTime.RETRIAL_MIN_WAIT, + fireauth.TokenRefreshTime.RETRIAL_MAX_WAIT, + false).$does( + function(operation, retryPolicy, getWaitDuration, lowerBound, + upperBound, runsInBackground) { + // Confirm operation forces refresh of token. + assertEquals( + 0, fireauth.AuthUser.prototype.getIdToken.getCallCount()); + // Run operation. + operation(); + // getIdToken(true) should be called underneath. + assertEquals( + 1, fireauth.AuthUser.prototype.getIdToken.getCallCount()); + assertTrue( + fireauth.AuthUser.prototype.getIdToken.getLastCall() + .getArgument(0)); + // Confirm retry policy only returns true for network errors. + assertTrue(retryPolicy(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED))); + // Do not retry for all other common errors. + assertFalse(retryPolicy(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR))); + assertFalse(retryPolicy(new fireauth.AuthError( + fireauth.authenum.Error.TOKEN_EXPIRED))); + assertFalse(retryPolicy(new fireauth.AuthError( + fireauth.authenum.Error.USER_DISABLED))); + // Confirm getWaitDuration returns expected value. + assertEquals( + 3600 * 1000 - fireauth.TokenRefreshTime.OFFSET_DURATION, + getWaitDuration()); + return proactiveRefreshInstance; + }).$once(); + // Confirm proactive refresh start and stop called. + proactiveRefreshInstance.isRunning().$returns(false); + proactiveRefreshInstance.start().$once(); + proactiveRefreshInstance.stop().$once(); + mockControl.$replayAll(); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.startProactiveRefresh(); + user.stopProactiveRefresh(); +} + + +function testUser_proactiveRefresh_externalTokenRefresh() { + // Test proactive token refresh is reset on each external token refresh call. + var proactiveRefreshInstance = mockControl.createStrictMock( + fireauth.ProactiveRefresh); + var proactiveRefreshConstructor = mockControl.createConstructorMock( + fireauth, 'ProactiveRefresh'); + proactiveRefreshConstructor( + ignoreArgument, + ignoreArgument, + ignoreArgument, + fireauth.TokenRefreshTime.RETRIAL_MIN_WAIT, + fireauth.TokenRefreshTime.RETRIAL_MAX_WAIT, + false).$returns(proactiveRefreshInstance); + proactiveRefreshInstance.isRunning().$returns(false); + proactiveRefreshInstance.start(); + // Token change event. + proactiveRefreshInstance.isRunning().$returns(true); + proactiveRefreshInstance.stop(); + proactiveRefreshInstance.start(); + // Second token change event. + proactiveRefreshInstance.isRunning().$returns(true); + proactiveRefreshInstance.stop(); + proactiveRefreshInstance.start(); + mockControl.$replayAll(); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.startProactiveRefresh(); + // Force an external token refresh and confirm the proactive refresh is reset. + user.dispatchEvent( + fireauth.UserEventType.TOKEN_CHANGED); + // Force another token change event. + user.dispatchEvent( + fireauth.UserEventType.TOKEN_CHANGED); +} + + +function testUser_proactiveRefresh_destroy() { + // Test proactive token refresh stopped on user destruction. + var proactiveRefreshInstance = mockControl.createStrictMock( + fireauth.ProactiveRefresh); + var proactiveRefreshConstructor = mockControl.createConstructorMock( + fireauth, 'ProactiveRefresh'); + proactiveRefreshConstructor( + ignoreArgument, + ignoreArgument, + ignoreArgument, + fireauth.TokenRefreshTime.RETRIAL_MIN_WAIT, + fireauth.TokenRefreshTime.RETRIAL_MAX_WAIT, + false).$returns(proactiveRefreshInstance); + proactiveRefreshInstance.isRunning().$returns(false); + proactiveRefreshInstance.start().$once(); + // Should be called on user.destroy(). + proactiveRefreshInstance.stop().$once(); + mockControl.$replayAll(); + + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo2); + user.startProactiveRefresh(); + // This should stop proactive refresh. + user.destroy(); +} + + +function testLinkWithPhoneNumber_success() { + app = firebase.initializeApp(config1, config1['appName']); + auth = new fireauth.Auth(app); + // Stub Auth on the App instance above. + stubs.set(app, 'auth', function() { + return auth; + }); + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedCode = '123456'; + var expectedCredential = fireauth.PhoneAuthProvider.credential( + expectedVerificationId, expectedCode); + var getAccountInfoByIdToken = mockControl.createMethodMock( + fireauth.RpcHandler.prototype, 'getAccountInfoByIdToken'); + // Expected promise to be returned by linkAndRetrieveDataWithCredential. + var expectedPromise = new goog.Promise(function(resolve, reject) {}); + // Phone Auth provider instance. + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + // Phone Auth provider constructor mock. + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + getAccountInfoByIdToken(tokenResponse['idToken']).$returns( + goog.Promise.resolve(getAccountInfoResponse)).$once(); + // Provider instance should be initialized with the expected Auth instance + // and return the expected phone Auth provider instance. + phoneAuthProviderConstructor(auth) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would resolve with the expected verification + // ID. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.resolve(expectedVerificationId)).$once(); + // Code confirmation should call linkAndRetrieveDataWithCredential with the + // expected credential. + stubs.replace( + fireauth.AuthUser.prototype, + 'linkAndRetrieveDataWithCredential', + goog.testing.recordFunction(function(cred) { + // Confirm expected credential passed. + assertObjectEquals( + expectedCredential.toPlainObject(), + cred.toPlainObject()); + // Return expected promise. + return expectedPromise; + })); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.linkWithPhoneNumber(expectedPhoneNumber, appVerifier) + .then(function(confirmationResult) { + // Confirmation result returned should contain expected verification ID. + assertEquals( + expectedVerificationId, confirmationResult['verificationId']); + // Code confirmation should return the same response as the underlying + // linkAndRetrieveDataWithCredential. + assertEquals(expectedPromise, confirmationResult.confirm(expectedCode)); + // Confirm linkAndRetrieveDataWithCredential called once. + assertEquals( + 1, + fireauth.AuthUser.prototype.linkAndRetrieveDataWithCredential + .getCallCount()); + // Confirm linkAndRetrieveDataWithCredential is bound to current user. + assertEquals( + user, + fireauth.AuthUser.prototype.linkAndRetrieveDataWithCredential + .getLastCall().getThis()); + asyncTestCase.signal(); + }); +} + + +function testLinkWithPhoneNumber_error_noAuthInstance() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'No firebase.auth.Auth instance is available for the Firebase App ' + + '\'' + config1['appName'] + '\'!'); + var getAccountInfoByIdToken = mockControl.createMethodMock( + fireauth.RpcHandler.prototype, 'getAccountInfoByIdToken'); + getAccountInfoByIdToken(tokenResponse['idToken']).$returns( + goog.Promise.resolve(getAccountInfoResponse)).$once(); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // This should fail since no corresponding Auth instance is found. + user.linkWithPhoneNumber(expectedPhoneNumber, appVerifier) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testLinkWithPhoneNumber_error_alreadyLinked() { + app = firebase.initializeApp(config1, config1['appName']); + auth = new fireauth.Auth(app); + // Stub Auth on the App instance above. + stubs.set(app, 'auth', function() { + return auth; + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.PROVIDER_ALREADY_LINKED); + // Add a phone Auth provider to the current user to trigger the provider + // already linked error. + getAccountInfoResponse['users'][0]['providerUserInfo'] + .push(getAccountInfoResponsePhoneAuthProviderData); + var getAccountInfoByIdToken = mockControl.createMethodMock( + fireauth.RpcHandler.prototype, 'getAccountInfoByIdToken'); + getAccountInfoByIdToken(tokenResponse['idToken']).$returns( + goog.Promise.resolve(getAccountInfoResponse)).$once(); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // No provider data linked yet. + assertEquals(0, user.providerData.length); + // This should fail since there is already a phone number provider on the + // current user. + user.linkWithPhoneNumber(expectedPhoneNumber, appVerifier) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateWithPhoneNumber_success() { + app = firebase.initializeApp(config1, config1['appName']); + auth = new fireauth.Auth(app); + // Stub Auth on the App instance above. + stubs.set(app, 'auth', function() { + return auth; + }); + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedCode = '123456'; + var expectedCredential = fireauth.PhoneAuthProvider.credential( + expectedVerificationId, expectedCode); + // Expected promise to be returned by + // reauthenticateAndRetrieveDataWithCredential. + var expectedPromise = new goog.Promise(function(resolve, reject) {}); + // Phone Auth provider instance. + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + // Phone Auth provider constructor mock. + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + // Provider instance should be initialized with the expected Auth instance + // and return the expected phone Auth provider instance. + phoneAuthProviderConstructor(auth) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would resolve with the expected verification + // ID. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.resolve(expectedVerificationId)).$once(); + // Code confirmation should call reauthenticateAndRetrieveDataWithCredential + // with the expected credential. + stubs.replace( + fireauth.AuthUser.prototype, + 'reauthenticateAndRetrieveDataWithCredential', + goog.testing.recordFunction(function(cred) { + // Confirm expected credential passed. + assertObjectEquals( + expectedCredential.toPlainObject(), + cred.toPlainObject()); + // Return expected promise. + return expectedPromise; + })); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.reauthenticateWithPhoneNumber(expectedPhoneNumber, appVerifier) + .then(function(confirmationResult) { + // Confirmation result returned should contain expected verification ID. + assertEquals( + expectedVerificationId, confirmationResult['verificationId']); + // Code confirmation should return the same response as the underlying + // reauthenticateAndRetrieveDataWithCredential. + assertEquals(expectedPromise, confirmationResult.confirm(expectedCode)); + // Confirm reauthenticateAndRetrieveDataWithCredential called once. + assertEquals( + 1, + fireauth.AuthUser.prototype + .reauthenticateAndRetrieveDataWithCredential.getCallCount()); + // Confirm reauthenticateAndRetrieveDataWithCredential is bound to + // current user. + assertEquals( + user, + fireauth.AuthUser.prototype + .reauthenticateAndRetrieveDataWithCredential + .getLastCall() + .getThis()); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateWithPhoneNumber_success_skipInvalidation() { + // Test that reauthenticateWithPhoneNumber will be allowed to run after token + // expiration. + app = firebase.initializeApp(config1, config1['appName']); + auth = new fireauth.Auth(app); + // Stub Auth on the App instance above. + stubs.set(app, 'auth', function() { + return auth; + }); + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedCode = '123456'; + var expectedCredential = fireauth.PhoneAuthProvider.credential( + expectedVerificationId, expectedCode); + // Expected promise to be returned by + // reauthenticateAndRetrieveDataWithCredential. + var expectedPromise = new goog.Promise(function(resolve, reject) {}); + // Mock StsTokenManager.prototype.getToken. + var getToken = mockControl.createMethodMock( + fireauth.StsTokenManager.prototype, 'getToken'); + // Phone Auth provider instance. + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + // Phone Auth provider constructor mock. + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + // Initial call to getToken should return token expired. + getToken(true).$does(function(opt_forceRefresh) { + return goog.Promise.reject(expectedError); + }).$once(); + // Provider instance should be initialized with the expected Auth instance + // and return the expected phone Auth provider instance. + phoneAuthProviderConstructor(auth) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would resolve with the expected verification + // ID. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.resolve(expectedVerificationId)).$once(); + // Code confirmation should call reauthenticateAndRetrieveDataWithCredential + // with the expected credential. + stubs.replace( + fireauth.AuthUser.prototype, + 'reauthenticateAndRetrieveDataWithCredential', + goog.testing.recordFunction(function(cred) { + // Confirm expected credential passed. + assertObjectEquals( + expectedCredential.toPlainObject(), + cred.toPlainObject()); + // Return expected promise. + return expectedPromise; + })); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.getIdToken(true).thenCatch(function(error) { + // Expected token expired error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // This should be allowed to run even though the token is expired. + // All other non reauth operation will fail immediately and throw the cached + // error. + return user.reauthenticateWithPhoneNumber(expectedPhoneNumber, appVerifier); + }).then(function(confirmationResult) { + // Confirmation result returned should contain expected verification ID. + assertEquals( + expectedVerificationId, confirmationResult['verificationId']); + // Code confirmation should return the same response as the underlying + // reauthenticateAndRetrieveDataWithCredential. + assertEquals(expectedPromise, confirmationResult.confirm(expectedCode)); + // Confirm reauthenticateAndRetrieveDataWithCredential called once. + assertEquals( + 1, + fireauth.AuthUser.prototype.reauthenticateAndRetrieveDataWithCredential + .getCallCount()); + // Confirm reauthenticateAndRetrieveDataWithCredential is bound to current + // user. + assertEquals( + user, + fireauth.AuthUser.prototype.reauthenticateAndRetrieveDataWithCredential + .getLastCall().getThis()); + asyncTestCase.signal(); + }); +} + + +function testReauthenticateWithPhoneNumber_error_noAuthInstance() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'No firebase.auth.Auth instance is available for the Firebase App ' + + '\'' + config1['appName'] + '\'!'); + + asyncTestCase.waitForSignals(1); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + // This should fail since no corresponding Auth instance is found. + user.reauthenticateWithPhoneNumber(expectedPhoneNumber, appVerifier) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testGetToken_error() { + // Test that getToken calls getIdToken underneath and funnels any underlying + // error thrown. + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_refreshToken) { + assertFalse(opt_refreshToken); + return goog.Promise.reject(expectedError); + }); + // Record deprecation warning calls. + stubs.replace( + fireauth.deprecation, + 'log', + goog.testing.recordFunction()); + asyncTestCase.waitForSignals(1); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.getToken(false).thenCatch(function(error) { + // Confirm expected error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Confirm warning shown. + /** @suppress {missingRequire} */ + assertEquals(1, fireauth.deprecation.log.getCallCount()); + /** @suppress {missingRequire} */ + assertEquals( + fireauth.deprecation.Deprecations.USER_GET_TOKEN, + fireauth.deprecation.log.getLastCall().getArgument(0)); +} + + +function testGetToken_success() { + // Test that getToken calls getIdToken underneath and returns the same result + // on success. + // Stub getIdToken and confirm same response is used for getToken. + stubs.replace( + fireauth.AuthUser.prototype, + 'getIdToken', + function(opt_refreshToken) { + assertTrue(opt_refreshToken); + return goog.Promise.resolve(expectedIdToken); + }); + // Record deprecation warning calls. + stubs.replace( + fireauth.deprecation, + 'log', + goog.testing.recordFunction()); + asyncTestCase.waitForSignals(1); + var expectedIdToken = 'NEW_ID_TOKEN'; + var user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + user.getToken(true).then(function(idToken) { + // Confirm expected ID token. + assertEquals(expectedIdToken, idToken); + asyncTestCase.signal(); + }); + // Confirm warning shown. + /** @suppress {missingRequire} */ + assertEquals(1, fireauth.deprecation.log.getCallCount()); + /** @suppress {missingRequire} */ + assertEquals( + fireauth.deprecation.Deprecations.USER_GET_TOKEN, + fireauth.deprecation.log.getLastCall().getArgument(0)); +} + + +function testUser_customLocaleChanges() { + // Listen to all custom locale header calls on RpcHandler. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateCustomLocaleHeader', + goog.testing.recordFunction()); + // Dummy event dispatchers. + var dispatcher1 = createEventDispatcher(); + var dispatcher2 = createEventDispatcher(); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + + // Set language to German. + user.setLanguageCode('de'); + // User language should be updated. + assertEquals('de', user.getLanguageCode()); + // Rpc handler language should be updated. + assertEquals( + 1, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'de', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + + // Set language to null. + user.setLanguageCode(null); + assertEquals( + 2, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertNull( + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + assertNull(user.getLanguageCode()); + + // Set dispatcher1 as language code dispatcher. + user.setLanguageCodeChangeDispatcher(dispatcher1); + dispatcher1.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('fr')); + dispatcher2.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('ru')); + // Only first dispatcher should be detected. + assertEquals( + 3, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'fr', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + assertEquals('fr', user.getLanguageCode()); + + // Set dispatcher2 as language code dispatcher. + user.setLanguageCodeChangeDispatcher(dispatcher2); + dispatcher1.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('fr')); + dispatcher2.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('ru')); + // Only second dispatcher should be detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + assertEquals( + 'ru', + fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getLastCall() + .getArgument(0)); + assertEquals('ru', user.getLanguageCode()); + + // Remove all dispatchers. + user.setLanguageCodeChangeDispatcher(null); + dispatcher1.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('fr')); + dispatcher2.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('ru')); + // No additional events detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); + // Last set language remains. + assertEquals('ru', user.getLanguageCode()); + + // Set dispatcher2 as language code change dispatcher and destroy the user. + user.setLanguageCodeChangeDispatcher(dispatcher2); + user.destroy(); + dispatcher2.dispatchEvent(new fireauth.Auth.LanguageCodeChangeEvent('ar')); + // No additional events detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateCustomLocaleHeader.getCallCount()); +} + + +function testUser_frameworkLoggingChanges() { + // Helper function to get the client version for the test. + var getVersion = function(frameworks) { + return fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + frameworks); + }; + // Pipe through all framework IDs. + stubs.replace( + fireauth.util, + 'getFrameworkIds', + function(providedFrameworks) { + return providedFrameworks; + }); + // Listen to all client version header update calls on RpcHandler. + stubs.replace( + fireauth.RpcHandler.prototype, + 'updateClientVersion', + goog.testing.recordFunction()); + // Dummy event dispatchers. + var dispatcher1 = createEventDispatcher(); + var dispatcher2 = createEventDispatcher(); + user = new fireauth.AuthUser(config1, tokenResponse, accountInfo); + + // Set framework to version1. + user.setFramework(['v1']); + // Framework version should be updated. + assertArrayEquals(['v1'], user.getFramework()); + // Rpc handler language should be updated. + assertEquals( + 1, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion(['v1']), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); + + // Set framework to empty array. + user.setFramework([]); + assertEquals( + 2, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion([]), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); + assertArrayEquals([], user.getFramework()); + + // Set dispatcher1 as framework change dispatcher. + user.setFrameworkChangeDispatcher(dispatcher1); + dispatcher1.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v1', 'v2'])); + dispatcher2.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v3', 'v4'])); + // Only first dispatcher should be detected. + assertEquals( + 3, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion(['v1', 'v2']), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); + assertArrayEquals(['v1', 'v2'], user.getFramework()); + + // Set dispatcher2 as framework change dispatcher. + user.setFrameworkChangeDispatcher(dispatcher2); + dispatcher1.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v1', 'v2'])); + dispatcher2.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v3', 'v4'])); + // Only second dispatcher should be detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + assertEquals( + getVersion(['v3', 'v4']), + fireauth.RpcHandler.prototype.updateClientVersion.getLastCall() + .getArgument(0)); + assertArrayEquals(['v3', 'v4'], user.getFramework()); + + // Remove all dispatchers. + user.setFrameworkChangeDispatcher(null); + dispatcher1.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v1', 'v2'])); + dispatcher2.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v3', 'v4'])); + // No additional events detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); + // Last framework list remains. + assertArrayEquals(['v3', 'v4'], user.getFramework()); + + // Set dispatcher2 as framework change dispatcher and destroy the user. + user.setFrameworkChangeDispatcher(dispatcher2); + user.destroy(); + dispatcher2.dispatchEvent( + new fireauth.Auth.FrameworkChangeEvent(['v1', 'v2'])); + // No additional events detected. + assertEquals( + 4, fireauth.RpcHandler.prototype.updateClientVersion.getCallCount()); +} + + +function testUserMetadata() { + // Test initialization. + var userMetadata1 = new fireauth.UserMetadata(createdAt, lastLoginAt); + assertEquals( + fireauth.util.utcTimestampToDateString(lastLoginAt), + userMetadata1['lastSignInTime']); + assertEquals( + fireauth.util.utcTimestampToDateString(createdAt), + userMetadata1['creationTime']); + // Confirm read-only. + userMetadata1['lastSignInTime'] = 'bla'; + userMetadata1['creationTime'] = 'bla'; + assertEquals( + fireauth.util.utcTimestampToDateString(lastLoginAt), + userMetadata1['lastSignInTime']); + assertEquals( + fireauth.util.utcTimestampToDateString(createdAt), + userMetadata1['creationTime']); + + var userMetadata2 = new fireauth.UserMetadata(createdAt); + assertEquals( + fireauth.util.utcTimestampToDateString(createdAt), + userMetadata2['creationTime']); + assertNull(userMetadata2['lastSignInTime']); + + var userMetadata3 = new fireauth.UserMetadata(); + assertNull(userMetadata3['creationTime']); + assertNull(userMetadata3['lastSignInTime']); + + // Test cloning. + assertObjectEquals(userMetadata1, userMetadata1.clone()); + assertObjectEquals(userMetadata2, userMetadata2.clone()); + assertObjectEquals(userMetadata3, userMetadata3.clone()); + + // Test toPlainObject. + assertObjectEquals( + { + 'lastLoginAt': lastLoginAt, + 'createdAt': createdAt + }, + userMetadata1.toPlainObject()); + assertObjectEquals( + { + 'lastLoginAt': null, + 'createdAt': createdAt + }, + userMetadata2.toPlainObject()); + assertObjectEquals( + { + 'lastLoginAt': null, + 'createdAt': null + }, + userMetadata3.toPlainObject()); +} diff --git a/packages/auth/test/cacherequest_test.js b/packages/auth/test/cacherequest_test.js new file mode 100644 index 00000000000..6f714a298c4 --- /dev/null +++ b/packages/auth/test/cacherequest_test.js @@ -0,0 +1,212 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for cacherequest.js. + */ + +goog.provide('fireauth.CacheRequestTest'); + +goog.require('fireauth.CacheRequest'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.TestCase'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.CacheRequestTest'); + + +var cacheRequest; +var MyClass; +var instance; +var clock; + +function setUp() { + // Test class. + MyClass = function() { + this.counter = 0; + this.err = 0; + }; + // Method that always resolves with an incremented counter. + MyClass.prototype.func = function(par1, par2) { + assertEquals(1, par1); + assertEquals(2, par2); + this.counter++; + return goog.Promise.resolve(this.counter); + }; + // Method that always rejects with an incremented error message. + MyClass.prototype.func2 = function(par1, par2) { + assertEquals(1, par1); + assertEquals(2, par2); + this.err++; + return goog.Promise.reject(new Error(this.err)); + }; + instance = new MyClass(); + cacheRequest = new fireauth.CacheRequest(); +} + + +function tearDown() { + cacheRequest = null; +} + + +/** + * Test cache request when the cached request resolves successfully. + * @return {!goog.Promise} The result of the test. + */ +function cacheRequestWithoutErrors() { + clock = new goog.testing.MockClock(true); + cacheRequest.cache(instance.func, instance, [1, 2], 60 * 1000); + return cacheRequest.run().then(function(result) { + assertEquals(result, 1); + // This response should be returned from cache. + return cacheRequest.run(); + }).then(function(result) { + assertEquals(result, 1); + clock.tick(60 * 1000); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).then(function(result) { + assertEquals(result, 2); + clock.tick(30 * 1000); + // This response should be returned from cache. + return cacheRequest.run(); + }).then(function(result) { + assertEquals(result, 2); + clock.tick(30 * 1000); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).then(function(result) { + assertEquals(result, 3); + cacheRequest.purge(); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).then(function(result) { + assertEquals(result, 4); + goog.dispose(clock); + }); +} + + +/** + * Test cache request when the cached request rejects with an error and errors + * are not to be cached. + * @return {!goog.Promise} The result of the test. + */ +function cacheRequestWithErrorsNotCached() { + clock = new goog.testing.MockClock(true); + cacheRequest.cache(instance.func2, instance, [1, 2], 60 * 1000); + return cacheRequest.run().thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 1); + // This response should not be returned from cache. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 2); + cacheRequest.purge(); + // This response should not be returned from cache. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 3); + goog.dispose(clock); + }); +} + + +/** + * Test cache request when the cached request rejects with an error and errors + * are set to be cached. + * @return {!goog.Promise} The result of the test. + */ +function cacheRequestWithErrorsCached() { + clock = new goog.testing.MockClock(true); + cacheRequest.cache(instance.func2, instance, [1, 2], 60 * 1000, true); + return cacheRequest.run().thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 1); + // This response should be returned from cache. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 1); + clock.tick(60 * 1000); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 2); + clock.tick(30 * 1000); + // This response should be returned from cache. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 2); + clock.tick(30 * 1000); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 3); + cacheRequest.purge(); + // This should bust the cache and send a request. + return cacheRequest.run(); + }).thenCatch(function(error) { + assertEquals(parseInt(error.message, 10), 4); + goog.dispose(clock); + }); +} + + +/** + * Install the test to run and runs it. + * @param {string} id The test identifier. + * @param {function():!goog.Promise} func The test function to run. + * @return {!goog.Promise} The result of the test. + */ +function installAndRunTest(id, func) { + var testCase = new goog.testing.TestCase(); + testCase.addNewTest(id, func); + return testCase.runTestsReturningPromise().then(function(result) { + assertTrue(result.complete); + assertEquals(1, result.totalCount); + assertEquals(1, result.runCount); + assertEquals(1, result.successCount); + assertEquals(0, result.errors.length); + }); +} + + +function testNoAvailableConfiguration() { + try { + cacheRequest.run(); + fail('Missing cache configuration should throw an error!'); + } catch(e) { + assertEquals('No available configuration cached!', e.message); + } +} + + +function testCacheRequestWithoutErrors() { + return installAndRunTest( + 'cacheRequestWithoutErrors', cacheRequestWithoutErrors); +} + + +function testCacheRequestWithErrorsNotCached() { + return installAndRunTest( + 'cacheRequestWithErrorsNotCached', cacheRequestWithErrorsNotCached); +} + + +function testCacheRequestWithErrorsCached() { + return installAndRunTest( + 'cacheRequestWithErrorsCached', cacheRequestWithErrorsCached); +} diff --git a/packages/auth/test/confirmationresult_test.js b/packages/auth/test/confirmationresult_test.js new file mode 100644 index 00000000000..891de87065f --- /dev/null +++ b/packages/auth/test/confirmationresult_test.js @@ -0,0 +1,202 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for confirmationresult.js + */ + +goog.provide('fireauth.ConfirmationResultTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.ConfirmationResult'); +goog.require('fireauth.PhoneAuthCredential'); +goog.require('fireauth.PhoneAuthProvider'); +goog.require('fireauth.authenum.Error'); +/** @suppress {extraRequire} Needed for firebase.app().auth() */ +goog.require('fireauth.exports'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.TestCase'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.ConfirmationResultTest'); + + +var mockControl; +var app; +var auth; + + +function setUp() { + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +function tearDown() { + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } +} + + +/** + * Install the test to run and runs it. + * @param {string} id The test identifier. + * @param {function():!goog.Promise} func The test function to run. + * @return {!goog.Promise} The result of the test. + */ +function installAndRunTest(id, func) { + // Initialize App and Auth before running the test. + app = firebase.initializeApp({ + apiKey: 'API_KEY' + }, 'test'); + auth = app.auth(); + var testCase = new goog.testing.TestCase(); + testCase.addNewTest(id, func); + return testCase.runTestsReturningPromise().then(function(result) { + assertTrue(result.complete); + // Display error detected. + if (result.errors.length) { + fail(result.errors.join('\n')); + } + assertEquals(1, result.totalCount); + assertEquals(1, result.runCount); + assertEquals(1, result.successCount); + assertEquals(0, result.errors.length); + // Delete App and Auth instances before resolving. + auth.delete(); + return app.delete(); + }); +} + + +function testConfirmationResult() { + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedCode = '123456'; + var expectedPromise = new goog.Promise(function(resolve, reject) {}); + var credentialInstance = + mockControl.createStrictMock(fireauth.PhoneAuthCredential); + var credential = + mockControl.createMethodMock(fireauth.PhoneAuthProvider, 'credential'); + var credentialResolver = mockControl.createFunctionMock('credentialResolver'); + // Phone Auth credential will be initialized first. + credential(expectedVerificationId, expectedCode) + .$returns(credentialInstance).$once(); + // Credential resolver will be caled with the initialized credential instance. + // Return expected pending promise. + credentialResolver(credentialInstance).$returns(expectedPromise).$once(); + mockControl.$replayAll(); + + // Initialize a confirmation result with the expected parameters. + var confirmationResult = new fireauth.ConfirmationResult( + expectedVerificationId, credentialResolver); + // Check verificationId property. + assertEquals(expectedVerificationId, confirmationResult['verificationId']); + // Confirm read-only. + confirmationResult['verificationId'] = 'not readonly'; + assertEquals(expectedVerificationId, confirmationResult['verificationId']); + // Confirm expected credential resolver promise returned on confirmation. + assertEquals(expectedPromise, confirmationResult.confirm(expectedCode)); +} + + +function testConfirmationResult_initialize_success() { + return installAndRunTest('confirmationResult_initialize_success', function() { + var expectedVerificationId = 'VERIFICATION_ID'; + var expectedPhoneNumber = '+16505550101'; + var expectedRecaptchaToken = 'RECAPTCHA_TOKEN'; + var appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(expectedRecaptchaToken); + } + }; + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + var confirmationResultInstance = + mockControl.createStrictMock(fireauth.ConfirmationResult); + var confirmationResultConstructor = mockControl.createConstructorMock( + fireauth, 'ConfirmationResult'); + var credentialResolver = + mockControl.createFunctionMock('credentialResolver'); + // Provider instance should be initialized with the expected Auth instance. + phoneAuthProviderConstructor(auth) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would resolve with the expected verification + // ID. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.resolve(expectedVerificationId)).$once(); + // ConfirmationResult instance should be initialized with the expected + // verification ID and the credential resolver. + confirmationResultConstructor(expectedVerificationId, credentialResolver) + .$returns(confirmationResultInstance).$once(); + mockControl.$replayAll(); + + // Initialize a confirmation result. + return fireauth.ConfirmationResult.initialize( + auth, expectedPhoneNumber, appVerifier, credentialResolver) + .then(function(result) { + // Expected confirmation result instance returned. + assertEquals(confirmationResultInstance, result); + }); + }); +} + + +function testConfirmationResult_initialize_error() { + return installAndRunTest('confirmationResult_initialize_error', function() { + var expectedPhoneNumber = '+16505550101'; + var expectedRecaptchaToken = 'RECAPTCHA_TOKEN'; + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + var appVerifier = { + 'type': 'recaptcha', + 'verify': function() { + return goog.Promise.resolve(expectedRecaptchaToken); + } + }; + var phoneAuthProviderInstance = + mockControl.createStrictMock(fireauth.PhoneAuthProvider); + var phoneAuthProviderConstructor = mockControl.createConstructorMock( + fireauth, 'PhoneAuthProvider'); + var credentialResolver = + mockControl.createFunctionMock('credentialResolver'); + // Provider instance should be initialized with the expected Auth instance. + phoneAuthProviderConstructor(auth) + .$returns(phoneAuthProviderInstance).$once(); + // verifyPhoneNumber called on provider instance with the expected phone + // number and appVerifier. This would reject with the expected error. + phoneAuthProviderInstance.verifyPhoneNumber( + expectedPhoneNumber, appVerifier) + .$returns(goog.Promise.reject(expectedError)).$once(); + mockControl.$replayAll(); + + // Initialize a confirmation result. + return fireauth.ConfirmationResult.initialize( + auth, expectedPhoneNumber, appVerifier, credentialResolver) + .then(fail, function(error) { + // Expected error returned. + assertEquals(expectedError, error); + }); + }); +} diff --git a/packages/auth/test/cordovahandler_test.js b/packages/auth/test/cordovahandler_test.js new file mode 100644 index 00000000000..6ebc1515347 --- /dev/null +++ b/packages/auth/test/cordovahandler_test.js @@ -0,0 +1,2285 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for cordovahandler.js. + */ + +goog.provide('fireauth.CordovaHandlerTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.CordovaHandler'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.storage.AuthEventManager'); +goog.require('fireauth.storage.OAuthHandlerManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Timer'); +goog.require('goog.crypt'); +goog.require('goog.crypt.Sha256'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.TestCase'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.CordovaHandlerTest'); + + +var stubs = new goog.testing.PropertyReplacer(); +var universalLinks; +var BuildInfo; +var cordova; +var universalLinkCb; + +var cordovaHandler; +var authDomain = 'subdomain.firebaseapp.com'; +var apiKey = 'apiKey1'; +var appName = 'appName1'; +var version = '3.0.0'; +var savePartialEventManager; +var storageKey; +var androidUA = 'Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Buil' + + 'd/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Sa' + + 'fari/534.30'; +var iOS8iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) A' + + 'ppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A366 Safar' + + 'i/600.1.4'; +var iOS9iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) A' + + 'ppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13C75 Safar' + + 'i/601.1'; + + +/** + * @param {string} str The string to hash. + * @return {string} The hashed string. + */ +function sha256(str) { + var sha256 = new goog.crypt.Sha256(); + sha256.update(str); + return goog.crypt.byteArrayToHex(sha256.digest()); +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +function assertErrorEquals(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +} + + +/** + * Asserts that two Auth events are equivalent. + * @param {!fireauth.AuthEvent} expectedEvent + * @param {!fireauth.AuthEvent} actualEvent + */ +function assertAuthEventEquals(expectedEvent, actualEvent) { + assertObjectEquals( + expectedEvent.toPlainObject(), actualEvent.toPlainObject()); +} + + +/** + * Utility function to initialize the Cordova mock plugins. + * @param {?function(?string, function(!Object))} subscribe The universal link + * subscriber. + * @param {?string} packageName The package name. + * @param {?string} displayName The app display name. + * @param {boolean} isAvailable Whether browsertab is supported. + * @param {?function(string, ?function(), ?function())} openUrl The URL opener. + * @param {?function()} close The browsertab closer if applicable. + * @param {?function()} open The inappbrowser opener if available. + */ +function initializePlugins( + subscribe, packageName, displayName, isAvailable, openUrl, close, open) { + // Initializes all mock plugins. + universalLinks = { + subscribe: subscribe + }; + BuildInfo = { + packageName: packageName, + displayName: displayName + }; + cordova = { + plugins: { + browsertab: { + isAvailable: function(cb) { + cb(isAvailable); + }, + openUrl: openUrl, + close: close + } + }, + InAppBrowser: { + open: open + } + }; +} + + +function setUp() { + // This would never run in IE environment anyway. + // Simulate localStorage synchronized. + simulateLocalStorageSynchronized(); + // Initialize plugins. + initializePlugins( + function(eventName, cb) { + universalLinkCb = cb; + }, + 'com.example.app', + 'Test App', + true, + function(url, resolve, reject) {}, + goog.testing.recordFunction(), + goog.testing.recordFunction()); + stubs.replace( + fireauth.util, + 'checkIfCordova', + function() { + return goog.Promise.resolve(); + }); + // Storage key. + storageKey = 'apiKey1:appName1'; + // Storage manager helpers. + savePartialEventManager = new fireauth.storage.OAuthHandlerManager(); + getAndDeletePartialEventManager = + new fireauth.storage.AuthEventManager(storageKey); +} + + +function tearDown() { + // Clear storage. + window.localStorage.clear(); + window.sessionStorage.clear(); + // Reset stubs. + stubs.reset(); + // Clear plugins. + universalLinks = {}; + BuildInfo = {}; + cordova = {}; + cordovaHandler = null; + universalLinkCb = null; + if (goog.global['handleOpenURL']) { + delete goog.global['handleOpenURL']; + } +} + + +/** Simulates that local storage synchronizes across tabs. */ +function simulateLocalStorageSynchronized() { + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() {return false;}); +} + + +/** + * Install the test to run and runs it. + * @param {string} id The test identifier. + * @param {function():!goog.Promise} func The test function to run. + * @return {!goog.Promise} The result of the test. + */ +function installAndRunTest(id, func) { + var testCase = new goog.testing.TestCase(); + testCase.addNewTest(id, func); + return testCase.runTestsReturningPromise().then(function(result) { + assertTrue(result.complete); + // Display error detected. + if (result.errors.length) { + fail(result.errors.join('\n')); + } + assertEquals(1, result.totalCount); + assertEquals(1, result.runCount); + assertEquals(1, result.successCount); + assertEquals(0, result.errors.length); + }); +} + + +function testCordovaHandler_initializeAndWait_success() { + return installAndRunTest('initializeAndWait_success', function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + // Confirm should be initialized early. + assertTrue(cordovaHandler.shouldBeInitializedEarly()); + // Confirm has volatile sessionStorage. + assertTrue(cordovaHandler.hasVolatileStorage()); + // Confirm does not unload on redirect. + assertFalse(cordovaHandler.unloadsOnRedirect()); + // Should resolve successfully. + return cordovaHandler.initializeAndWait(); + }); +} + + +function testCordovaHandler_initializeAndWait_notCordovaError() { + return installAndRunTest('initializeAndWait_notCordovaError', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.CORDOVA_NOT_READY); + // Simulate non Cordova environment. + stubs.replace( + fireauth.util, + 'checkIfCordova', + function() { + return goog.Promise.reject(expectedError); + }); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + return cordovaHandler.initializeAndWait().then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + }); + }); +} + + +function testCordovaHandler_initializeAndWait_universalLinkError() { + return installAndRunTest('initializeAndWait_universalLinkError', function() { + // Universal links plugin not installed. + universalLinks = {}; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION, + 'cordova-universal-links-plugin is not installed'); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + var p = new goog.Promise(function(resolve, reject) { + cordovaHandler.addAuthEventListener(function(event) { + assertAuthEventEquals(noEvent, event); + resolve(); + }); + }); + return cordovaHandler.initializeAndWait().then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + return p; + }); + }); +} + + +function testCordovaHandler_initializeAndWait_buildInfoError() { + return installAndRunTest('initializeAndWait_buildInfoError', function() { + // Buildinfo plugin not installed. + BuildInfo = {}; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION, + 'cordova-plugin-buildinfo is not installed'); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + var p = new goog.Promise(function(resolve, reject) { + cordovaHandler.addAuthEventListener(function(event) { + assertAuthEventEquals(noEvent, event); + resolve(); + }); + }); + return cordovaHandler.initializeAndWait().then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + return p; + }); + }); +} + + +function testCordovaHandler_initializeAndWait_browserTabError() { + return installAndRunTest('initializeAndWait_browserTabError', function() { + // browsertab plugin not installed. + cordova = {}; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION, + 'cordova-plugin-browsertab is not installed'); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + var p = new goog.Promise(function(resolve, reject) { + cordovaHandler.addAuthEventListener(function(event) { + assertAuthEventEquals(noEvent, event); + resolve(); + }); + }); + return cordovaHandler.initializeAndWait().then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + return p; + }); + }); +} + + +function testCordovaHandler_initializeAndWait_inAppBrowserError() { + return installAndRunTest('initializeAndWait_inAppBrowserError', function() { + // inappbrowser plugin not installed. + cordova.InAppBrowser = null; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CORDOVA_CONFIGURATION, + 'cordova-plugin-inappbrowser is not installed'); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + var p = new goog.Promise(function(resolve, reject) { + cordovaHandler.addAuthEventListener(function(event) { + assertAuthEventEquals(noEvent, event); + resolve(); + }); + }); + return cordovaHandler.initializeAndWait().then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + return p; + }); + }); +} + + +function testCordovaHandler_startPopupTimeout() { + return installAndRunTest('startPopupTimeout', function() { + // Should fail with operation not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + var onError = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + return cordovaHandler.startPopupTimeout({}, onError, 1000).then(function() { + assertEquals(1, onError.getCallCount()); + assertErrorEquals(expectedError, onError.getLastCall().getArgument(0)); + }); + }); +} + + +function testCordovaHandler_processPopup() { + return installAndRunTest('processPopup', function() { + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var provider = new fireauth.GoogleAuthProvider(); + // Popup requests should fail with operation not supported error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + return cordovaHandler.processPopup( + {}, 'linkViaPopup', provider, onInit, onError, '1234', false) + .then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + assertErrorEquals(expectedError, error); + }); + }); +} + + +function testCordovaHandler_addRemoveAuthEventListeners() { + return installAndRunTest('addRemoveAuthEventListeners', function() { + // Wait for event to be handled. + var waitWhileInStorage = function() { + return goog.Timer.promise(10).then(function() { + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(authEvent) { + if (authEvent) { + return waitWhileInStorage(); + } + }); + }; + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + 'SESSION_ID'); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var handler1 = goog.testing.recordFunction(); + var handler2 = goog.testing.recordFunction(); + var handler3 = goog.testing.recordFunction(); + // Save some pending redirect that won't be handled and confirm it was + // cleared. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent).then( + function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler1); + cordovaHandler.addAuthEventListener(handler2); + cordovaHandler.addAuthEventListener(handler3); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // Wait for event to be cleared. + return waitWhileInStorage(); + }).then(function() { + // All handlers should be called with no event. + assertEquals(1, handler1.getCallCount()); + assertAuthEventEquals(noEvent, handler1.getLastCall().getArgument(0)); + assertEquals(1, handler2.getCallCount()); + assertAuthEventEquals(noEvent, handler2.getLastCall().getArgument(0)); + assertEquals(1, handler3.getCallCount()); + assertAuthEventEquals(noEvent, handler3.getLastCall().getArgument(0)); + // Remove 2 handlers and trigger a new event. + cordovaHandler.removeAuthEventListener(handler1); + cordovaHandler.removeAuthEventListener(handler2); + // Simulate a redirect operation and the partial event is saved. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent); + }).then(function() { + // Trigger the universal link with the OAuth response. + universalLinkCb({ + url: incomingUrl + }); + // Wait for event to be cleared from storage. + return waitWhileInStorage(); + }).then(function() { + // Only third handler called with completed event. + assertEquals(1, handler1.getCallCount()); + assertEquals(1, handler2.getCallCount()); + assertEquals(2, handler3.getCallCount()); + assertAuthEventEquals( + completeEvent, handler3.getLastCall().getArgument(0)); + }); + }); +} + + + +function testCordovaHandler_initialNoAuthEvent() { + return installAndRunTest('initialNoAuthEvent', function() { + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Use quick timeout to simulate no event triggered on initialization. + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait().then(function() { + // Wait for handler to be called. + return waitForHandler; + }).then(function(authEvent) { + // No Auth event triggered. + assertAuthEventEquals(noEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_initialNoAuthEvent_invalidLink() { + return installAndRunTest('initialNoAuthEvent_invalidLink', function() { + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'http://example.firebaseapp.com/some/other/link'; + // The final resolved event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Trigger some non callback link. + universalLinks.subscribe = function(eventType, cb) { + cb({url: incomingUrl}); + }; + // Assume pending redirect event. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // Wait for handler to be called. + return waitForHandler; + }).then(function(authEvent) { + // Unknown event triggered. + assertAuthEventEquals(noEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_initialValidAuthEvent_direct() { + return installAndRunTest('initialValidAuthEvent_direct', function() { + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // The final resolved event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + 'SESSION_ID'); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Trigger the universal link with the OAuth response on subscription. + universalLinks.subscribe = function(eventType, cb) { + cb({url: incomingUrl}); + }; + // Assume pending redirect event. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // Wait for handler to be called. + return waitForHandler; + }).then(function(authEvent) { + // Auth event triggered. It should be the complete event. + assertAuthEventEquals(completeEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_initialValidAuthEvent_link() { + return installAndRunTest('initialValidAuthEvent_link', function() { + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'https://example.app.goo.gl/?link=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + // The expected event constructed from incoming link. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + 'SESSION_ID'); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Trigger the universal link with the OAuth response on subscription. + universalLinks.subscribe = function(eventType, cb) { + cb({url: incomingUrl}); + }; + // Assume pending redirect event. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // Wait for handler to be called. + return waitForHandler; + }).then(function(authEvent) { + // Auth event triggered. The expected event should be dispatched. + assertAuthEventEquals(completeEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_initialValidAuthEvent_deepLinkId() { + return installAndRunTest('initialValidAuthEvent_deepLinkId', function() { + // This should have been previously saved in a process redirect call. + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var incomingUrl = + 'comexampleiosurl://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + 'SESSION_ID'); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Trigger the universal link with the OAuth response on subscription. + universalLinks.subscribe = function(eventType, cb) { + cb({url: incomingUrl}); + }; + // Assume pending redirect event. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // This should have been previously saved in a process redirect call. + return waitForHandler; + }).then(function(authEvent) { + // Auth event triggered. The expected event should be triggered. + assertAuthEventEquals(completeEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_initialErrorAuthEvent() { + return installAndRunTest('initialErrorAuthEvent', function() { + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var partialEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + 'SESSION_ID', + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // The callback URL should contain the error. + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback?firebaseError=' + + JSON.stringify(expectedError.toPlainObject()); + // Triggered event with the error. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + null, + expectedError); + var handler; + var waitForHandler = new goog.Promise(function(resolve, reject) { + handler = resolve; + }); + // Trigger the universal link with the OAuth response on subscription. + universalLinks.subscribe = function(eventType, cb) { + cb({url: incomingUrl}); + }; + // Assume pending redirect event. + return savePartialEventManager.setAuthEvent(storageKey, partialEvent) + .then(function() { + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.initializeAndWait(); + }).then(function() { + // Wait for handler to trigger. + return waitForHandler; + }).then(function(authEvent) { + // Auth event triggered with the expected error event. + assertAuthEventEquals(completeEvent, authEvent); + }); + }); +} + + +function testCordovaHandler_processRedirect_success_android() { + return installAndRunTest('processRedirect_success_android', function() { + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }, + // Confirm expected endpoint ID passed. + fireauth.constants.Endpoint.STAGING.id); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial unknown event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + savedCb({url: incomingUrl}); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10, undefined, + fireauth.constants.Endpoint.STAGING.id); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_success_parallelCalls() { + return installAndRunTest('processRedirect_success_parallelCalls', function() { + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL for first operation. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Construct OAuth handler URL for second operation. + var expectedUrl2 = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '5678', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event for first completed call. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Completed event for second completed call. + var completeEvent2 = new fireauth.AuthEvent( + 'linkViaRedirect', + '5678', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial unknown event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + var openUrlCalls = 0; + cordova.plugins.browsertab.openUrl = function(url) { + openUrlCalls++; + // Confirm expected URL on each completed call. + if (openUrlCalls == 1) { + // First operation expected URL. + assertEquals(expectedUrl, url); + } else { + // Second operation expected URL. + assertEquals(expectedUrl2, url); + } + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + savedCb({url: incomingUrl}); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + var successiveCallResult = null; + var p = cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // The other parallel operation should have failed with the expected + // error. + assertErrorEquals(expectedError, successiveCallResult); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Trigger again. As the operation already completed, this should + // eventually succeed too. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '5678'); + }).then(function() { + // Confirm browsertab close called again. + assertEquals(2, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered thrice. + assertEquals(3, handler.getCallCount()); + // Last call should have resolved event with the second event. + assertAuthEventEquals( + completeEvent2, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + // This should fail as there is a pending operation already. + cordovaHandler.processRedirect('linkViaRedirect', provider, '5678') + .thenCatch(function(error) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + // Save the pending redirect error. + successiveCallResult = error; + }); + return p; + }); +} + + +function testCordovaHandler_processRedirect_success_ios() { + return installAndRunTest('processRedirect_success_ios', function() { + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return iOS9iPhoneUA; + }); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + // This is currently not the default behavior but will be supported in the + // future. + savedCb({url: incomingUrl}); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_success_ios_custom() { + return installAndRunTest('processRedirect_success_ios_custom', function() { + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + // Even though this is iOS9, custom scheme redirects can still be + // used. + return iOS9iPhoneUA; + }); + // Valid custom scheme URL. + var incomingUrl = 'com.example.app://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + // Not used. + }; + cordova.plugins.browsertab.openUrl = function(url) { + // browsertab available in iOS 9+. + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the custom scheme + // redirect callback with the OAuth response. + goog.global['handleOpenURL'](incomingUrl); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_success_ios_caseInsensitive() { + return installAndRunTest( + 'processRedirect_success_ios_caseInsensitive', function() { + // Use an upper case character in the bundle ID. This should not matter. + BuildInfo.packageName = 'com.example.App'; + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.App', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + // Even though this is iOS9, custom scheme redirects can still be + // used. + return iOS9iPhoneUA; + }); + // Valid custom scheme URL. For some reason, the scheme is lower cased. + var incomingUrl = 'com.example.app://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + // Not used. + }; + cordova.plugins.browsertab.openUrl = function(url) { + // browsertab available in iOS 9+. + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the custom scheme + // redirect callback with the OAuth response. + goog.global['handleOpenURL'](incomingUrl); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + // This should still work. + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_browserTabUnavailable() { + return installAndRunTest('processRedirect_browserTabUnavailable', function() { + // Test when browser tab is not available, inappbrowser should be used and + // a system browser opened. + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial unknown event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + var expectedInAppBrowserType = '_system'; + var expectedInAppBrowserOptions = 'location=yes'; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + // Simulate browser tab not available. + cordova.plugins.browsertab.isAvailable = function(cb) { + cb(false); + }; + // Should not be called. + cordova.plugins.browsertab.openUrl = function(url) { + fail('browsertab.openUrl should not call!'); + }; + // InAppBrowser opener should be called as browsertab is unavailable. + cordova.InAppBrowser.open = function(url, type, options) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // Confirm system browser used as this is Android. + assertEquals(expectedInAppBrowserType, type); + // Confirm expected options. + assertEquals(expectedInAppBrowserOptions, options); + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + savedCb({url: incomingUrl}); + // Cannot close a system browser. + return null; + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_webview_withNoHandler() { + return installAndRunTest('processRedirect_webview_withNoHandler', function() { + // This will test embedded webview when handleOpenURL is not defined by the + // developer. Emebedded webviews are used in iOS8 and under and need to be + // closed explicitly. + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS 8 environment where SFSafariViewController is not supported. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return iOS8iPhoneUA; + }); + // Valid custom scheme URL. + var incomingUrl = 'com.example.app://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var expectedInAppBrowserType = '_blank'; + var expectedInAppBrowserOptions = 'location=yes'; + var inAppBrowserWindowRef = { + close: goog.testing.recordFunction() + }; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + // Not used. + }; + // Simulate browser tab not available. + cordova.plugins.browsertab.isAvailable = function(cb) { + cb(false); + }; + // Should not be called. + cordova.plugins.browsertab.openUrl = function(url) { + fail('browsertab.openUrl should not call!'); + }; + // InAppBrowser opener should be called as browsertab is unavailable. + cordova.InAppBrowser.open = function(url, type, options) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // Confirm embedded webview browser used as this is iOS8. + assertEquals(expectedInAppBrowserType, type); + // Confirm expected options. + assertEquals(expectedInAppBrowserOptions, options); + // Custom schemes should be used here. + // This should be handled. + goog.global['handleOpenURL'](incomingUrl); + // Emebdded webview can be closed. + return inAppBrowserWindowRef; + }; + // handleOpenURL is not defined here. + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm inappbrowser window close called. + assertEquals(1, inAppBrowserWindowRef.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_webview_withHandler() { + return installAndRunTest('processRedirect_webview_withHandler', function() { + // This will test embedded webview when handleOpenURL is already defined by + // the developer. Embedded webviews are used in iOS8 and under and need to + // be closed explicitly. + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS 8 environment where SFSafariViewController is not supported. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return iOS8iPhoneUA; + }); + // Invalid URL that should be ignored. + var invalidUrl1 = + 'http://example.firebaseapp.com/__/auth/callback#invalid'; + var invalidUrl2 = 'comexampleapp://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#invalid'); + // Valid custom scheme URL. + var incomingUrl = 'com.example.app://google/link?deep_link_id=' + + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var expectedInAppBrowserType = '_blank'; + var expectedInAppBrowserOptions = 'location=yes'; + var inAppBrowserWindowRef = { + close: goog.testing.recordFunction() + }; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + // Not used. + }; + // Simulate browser tab not available. + cordova.plugins.browsertab.isAvailable = function(cb) { + cb(false); + }; + // Should not be called. + cordova.plugins.browsertab.openUrl = function(url) { + fail('browsertab.openUrl should not call!'); + }; + // InAppBrowser opener should be called as browsertab is unavailable. + cordova.InAppBrowser.open = function(url, type, options) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // Confirm embedded webview browser used as this is iOS8. + assertEquals(expectedInAppBrowserType, type); + // Confirm expected options. + assertEquals(expectedInAppBrowserOptions, options); + // Custom schemes should be used here. + // Invalid URLs should be ignored. + goog.global['handleOpenURL'](invalidUrl1); + assertEquals(1, recordedFunction.getCallCount()); + assertEquals(invalidUrl1, recordedFunction.getLastCall().getArgument(0)); + goog.global['handleOpenURL'](invalidUrl2); + assertEquals(2, recordedFunction.getCallCount()); + assertEquals(invalidUrl2, recordedFunction.getLastCall().getArgument(0)); + // This should be handled. + goog.global['handleOpenURL'](incomingUrl); + assertEquals(3, recordedFunction.getCallCount()); + assertEquals(incomingUrl, recordedFunction.getLastCall().getArgument(0)); + // Emebdded webview can be closed. + return inAppBrowserWindowRef; + }; + // Assume developer already using custom scheme plugin. Make sure + // their handler is not overwritten. + var recordedFunction = goog.testing.recordFunction(); + goog.global['handleOpenURL'] = recordedFunction; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm inappbrowser window close called. + assertEquals(1, inAppBrowserWindowRef.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_doubleDeepLink() { + return installAndRunTest('processRedirect_doubleDeepLink', function() { + // Tests when the incoming URL has another deep link embedded. + // This happens when the auto redirect to FDL is intercepted by the app. + // Another deep link is contained within in case it is not intercepted and + // the page can then display the FDL button using the deep link embedded. + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + // The automatic FDL redirect link in Android will have the following + // format if intercepted by the app. + var deepLink = 'http://example.firebaseapp.com/__/auth/callback' + + '/?link=' + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + var incomingUrl = + 'https://example.app.goo.gl/?link=' + encodeURIComponent(deepLink); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial unknown event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + savedCb({url: incomingUrl}); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_ios7or8doubleDeepLink() { + return installAndRunTest('processRedirect_ios7or8doubleDeepLink', function() { + // Tests when the incoming URL has another deep link embedded. + // This happens when the auto redirect to FDL is intercepted by the app. + // Another deep link is contained within in case it is not intercepted and + // the page can then display the FDL button using the deep link embedded. + // This tests the flow in iOS 7 or 8 where custom URL schemes are used. + // Realistically, this is not needed as custom URL scheme redirects do not + // requires clicks. + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Simulate iOS environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return iOS8iPhoneUA; + }); + // This would happen if auto redirect for iOS 8 using custom schemes + // contains a double link. In reality, this is not needed as custom scheme + // redirects always get intercepted by an app without an additional click. + var deepLink = 'http://example.firebaseapp.com/__/auth/callback' + + '/?link=' + encodeURIComponent( + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'); + var incomingUrl = 'com.example.app://google/link?deep_link_id=' + + encodeURIComponent(deepLink); + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Initial no event. + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var expectedInAppBrowserType = '_blank'; + var expectedInAppBrowserOptions = 'location=yes'; + var inAppBrowserWindowRef = { + close: goog.testing.recordFunction() + }; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + // Not used. + }; + // Simulate browser tab not available. + cordova.plugins.browsertab.isAvailable = function(cb) { + cb(false); + }; + // Should not be called. + cordova.plugins.browsertab.openUrl = function(url) { + fail('browsertab.openUrl should not call!'); + }; + // InAppBrowser opener should be called as browsertab is unavailable. + cordova.InAppBrowser.open = function(url, type, options) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // Confirm embedded webview browser used as this is iOS8. + assertEquals(expectedInAppBrowserType, type); + // Confirm expected options. + assertEquals(expectedInAppBrowserOptions, options); + // Custom schemes should be used here. + // This should be handled. + goog.global['handleOpenURL'](incomingUrl); + // Emebdded webview can be closed. + return inAppBrowserWindowRef; + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_invalidProvider() { + return installAndRunTest('processRedirect_invalidProvider', function() { + // Invalid OAuth provider. + var provider = new fireauth.EmailAuthProvider(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + // Handler not triggered due to invalid provider error. + assertEquals(0, handler.getCallCount()); + // Expected invalid provider error. + assertErrorEquals(expectedError, error); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_error_parallelCalls() { + return installAndRunTest('processRedirect_error_parallelCalls', function() { + // Invalid OAuth provider. + var provider = new fireauth.EmailAuthProvider(); + // Expected invalid provider error. + var expectedInvalidProviderError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + // Expected error when there is a pending redirect operation. + var expectedProcessRedirectError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_OPERATION_PENDING); + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + var p = cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // The other parallel operation should have failed with the expected + // pending redirect error. + assertErrorEquals(expectedProcessRedirectError, successiveCallResult); + // Handler not triggered due to invalid provider error. + assertEquals(0, handler.getCallCount()); + // Expected invalid provider error. + assertErrorEquals(expectedInvalidProviderError, error); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Call again, this should throw the expected error and complete. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '1234'); + }).then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Handler not triggered due to invalid provider error. + assertEquals(0, handler.getCallCount()); + // Expected invalid provider error. + assertErrorEquals(expectedInvalidProviderError, error); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + assertNull(event); + }); + // This should fail as there is a pending operation already. + cordovaHandler.processRedirect('linkViaRedirect', provider, '5678') + .thenCatch(function(error) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + successiveCallResult = error; + }); + return p; + }); +} + + +function testCordovaHandler_processRedirect_error() { + return installAndRunTest('processRedirect_error', function() { + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + // Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + // Append error to callback URL. + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback?firebaseError=' + + JSON.stringify(expectedError.toPlainObject()); + // Completed event with error. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + null, + null, expectedError); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + // On openUrl, simulate completion by triggering the universal link + // callback with the OAuth response. + savedCb({url: incomingUrl}); + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10); + cordovaHandler.addAuthEventListener(handler); + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice. + assertEquals(2, handler.getCallCount()); + // First with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Then with resolved event. In this case, this contains an error. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_redirectCancelledError() { + return installAndRunTest('processRedirect_redirectCancelledErr', function() { + var doc = goog.global.document; + stubs.replace( + doc, + 'addEventListener', + function(eventName, onResume) { + // Trigger on resume event. This will eventually trigger the redirect + // cancelled by user error after it times out. + if (eventName == 'resume') { + onResume(); + } + }); + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + apn: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_CANCELLED_BY_USER); + // Android environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return androidUA; + }); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // openUrl call counter. + var openUrlCounter = 0; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + openUrlCounter++; + // On second call, succeed. + if (openUrlCounter == 2) { + savedCb({url: incomingUrl}); + } + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10, 10); + cordovaHandler.addAuthEventListener(handler); + // Test all possible scenarios: + // 1. redirect operation is cancelled. + // 2. redirect operation called again successfully. + // 3. redirect operation called again and cancelled. + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + // Handler should be triggered only for first event. + assertEquals(1, handler.getCallCount()); + // Handler called with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Expected cancellation error. + assertErrorEquals(expectedError, error); + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Try again, this time it should succeed. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '1234'); + }).then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice at this point. + assertEquals(2, handler.getCallCount()); + // Handler last call will be with the completed event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Try again, this time it will be cancelled again. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '1234'); + }).then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Confirm browsertab close not called again. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler should not be triggered any more. + assertEquals(2, handler.getCallCount()); + // Expected cancellation error. + assertErrorEquals(expectedError, error); + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} + + +function testCordovaHandler_processRedirect_visibilityChange_cancelledErr() { + return installAndRunTest('processRedirect_visibility_cancelErr', function() { + // Test visibilitychange event in iOS where resume does not trigger in the + // case of SFSVC, + var doc = goog.global.document; + // Stub fireauth.util.isAppVisible() to return true. + stubs.replace( + fireauth.util, + 'isAppVisible', + function() { + return true; + }); + stubs.replace( + doc, + 'addEventListener', + function(eventName, onVisibilityChange) { + // We will ignore resume event and only trigger visibilityChange. + if (eventName == 'visibilitychange') { + onVisibilityChange(); + } + }); + var provider = new fireauth.GoogleAuthProvider(); + // Construct OAuth handler URL. + var expectedUrl = + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + null, + '1234', + version, + { + ibi: 'com.example.app', + appDisplayName: 'Test App', + sessionId: sha256('11111111111111111111') + }); + var incomingUrl = + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse'; + // Completed event. + var completeEvent = new fireauth.AuthEvent( + 'linkViaRedirect', + '1234', + 'http://example.firebaseapp.com/__/auth/callback#oauthResponse', + '11111111111111111111'); + // Stub this so the session ID generated can be predictable. + stubs.replace( + Math, + 'random', + function() { + return 0; + }); + // Expected error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.REDIRECT_CANCELLED_BY_USER); + // iOS environment. + stubs.replace( + fireauth.util, + 'getUserAgentString', + function() { + return iOS9iPhoneUA; + }); + var noEvent = new fireauth.AuthEvent( + fireauth.AuthEvent.Type.UNKNOWN, + null, + null, + null, + new fireauth.AuthError(fireauth.authenum.Error.NO_AUTH_EVENT)); + var savedCb = null; + // openUrl call counter. + var openUrlCounter = 0; + // Save the universal link callback on subscription. + universalLinks.subscribe = function(eventType, cb) { + savedCb = cb; + }; + cordova.plugins.browsertab.openUrl = function(url) { + // Confirm expected URL. + assertEquals(expectedUrl, url); + openUrlCounter++; + // On second call, succeed. + if (openUrlCounter == 2) { + savedCb({url: incomingUrl}); + } + }; + var handler = goog.testing.recordFunction(); + cordovaHandler = new fireauth.CordovaHandler( + authDomain, apiKey, appName, version, 10, 10); + cordovaHandler.addAuthEventListener(handler); + // Test all possible scenarios: + // 1. redirect operation is cancelled. + // 2. redirect operation called again successfully. + // 3. redirect operation called again and cancelled. + return cordovaHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Confirm browsertab close not called. + assertEquals(0, cordova.plugins.browsertab.close.getCallCount()); + // Handler should be triggered only for first event. + assertEquals(1, handler.getCallCount()); + // Handler called with no event. + assertAuthEventEquals( + noEvent, + handler.getCalls()[0].getArgument(0)); + // Expected cancellation error. + assertErrorEquals(expectedError, error); + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Try again, this time it should succeed. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '1234'); + }).then(function() { + // Confirm browsertab close called. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler triggered twice at this point. + assertEquals(2, handler.getCallCount()); + // Handler last call will be with the completed event. + assertAuthEventEquals( + completeEvent, + handler.getLastCall().getArgument(0)); + // Confirm event deleted from storage. + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + // Try again, this time it will be cancelled again. + return cordovaHandler.processRedirect( + 'linkViaRedirect', provider, '1234'); + }).then(function() { + throw new Error('Unexpected success!'); + }, function(error) { + // Confirm browsertab close not called again. + assertEquals(1, cordova.plugins.browsertab.close.getCallCount()); + // Handler should not be triggered any more. + assertEquals(2, handler.getCallCount()); + // Expected cancellation error. + assertErrorEquals(expectedError, error); + return getAndDeletePartialEventManager.getAuthEvent(); + }).then(function(event) { + assertNull(event); + }); + }); +} diff --git a/packages/auth/test/defines_test.js b/packages/auth/test/defines_test.js new file mode 100644 index 00000000000..b6e60156437 --- /dev/null +++ b/packages/auth/test/defines_test.js @@ -0,0 +1,78 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for defines.js. + */ + +goog.provide('fireauth.constantsTest'); + +goog.require('fireauth.constants'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.constantsTest'); + + +function testGetEndpointConfig() { + var productionEndpoint = fireauth.constants.Endpoint.PRODUCTION; + var stagingEndpoint = fireauth.constants.Endpoint.STAGING; + var testEndpoint = fireauth.constants.Endpoint.TEST; + assertObjectEquals( + { + 'firebaseEndpoint': productionEndpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': productionEndpoint.secureTokenEndpoint + }, + fireauth.constants.getEndpointConfig( + fireauth.constants.Endpoint.PRODUCTION.id)); + assertObjectEquals( + { + 'firebaseEndpoint': stagingEndpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': stagingEndpoint.secureTokenEndpoint + }, + fireauth.constants.getEndpointConfig( + fireauth.constants.Endpoint.STAGING.id)); + assertObjectEquals( + { + 'firebaseEndpoint': testEndpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': testEndpoint.secureTokenEndpoint + }, + fireauth.constants.getEndpointConfig( + fireauth.constants.Endpoint.TEST.id)); + assertNull(fireauth.constants.getEndpointConfig()); + assertNull(fireauth.constants.getEndpointConfig(null)); + assertNull(fireauth.constants.getEndpointConfig(undefined)); + assertNull(fireauth.constants.getEndpointConfig('invalid')); +} + + +function testGetEndpointId() { + assertEquals( + fireauth.constants.Endpoint.PRODUCTION.id, + fireauth.constants.getEndpointId( + fireauth.constants.Endpoint.PRODUCTION.id)); + assertEquals( + fireauth.constants.Endpoint.STAGING.id, + fireauth.constants.getEndpointId( + fireauth.constants.Endpoint.STAGING.id)); + assertEquals( + fireauth.constants.Endpoint.TEST.id, + fireauth.constants.getEndpointId( + fireauth.constants.Endpoint.TEST.id)); + assertUndefined(fireauth.constants.getEndpointId()); + assertUndefined(fireauth.constants.getEndpointId(null)); + assertUndefined(fireauth.constants.getEndpointId(undefined)); + assertUndefined(fireauth.constants.getEndpointId('invalid')); +} diff --git a/packages/auth/test/deprecation_test.js b/packages/auth/test/deprecation_test.js new file mode 100644 index 00000000000..8785844abba --- /dev/null +++ b/packages/auth/test/deprecation_test.js @@ -0,0 +1,80 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.deprecationTest'); + +goog.require('fireauth.deprecation'); +goog.require('fireauth.deprecation.Deprecations'); +goog.require('fireauth.util'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.deprecationTest'); + + +var mockControl; +var mockLogger; + +// Add fake deprecation notices to the list of possible notices. +fireauth.deprecation.Deprecations.TEST_MESSAGE = 'This is a test.'; +fireauth.deprecation.Deprecations.TEST_MESSAGE_2 = 'This is another test.'; + + +function setUp() { + mockControl = new goog.testing.MockControl(); + mockWarning = mockControl.createMethodMock(fireauth.util, 'consoleWarn'); +} + + +function tearDown() { + mockControl.$verifyAll(); + mockControl.$tearDown(); + fireauth.deprecation.resetForTesting(); +} + + +function testLog() { + mockWarning(fireauth.deprecation.Deprecations.TEST_MESSAGE).$once(); + + mockControl.$replayAll(); + + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE); +} + + +function testLogMultiple() { + mockWarning(fireauth.deprecation.Deprecations.TEST_MESSAGE).$once(); + mockWarning(fireauth.deprecation.Deprecations.TEST_MESSAGE_2).$once(); + + mockControl.$replayAll(); + + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE); + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE_2); +} + + +function testLogMultipleDontRepeat() { + mockWarning(fireauth.deprecation.Deprecations.TEST_MESSAGE).$once(); + mockWarning(fireauth.deprecation.Deprecations.TEST_MESSAGE_2).$once(); + + mockControl.$replayAll(); + + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE); + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE_2); + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE); + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE_2); + fireauth.deprecation.log(fireauth.deprecation.Deprecations.TEST_MESSAGE_2); +} diff --git a/packages/auth/test/dynamiclink_test.js b/packages/auth/test/dynamiclink_test.js new file mode 100644 index 00000000000..29190a82b9a --- /dev/null +++ b/packages/auth/test/dynamiclink_test.js @@ -0,0 +1,360 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for dynamiclink.js. + */ + +goog.provide('fireauth.DynamicLinkTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.DynamicLink'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.util'); +goog.require('goog.Uri'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.DynamicLinkTest'); + + +var customSchemeLink; +var reverseOAuthClientIdCustomSchemeLink; +var customSchemeLinkUrl; +var reverseOAuthClientIdCustomSchemeLinkUrl; +var androidDynamicLink; +var iosDynamicLink; +var androidDynamicLinkUrl; +var clientId = '123456.apps.googleusercontent.com'; +var fdlDomain = 'example.app.goo.gl'; +var androidPlatform = fireauth.DynamicLink.Platform.ANDROID; +var iosPlatform = fireauth.DynamicLink.Platform.IOS; +var appIdentifier = 'com.example.application'; +var authDomain = 'example.firebaseapp.com'; +var payload = 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; +var defaultError = + new fireauth.AuthError(fireauth.authenum.Error.APP_NOT_INSTALLED); +var appName = 'Hello World!'; +var customFallbackUrl = 'https://example.firebaseapp.com/custom/fallback'; +var fallbackUrl; +var androidAutoRedirectDynamicLinkUrl; +var androidUserInteractionDynamicLinkUrl; +var noAppNameAndroidDynamicLinkUrl; +var noAppNameAndroidAutoRedirectDynamicLinkUrl; +var requiredDynamicLinkUrlFields = [ + 'fdlDomain', 'platform', 'appIdentifier', 'authDomain', 'link', 'appName']; + + +function setUp() { + fallbackUrl = 'https://' + authDomain + '/__/auth/handler?firebaseError=' + + encodeURIComponent(/** @type {string} */ ( + fireauth.util.stringifyJSON(defaultError.toPlainObject()))); + androidDynamicLinkUrl = 'https://example.firebaseapp.com/__/auth/callback?' + + 'fdlDomain=' + encodeURIComponent(fdlDomain) + + '&platform=' + encodeURIComponent(androidPlatform) + + '&appIdentifier=' + encodeURIComponent(appIdentifier) + + '&authDomain=' + encodeURIComponent(authDomain) + + '&link=' + encodeURIComponent(payload) + + '&appName=' + encodeURIComponent(appName); + noAppNameAndroidDynamicLinkUrl = + goog.Uri.parse(androidDynamicLinkUrl).setParameterValue('appName', '') + .toString(); + androidUserInteractionDynamicLinkUrl = 'https://' + fdlDomain + '/?' + + 'apn=' + encodeURIComponent(appIdentifier) + + '&afl=' + encodeURIComponent(fallbackUrl) + + '&link=' + encodeURIComponent(payload); + androidAutoRedirectDynamicLinkUrl = 'https://' + fdlDomain + '/?' + + 'apn=' + encodeURIComponent(appIdentifier) + + '&afl=' + encodeURIComponent(androidDynamicLinkUrl) + + '&link=' + encodeURIComponent(androidDynamicLinkUrl); + noAppNameAndroidAutoRedirectDynamicLinkUrl = 'https://' + fdlDomain + '/?' + + 'apn=' + encodeURIComponent(appIdentifier) + + '&afl=' + encodeURIComponent(noAppNameAndroidDynamicLinkUrl) + + '&link=' + encodeURIComponent(noAppNameAndroidDynamicLinkUrl); + iosDynamicLinkUrl = 'https://example.firebaseapp.com/__/auth/callback?' + + 'fdlDomain=' + encodeURIComponent(fdlDomain) + + '&platform=' + encodeURIComponent(iosPlatform) + + '&appIdentifier=' + encodeURIComponent(appIdentifier) + + '&authDomain=' + encodeURIComponent(authDomain) + + '&link=' + encodeURIComponent(payload) + + '&appName=' + encodeURIComponent(appName); + iosUserInteractionDynamicLinkUrl = 'https://' + fdlDomain + '/?' + + 'ibi=' + encodeURIComponent(appIdentifier) + + '&ifl=' + encodeURIComponent(fallbackUrl) + + '&link=' + encodeURIComponent(payload); + iosAutoRedirectDynamicLinkUrl = 'https://' + fdlDomain + '/?' + + 'ibi=' + encodeURIComponent(appIdentifier) + + '&ifl=' + encodeURIComponent(iosDynamicLinkUrl) + + '&link=' + encodeURIComponent(iosDynamicLinkUrl); + customSchemeLinkUrl = appIdentifier + '://google/link?deep_link_id=' + + encodeURIComponent(payload); + reverseOAuthClientIdCustomSchemeLinkUrl = + 'com.googleusercontent.apps.123456://firebaseauth/link?deep_link_id=' + + encodeURIComponent(payload); +} + + +function tearDown() { + androidDynamicLink = null; + androidDynamicLinkUrl = null; + androidAutoRedirectDynamicLinkUrl = null; + androidUserInteractionDynamicLinkUrl = null; + iosDynamicLink = null; + iosDynamicLinkUrl = null; + iosAutoRedirectDynamicLinkUrl = null; + iosUserInteractionDynamicLinkUrl = null; + noAppNameAndroidDynamicLinkUrl = null; + noAppNameAndroidAutoRedirectDynamicLinkUrl = null; + customSchemeLink = null; + customSchemeLinkUrl = null; + reverseOAuthClientIdCustomSchemeLink = null; + reverseOAuthClientIdCustomSchemeLinkUrl = null; +} + + +function testDynamicLink_initialization() { + // Initialize the dynamic link. + androidDynamicLink = new fireauth.DynamicLink( + fdlDomain, androidPlatform, appIdentifier, authDomain, payload); + // Confirm all properties set correctly. + assertEquals(fallbackUrl, androidDynamicLink['fallbackUrl']); + assertEquals(fdlDomain, androidDynamicLink['fdlDomain']); + assertEquals(androidPlatform, androidDynamicLink['platform']); + assertEquals(appIdentifier, androidDynamicLink['appIdentifier']); + assertEquals(authDomain, androidDynamicLink['authDomain']); + assertEquals(payload, androidDynamicLink['payload']); + assertNull(androidDynamicLink['appName']); + assertNull(androidDynamicLink['clientId']); + // Set app name. + androidDynamicLink.setAppName(appName); + assertEquals(appName, androidDynamicLink['appName']); + // Override the default fallback URL. + androidDynamicLink.setFallbackUrl(customFallbackUrl); + assertEquals(customFallbackUrl, androidDynamicLink['fallbackUrl']); +} + + +function testDynamicLink_initialization_noFdlDomain() { + // Initialize the dynamic link. + customSchemeLink = new fireauth.DynamicLink( + null, iosPlatform, appIdentifier, authDomain, payload); + // Confirm all properties set correctly. + assertEquals(fallbackUrl, customSchemeLink['fallbackUrl']); + assertNull(customSchemeLink['fdlDomain']); + assertEquals(iosPlatform, customSchemeLink['platform']); + assertEquals(appIdentifier, customSchemeLink['appIdentifier']); + assertEquals(authDomain, customSchemeLink['authDomain']); + assertEquals(payload, customSchemeLink['payload']); + assertNull(customSchemeLink['appName']); + assertNull(customSchemeLink['clientId']); + // Set app name. + customSchemeLink.setAppName(appName); + assertEquals(appName, customSchemeLink['appName']); + // Override the default fallback URL. + customSchemeLink.setFallbackUrl(customFallbackUrl); + assertEquals(customFallbackUrl, customSchemeLink['fallbackUrl']); +} + + +function testDynamicLink_initialization_clientIdAndNoFdlDomain() { + // Initialize the dynamic link. + reverseOAuthClientIdCustomSchemeLink = new fireauth.DynamicLink( + null, iosPlatform, appIdentifier, authDomain, payload, clientId); + // Confirm all properties set correctly. + assertEquals( + fallbackUrl, reverseOAuthClientIdCustomSchemeLink['fallbackUrl']); + assertNull(reverseOAuthClientIdCustomSchemeLink['fdlDomain']); + assertEquals(iosPlatform, reverseOAuthClientIdCustomSchemeLink['platform']); + assertEquals( + appIdentifier, reverseOAuthClientIdCustomSchemeLink['appIdentifier']); + assertEquals(authDomain, reverseOAuthClientIdCustomSchemeLink['authDomain']); + assertEquals(payload, reverseOAuthClientIdCustomSchemeLink['payload']); + assertEquals(clientId, reverseOAuthClientIdCustomSchemeLink['clientId']); + assertNull(reverseOAuthClientIdCustomSchemeLink['appName']); + // Set app name. + reverseOAuthClientIdCustomSchemeLink.setAppName(appName); + assertEquals(appName, reverseOAuthClientIdCustomSchemeLink['appName']); + // Override the default fallback URL. + reverseOAuthClientIdCustomSchemeLink.setFallbackUrl(customFallbackUrl); + assertEquals( + customFallbackUrl, reverseOAuthClientIdCustomSchemeLink['fallbackUrl']); +} + + +function testDynamicLink_fromURL() { + // Invalid dynamic link. + assertNull(fireauth.DynamicLink.fromURL(payload)); + // Valid Android dynamic link. + androidDynamicLink = new fireauth.DynamicLink( + fdlDomain, androidPlatform, appIdentifier, authDomain, payload); + androidDynamicLink.setAppName(appName); + assertObjectEquals( + androidDynamicLink, fireauth.DynamicLink.fromURL(androidDynamicLinkUrl)); + // Valid iOS dynamic link. + iosDynamicLink = new fireauth.DynamicLink( + fdlDomain, iosPlatform, appIdentifier, authDomain, payload); + iosDynamicLink.setAppName(appName); + assertObjectEquals( + iosDynamicLink, fireauth.DynamicLink.fromURL(iosDynamicLinkUrl)); + // Any missing field should resolve to null. + for (var i = 0; i < requiredDynamicLinkUrlFields.length; i++) { + // Remove one required field and confirm it resolves to null. + assertNull(fireauth.DynamicLink.fromURL( + goog.Uri.parse(androidDynamicLinkUrl) + .removeParameter(requiredDynamicLinkUrlFields[i]) + .toString())); + } + // Custom scheme URLs can't be constructed from URL string. + assertNull(fireauth.DynamicLink.fromURL(customSchemeLinkUrl)); + assertNull(fireauth.DynamicLink.fromURL( + reverseOAuthClientIdCustomSchemeLinkUrl)); +} + + +function testDynamicLink_toString_isAutoRedirect_android() { + androidDynamicLink = new fireauth.DynamicLink( + fdlDomain, androidPlatform, appIdentifier, authDomain, payload); + androidDynamicLink.setAppName(appName); + assertEquals( + androidAutoRedirectDynamicLinkUrl, androidDynamicLink.toString(true)); +} + + +function testDynamicLink_toString_isAutoRedirect_android_noAppName() { + androidDynamicLink = new fireauth.DynamicLink( + fdlDomain, androidPlatform, appIdentifier, authDomain, payload); + // Assume no app name provided. + androidDynamicLink.setAppName(null); + assertEquals( + noAppNameAndroidAutoRedirectDynamicLinkUrl, + androidDynamicLink.toString(true)); +} + + +function testDynamicLink_toString_isAutoRedirect_ios() { + iosDynamicLink = new fireauth.DynamicLink( + fdlDomain, iosPlatform, appIdentifier, authDomain, payload); + iosDynamicLink.setAppName(appName); + assertEquals(iosAutoRedirectDynamicLinkUrl, iosDynamicLink.toString(true)); +} + + +function testDynamicLink_toString_isNotAutoRedirect_android() { + androidDynamicLink = new fireauth.DynamicLink( + fdlDomain, androidPlatform, appIdentifier, authDomain, payload); + androidDynamicLink.setAppName(appName); + assertEquals( + androidUserInteractionDynamicLinkUrl, androidDynamicLink.toString()); +} + + +function testDynamicLink_toString_isNotAutoRedirect_ios() { + iosDynamicLink = new fireauth.DynamicLink( + fdlDomain, iosPlatform, appIdentifier, authDomain, + payload); + iosDynamicLink.setAppName(appName); + assertEquals( + iosUserInteractionDynamicLinkUrl, iosDynamicLink.toString()); +} + + +function testDynamicLink_toString_customSchemeUrl() { + customSchemeLink = new fireauth.DynamicLink( + null, iosPlatform, appIdentifier, authDomain, payload); + customSchemeLink.setAppName(appName); + assertEquals( + customSchemeLinkUrl, customSchemeLink.toString(false)); + assertEquals( + customSchemeLinkUrl, customSchemeLink.toString(true)); +} + + +function testDynamicLink_toString_reverseOAuthClientIdCustomSchemeUrl() { + reverseOAuthClientIdCustomSchemeLink = new fireauth.DynamicLink( + null, iosPlatform, appIdentifier, authDomain, payload, clientId); + reverseOAuthClientIdCustomSchemeLink.setAppName(appName); + assertEquals( + reverseOAuthClientIdCustomSchemeLinkUrl, + reverseOAuthClientIdCustomSchemeLink.toString(false)); + assertEquals( + reverseOAuthClientIdCustomSchemeLinkUrl, + reverseOAuthClientIdCustomSchemeLink.toString(true)); +} + + +function testDynamicLink_parseDeepLink_urlItself() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(deepLink)); +} + + +function testDynamicLink_parseDeepLink_deepLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var url = 'https://example.app.goo.gl/?link=' + encodeURIComponent(deepLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} + + +function testDynamicLink_parseDeepLink_linkWithinLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var linkWithLink = 'https://example.firebaseapp.com/__/auth/callback?link=' + + encodeURIComponent(deepLink); + var url = 'https://example.app.goo.gl/?link=' + + encodeURIComponent(linkWithLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} + + +function testDynamicLink_parseDeepLink_customUrlSchemeDeepLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var url = 'comexampleiosurl://google/link?deep_link_id=' + + encodeURIComponent(deepLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} + + +function testDynamicLink_parseDeepLink_reverseClientIdSchemeDeepLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var url = 'com.googleusercontent.apps.123456://firebaseauth/link?' + + 'deep_link_id=' + encodeURIComponent(deepLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} + + +function testDynamicLink_parseDeepLink_customUrlSchemeLinkWithinLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var linkWithLink = 'https://example.firebaseapp.com/__/auth/callback?link=' + + encodeURIComponent(deepLink); + var url = 'comexampleiosurl://google/link?deep_link_id=' + + encodeURIComponent(linkWithLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} + + +function testDynamicLink_parseDeepLink_clientIdCustomUrlSchemeLinkWithinLink() { + var deepLink = + 'https://example.firebaseapp.com/__/auth/callback#oauthResponse'; + var linkWithLink = 'https://example.firebaseapp.com/__/auth/callback?link=' + + encodeURIComponent(deepLink); + var url = 'com.googleusercontent.apps.123456://firebaseauth/link?' + + 'deep_link_id=' + encodeURIComponent(linkWithLink); + assertEquals(deepLink, fireauth.DynamicLink.parseDeepLink(url)); +} diff --git a/packages/auth/test/error_test.js b/packages/auth/test/error_test.js new file mode 100644 index 00000000000..0a901b27a91 --- /dev/null +++ b/packages/auth/test/error_test.js @@ -0,0 +1,399 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for error_app.js and error_auth.js. + */ + +goog.provide('fireauth.errorTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.FacebookAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.InvalidOriginError'); +goog.require('fireauth.authenum.Error'); +goog.require('goog.object'); +goog.require('goog.string.format'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.errorTest'); + + +function testAuthError() { + var error = new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + assertEquals('auth/internal-error', error['code']); + assertEquals('An internal error has occurred.', error['message']); + // Test toJSON(). + assertObjectEquals({ + code: error['code'], + message: error['message'] + }, error.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); +} + + +function testAuthError_errorTranslation_match() { + var error = new fireauth.AuthError(fireauth.authenum.Error.USER_DELETED); + // Translate USER_DELETED to USER_MISMATCH. + var translatedError = fireauth.AuthError.translateError( + error, + fireauth.authenum.Error.USER_DELETED, + fireauth.authenum.Error.USER_MISMATCH); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.USER_MISMATCH); + // Expected new error should be returned. + assertEquals( + JSON.stringify(expectedError), JSON.stringify(translatedError.toJSON())); +} + + +function testAuthError_errorTranslation_mismatch() { + var error = new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR); + // Translate USER_DELETED to USER_MISMATCH. + var translatedError = fireauth.AuthError.translateError( + error, + fireauth.authenum.Error.USER_DELETED, + fireauth.authenum.Error.USER_MISMATCH); + // Same error should be returned. + assertEquals(error, translatedError); +} + + +function testAuthErrorWithCredential() { + var credential = fireauth.FacebookAuthProvider.credential('ACCESS_TOKEN'); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com', + credential: credential + }, + 'Account already exists, please confirm and link.'); + + assertEquals('user@example.com', error['email']); + assertUndefined(error['phoneNumber']); + assertEquals('auth/account-exists-with-different-credential', error['code']); + assertEquals(credential, error['credential']); + assertEquals( + 'Account already exists, please confirm and link.', error['message']); + // Test toJSON(). + assertObjectEquals({ + code: error['code'], + message: error['message'], + email: 'user@example.com', + providerId: 'facebook.com', + oauthAccessToken: 'ACCESS_TOKEN' + }, error.toJSON()); + assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); +} + + +function testInvalidOriginError() { + // HTTP origin. + var error = new fireauth.InvalidOriginError('http://www.example.com'); + assertEquals('auth/unauthorized-domain', error['code']); + assertEquals( + 'This domain (www.example.com) is not authorized to run this operation' + + '. Add it to the OAuth redirect domains list in the Firebase console -' + + '> Auth section -> Sign in method tab.', + error['message']); + // File origin. + var error2 = new fireauth.InvalidOriginError('file://path/index.html'); + assertEquals( + 'auth/operation-not-supported-in-this-environment', error2['code']); + assertEquals( + 'This operation is not supported in the environment this application i' + + 's running on. "location.protocol" must be http, https or chrome-exten' + + 'sion and web storage must be enabled.', + error2['message']); + // Test toJSON(). + assertObjectEquals({ + code: error['code'], + message: error['message'] + }, error.toJSON()); + assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); + // Chrome extension origin. + error3 = new fireauth.InvalidOriginError('chrome-extension://1234567890'); + assertEquals('auth/unauthorized-domain', error3['code']); + assertEquals( + 'This chrome extension ID (chrome-extension://1234567890) is not autho' + + 'rized to run this operation. Add it to the OAuth redirect domains lis' + + 't in the Firebase console -> Auth section -> Sign in method tab.', + error3['message']); +} + + +/** + * The allowed characters in the error code, as per style guide. + */ +var ERROR_CODE_FORMAT = /^[a-z\-\/]+$/; + + +/** + * Asserts that the error codes conform to the Firebase JS error format. + * @param {!Object} codes + */ +function assertErrorCodesHaveCorrectFormat(codes) { + goog.object.forEach(codes, function(code) { + var assertMessage = goog.string.format('Error code %s should only ' + + 'contain lower-case ASCII characters, forward slashes, and hyphens.', + code); + assertTrue(assertMessage, ERROR_CODE_FORMAT.test(code)); + }); +} + + +function testAuthErrorCodeFormat() { + assertErrorCodesHaveCorrectFormat(fireauth.authenum.Error); +} + + +function testAuthError_toPlainObject() { + var authError = new fireauth.AuthError('error1', 'message1'); + var authErrorObject = { + 'code': 'auth/error1', + 'message': 'message1' + }; + assertObjectEquals( + authErrorObject, + authError.toPlainObject()); +} + + +function testInvalidOriginError_toPlainObject() { + var invalidOriginError = new fireauth.InvalidOriginError( + 'http://www.example.com'); + var invalidOriginErrorObject = { + 'code': 'auth/unauthorized-domain', + 'message': invalidOriginError['message'] + }; + assertObjectEquals( + invalidOriginErrorObject, + invalidOriginError.toPlainObject()); +} + + +function testAuthErrorWithCredential_toPlainObject() { + var credential = fireauth.FacebookAuthProvider.credential('ACCESS_TOKEN'); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com', + credential: credential + }, + 'Account already exists, please confirm and link.'); + var errorObject = { + 'code': 'auth/account-exists-with-different-credential', + 'email': 'user@example.com', + 'message': 'Account already exists, please confirm and link.', + 'providerId': 'facebook.com', + 'oauthAccessToken': 'ACCESS_TOKEN' + }; + assertObjectEquals( + errorObject, + error.toPlainObject()); + + // Test with no credential and default message to be used. + var error2 = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com' + }, + null); + var errorObject2 = { + 'code': 'auth/account-exists-with-different-credential', + 'email': 'user@example.com', + 'message': 'An account already exists with the same email address but ' + + 'different sign-in credentials. Sign in using a provider associated wi' + + 'th this email address.' + }; + assertObjectEquals( + errorObject2, + error2.toPlainObject()); + + // Credential with ID Token. + var credential3 = fireauth.GoogleAuthProvider.credential('ID_TOKEN'); + var error3 = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.EMAIL_EXISTS, + { + email: 'user@example.com', + credential: credential3 + }, + 'The email address is already in use by another account.'); + var errorObject3 = { + 'code': 'auth/email-already-in-use', + 'email': 'user@example.com', + 'message': 'The email address is already in use by another account.', + 'providerId': 'google.com', + 'oauthIdToken': 'ID_TOKEN' + }; + assertObjectEquals( + errorObject3, + error3.toPlainObject()); +} + + +function testAuthErrorWithCredential_fromPlainObject() { + var credential = fireauth.FacebookAuthProvider.credential('ACCESS_TOKEN'); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com', + credential: credential + }, + 'Account already exists, please confirm and link.'); + var errorObject = { + 'code': 'auth/account-exists-with-different-credential', + 'email': 'user@example.com', + 'message': 'Account already exists, please confirm and link.', + 'providerId': 'facebook.com', + 'oauthAccessToken': 'ACCESS_TOKEN' + }; + var errorObject2 = { + 'email': 'user@example.com', + 'providerId': 'facebook.com', + 'oauthAccessToken': 'ACCESS_TOKEN' + }; + var internalError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + var needConfirmationError = new fireauth.AuthError( + fireauth.authenum.Error.NEED_CONFIRMATION); + // Empty response will return an invalid error. + assertNull(fireauth.AuthErrorWithCredential.fromPlainObject({})); + // Response with no error code is invalid. + assertNull(fireauth.AuthErrorWithCredential.fromPlainObject(errorObject2)); + // Regular error. + assertObjectEquals( + internalError, + fireauth.AuthErrorWithCredential.fromPlainObject( + {'code': 'auth/internal-error'})); + // Confirmation error with no credential or email will return a regular error. + assertObjectEquals( + needConfirmationError, + fireauth.AuthErrorWithCredential.fromPlainObject( + {'code': 'auth/account-exists-with-different-credential'})); + // Auth email credential error. + assertObjectEquals( + error, + fireauth.AuthErrorWithCredential.fromPlainObject(errorObject)); + + // Credential with ID token. + var credential3 = fireauth.GoogleAuthProvider.credential('ID_TOKEN'); + var error3 = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + email: 'user@example.com', + credential: credential3 + }, + 'This credential is already associated with a different user account.'); + var errorObject3 = { + 'code': 'auth/credential-already-in-use', + 'email': 'user@example.com', + 'message': 'This credential is already associated with a different user ' + + 'account.', + 'providerId': 'google.com', + 'oauthIdToken': 'ID_TOKEN' + }; + var errorObject3NoPrefix = { + 'code': 'credential-already-in-use', + 'email': 'user@example.com', + 'message': 'This credential is already associated with a different user ' + + 'account.', + 'providerId': 'google.com', + 'oauthIdToken': 'ID_TOKEN' + }; + assertObjectEquals( + error3, + fireauth.AuthErrorWithCredential.fromPlainObject(errorObject3)); + // If the error code prefix is missing. + assertObjectEquals( + error3, + fireauth.AuthErrorWithCredential.fromPlainObject(errorObject3NoPrefix)); +} + + +function testAuthErrorWithCredential_phoneCredential() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + phoneNumber: phoneNumber, + credential: credential + }); + + assertEquals(phoneNumber, error['phoneNumber']); + assertEquals(credential, error['credential']); + assertUndefined(error['email']); +} + + +function testAuthErrorWithCredential_phoneCredential_fromPlainObject() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + phoneNumber: phoneNumber, + credential: credential + }); + var errorObject = { + 'code': 'auth/credential-already-in-use', + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }; + assertObjectEquals( + error, + fireauth.AuthErrorWithCredential.fromPlainObject(errorObject)); +} + + +function testAuthErrorWithCredential_phoneCredential_toPlainObject() { + var temporaryProof = 'theTempProof'; + var phoneNumber = '+16505550101'; + var credential = fireauth.AuthProvider.getCredentialFromResponse({ + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber + }); + var error = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + phoneNumber: phoneNumber, + credential: credential + }); + var errorObject = { + 'code': 'auth/credential-already-in-use', + 'message': 'This credential is already associated with a different user ' + + 'account.', + 'temporaryProof': temporaryProof, + 'phoneNumber': phoneNumber, + 'providerId': 'phone' + }; + assertObjectEquals( + errorObject, + error.toPlainObject()); +} + diff --git a/packages/auth/test/exports_lib_test.js b/packages/auth/test/exports_lib_test.js new file mode 100644 index 00000000000..5f599992235 --- /dev/null +++ b/packages/auth/test/exports_lib_test.js @@ -0,0 +1,264 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.exportlibTest'); + +goog.require('fireauth.args'); +goog.require('fireauth.exportlib'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.exportlibTest'); + +var Dog; +var obj; +var Provider; + +function setUp() { + Dog = function(name) { + this.name_ = name; + }; + + Dog.prototype.talk = function() { + return 'Woof'; + }; + + Dog.prototype.eat = function(food) { + return '*' + this.name_ + ' eats ' + food + '*'; + }; + + obj = { + myFunc: function(myBool) { + return 'I got the boolean ' + myBool; + } + }; + Provider = function() { + this.scopes_ = []; + }; + Provider.credential = function(cred) { + return { + 'cred': cred + }; + }; + Provider.prototype.addScope = function(scope) { + this.scopes_.push(scope); + }; + Provider.prototype.getScopes = function() { + return this.scopes_; + }; + Provider.PROVIDER_ID = 'providerId'; +} + + +function testWrapMethodWithArgumentVerifier_wrapperStaticAndProto_valid() { + fireauth.exportlib.wrapMethodWithArgumentVerifier_( + 'Provider', Provider, []); + fireauth.exportlib.wrapMethodWithArgumentVerifier_( + 'Provider.prototype.addScope', + Provider.prototype.addScope, [ + fireauth.args.string('scope') + ]); + fireauth.exportlib.wrapMethodWithArgumentVerifier_( + 'Provider.prototype.getScopes', + Provider.prototype.getScopes, []); + fireauth.exportlib.wrapMethodWithArgumentVerifier_( + 'Provider.credential', + Provider.credential, [ + fireauth.args.object() + ]); + var provider = new Provider(); + provider.addScope('s1'); + provider.addScope('s2'); + assertArrayEquals(['s1', 's2'], provider.getScopes()); + assertObjectEquals({'cred': 'something'}, Provider.credential('something')); + assertEquals('providerId', Provider.PROVIDER_ID); +} + + +function testWrapMethodWithArgumentVerifier_constructor_noArgs_valid() { + var ExportedDog = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('Dog', Dog, + [fireauth.args.string('name')]); + new ExportedDog('Snoopy'); +} + + +function testWrapMethodWithArgumentVerifier_constructor_noArgs_invalid() { + var ExportedDog = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('Dog', Dog, + [fireauth.args.string('name')]); + assertThrows(function() { + new ExportedDog(); + }); +} + + +function testWrapMethodWithArgumentVerifier_method_noArgs_valid() { + Dog.prototype.exportedTalk = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('talk', + Dog.prototype.talk, []); + var snoopy = new Dog('Snoopy'); + assertEquals('Woof', snoopy.exportedTalk()); +} + + +function testWrapMethodWithArgumentVerifier_method_noArgs_invalid() { + Dog.prototype.exportedTalk = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('talk', + Dog.prototype.talk, []); + var snoopy = new Dog('Snoopy'); + assertThrows(function() { + snoopy.exportedTalk('badArgument'); + }); +} + + +function testWrapMethodWithArgumentVerifier_method_oneArg_valid() { + Dog.prototype.exportedEat = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('eat', + Dog.prototype.eat, + [fireauth.args.string('food')]); + var snoopy = new Dog('Snoopy'); + assertEquals('*Snoopy eats pizza*', snoopy.exportedEat('pizza')); +} + + +function testWrapMethodWithArgumentVerifier_method_oneArg_invalid() { + Dog.prototype.exportedEat = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('eat', + Dog.prototype.eat, + [fireauth.args.string('food')]); + assertThrows(function() { + snoopy.exportedEat(13); + }); +} + + +function testWrapMethodWithArgumentVerifier_static_oneArg_valid() { + obj.exportedFunc = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('myFunc', obj.myFunc, + [fireauth.args.bool('myBool')]); + assertEquals('I got the boolean true', obj.exportedFunc(true)); +} + + +function testWrapMethodWithArgumentVerifier_static_oneArg_invalid() { + obj.exportedFunc = + fireauth.exportlib.wrapMethodWithArgumentVerifier_('myFunc', obj.myFunc, + [fireauth.args.bool('myBool')]); + assertThrows(function() { + assertEquals('I got the boolean true', obj.exportedFunc('hello')); + }); +} + + +function testExportPrototypeProperties() { + var obj = { + originalProp: 10 + }; + fireauth.exportlib.exportPrototypeProperties(obj, { + originalProp: { + name: 'newProp', + arg: fireauth.args.number('newProp') + } + }); + + assertEquals(10, obj.originalProp); + assertEquals(10, obj['newProp']); + + // Changing the new property should update the old. + obj['newProp'] = 20; + assertEquals(20, obj.originalProp); + assertEquals(20, obj['newProp']); + + // Changing the old property should update the new. + obj.originalProp = 30; + assertEquals(30, obj.originalProp); + assertEquals(30, obj['newProp']); + + // Check argument validation. + assertThrows(function() { + obj['newProp'] = false; + }); + // Previous value should remain. + assertEquals(30, obj['newProp']); +} + + +/** + * Tests that exportPrototypeProperties works when run on an object prototype. + */ +function testExportPrototypeProperties_prototype() { + var Obj = function() {}; + Obj.prototype.originalProp = 10; + fireauth.exportlib.exportPrototypeProperties(Obj.prototype, { + originalProp: { + name: 'newProp', + arg: fireauth.args.number('newProp') + } + }); + var obj = new Obj(); + + assertEquals(10, obj.originalProp); + assertEquals(10, obj['newProp']); + + // Changing the new property should update the old. + obj['newProp'] = 20; + assertEquals(20, obj.originalProp); + assertEquals(20, obj['newProp']); + + // Changing the old property should update the new. + obj.originalProp = 30; + assertEquals(30, obj.originalProp); + assertEquals(30, obj['newProp']); + + // Check argument validation. + assertThrows(function() { + obj['newProp'] = false; + }); + // Previous value should remain. + assertEquals(30, obj['newProp']); +} + + +/** + * Tests that exportPrototypeProperties works when you try to export a property + * without changing the symbol. + */ +function testExportPrototypeProperties_exportSelf() { + var Obj = function() { + this['propName'] = 10; + }; + fireauth.exportlib.exportPrototypeProperties(Obj.prototype, { + propName: { + name: 'propName', + arg: fireauth.args.number('newProp') + } + }); + var obj = new Obj(); + + assertEquals(10, obj['propName']); + + // We should still be able to change the property. + obj['propName'] = 20; + assertEquals(20, obj['propName']); + + // Check argument validation. This should not throw since the property name is + // the same. + assertNotThrows(function() { + obj['newProp'] = false; + }); + assertEquals(false, obj['newProp']); +} diff --git a/packages/auth/test/idp_test.js b/packages/auth/test/idp_test.js new file mode 100644 index 00000000000..8595d7c325e --- /dev/null +++ b/packages/auth/test/idp_test.js @@ -0,0 +1,81 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for idp.js + */ + +goog.provide('fireauth.idpTest'); + +goog.require('fireauth.idp'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.idpTest'); + + +function testGetIdpSetting_unknown() { + var settings = fireauth.idp.getIdpSettings('unknown'); + assertNull(settings); + assertArrayEquals( + [], + fireauth.idp.getReservedOAuthParams('unknown')); + settings = fireauth.idp.getIdpSettings('password'); + assertArrayEquals( + [], + fireauth.idp.getReservedOAuthParams('password')); + assertNull(settings); + assertArrayEquals( + [], + fireauth.idp.getReservedOAuthParams('anonymous')); +} + + +function testGetIdpSetting_google() { + var settings = fireauth.idp.getIdpSettings('google.com'); + assertObjectEquals(fireauth.idp.Settings.GOOGLE, settings); + assertArrayEquals( + ['client_id', 'response_type', 'scope', 'redirect_uri', 'state'], + fireauth.idp.getReservedOAuthParams('google.com')); +} + + +function testGetIdpSetting_facebook() { + var settings = fireauth.idp.getIdpSettings('facebook.com'); + assertObjectEquals(fireauth.idp.Settings.FACEBOOK, settings); + assertArrayEquals( + ['client_id', 'response_type', 'scope', 'redirect_uri', 'state'], + fireauth.idp.getReservedOAuthParams('facebook.com')); +} + + +function testGetIdpSetting_github() { + var settings = fireauth.idp.getIdpSettings('github.com'); + assertObjectEquals(fireauth.idp.Settings.GITHUB, settings); + assertArrayEquals( + ['client_id', 'response_type', 'scope', 'redirect_uri', 'state'], + fireauth.idp.getReservedOAuthParams('github.com')); +} + + +function testGetIdpSetting_twitter() { + var settings = fireauth.idp.getIdpSettings('twitter.com'); + assertObjectEquals(fireauth.idp.Settings.TWITTER, settings); + assertArrayEquals( + ['oauth_consumer_key', 'oauth_nonce', 'oauth_signature', + 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', + 'oauth_version'], + fireauth.idp.getReservedOAuthParams('twitter.com')); +} diff --git a/packages/auth/test/idtoken_test.js b/packages/auth/test/idtoken_test.js new file mode 100644 index 00000000000..058e2a64438 --- /dev/null +++ b/packages/auth/test/idtoken_test.js @@ -0,0 +1,242 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for idtoken.js + */ + +goog.provide('fireauth.IdTokenTest'); + +goog.require('fireauth.IdToken'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.IdTokenTest'); + + +// exp: 1326439044 +// sub: "679" +// aud: "204241631686" +// provider_id: "gmail.com" +// email: "test123456@gmail.com" +// federated_id: "https://www.google.com/accounts/123456789" +var tokenGmail = 'HEADER.ew0KICAiaXNzIjogIkdJVGtpdCIsDQogICJleHAiOiAxMzI2NDM5' + + 'MDQ0LA0KICAic3ViIjogIjY3OSIsDQogICJhdWQiOiAiMjA0MjQxNjMxNjg2IiwNCiAgImZl' + + 'ZGVyYXRlZF9pZCI6ICJodHRwczovL3d3dy5nb29nbGUuY29tL2FjY291bnRzLzEyMzQ1Njc4' + + 'OSIsDQogICJwcm92aWRlcl9pZCI6ICJnbWFpbC5jb20iLA0KICAiZW1haWwiOiAidGVzdDEy' + + 'MzQ1NkBnbWFpbC5jb20iDQp9.SIGNATURE'; + + +// exp: 1326446190 +// sub: "274" +// aud: "204241631686" +// provider_id: "yahoo.com" +// email: "user123@yahoo.com" +// federated_id: "https://me.yahoo.com/whoamiwhowhowho#4a4ac" +var tokenYahoo = 'HEADER.ew0KICAiaXNzIjogIkdJVGtpdCIsDQogICJleHAiOiAxMzI2NDQ2' + + 'MTkwLA0KICAic3ViIjogIjI3NCIsDQogICJhdWQiOiAiMjA0MjQxNjMxNjg2IiwNCiAgImZl' + + 'ZGVyYXRlZF9pZCI6ICJodHRwczovL21lLnlhaG9vLmNvbS93aG9hbWl3aG93aG93aG8jNGE0' + + 'YWMiLA0KICAicHJvdmlkZXJfaWQiOiAieWFob28uY29tIiwNCiAgImVtYWlsIjogInVzZXIx' + + 'MjNAeWFob28uY29tIg0KfQ==.SIGNATURE'; + + +// iss: "https://identitytoolkit.google.com/" +// aud: "12345678.apps.googleusercontent.com" +// iat: 1441246088 +// exp: 2442455688 +// sub: "1458474" +// email: "testuser@gmail.com" +// provider_id: "google.com" +// verified: true +// display_name: "John Doe" +// photo_url: "https://lh5.googleusercontent.com/1458474/photo.jpg" +var tokenGoogleWithFederatedId = 'HEADER.ew0KICAiaXNzIjogImh0dHBzOi8vaWRlbnRp' + + 'dHl0b29sa2l0Lmdvb2dsZS5jb20vIiwNCiAgImF1ZCI6ICIxMjM0NTY3OC5hcHBzLmdvb2ds' + + 'ZXVzZXJjb250ZW50LmNvbSIsDQogICJpYXQiOiAxNDQxMjQ2MDg4LA0KICAiZXhwIjogMjQ0' + + 'MjQ1NTY4OCwNCiAgInN1YiI6ICIxNDU4NDc0IiwNCiAgImVtYWlsIjogInRlc3R1c2VyQGdt' + + 'YWlsLmNvbSIsDQogICJwcm92aWRlcl9pZCI6ICJnb29nbGUuY29tIiwNCiAgInZlcmlmaWVk' + + 'IjogdHJ1ZSwNCiAgImRpc3BsYXlfbmFtZSI6ICJKb2huIERvZSIsDQogICJwaG90b191cmwi' + + 'OiAiaHR0cHM6Ly9saDUuZ29vZ2xldXNlcmNvbnRlbnQuY29tLzE0NTg0NzQvcGhvdG8uanBn' + + 'Ig0KfQ==.SIGNATURE'; + + +// exp: 1326446190 +// sub: "365" +// aud: "204241631686" +// is_anonymous: true +var tokenAnonymous = 'HEAD.eyJpc3MiOiJHSVRraXQiLCJleHAiOjEzMjY0NDYxOTAsInN1Yi' + + 'I6IjM2NSIsImF1ZCI6IjIwNDI0MTYzMTY4NiIsImlzX2Fub255bW91cyI6dHJ1ZX0' + + '.SIGNATURE'; + + +// iss: "https://securetoken.google.com/projectId" +// aud: "projectId" +// auth_time: 1506050282 +// user_id: "123456" +// sub: "123456" +// iat: 1506050283 +// exp: 1506053883 +// email: "user@example.com" +// email_verified: false +// phone_number: "+11234567890" +// firebase: {identities: {phone: ["+11234567890"], +// email: ["user@example.com"] +// }, sign_in_provider: "phone"} +var tokenPhone = 'HEAD.ew0KICAiaXNzIjogImh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLm' + + 'NvbS9wcm9qZWN0SWQiLA0KICAiYXVkIjogInByb2plY3RJZCIsDQogICJhdXRoX3RpbWUiOi' + + 'AxNTA2MDUwMjgyLA0KICAidXNlcl9pZCI6ICIxMjM0NTYiLA0KICAic3ViIjogIjEyMzQ1Ni' + + 'IsDQogICJpYXQiOiAxNTA2MDUwMjgzLA0KICAiZXhwIjogMTUwNjA1Mzg4MywNCiAgImVtYW' + + 'lsIjogInVzZXJAZXhhbXBsZS5jb20iLA0KICAiZW1haWxfdmVyaWZpZWQiOiBmYWxzZSwNCi' + + 'AgInBob25lX251bWJlciI6ICIrMTEyMzQ1Njc4OTAiLA0KICAiZmlyZWJhc2UiOiB7DQogIC' + + 'AgImlkZW50aXRpZXMiOiB7DQogICAgICAicGhvbmUiOiBbDQogICAgICAgICIrMTEyMzQ1Nj' + + 'c4OTAiDQogICAgICBdLA0KICAgICAgImVtYWlsIjogWw0KICAgICAgICAidXNlckBleGFtcG' + + 'xlLmNvbSINCiAgICAgIF0NCiAgICB9LA0KICAgICJzaWduX2luX3Byb3ZpZGVyIjogInBob2' + + '5lIg0KICB9DQp9.SIGNATURE'; + + +/** + * Asserts the values in the token provided. + * @param {!fireauth.IdToken} token The ID token to assert. + * @param {?string} email The expected email. + * @param {number} exp The expected expiration field. + * @param {?string} providerId The expected provider ID. + * @param {?string} displayName The expected display name. + * @param {?string} photoURL The expected photo URL. + * @param {boolean} anonymous The expected anonymous status. + * @param {string} localId The expected user ID. + * @param {?string} federatedId The expected federated ID. + * @param {boolean} verified The expected verified status. + * @param {?string} phoneNumber The expected phone number. + */ +function assertToken( + token, + email, + exp, + providerId, + displayName, + photoURL, + anonymous, + localId, + federatedId, + verified, + phoneNumber) { + assertEquals(email, token.getEmail()); + assertEquals(exp, token.getExp()); + assertEquals(providerId, token.getProviderId()); + assertEquals(displayName, token.getDisplayName()); + assertEquals(photoURL, token.getPhotoUrl()); + assertEquals(localId, token.getLocalId()); + assertEquals(federatedId, token.getFederatedId()); + assertEquals(anonymous, token.isAnonymous()); + assertEquals(verified, token.isVerified()); + assertEquals(phoneNumber, token.getPhoneNumber()); +} + + +function testParse_invalid() { + assertNull(fireauth.IdToken.parse('gegege.invalid.ggrgheh')); +} + + +function testParse_anonymous() { + var token = fireauth.IdToken.parse(tokenAnonymous); + assertToken( + token, + null, + 1326446190, + null, + null, + null, + true, + '365', + null, + false, + null); +} + + +function testParse_needPadding() { + var token = fireauth.IdToken.parse(tokenGmail); + assertToken( + token, + 'test123456@gmail.com', + 1326439044, + 'gmail.com', + null, + null, + false, + '679', + 'https://www.google.com/accounts/123456789', + false, + null); + assertTrue(token.isExpired()); +} + + +function testParse_noPadding() { + var token = fireauth.IdToken.parse(tokenYahoo); + assertToken( + token, + 'user123@yahoo.com', + 1326446190, + 'yahoo.com', + null, + null, + false, + '274', + 'https://me.yahoo.com/whoamiwhowhowho#4a4ac', + false, + null); + assertTrue(token.isExpired()); +} + + +function testParse_unexpired() { + // This token will expire in year 2047. + var token = fireauth.IdToken.parse(tokenGoogleWithFederatedId); + assertToken( + token, + 'testuser@gmail.com', + 2442455688, + 'google.com', + 'John Doe', + 'https://lh5.googleusercontent.com/1458474/photo.jpg', + false, + '1458474', + null, + true, + null); + // Check issuer of token. + assertEquals('https://identitytoolkit.google.com/', token.getIssuer()); + assertFalse(token.isExpired()); +} + + +function testParse_phoneAndFirebaseProviderId() { + var token = fireauth.IdToken.parse(tokenPhone); + assertToken( + token, + 'user@example.com', + 1506053883, + 'phone', + null, + null, + false, + '123456', + null, + false, + '+11234567890'); + assertEquals('https://securetoken.google.com/projectId', token.getIssuer()); +} diff --git a/packages/auth/test/iframeclient/ifchandler_test.js b/packages/auth/test/iframeclient/ifchandler_test.js new file mode 100644 index 00000000000..6e73cae31c9 --- /dev/null +++ b/packages/auth/test/iframeclient/ifchandler_test.js @@ -0,0 +1,1588 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for ifchandler.js + */ + +goog.provide('fireauth.iframeclient.IfcHandlerTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.EmailAuthProvider'); +goog.require('fireauth.FacebookAuthProvider'); +goog.require('fireauth.FederatedProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.InvalidOriginError'); +goog.require('fireauth.OAuthProvider'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.TwitterAuthProvider'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.constants'); +goog.require('fireauth.iframeclient.IfcHandler'); +goog.require('fireauth.iframeclient.IframeUrlBuilder'); +goog.require('fireauth.iframeclient.IframeWrapper'); +goog.require('fireauth.iframeclient.OAuthUrlBuilder'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.iframeclient.IfcHandlerTest'); + + +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +var stubs = new goog.testing.PropertyReplacer(); +var ifcHandler; +var authDomain = 'subdomain.firebaseapp.com'; +var apiKey = 'apiKey1'; +var appName = 'appName1'; +var authEvent; +var eventsMap = {}; +var version = '3.0.0'; +var clock; +var mockControl; +var ignoreArgument; + +function setUp() { + eventsMap = {}; + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'open_', + function() { + return goog.Promise.resolve(); + }); + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, 'onReady', + goog.testing.recordFunction(goog.Promise.resolve)); + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'registerEvent', + function(eventName, handler) { + eventsMap[eventName] = handler; + }); + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'sendMessage', + function(message) { + assertEquals('webStorageSupport', message['type']); + var response = [{'status': 'ACK', 'webStorageSupport': true}]; + return goog.Promise.resolve(response); + }); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +function tearDown() { + stubs.reset(); + ifcHandler = null; + goog.dispose(clock); + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } +} + + +/** + * Simulates the current Auth language/frameworks on the specified App instance. + * @param {string} appName The expected App instance identifier. + * @param {?string} languageCode The default Auth language. + * @param {?Array} frameworks The list of frameworks on the Auth + * instance. + */ +function simulateAuthService(appName, languageCode, frameworks) { + stubs.replace( + firebase, + 'app', + function(name) { + assertEquals(appName, name); + return { + auth: function() { + return { + getLanguageCode: function() { + return languageCode; + }, + getFramework: function() { + return frameworks || []; + } + }; + } + }; + }); +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +function assertErrorEquals(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +} + + +function testIframeUrlBuilder() { + var builder = new fireauth.iframeclient.IframeUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'MY_APP'); + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP', + builder.setVersion(null).toString()); + // Set version parameter. + builder.setVersion('3.4.0'); + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&v=3.4.0', + builder.toString()); + // Modify version parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&v=3.4.1', + builder.setVersion('3.4.1').toString()); + // Remove version parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP', + builder.setVersion(null).toString()); + // Set eid parameter. + builder.setEndpointId('p'); + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&eid=p', + builder.toString()); + // Modify eid parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&eid=s', + builder.setEndpointId('s').toString()); + // Remove eid parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP', + builder.setEndpointId(null).toString()); + // Set fw parameter. + builder.setFrameworks(['f1', 'f2']); + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&fw=' + encodeURIComponent('f1,f2'), + builder.toString()); + // Modify fw parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP&fw=f3', + builder.setFrameworks(['f3']).toString()); + // Remove fw parameter. + assertEquals( + 'https://example.firebaseapp.com/__/auth/iframe?apiKey=API_KEY&appName' + + '=MY_APP', + builder.setFrameworks(null).toString()); +} + + +function testOAuthUrlBuilder() { + var redirectUrl = 'http://www.example.com/redirect?a=b#c'; + var redirectUrl2 = 'http://www.example.com/redirect2?d=e#f'; + var provider = new fireauth.GoogleAuthProvider(); + var additionalParams = { + // This entry should be ignored. + 'apiKey': 'OTHER_KEY', + 'apn': 'com.example.android', + 'sessionId': '1234' + }; + var additionalParams2 = { + 'ibi': 'com.example.ios', + 'sessionId': '5678' + }; + provider.addScope('scope1'); + provider.addScope('scope2'); + var customParams = { + 'hd': 'example.com', + 'login_hint': 'user@example.com', + 'state': 'bla' + }; + var filteredCustomParams = { + 'hd': 'example.com', + 'login_hint': 'user@example.com' + }; + provider.setCustomParameters(customParams); + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'APP_NAME', 'signInWithPopup', + provider); + var partialUrl = 'https://example.firebaseapp.com/__/auth/handler?apiKey=A' + + 'PI_KEY&appName=APP_NAME&authType=signInWithPopup&providerId=google.co' + + 'm&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON(filteredCustomParams)) + + '&scopes=' + encodeURIComponent('profile,scope1,scope2'); + assertEquals(partialUrl, builder.toString()); + // Add redirect URL. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl), + builder.setRedirectUrl(redirectUrl).toString()); + // Modify redirect URL. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2), + builder.setRedirectUrl(redirectUrl2).toString()); + // Add eventId. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=1234', + builder.setEventId('1234').toString()); + // Modify eventId. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678', + builder.setEventId('5678').toString()); + // Add version. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678&v=3.4.0', + builder.setVersion('3.4.0').toString()); + // Modify version. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678&v=3.4.1', + builder.setVersion('3.4.1').toString()); + // Add additional parameters. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678&v=3.4.1&apn=' + encodeURIComponent('com.example.android') + + '&sessionId=1234', + builder.setAdditionalParameters(additionalParams).toString()); + // Modify additional parameters. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678&v=3.4.1&ibi=' + encodeURIComponent('com.example.ios') + + '&sessionId=5678', + builder.setAdditionalParameters(additionalParams2).toString()); + // Delete additional parameters. + assertEquals( + partialUrl + '&redirectUrl=' + encodeURIComponent(redirectUrl2) + + '&eventId=5678&v=3.4.1', + builder.setAdditionalParameters(null).toString()); + // Delete redirect URL. + assertEquals( + partialUrl + '&eventId=5678&v=3.4.1', + builder.setRedirectUrl(null).toString()); + // Delete eventId. + assertEquals( + partialUrl + '&v=3.4.1', + builder.setEventId(null).toString()); + // Delete version. + assertEquals(partialUrl, builder.setVersion(null).toString()); + // Set eid parameter. + builder.setEndpointId('p'); + assertEquals( + partialUrl + '&eid=p', + builder.toString()); + // Modify eid parameter. + assertEquals( + partialUrl + '&eid=s', + builder.setEndpointId('s').toString()); + // Remove eid parameter. + assertEquals( + partialUrl, + builder.setEndpointId(null).toString()); + + // Simulate frameworks logged. + var frameworks = ['firebaseui', 'angularfire']; + simulateAuthService('APP_NAME', null, frameworks); + assertEquals( + partialUrl + '&fw=' + encodeURIComponent(frameworks.join(',')), + builder.toString()); + // Remove frameworks. + simulateAuthService('APP_NAME', null, []); + assertEquals(partialUrl, builder.toString()); +} + + +function testOAuthUrlBuilder_localization() { + var expectedUrl; + var provider = new fireauth.GoogleAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + // Custom parameters with no language field as provided by the developer. + var customParams = { + 'hd': 'example.com', + 'login_hint': 'user@example.com', + 'state': 'bla' + }; + // Custom parameters with a language field as provided by the developer. + var customParamsWithLang = { + 'hd': 'example.com', + 'login_hint': 'user@example.com', + 'state': 'bla', + 'hl': 'de' + }; + // Expected filtered custom parameters with the default language added. + var expectedCustomParams = { + 'hd': 'example.com', + 'login_hint': 'user@example.com', + // Default language set. + 'hl': 'fr' + }; + // Expected filtered custom parameters with no language added. + var expectedCustomParamsWithoutLang = { + 'hd': 'example.com', + 'login_hint': 'user@example.com' + }; + // Expected filtered custom parameters with non-default language added. + var expectedCustomParamsWithLang = { + 'hd': 'example.com', + 'login_hint': 'user@example.com', + // Default language overridden. + 'hl': 'de' + }; + // OAuth URL builder. + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'APP_NAME', 'signInWithPopup', + provider); + + // Simulate Auth language set. + simulateAuthService('APP_NAME', 'fr'); + // Pass custom parameters with no language field. + provider.setCustomParameters(customParams); + // Expected URL should include the default language. + expectedUrl = 'https://example.firebaseapp.com/__/auth/handler?apiKey=A' + + 'PI_KEY&appName=APP_NAME&authType=signInWithPopup&providerId=google.co' + + 'm&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON(expectedCustomParams)) + + '&scopes=' + encodeURIComponent('profile,scope1,scope2'); + assertEquals(expectedUrl, builder.toString()); + + // Modify custom parameters to include a language parameter. + provider.setCustomParameters(customParamsWithLang); + // Expected URL should include the non-default language. + expectedUrl = 'https://example.firebaseapp.com/__/auth/handler?apiKey=A' + + 'PI_KEY&appName=APP_NAME&authType=signInWithPopup&providerId=google.co' + + 'm&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON( + expectedCustomParamsWithLang)) + + '&scopes=' + encodeURIComponent('profile,scope1,scope2'); + assertEquals(expectedUrl, builder.toString()); + + // Simulate no default language. + simulateAuthService('APP_NAME', null); + // Set custom parameters without a language field. + provider.setCustomParameters(customParams); + expectedUrl = 'https://example.firebaseapp.com/__/auth/handler?apiKey=A' + + 'PI_KEY&appName=APP_NAME&authType=signInWithPopup&providerId=google.co' + + 'm&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON( + expectedCustomParamsWithoutLang)) + + '&scopes=' + encodeURIComponent('profile,scope1,scope2'); + assertEquals(expectedUrl, builder.toString()); +} + + +function testOAuthUrlBuilder_genericIdp() { + var provider = new fireauth.OAuthProvider('idp.com'); + provider.addScope('thescope'); + var customParams = { + 'hl': 'es' + }; + provider.setCustomParameters(customParams); + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'APP_NAME', 'signInWithPopup', + provider); + var url = 'https://example.firebaseapp.com/__/auth/handler?' + + 'apiKey=API_KEY&appName=APP_NAME&authType=signInWithPopup&' + + 'providerId=idp.com&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON(customParams)) + + '&scopes=thescope'; + assertEquals(url, builder.toString()); +} + + +function testOAuthUrlBuilder_twitter() { + var provider = new fireauth.TwitterAuthProvider(); + var customParams = { + 'lang': 'es' + }; + provider.setCustomParameters(customParams); + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'APP_NAME', 'signInWithPopup', + provider); + var url = 'https://example.firebaseapp.com/__/auth/handler?' + + 'apiKey=API_KEY&appName=APP_NAME&authType=signInWithPopup&' + + 'providerId=twitter.com&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON(customParams)); + assertEquals(url, builder.toString()); +} + + +function testOAuthUrlBuilder_notOAuthProviderInstance() { + // instanceof doesn't always work due to renaming when compiling. Make sure + // that adding OAuth2 scopes works even if the provider class isn't an + // instance of OAuthProvider. + var provider = new fireauth.FederatedProvider('example.com'); + provider.getScopes = function() { + return ['scope1']; + }; + var customParams = { + 'lang': 'es' + }; + provider.setCustomParameters(customParams); + var builder = new fireauth.iframeclient.OAuthUrlBuilder( + 'example.firebaseapp.com', 'API_KEY', 'APP_NAME', 'signInWithPopup', + provider); + var url = 'https://example.firebaseapp.com/__/auth/handler?' + + 'apiKey=API_KEY&appName=APP_NAME&authType=signInWithPopup&' + + 'providerId=example.com&customParameters=' + + encodeURIComponent(fireauth.util.stringifyJSON(customParams)) + + '&scopes=scope1'; + assertEquals(url, builder.toString()); +} + + +/** + * Tests initialization of Auth iframe and its event listeners. + */ +function testIfcHandler() { + asyncTestCase.waitForSignals(6); + // The expected iframe URL. + var expectedUrl = fireauth.iframeclient.IfcHandler.getAuthIframeUrl( + authDomain, apiKey, appName, version); + var authEvent = new fireauth.AuthEvent( + 'unknown', '1234', 'http://www.example.com/#oauthResponse', 'SESSION_ID'); + var resp = { + 'authEvent': authEvent.toPlainObject() + }; + var invalid = undefined; + var handler1 = goog.testing.recordFunction(function() {return true;}); + var handler2 = goog.testing.recordFunction(function() {return true;}); + var handler3 = goog.testing.recordFunction(function() {return false;}); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should not have volatile storage. + assertFalse(ifcHandler.hasVolatileStorage()); + // Should unload on redirect. + assertTrue(ifcHandler.unloadsOnRedirect()); + // Confirm expected iframe URL. + assertEquals(ifcHandler.getIframeUrl(), expectedUrl); + // Should not be initialized in constructor. + assertEquals( + 0, fireauth.iframeclient.IframeWrapper.prototype.onReady.getCallCount()); + ifcHandler.initialize().then(function() { + // Test Auth event listeners. + // Add both handlers. + ifcHandler.addAuthEventListener(handler1); + ifcHandler.addAuthEventListener(handler2); + ifcHandler.addAuthEventListener(handler3); + eventsMap[fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT](resp) + .then(function(resolvedResp) { + assertObjectEquals({'status': 'ACK'}, resolvedResp); + asyncTestCase.signal(); + }); + // All should be called. + assertEquals(1, handler1.getCallCount()); + assertObjectEquals( + authEvent, handler1.getLastCall().getArgument(0)); + assertEquals(1, handler2.getCallCount()); + assertObjectEquals( + authEvent, handler2.getLastCall().getArgument(0)); + assertEquals(1, handler3.getCallCount()); + assertObjectEquals( + authEvent, handler3.getLastCall().getArgument(0)); + // Remove handler2. + ifcHandler.removeAuthEventListener(handler2); + eventsMap[fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT](resp) + .then(function(resolvedResp) { + assertObjectEquals({'status': 'ACK'}, resolvedResp); + asyncTestCase.signal(); + }); + // Only handler1 and handler3 should be called. + assertEquals(2, handler1.getCallCount()); + assertObjectEquals( + authEvent, handler1.getLastCall().getArgument(0)); + assertEquals(2, handler3.getCallCount()); + assertObjectEquals( + authEvent, handler3.getLastCall().getArgument(0)); + assertEquals(1, handler2.getCallCount()); + // Remove handler1. + ifcHandler.removeAuthEventListener(handler1); + eventsMap[fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT](resp) + .then(function(resolvedResp) { + // No handler for event so error returned. + assertObjectEquals({'status': 'ERROR'}, resolvedResp); + asyncTestCase.signal(); + }); + // Only handler3 called. + assertEquals(2, handler1.getCallCount()); + assertEquals(1, handler2.getCallCount()); + assertEquals(3, handler3.getCallCount()); + assertObjectEquals( + authEvent, handler3.getLastCall().getArgument(0)); + // Remove handler3. + ifcHandler.removeAuthEventListener(handler3); + eventsMap[fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT](resp) + .then(function(resolvedResp) { + // No handler for event so error returned. + assertObjectEquals({'status': 'ERROR'}, resolvedResp); + asyncTestCase.signal(); + }); + // No handler called again. + assertEquals(2, handler1.getCallCount()); + assertEquals(1, handler2.getCallCount()); + assertEquals(3, handler3.getCallCount()); + + // Test when error occurs. + ifcHandler.addAuthEventListener(handler1); + eventsMap[fireauth.iframeclient.IfcHandler.ReceiverEvent.AUTH_EVENT]( + invalid).then(function(resolvedResp) { + assertObjectEquals({'status': 'ERROR'}, resolvedResp); + asyncTestCase.signal(); + }); + // Test isWebStorageSupported. + ifcHandler.isWebStorageSupported().then(function(status) { + assertTrue(status); + asyncTestCase.signal(); + }); + }); +} + + +function testIfcHandler_shouldNotBeInitializedEarly() { + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Can run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Iframe can sync web storage. + stubs.replace( + fireauth.util, + 'iframeCanSyncWebStorage', + function() { + return true; + }); + assertFalse(ifcHandler.shouldBeInitializedEarly()); + // Iframe cannot sync web storage. + stubs.replace( + fireauth.util, + 'iframeCanSyncWebStorage', + function() { + return false; + }); + assertFalse(ifcHandler.shouldBeInitializedEarly()); + // Cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + // Iframe can sync web storage. + stubs.replace( + fireauth.util, + 'iframeCanSyncWebStorage', + function() { + return true; + }); + assertFalse(ifcHandler.shouldBeInitializedEarly()); +} + + +function testIfcHandler_shouldBeInitializedEarly() { + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Cannot run in background. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + // Iframe cannot sync web storage. + stubs.replace( + fireauth.util, + 'iframeCanSyncWebStorage', + function() { + return false; + }); + assertTrue(ifcHandler.shouldBeInitializedEarly()); +} + + +function testIfcHandler_initializeAndWait_success() { + // Confirm expected endpoint config and frameworks passed to underlying RPC + // handler. + var provider = new fireauth.GoogleAuthProvider(); + var expectedFrameworks = [fireauth.util.Framework.FIREBASEUI]; + var expectedClientVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + version, + expectedFrameworks); + var endpoint = fireauth.constants.Endpoint.STAGING; + var endpointConfig = { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + // Simulate expected frameworks on the Auth instance corresponding to appName. + simulateAuthService(appName, null, expectedFrameworks); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + stubs.replace( + fireauth.constants, + 'getEndpointConfig', + function(opt_id) { + assertEquals(endpoint.id, opt_id); + return endpointConfig; + }); + var getAuthIframeUrl = mockControl.createMethodMock( + fireauth.iframeclient.IfcHandler, 'getAuthIframeUrl'); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + var iframeWrapper = mockControl.createStrictMock( + fireauth.iframeclient.IframeWrapper); + var iframeWrapperConstructor = mockControl.createConstructorMock( + fireauth.iframeclient, 'IframeWrapper'); + // Iframe initialized with expected endpoint ID. + getAuthIframeUrl(authDomain, apiKey, appName, ignoreArgument, + fireauth.constants.Endpoint.STAGING.id, expectedFrameworks) + .$returns('https://url'); + // Confirm iframe URL returned by getAuthIframeUrl used to initialize the + // IframeWrapper. + iframeWrapperConstructor('https://url').$returns(iframeWrapper); + iframeWrapper.registerEvent('authEvent', ignoreArgument); + iframeWrapper.onReady().$returns(goog.Promise.resolve()); + // Should be initialized with expected endpoint config and client version. + rpcHandlerConstructor(apiKey, endpointConfig, expectedClientVersion) + .$returns(rpcHandler); + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + rpcHandler.getAuthorizedDomains().$returns(goog.Promise.resolve([domain])); + mockControl.$replayAll(); + + asyncTestCase.waitForSignals(1); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version, + fireauth.constants.Endpoint.STAGING.id); + ifcHandler.initializeAndWait().then(function() { + return ifcHandler.processRedirect('linkViaRedirect', provider, '1234'); + }).then(function() { + assertEquals(1, fireauth.util.goTo.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_initializeAndWait_error() { + asyncTestCase.waitForSignals(1); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'onReady', + function() { + return goog.Promise.reject(new Error('Network Error')); + }); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw network error. + ifcHandler.initializeAndWait().thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_blocked() { + asyncTestCase.waitForSignals(1); + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.POPUP_BLOCKED); + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw popup blocked error. + ifcHandler.processPopup( + null, 'linkViaPopup', provider, onInit, onError, '1234', false) + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_invalidOrigin() { + asyncTestCase.waitForSignals(1); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + // Assume origin is an invalid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + return goog.Promise.resolve([]); + }); + var expectedError = + new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw invalid origin error. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_invalidProvider() { + asyncTestCase.waitForSignals(1); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + // Non OAuth provider should throw invalid provider error. + var provider = new fireauth.EmailAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw invalid provider error. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_initializeAndWaitError() { + asyncTestCase.waitForSignals(1); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + // Simulate error embedding the iframe. + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'onReady', + function() { + return goog.Promise.reject(new Error('Network Error')); + }); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw network error. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .thenCatch(function(error) { + // Should be initialized. + assertEquals(1, onInit.getCallCount()); + // Should channel error. + assertEquals(1, onError.getCallCount()); + assertEquals(error, onError.getLastCall().getArgument(0)); + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_notAlreadyRedirected_success() { + asyncTestCase.waitForSignals(1); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + var provider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaPopup', + provider, + null, + '1234', + version, + undefined, + // Check expected endpoint ID appended. + fireauth.constants.Endpoint.STAGING.id); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version, + fireauth.constants.Endpoint.STAGING.id); + // Should succeed. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .then(function() { + // On init should be called as the iframe is initialized. + assertEquals(1, onInit.getCallCount()); + // No error. + assertEquals(0, onError.getCallCount()); + // Popup redirected. + assertEquals( + expectedUrl, + fireauth.util.goTo.getLastCall().getArgument(0)); + assertEquals( + popupWin, + fireauth.util.goTo.getLastCall().getArgument(1)); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_redirected_iframeCanRunInBG_success() { + asyncTestCase.waitForSignals(1); + // Simulate that the app can run in the background but is running in an + // iframe. This typically triggers processPopup call with + // opt_alreadyRedirected set to true. This is due to the fact that sandboxed + // iframes may not be able to redirect a popup window that they opened. + // In this case, simulate the origin is whitelisted. This should succeed with + // no additional redirect call. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var popupWin = { + close: goog.testing.recordFunction() + }; + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Call with alreadyRedirected true. + // Should succeed. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', true) + .then(function() { + // On init should be called as the iframe is initialized. + assertEquals(1, onInit.getCallCount()); + // No error. + assertEquals(0, onError.getCallCount()); + // No additional redirect. + assertEquals(0, fireauth.util.goTo.getCallCount(0)); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_redirected_iframeCanRunInBG_error() { + asyncTestCase.waitForSignals(1); + // Simulate that the app can run in the background but is running in an + // iframe. This typically triggers processPopup call with + // opt_alreadyRedirected set to true. This is due to the fact that sandboxed + // iframes may not be able to redirect a popup window that they opened. + // In this case, simulate the origin is not whitelisted. This should throw the + // expected error. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return true; + }); + // Assume origin is an invalid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + return goog.Promise.resolve([]); + }); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var popupWin = { + close: goog.testing.recordFunction() + }; + var provider = new fireauth.GoogleAuthProvider(); + var expectedError = + new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Call with alreadyRedirected true. + // Should succeed. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', true) + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + // On init should not be called. + assertEquals(0, onInit.getCallCount()); + // No on error call. + assertEquals(0, onError.getCallCount()); + // No redirect. + assertEquals(0, fireauth.util.goTo.getCallCount(0)); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_alreadyRedirected_success() { + asyncTestCase.waitForSignals(1); + // Simulate that the app cannot run in the background. This typically triggers + // processPopup call with opt_alreadyRedirected set to true. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var popupWin = { + close: goog.testing.recordFunction() + }; + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Call with alreadyRedirected true. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', true) + .then(function() { + // On initialization called. + assertEquals(1, onInit.getCallCount()); + // No error. + assertEquals(0, onError.getCallCount()); + // As popup already redirected, no redirect triggered. + /** @suppress {missingRequire} */ + assertEquals(0, fireauth.util.goTo.getCallCount()); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processPopup_alreadyRedirect_initializeAndWaitError() { + asyncTestCase.waitForSignals(1); + // Simulate that the app cannot run in the background. This typically triggers + // processPopup call with opt_alreadyRedirected set to true. + stubs.replace( + fireauth.util, + 'runsInBackground', + function() { + return false; + }); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + stubs.replace( + fireauth.iframeclient.IframeWrapper.prototype, + 'onReady', + function() { + return goog.Promise.reject(new Error('Network Error')); + }); + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + var popupWin = { + close: goog.testing.recordFunction() + }; + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Call with alreadyRedirected true. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', true) + .then(function() { + // On initialization called. + assertEquals(1, onInit.getCallCount()); + // No popup redirect. + /** @suppress {missingRequire} */ + assertEquals(0, fireauth.util.goTo.getCallCount()); + // onError should be called in the background if an error occurs + // during embed of iframe. + ifcHandler.initializeAndWait().thenCatch(function(error) { + assertEquals(1, onError.getCallCount()); + assertObjectEquals(error, onError.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); + }); +} + + +function testIfcHandler_processPopup_networkError_then_success() { + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onInit = goog.testing.recordFunction(); + var onError = goog.testing.recordFunction(); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + var calls = 0; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + calls++; + // Simulate network error on first call. + if (calls == 1) { + return goog.Promise.reject(expectedError); + } + // Simulate valid origin on next call. + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + var provider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaPopup', + provider, + null, + '1234', + version); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // First call fails with origin check network error. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + // Second call should succeed and confirm origin check not cached. + ifcHandler.processPopup( + popupWin, 'linkViaPopup', provider, onInit, onError, '1234', false) + .then(function() { + // Success on retrial. Invalid origin not cached. + assertEquals(1, onInit.getCallCount()); + assertEquals(0, onError.getCallCount()); + assertEquals( + expectedUrl, + fireauth.util.goTo.getLastCall().getArgument(0)); + assertEquals( + popupWin, + fireauth.util.goTo.getLastCall().getArgument(1)); + asyncTestCase.signal(); + }); + }); +} + + +function testIfcHandler_startPopupTimeout_webStorageNotSupported() { + stubs.replace( + fireauth.iframeclient.IfcHandler.prototype, + 'isWebStorageSupported', + function() { + return goog.Promise.resolve(false); + }); + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + var popupWin = { + close: goog.testing.recordFunction() + }; + // On error should be triggered with web storage not supported error. + var onError = function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }; + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + ifcHandler.startPopupTimeout(popupWin, onError, 1000); +} + + +function testIfcHandler_startPopupTimeout_popupClosedByUser() { + clock = new goog.testing.MockClock(true); + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.POPUP_CLOSED_BY_USER); + var popupWin = { + close: goog.testing.recordFunction() + }; + var onError = goog.testing.recordFunction(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // onError should be called with the expected popup closed by user error. + ifcHandler.startPopupTimeout(popupWin, onError, 2000).then(function() { + assertEquals(1, onError.getCallCount()); + assertObjectEquals(expectedError, onError.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); + // Exceed timeout after close to trigger popup closed by user. + popupWin.closed = true; + clock.tick(4000); +} + + +function testIfcHandler_processRedirect_invalidOrigin() { + asyncTestCase.waitForSignals(1); + // Assume origin is an invalid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + return goog.Promise.resolve([]); + }); + var expectedError = + new fireauth.InvalidOriginError(fireauth.util.getCurrentUrl()); + var provider = new fireauth.GoogleAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Invalid origin error should be thrown. + ifcHandler.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processRedirect_invalidProvider() { + asyncTestCase.waitForSignals(1); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OAUTH_PROVIDER); + // Invalid OAuth provider. + var provider = new fireauth.EmailAuthProvider(); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // Should throw invalid provider error. + ifcHandler.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processRedirect_success() { + var provider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + fireauth.util.getCurrentUrl(), + '1234', + version, + undefined, + // Check expected endpoint ID appended. + fireauth.constants.Endpoint.STAGING.id); + asyncTestCase.waitForSignals(1); + // Assume origin is a valid one. + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version, + fireauth.constants.Endpoint.STAGING.id); + // Should succeed and redirect. + ifcHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + /** @suppress {missingRequire} */ + assertEquals( + expectedUrl, + fireauth.util.goTo.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); +} + + +function testIfcHandler_processRedirect_networkError_then_success() { + asyncTestCase.waitForSignals(1); + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + stubs.replace( + fireauth.util, + 'goTo', + goog.testing.recordFunction()); + var calls = 0; + stubs.replace( + fireauth.RpcHandler.prototype, + 'getAuthorizedDomains', + function() { + calls++; + // Simulate network error on first call. + if (calls == 1) { + return goog.Promise.reject(expectedError); + } + // Simulate valid origin on next call. + var uri = goog.Uri.parse(fireauth.util.getCurrentUrl()); + var domain = uri.getDomain(); + return goog.Promise.resolve([domain]); + }); + var provider = new fireauth.GoogleAuthProvider(); + var expectedUrl = fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + 'linkViaRedirect', + provider, + fireauth.util.getCurrentUrl(), + '1234', + version); + ifcHandler = new fireauth.iframeclient.IfcHandler( + authDomain, apiKey, appName, version); + // First call fails with origin check network error. + ifcHandler.processRedirect('linkViaRedirect', provider, '1234') + .thenCatch(function(error) { + assertErrorEquals(expectedError, error); + // Second call should succeed and confirm origin check not cached. + ifcHandler.processRedirect('linkViaRedirect', provider, '1234') + .then(function() { + // Success on retrial. Redirect succeeded. + /** @suppress {missingRequire} */ + assertEquals(1, fireauth.util.goTo.getCallCount()); + /** @suppress {missingRequire} */ + assertEquals( + expectedUrl, + fireauth.util.goTo.getLastCall().getArgument(0)); + asyncTestCase.signal(); + }); + }); +} + + +/** + * Tests getAuthIframeUrl. + */ +function testGetAuthIframeUrl() { + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var version = '3.0.0-rc.1'; + var endpointId = 's'; + assertEquals( + 'https://subdomain.firebaseapp.com/__/auth/iframe?apiKey=apiKey1&appNa' + + 'me=appName1&v=' + encodeURIComponent(version) + '&eid=' + endpointId, + fireauth.iframeclient.IfcHandler.getAuthIframeUrl( + authDomain, apiKey, appName, version, endpointId)); +} + + +/** + * Tests getAuthIframeUrl with frameworks. + */ +function testGetAuthIframeUrl_frameworks() { + var expectedFrameworks = ['firebaseui', 'angularfire']; + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var version = '3.0.0-rc.1'; + var endpointId = 's'; + assertEquals( + 'https://subdomain.firebaseapp.com/__/auth/iframe?apiKey=apiKey1&appNa' + + 'me=appName1&v=' + encodeURIComponent(version) + '&eid=' + endpointId + + '&fw=' + encodeURIComponent(expectedFrameworks.join(',')), + fireauth.iframeclient.IfcHandler.getAuthIframeUrl( + authDomain, apiKey, appName, version, endpointId, + expectedFrameworks)); +} + + +/** + * Tests getAuthIframeUrl with injections. + */ +function testGetAuthIframeUrl_injections() { + // Injecting an evil redirect for some reason. + var authDomain = 'subdomain.firebaseapp.com/?redirectUrl=evil.com#'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + assertEquals( + 'https://subdomain.firebaseapp.com%2F%3FredirectUrl%3Devil.com%23/__/a' + + 'uth/iframe?apiKey=apiKey1&appName=appName1', + fireauth.iframeclient.IfcHandler.getAuthIframeUrl( + authDomain, apiKey, appName)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with redirect URL, event ID, version and no + * scopes. + */ +function testGetOAuthHelperWidgetUrl_redirectUrlEventIdVersionAndNoScopes() { + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var providerId = 'facebook.com'; + var provider = new fireauth.FacebookAuthProvider(); + var redirectUrl = 'http://www.example.com/redirect?a=b#c'; + var eventId = '12345678'; + var version = '3.0.0-rc.1'; + var endpointId = 's'; + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId) + + '&redirectUrl=' + encodeURIComponent(redirectUrl) + + '&eventId=' + encodeURIComponent(eventId) + + '&v=' + encodeURIComponent(version) + + '&eid=' + endpointId; + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + authType, + provider, + redirectUrl, + eventId, + version, + null, + endpointId)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with injections. + */ +function testGetOAuthHelperWidgetUrl_injections() { + // Injecting an evil redirect for some reason. + var authDomain = 'subdomain.firebaseapp.com/?redirectUrl=evil.com#'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var provider = new fireauth.FacebookAuthProvider(); + var providerId = 'facebook.com'; + var redirectUrl = 'http://www.example.com/redirect?a=b#c'; + var eventId = '12345678'; + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com%2F%3FredirectUr' + + 'l%3Devil.com%23/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId) + + '&redirectUrl=' + encodeURIComponent(redirectUrl) + + '&eventId=' + encodeURIComponent(eventId); + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + authType, + provider, + redirectUrl, + eventId)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with scope, event ID and redirect URL. + */ +function testGetOAuthHelperWidgetUrl_redirectUrlEventIdAndScopes() { + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var providerId = 'facebook.com'; + var redirectUrl = 'http://www.example.com/redirect?a=b#c'; + var provider = new fireauth.FacebookAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + provider.addScope('scope3'); + var eventId = '12345678'; + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId) + + '&scopes=' + encodeURIComponent('scope1,scope2,scope3') + + '&redirectUrl=' + encodeURIComponent(redirectUrl) + + '&eventId=' + encodeURIComponent(eventId); + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, + apiKey, + appName, + authType, + provider, + redirectUrl, + eventId)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with no redirect URL and no scopes. + */ +function testGetOAuthHelperWidgetUrl_noRedirectUrlAndNoScopes() { + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var providerId = 'facebook.com'; + var provider = new fireauth.FacebookAuthProvider(); + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId); + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, apiKey, appName, authType, provider)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with scopes and no redirect URL. + */ +function testGetOAuthHelperWidgetUrl_scopesAndNoRedirectUrl() { + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var providerId = 'facebook.com'; + var provider = new fireauth.FacebookAuthProvider(); + provider.addScope('scope1'); + provider.addScope('scope2'); + provider.addScope('scope3'); + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId) + + '&scopes=' + encodeURIComponent('scope1,scope2,scope3'); + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, apiKey, appName, authType, provider)); +} + + +/** + * Tests getOAuthHelperWidgetUrl with Auth languageCode and frameworks. + */ +function testGetOAuthHelperWidgetUrl_frameworksAndLanguageCode() { + // Test getOAuthHelperWidgetUrl picks up Auth languageCode and frameworks. + var authDomain = 'subdomain.firebaseapp.com'; + var apiKey = 'apiKey1'; + var appName = 'appName1'; + var authType = 'signInWithPopup'; + var providerId = 'facebook.com'; + var provider = new fireauth.FacebookAuthProvider(); + var languageCode = 'fr'; + var frameworks = ['firebaseui', 'angularfire']; + provider.addScope('scope1'); + provider.addScope('scope2'); + provider.addScope('scope3'); + simulateAuthService(appName, languageCode, frameworks); + var expectedWidgetUrl = 'https://subdomain.firebaseapp.com/__/auth/handler' + + '?apiKey=' + encodeURIComponent(apiKey) + + '&appName=' + encodeURIComponent(appName) + + '&authType=' + encodeURIComponent(authType) + + '&providerId=' + encodeURIComponent(providerId) + + '&customParameters=' + encodeURIComponent( + fireauth.util.stringifyJSON({'locale': 'fr'})) + + '&scopes=' + encodeURIComponent('scope1,scope2,scope3') + + '&fw=' + encodeURIComponent(frameworks.join(',')); + assertEquals( + expectedWidgetUrl, + fireauth.iframeclient.IfcHandler.getOAuthHelperWidgetUrl( + authDomain, apiKey, appName, authType, provider)); +} + diff --git a/packages/auth/test/iframeclient/iframewrapper_test.js b/packages/auth/test/iframeclient/iframewrapper_test.js new file mode 100644 index 00000000000..75eaadafa4a --- /dev/null +++ b/packages/auth/test/iframeclient/iframewrapper_test.js @@ -0,0 +1,512 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for dataiframe.js + */ + +goog.provide('fireauth.iframeclient.IframeWrapperTest'); + +goog.require('fireauth.iframeclient.IframeWrapper'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.net.jsloader'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); + +goog.setTestOnly('fireauth.iframeclient.IframeWrapperTest'); + + +var ignoreArgument; +var mockControl; +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +var clock; +var gapi; +var stubs = new goog.testing.PropertyReplacer(); + +function setUp() { + clock = new goog.testing.MockClock(true); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +function tearDown() { + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } + // Reset GApi for each test. + gapi = null; + goog.dispose(clock); + stubs.reset(); + // Reset cached GApi loader. + fireauth.iframeclient.IframeWrapper.resetCachedGApiLoader(); +} + + +function testIframeWrapper() { + var expectedHandler = function(resp) {}; + var path = 'https://data_iframe_url'; + var iframesGetContext = mockControl.createFunctionMock('getContext'); + // Simulate gapi.iframes loaded. + gapi = window['gapi'] || {}; + gapi.iframes = { + 'Iframe': {}, + 'getContext': iframesGetContext, + 'CROSS_ORIGIN_IFRAMES_FILTER': 'CROSS_ORIGIN_IFRAMES_FILTER' + }; + var openIframe = mockControl.createFunctionMock('openIframe'); + var send = mockControl.createFunctionMock('send'); + var register = mockControl.createFunctionMock('register'); + var unregister = mockControl.createFunctionMock('unregister'); + var restyle = mockControl.createFunctionMock('restyle'); + iframesGetContext().$returns({ + 'open': openIframe + }); + openIframe(ignoreArgument, ignoreArgument).$does(function(params, onOpen) { + assertEquals(params['url'], 'https://data_iframe_url'); + assertObjectEquals(params['where'], document.body); + assertObjectEquals(params['attributes']['style'], { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + }); + assertTrue(params['dontclear']); + onOpen({ + 'send': send, + 'register': register, + 'unregister': unregister, + 'restyle': restyle, + 'ping': function(callback, opt_data) { + // Successfully embedded. + callback(); + return new goog.Promise(function(resolve, reject) {}); + } + }); + }).$once(); + restyle({'setHideOnLeave': false}).$once(); + + send( + 'messageType', + {'type': 'messageType', 'field1': 'value1', 'field2': 'value2'}, + ignoreArgument, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER) + .$does(function(type, message, resolve) { + // Iframe should be ready. + assertTrue(iframeReady); + resolve({'status': 'OK'}); + }) + .$once(); + + register( + 'eventName', ignoreArgument, gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER) + .$does(function(eventName, handler) { + // Iframe should be ready. + assertTrue(iframeReady); + assertEquals(expectedHandler, handler); + }) + .$once(); + + unregister('eventName', ignoreArgument) + .$does(function(eventName, handler) { + // Iframe should be ready. + assertTrue(iframeReady); + assertEquals(expectedHandler, handler); + }) + .$once(); + + mockControl.$replayAll(); + asyncTestCase.waitForSignals(1); + + // Test initialization of data iframe. + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + // Iframe wrapper should become ready. + iframeWrapper.onReady().then(function() { iframeReady = true; }); + assertEquals(path, iframeWrapper.getPath_()); + // sendMessage. + iframeWrapper.sendMessage({ + 'type': 'messageType', + 'field1': 'value1', + 'field2': 'value2' + }).then(function(response) { + assertObjectEquals( + {'status': 'OK'}, + response); + asyncTestCase.signal(); + }); + // Flag to track iframe readiness. + var iframeReady = false; + // registerEvent. + iframeWrapper.registerEvent('eventName', expectedHandler); + // unregisterEvent. + iframeWrapper.unregisterEvent('eventName', expectedHandler); +} + + +function testIframeWrapper_failedToOpen() { + // Test when iframe fails to open. + var path = 'https://data_iframe_url'; + var iframesGetContext = mockControl.createFunctionMock('getContext'); + // Simulate gapi.iframes loaded. + gapi = window['gapi'] || {}; + gapi.iframes = { + 'Iframe': {}, + 'getContext': iframesGetContext, + 'CROSS_ORIGIN_IFRAMES_FILTER': 'CROSS_ORIGIN_IFRAMES_FILTER' + }; + var openIframe = mockControl.createFunctionMock('openIframe'); + var send = mockControl.createFunctionMock('send'); + var register = mockControl.createFunctionMock('register'); + var unregister = mockControl.createFunctionMock('unregister'); + var restyle = mockControl.createFunctionMock('restyle'); + iframesGetContext().$returns({'open': openIframe}); + openIframe(ignoreArgument, ignoreArgument) + .$does(function(params, onOpen) { + assertEquals(params['url'], 'https://data_iframe_url'); + assertObjectEquals(params['where'], document.body); + assertObjectEquals(params['attributes']['style'], { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + }); + assertTrue(params['dontclear']); + onOpen({ + 'send': send, + 'register': register, + 'unregister': unregister, + 'restyle': restyle, + // Unresponsive ping. + 'ping': function() { + return new goog.Promise(function(resolve, reject) {}); + } + }); + }) + .$once(); + restyle({'setHideOnLeave': false}).$once(); + + mockControl.$replayAll(); + + // Test initialization of data iframe. + asyncTestCase.waitForSignals(2); + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + // Iframe wrapper should not become ready and timeout. + iframeWrapper.onReady().thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); + iframeWrapper + .sendMessage( + {'type': 'messageType', 'field1': 'value1', 'field2': 'value2'}) + .thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); + // Simulate iframe ping is not responsive. + clock.tick(10000); +} + + +function testIframeWrapper_offline() { + // Test when iframe fails to open due to app being offline. + // Simulate app offline. + stubs.reset(); + stubs.replace( + fireauth.util, + 'isOnline', + function() {return false;}); + var path = 'https://data_iframe_url'; + + // Test initialization of data iframe. + asyncTestCase.waitForSignals(2); + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + // Iframe wrapper should not become ready and timeout as the app is offline. + // Mockclock is already set in setUp and does not tick. This means the call + // is getting rejected immediately and not listening to any timer. + iframeWrapper.onReady().thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); + iframeWrapper + .sendMessage( + {'type': 'messageType', 'field1': 'value1', 'field2': 'value2'}) + .thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); +} + + +/** + * Simulate successful gapi.iframes being loaded. + * @param {function()} iframesGetContext The iframes getContext mock function. + */ +function simulateSuccessfulGapiIframesLoading(iframesGetContext) { + var gapiLoadCounter = 0; + var jsloaderCounter = 0; + var setGapiLoader = function() { + gapi.load = function(features, options) { + // Run asynchronously to give a chance for multiple parallel calls to be + // caught. + goog.Promise.resolve().then(function() { + // gapi.load should never try to load successfully more than once and + // the successful result should be cached and returned on successive + // calls. + gapiLoadCounter++; + assertEquals(1, gapiLoadCounter); + // gapi.load should load gapi.iframes. + var callback = options['callback']; + gapi.iframes = { + 'Iframe': {}, + 'getContext': iframesGetContext, + 'CROSS_ORIGIN_IFRAMES_FILTER': 'CROSS_ORIGIN_IFRAMES_FILTER' + }; + callback(); + }); + }; + }; + if (!gapi) { + // GApi not available, it will try to load api.js and then gapi.iframes. + stubs.replace(goog.net.jsloader, 'safeLoad', function(url) { + // Run asynchronously to give a chance for multiple parallel calls to + // be caught. + return goog.Promise.resolve().then(function() { + // jsloader should never try to load successfully more than once and + // the successful result should be cached and returned on successive + // calls. + jsloaderCounter++; + assertEquals(1, jsloaderCounter); + // After successful loading of API. + gapi = {}; + // Set gapi.load. + setGapiLoader(); + // Parse URL and get onload cb name. + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var cbName = uri.getParameterValue('onload'); + // Run onload callback. + goog.global[cbName](); + }); + }); + } else if (!gapi.iframes) { + // gapi.load available, it will try to load gapi.iframes. + setGapiLoader(); + } +} + + +function testIframeWrapper_gapiNotLoadedError() { + // Test when GApi fails to load. + gapi = null; + stubs.replace(goog.net.jsloader, 'safeLoad', function(url) { + return goog.Promise.reject(); + }); + var path = 'https://data_iframe_url'; + var iframesGetContext = mockControl.createFunctionMock('getContext'); + var openIframe = mockControl.createFunctionMock('openIframe'); + var send = mockControl.createFunctionMock('send'); + var register = mockControl.createFunctionMock('register'); + var unregister = mockControl.createFunctionMock('unregister'); + var restyle = mockControl.createFunctionMock('restyle'); + iframesGetContext().$returns({'open': openIframe}); + openIframe(ignoreArgument, ignoreArgument) + .$does(function(params, onOpen) { + assertEquals(params['url'], 'https://data_iframe_url'); + assertObjectEquals(params['where'], document.body); + assertObjectEquals(params['attributes']['style'], { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + }); + assertTrue(params['dontclear']); + onOpen({ + 'send': send, + 'register': register, + 'unregister': unregister, + 'restyle': restyle, + 'ping': function(callback) { + callback(); + return new goog.Promise(function(resolve, reject) {}); + } + }); + }) + .$once(); + restyle({'setHideOnLeave': false}).$once(); + + mockControl.$replayAll(); + + // Test initialization of data iframe. + asyncTestCase.waitForSignals(2); + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + // Iframe wrapper should not become ready as api.js fails to load. + iframeWrapper.onReady().thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); + iframeWrapper + .sendMessage( + {'type': 'messageType', 'field1': 'value1', 'field2': 'value2'}) + .thenCatch(function(error) { + assertEquals('Network Error', error.message); + // Try again and make sure failing result was not cached. + // This time gapi.iframes will load correctly. + simulateSuccessfulGapiIframesLoading(iframesGetContext); + var iframeWrapper2 = new fireauth.iframeclient.IframeWrapper(path); + // This time it should succeed. + iframeWrapper2.onReady().then(function() { asyncTestCase.signal(); }); + }); +} + + +function testIframeWrapper_gapiDotLoadError() { + var path = 'https://data_iframe_url'; + var resetUnloadedGapiModules = + mockControl.createFunctionMock('resetUnloadedGapiModules'); + gapi = {}; + // Simulate error while loading gapi.iframes. + gapi.load = function(features, options) { + assertEquals('gapi.iframes', features); + options['ontimeout'](); + }; + // Record fireauth.util.resetUnloadedGapiModules. + stubs.replace( + fireauth.util, 'resetUnloadedGapiModules', resetUnloadedGapiModules); + // Called once to reset any unloaded module the developer may have requested. + resetUnloadedGapiModules(); + // Called on first gapi.iframe load timeout. + resetUnloadedGapiModules(); + // Called before second gapi.iframe load attempt. + resetUnloadedGapiModules(); + var iframesGetContext = mockControl.createFunctionMock('getContext'); + var openIframe = mockControl.createFunctionMock('openIframe'); + var send = mockControl.createFunctionMock('send'); + var register = mockControl.createFunctionMock('register'); + var unregister = mockControl.createFunctionMock('unregister'); + var restyle = mockControl.createFunctionMock('restyle'); + iframesGetContext().$returns({'open': openIframe}); + openIframe(ignoreArgument, ignoreArgument) + .$does(function(params, onOpen) { + assertEquals(params['url'], 'https://data_iframe_url'); + assertObjectEquals(params['where'], document.body); + assertObjectEquals(params['attributes']['style'], { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + }); + assertTrue(params['dontclear']); + onOpen({ + 'send': send, + 'register': register, + 'unregister': unregister, + 'restyle': restyle, + 'ping': function(callback) { + callback(); + return new goog.Promise(function(resolve, reject) {}); + } + }); + }) + .$once(); + restyle({'setHideOnLeave': false}).$once(); + mockControl.$replayAll(); + + // Test initialization of data iframe. + asyncTestCase.waitForSignals(2); + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + // Iframe wrapper should should fail due to error in loading gapi.iframes. + iframeWrapper.onReady().thenCatch(function(error) { + assertEquals('Network Error', error.message); + asyncTestCase.signal(); + }); + iframeWrapper + .sendMessage( + {'type': 'messageType', 'field1': 'value1', 'field2': 'value2'}) + .thenCatch(function(error) { + assertEquals('Network Error', error.message); + // Try again and make sure failing result was not cached. + // Simulate successful loading of gapi.iframes this time. + simulateSuccessfulGapiIframesLoading(iframesGetContext); + var iframeWrapper2 = new fireauth.iframeclient.IframeWrapper(path); + // This should succeed. + iframeWrapper2.onReady().then(function() { asyncTestCase.signal(); }); + }); +} + + +function testIframeWrapper_multipleInstances() { + // Tests when multiple iframe wrapper instance initialized that only one + // shared gapi.load attempt is called underneath. + var path = 'https://data_iframe_url'; + gapi = {}; + var iframesGetContext = mockControl.createFunctionMock('getContext'); + var openIframe = mockControl.createFunctionMock('openIframe'); + var send = mockControl.createFunctionMock('send'); + var register = mockControl.createFunctionMock('register'); + var unregister = mockControl.createFunctionMock('unregister'); + var restyle = mockControl.createFunctionMock('restyle'); + // Requests should be called twice. + for (var i = 0; i < 2; i++) { + iframesGetContext().$returns({'open': openIframe}); + openIframe(ignoreArgument, ignoreArgument) + .$does(function(params, onOpen) { + assertEquals(params['url'], 'https://data_iframe_url'); + assertObjectEquals(params['where'], document.body); + assertObjectEquals(params['attributes']['style'], { + 'position': 'absolute', + 'top': '-100px', + 'width': '1px', + 'height': '1px' + }); + assertTrue(params['dontclear']); + onOpen({ + 'send': send, + 'register': register, + 'unregister': unregister, + 'restyle': restyle, + 'ping': function(callback) { + callback(); + return new goog.Promise(function(resolve, reject) {}); + } + }); + }) + .$once(); + restyle({'setHideOnLeave': false}).$once(); + } + mockControl.$replayAll(); + + // Initialize 2 iframes. Both should resolve. + asyncTestCase.waitForSignals(2); + // Underneath this will check that gapi.load/jsloader aren't called more than + // once. + simulateSuccessfulGapiIframesLoading(iframesGetContext); + var iframeWrapper = new fireauth.iframeclient.IframeWrapper(path); + iframeWrapper.onReady().then(function() { + asyncTestCase.signal(); + }); + var iframeWrapper2 = new fireauth.iframeclient.IframeWrapper(path); + iframeWrapper2.onReady().then(function() { + asyncTestCase.signal(); + }); +} diff --git a/packages/auth/test/oauthhelperstate_test.js b/packages/auth/test/oauthhelperstate_test.js new file mode 100644 index 00000000000..329c10f42c1 --- /dev/null +++ b/packages/auth/test/oauthhelperstate_test.js @@ -0,0 +1,319 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for oauthhelperstate.js + */ + +goog.provide('fireauth.OAuthHelperStateTest'); + +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.OAuthHelperState'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.OAuthHelperStateTest'); + + +var state; +var state2; +var state3; +var state4; +var state5; +var state6; +var stateObject; +var stateObject2; +var stateObject3; +var stateObject4; +var stateObject5; +var stateObject6; + + +function setUp() { + state = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + null, + 'http://www.example.com/redirect', + '3.0.0'); + state2 = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + '12345678'); + state3 = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '12345678', + null, + '3.6.0', + 'Test App', + 'com.example.android', + null, + 't', + ['firebaseui', 'angularfire']); + state4 = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '12345678', + null, + '3.6.0', + 'Test App', + null, + 'com.example.ios', + 's'); + // State with OAuth client ID. + state5 = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '12345678', + null, + '3.6.0', + 'Test App', + null, + 'com.example.ios', + null, + null, + '123456.apps.googleusercontent.com'); + // State with SHA-1 cert. + state6 = new fireauth.OAuthHelperState( + 'API_KEY', + 'APP_NAME', + fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + '12345678', + null, + '3.6.0', + 'Test App', + 'com.example.android', + null, + null, + null, + null, + 'SHA_1_ANDROID_CERT'); + stateObject = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + 'redirectUrl': 'http://www.example.com/redirect', + 'eventId': null, + 'clientVersion': '3.0.0', + 'displayName': null, + 'apn': null, + 'ibi': null, + 'eid': null, + 'fw': [], + 'clientId': null, + 'sha1Cert': null + }; + stateObject2 = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + 'redirectUrl': null, + 'eventId': '12345678', + 'clientVersion': null, + 'displayName': null, + 'apn': null, + 'ibi': null, + 'eid': null, + 'fw': [], + 'clientId': null, + 'sha1Cert': null + }; + stateObject3 = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + 'redirectUrl': null, + 'eventId': '12345678', + 'clientVersion': '3.6.0', + 'displayName': 'Test App', + 'apn': 'com.example.android', + 'ibi': null, + 'eid': 't', + 'fw': ['firebaseui', 'angularfire'], + 'clientId': null, + 'sha1Cert': null + }; + stateObject4 = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + 'redirectUrl': null, + 'eventId': '12345678', + 'clientVersion': '3.6.0', + 'displayName': 'Test App', + 'apn': null, + 'ibi': 'com.example.ios', + 'eid': 's', + 'fw': [], + 'clientId': null, + 'sha1Cert': null + }; + stateObject5 = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + 'redirectUrl': null, + 'eventId': '12345678', + 'clientVersion': '3.6.0', + 'displayName': 'Test App', + 'apn': null, + 'ibi': 'com.example.ios', + 'eid': null, + 'fw': [], + 'clientId': '123456.apps.googleusercontent.com', + 'sha1Cert': null + }; + stateObject6 = { + 'apiKey': 'API_KEY', + 'appName': 'APP_NAME', + 'type': fireauth.AuthEvent.Type.SIGN_IN_VIA_REDIRECT, + 'redirectUrl': null, + 'eventId': '12345678', + 'clientVersion': '3.6.0', + 'displayName': 'Test App', + 'apn': 'com.example.android', + 'ibi': null, + 'eid': null, + 'fw': [], + 'clientId': null, + 'sha1Cert': 'SHA_1_ANDROID_CERT' + }; +} + + +function tearDown() { + state = null; + state2 = null; + state3 = null; + state4 = null; + state5 = null; + state6 = null; + stateObject = null; + stateObject2 = null; + stateObject3 = null; + stateObject4 = null; + stateObject5 = null; + stateObject6 = null; +} + + +function testOAuthHelperState() { + // Check state. + assertEquals('API_KEY', state.getApiKey()); + assertEquals('APP_NAME', state.getAppName()); + assertEquals(fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, state.getType()); + assertEquals('http://www.example.com/redirect', state.getRedirectUrl()); + assertEquals('3.0.0', state.getClientVersion()); + assertNull(state.getEventId()); + assertNull(state.getDisplayName()); + assertNull(state.getApn()); + assertNull(state.getIbi()); + assertNull(state.getEndpointId()); + assertArrayEquals([], state.getFrameworks()); + assertNull(state.getClientId()); + assertNull(state.getSha1Cert()); + // Check state2. + assertEquals('12345678', state2.getEventId()); + assertNull(state2.getClientVersion()); + assertNull(state2.getRedirectUrl()); + assertNull(state2.getDisplayName()); + assertNull(state2.getApn()); + assertNull(state2.getIbi()); + assertNull(state2.getEndpointId()); + assertArrayEquals([], state2.getFrameworks()); + assertNull(state2.getClientId()); + assertNull(state2.getSha1Cert()); + // Check state3. + assertEquals(state3.getDisplayName(), 'Test App'); + assertEquals(state3.getApn(), 'com.example.android'); + assertNull(state3.getIbi()); + assertEquals('t', state3.getEndpointId()); + assertArrayEquals(['firebaseui', 'angularfire'], state3.getFrameworks()); + assertNull(state3.getClientId()); + assertNull(state3.getSha1Cert()); + // Check state4. + assertEquals(state4.getDisplayName(), 'Test App'); + assertNull(state4.getApn()); + assertEquals(state4.getIbi(), 'com.example.ios'); + assertEquals('s', state4.getEndpointId()); + assertArrayEquals([], state4.getFrameworks()); + assertNull(state4.getClientId()); + assertNull(state4.getSha1Cert()); + // Check state5. + assertNull(state4.getApn()); + assertEquals(state4.getIbi(), 'com.example.ios'); + assertNull(state2.getEndpointId()); + assertArrayEquals([], state4.getFrameworks()); + assertEquals('123456.apps.googleusercontent.com', state5.getClientId()); + assertNull(state5.getSha1Cert()); + // Check state6. + assertEquals(state3.getApn(), 'com.example.android'); + assertNull(state3.getIbi()); + assertNull(state2.getEndpointId()); + assertArrayEquals([], state4.getFrameworks()); + assertNull(state6.getClientId()); + assertEquals('SHA_1_ANDROID_CERT', state6.getSha1Cert()); +} + + +function testOAuthHelperState_toPlainObject() { + assertObjectEquals( + stateObject, + state.toPlainObject()); + assertObjectEquals( + stateObject2, + state2.toPlainObject()); + assertObjectEquals( + stateObject3, + state3.toPlainObject()); + assertObjectEquals( + stateObject4, + state4.toPlainObject()); + assertObjectEquals( + stateObject5, + state5.toPlainObject()); + assertObjectEquals( + stateObject6, + state6.toPlainObject()); +} + + +function testOAuthHelperState_fromPlainObject() { + assertObjectEquals( + state, + fireauth.OAuthHelperState.fromPlainObject(stateObject)); + assertObjectEquals( + state2, + fireauth.OAuthHelperState.fromPlainObject(stateObject2)); + assertObjectEquals( + state3, + fireauth.OAuthHelperState.fromPlainObject(stateObject3)); + assertObjectEquals( + state4, + fireauth.OAuthHelperState.fromPlainObject(stateObject4)); + assertObjectEquals( + state5, + fireauth.OAuthHelperState.fromPlainObject(stateObject5)); + assertObjectEquals( + state6, + fireauth.OAuthHelperState.fromPlainObject(stateObject6)); + assertNull(fireauth.OAuthHelperState.fromPlainObject({})); +} diff --git a/packages/auth/test/object_test.js b/packages/auth/test/object_test.js new file mode 100644 index 00000000000..c97fb8d165f --- /dev/null +++ b/packages/auth/test/object_test.js @@ -0,0 +1,317 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for object.js. + */ + +goog.provide('fireauth.objectTest'); + +goog.require('fireauth.deprecation'); +goog.require('fireauth.object'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); +goog.require('goog.userAgent'); + +goog.setTestOnly('fireauth.objectTest'); + +var stubs; + +function setUp() { + stubs = new goog.testing.PropertyReplacer(); +} + +function tearDown() { + stubs.reset(); +} + + +/** @type {boolean} True if the browser supports Object.defineProperty. */ +var isReadonlyPropertyBrowser = !goog.userAgent.IE || + goog.userAgent.isDocumentModeOrHigher(9); + + +function testIsReadonlyConfigurable() { + assertEquals( + fireauth.object.isReadonlyConfigurable_(), isReadonlyPropertyBrowser); +} + + +function testSetReadonlyProperty() { + var myObj = { + 'Argentina': 'Buenos Aires' + }; + fireauth.object.setReadonlyProperty(myObj, 'Bolivia', 'Sucre'); + + assertEquals('Buenos Aires', myObj['Argentina']); + assertEquals('Sucre', myObj['Bolivia']); +} + + +function testSetReadonlyProperty_overwrite() { + var myObj = {}; + + fireauth.object.setReadonlyProperty(myObj, 'Argentina', 'Moscow'); + assertEquals('Moscow', myObj['Argentina']); + + fireauth.object.setReadonlyProperty(myObj, 'Argentina', 'Buenos Aires'); + assertEquals('Buenos Aires', myObj['Argentina']); +} + + +function testSetReadonlyProperty_instance() { + var MyObj = function() { + fireauth.object.setReadonlyProperty(this, 'Bolivia', 'Sucre'); + }; + var myInstance = new MyObj(); + assertEquals('Sucre', myInstance['Bolivia']); +} + + +function testSetReadonlyProperty_isActuallyReadonly() { + if (!isReadonlyPropertyBrowser) { + return; + } + + var myObj = {}; + fireauth.object.setReadonlyProperty(myObj, 'Argentina', 'Buenos Aires'); + assertEquals('Buenos Aires', myObj['Argentina']); + myObj['Argentina'] = 'Toronto'; + assertEquals('Buenos Aires', myObj['Argentina']); +} + + +function testSetReadonlyProperties() { + var myObj = {}; + fireauth.object.setReadonlyProperties(myObj, { + 'Brazil': 'Brasilia', + 'Chile': 'Santiago' + }); + assertEquals('Brasilia', myObj['Brazil']); + assertEquals('Santiago', myObj['Chile']); +} + + +function testSetReadonlyProperties_isActuallyReadonly() { + if (!isReadonlyPropertyBrowser) { + return; + } + + var myObj = {}; + fireauth.object.setReadonlyProperties(myObj, { + 'Brazil': 'Brasilia', + 'Chile': 'Santiago' + }); + + myObj['Brazil'] = 'Beijing'; + myObj['Chile'] = 'Seoul'; + + assertEquals('Brasilia', myObj['Brazil']); + assertEquals('Santiago', myObj['Chile']); +} + + +function testMakeReadonlyCopy() { + var myObj = { + 'Brazil': 'Brasilia', + 'Chile': 'Santiago' + }; + assertObjectEquals(myObj, fireauth.object.makeReadonlyCopy(myObj)); +} + + +function testMakeReadonlyCopy_isActuallyReadonly() { + if (!isReadonlyPropertyBrowser) { + return; + } + + var myObj = { + 'Brazil': 'Brasilia', + 'Chile': 'Santiago' + }; + var copy = fireauth.object.makeReadonlyCopy(myObj); + copy['Brazil'] = 'Paris'; + copy['Chile'] = 'London'; + assertObjectEquals(myObj, copy); +} + + +function testMakeWritableCopy() { + var myObj = fireauth.object.makeReadonlyCopy({ + 'Brazil': 'Brasilia', + 'Chile': 'Santiago' + }); + assertObjectEquals(myObj, fireauth.object.makeWritableCopy(myObj)); +} + + +function testMakeWritableCopy_isActuallyWritable() { + var myObj = fireauth.object.makeReadonlyCopy({ + 'Brazil': 'Brasilia', + 'Chile': 'Paris' + }); + var copy = fireauth.object.makeWritableCopy(myObj); + copy['Chile'] = 'Santiago'; + assertObjectEquals('Santiago', copy['Chile']); +} + + +function testhasNonEmptyFields_true() { + var obj = {'a': 1, 'b': 2, 'c': 3}; + assertTrue(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_false() { + var obj = {'a': 1, 'b': 2}; + assertFalse(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_empty() { + var obj = {'a': 1, 'b': 2, 'c': 3}; + assertTrue(fireauth.object.hasNonEmptyFields(obj, [])); +} + + +function testhasNonEmptyFields_objectUndefined() { + assertFalse(fireauth.object.hasNonEmptyFields(undefined, ['a'])); +} + + +function testhasNonEmptyFields_fieldsUndefined() { + assertTrue(fireauth.object.hasNonEmptyFields({}, undefined)); +} + + +function testhasNonEmptyFields_objectHasUndefinedField() { + var obj = {'a': 1, 'b': 2, 'c': undefined}; + assertFalse(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_objectHasNullField() { + var obj = {'a': null, 'b': 2, 'c': 3}; + assertFalse(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_objectHasEmptyStringField() { + var obj = {'a': '', 'b': 'foo', 'c': 'bar'}; + assertFalse(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_objectHasZeroField() { + var obj = {'a': 1, 'b': 2, 'c': 0}; + assertTrue(fireauth.object.hasNonEmptyFields(obj, ['a', 'c'])); +} + + +function testhasNonEmptyFields_objectHasFalseField() { + var obj = {'one': false, 'two': true, 'three': true}; + assertTrue(fireauth.object.hasNonEmptyFields(obj, ['one', 'three'])); +} + + +function testUnsafeCreateReadOnlyCopy() { + if (!isReadonlyPropertyBrowser) { + return; + } + + var myObj = { + 'a': [ + {'b': 1}, + 'str', + {} + ] + }; + // Create read-only copy. + var copy = fireauth.object.unsafeCreateReadOnlyCopy(myObj); + // Confirm object copied. + assertObjectEquals(myObj, copy); + assertEquals(1, copy['a'][0]['b']); + // This will have no effect. + copy['a'][0]['b'] = 2; + assertEquals(1, copy['a'][0]['b']); + // This should have no effect either. + copy['a'][0] = 2; + assertEquals(1, copy['a'][0]['b']); +} + + +function testUnsafeCreateReadOnlyCopy_nonCyclicalReferences() { + if (!isReadonlyPropertyBrowser) { + return; + } + + var c = {}; + // a depends on c. + var a = {'d': [c, 1]}; + var d = {'b': {'a': [0, a]}, 'c': c}; + var copy = fireauth.object.unsafeCreateReadOnlyCopy(d); + assertObjectEquals(d, copy); + // Should have no effect. + copy['c'] = null; + // Equal references copied. + assertObjectEquals(copy['c'], copy['b']['a'][1]['d'][0]); + + var e = { + 'f': c, + 'g': [c, c], + 'h': c + }; + var copy2 = fireauth.object.unsafeCreateReadOnlyCopy(e); + assertObjectEquals(e, copy2); + // Should have no effect. + copy2['f'] = 1; + // Equal references copied. + assertObjectEquals(copy2['f'], copy2['g'][0]); + assertObjectEquals(copy2['f'], copy2['h']); +} + + +function testSetDeprecatedReadonlyProperty() { + var spyLog = goog.testing.recordFunction(); + stubs.replace(fireauth.deprecation, 'log', spyLog); + + + var myObj = { + 'Argentina': 'Buenos Aires' + }; + var warning = /** @type {fireauth.deprecation.Deprecations} */ ( + 'Bolivia is deprecated.'); + fireauth.object.setDeprecatedReadonlyProperty(myObj, 'Bolivia', 'Sucre', + warning); + + // The warning should not be shown until Bolivia is referenced. + spyLog.assertCallCount(0); + + assertEquals('Buenos Aires', myObj['Argentina']); + spyLog.assertCallCount(0); + + assertEquals('Sucre', myObj['Bolivia']); + spyLog.assertCallCount(1); + assertEquals(warning, spyLog.getLastCall().getArgument(0)); + + assertEquals('Sucre', myObj['Bolivia']); + spyLog.assertCallCount(2); + assertEquals(warning, spyLog.getLastCall().getArgument(0)); + + assertEquals('Buenos Aires', myObj['Argentina']); + spyLog.assertCallCount(2); +} diff --git a/packages/auth/test/proactiverefresh_test.js b/packages/auth/test/proactiverefresh_test.js new file mode 100644 index 00000000000..33ed7e6a470 --- /dev/null +++ b/packages/auth/test/proactiverefresh_test.js @@ -0,0 +1,494 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for proactiverefresh.js. + */ + +goog.provide('fireauth.ProactiveRefreshTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.ProactiveRefresh'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.ProactiveRefreshTest'); + + +var clock; +var stubs = new goog.testing.PropertyReplacer(); +var forceRetryError = false; +var operation = null; +var unrecoverableOperation = null; +var retryPolicy = null; +var getWaitDuration = null; +var proactiveRefresh = null; +var lowerBound = null; +var upperBound = null; +var runsInBackground = true; +var lastTimestamp = 0; +var makeAppVisible = null; +var cycle = 1000; + + +function setUp() { + // Whether to force a retry error to be thrown in the operation to be run. + forceRetryError = false; + // The time stamp corresponding to the last operation run. + lastTimestamp = 0; + // Initialize mock clock. + clock = new goog.testing.MockClock(true); + // Stub on app visible utility. + makeAppVisible = null; + stubs.replace( + fireauth.util, + 'onAppVisible', + function() { + return new goog.Promise(function(resolve, reject) { + // On every call, save onAppVisible resolution function. + makeAppVisible = resolve; + }); + }); + // Operation to practively refresh. + operation = goog.testing.recordFunction(function() { + // Record last run time. + lastTimestamp = goog.now(); + if (!forceRetryError) { + // Do not force error retry. Resolve successfully. + return goog.Promise.resolve(); + } else { + // If retrial error should be forced, throw a network error. + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED)); + } + }); + // Operation which throws an unrecoverable error. + unrecoverableOperation = goog.testing.recordFunction(function() { + // Record last run time. + lastTimestamp = goog.now(); + // Throw unrecoverable error. + return goog.Promise.reject( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + }); + // Retry policy. Retry only on network errors. + retryPolicy = function(error) { + if (error && error.code == 'auth/network-request-failed') { + return true; + } + return false; + }; + // Return a cycle for next time to rerun after a successful run. + getWaitDuration = function() { + return cycle; + }; + // Upper bound is 90% of a cycle. + upperBound = 0.9 * cycle; + // Lower bound is 10 % of a cycle. + lowerBound = 0.1 * cycle; + // Whether to run refresh in the background. + runsInBackground = true; +} + + +/** Simulates an app becoming visible. */ +function simulateAppIsVisible() { + // Simulate the app becoming visible. + makeAppVisible(); + // This is needed for the internal promises to resolve. + clock.tick(0.1); +} + + +/** + * Rounds the actual value and then asserts it is equal to the expected rounded + * value. + * @param {number} expected The expected value. + * @param {number} actual The actual value. + */ +function assertRoundedEqual(expected, actual) { + // Round to the nearest and then compare. + assertEquals(Math.floor(expected), Math.floor(actual)); +} + + +function tearDown() { + // Reset stubs, mocks and all global test variables. + stubs.reset(); + forceRetryError = false; + operation = null; + retryPolicy = null; + getWaitDuration = null; + upperBound = null; + lowerBound = null; + runsInBackground = true; + makeAppVisible = null; + unrecoverableOperation = null; + if (proactiveRefresh) { + proactiveRefresh.stop(); + proactiveRefresh = null; + } + lastTimestamp = 0; + goog.dispose(clock); +} + + +function testProactiveRefresh_invalidBounds() { + var error = assertThrows(function() { + new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, upperBound, lowerBound, + runsInBackground); + }); + assertEquals( + 'Proactive refresh lower bound greater than upper bound!', error.message); +} + + +function testProactiveRefresh_runsInBackground_success() { + // Test proactive refresh with multiple successful runs when the refresh can + // run in the background. + // Can run in background. + runsInBackground = true; + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, upperBound, + runsInBackground); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); + // Start proactive refresh. + proactiveRefresh.start(); + // Assert proactive refresh is running. + assertTrue(proactiveRefresh.isRunning()); + // Simulate one cycle passed. + clock.tick(cycle); + // Confirm operation run after one cycle. + assertEquals(1, operation.getCallCount()); + assertEquals(cycle, lastTimestamp); + // Confirm operation run after 2 cycle. + clock.tick(cycle); + assertEquals(2, operation.getCallCount()); + assertEquals(2 * cycle, lastTimestamp); + // Confirm operation run after 3 cycles. + clock.tick(cycle); + assertEquals(3, operation.getCallCount()); + assertEquals(3 * cycle, lastTimestamp); + // Stop proactive refresh. + proactiveRefresh.stop(); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); + // Confirm proactive refresh stopped even after another cycle. + clock.tick(cycle); + assertEquals(3, operation.getCallCount()); + + // Restart should reset and start again. + proactiveRefresh.start(); + // Assert proactive refresh is running. + assertTrue(proactiveRefresh.isRunning()); + // Simulate another cycle. + clock.tick(cycle); + // Operation should run again. + assertEquals(4, operation.getCallCount()); + assertEquals(5 * cycle, lastTimestamp); + // Stop proactive refresh + proactiveRefresh.stop(); + // Confirm proactive refresh stopped. + clock.tick(cycle); + assertEquals(4, operation.getCallCount()); +} + + +function testProactiveRefresh_cannotRunInBackground_success() { + // Test proactive refresh with multiple successful runs when the refresh + // cannot run in the background. + // Run while forcing refresh only when app is visible. + runsInBackground = false; + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, upperBound, + runsInBackground); + // Start proactive refresh. + proactiveRefresh.start(); + // Simulate 3.5 cycles passed. + clock.tick(3.5 * cycle); + // As app is not visible, operation should not run. + assertEquals(0, operation.getCallCount()); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Operation should run immediately at that point in time. + assertEquals(1, operation.getCallCount()); + assertRoundedEqual(3.5 * cycle, lastTimestamp); + // Simulate 1.5 cycles passed and then app is visible again. + clock.tick(1.5 * cycle); + simulateAppIsVisible(); + // Operation should run immediately. + assertEquals(2, operation.getCallCount()); + assertRoundedEqual(5 * cycle, lastTimestamp); + // Simulate app immediately visible on next run after the typical success + // interval. + clock.tick(1 * cycle); + simulateAppIsVisible(); + // Operation should run. + assertEquals(3, operation.getCallCount()); + assertRoundedEqual(6 * cycle, lastTimestamp); + // Stop refresh. + proactiveRefresh.stop(); + // Simulate another cycle passed. + clock.tick(1 * cycle); + // Even after app is visible, operation should not run again. + simulateAppIsVisible(); + // No additional run. + assertEquals(3, operation.getCallCount()); +} + + +function testProactiveRefresh_unrecoverableError() { + // Test proactive refresh when an error that does not meet retry policy is + // thrown. + // Can run in background. + runsInBackground = true; + // Test with an operation that throws an unrecoverable error. + proactiveRefresh = new fireauth.ProactiveRefresh( + unrecoverableOperation, retryPolicy, getWaitDuration, lowerBound, + upperBound, runsInBackground); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); + // Start proactive refresh. + proactiveRefresh.start(); + // After 3 cycles, only one run should have been recorded. + clock.tick(3 * cycle); + // Should run once, fail and never run again. + assertEquals(1, unrecoverableOperation.getCallCount()); + // Operation should only run after first cycle. + assertEquals(cycle, lastTimestamp); + // Assert proactive refresh is running. + assertTrue(proactiveRefresh.isRunning()); + // Stop proactive refresh. + proactiveRefresh.stop(); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); +} + + +function testProactiveRefresh_runsInBackground_retryPolicy() { + // Test exponential backoff after a network error when the refresh can run in + // the background. + // Can run in background. + runsInBackground = true; + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, upperBound, + runsInBackground); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); + // Start proactive refresh. + proactiveRefresh.start(); + clock.tick(1 * cycle); + // Should run once after a cycle. + assertEquals(1, operation.getCallCount()); + assertEquals(cycle, lastTimestamp); + // Simulate error that meets retry policy. + forceRetryError = true; + // Wait for one cycle. + clock.tick(1 * cycle); + // Operation should run after one cycle. + assertEquals(2, operation.getCallCount()); + assertEquals(2 * cycle, lastTimestamp); + // As error that meets retry policy detected, this should rerun at lower bound + // interval. + clock.tick(0.1 * cycle); + // Rerun after 0.1 cycles. + assertEquals(3, operation.getCallCount()); + assertEquals(2.1 * cycle, lastTimestamp); + // Should run again in 0.2 cycles. + clock.tick(0.2 * cycle); + assertEquals(4, operation.getCallCount()); + assertEquals(2.3 * cycle, lastTimestamp); + // Should run again in 0.4 cycles. + clock.tick(0.4 * cycle); + assertEquals(5, operation.getCallCount()); + assertEquals(2.7 * cycle, lastTimestamp); + // Should run again in 0.8 cycles. + clock.tick(0.8 * cycle); + assertEquals(6, operation.getCallCount()); + assertEquals(3.5 * cycle, lastTimestamp); + // Should reach upper bound of 0.9. + clock.tick(0.9 * cycle); + // Reruns at upper bound interval. + assertEquals(7, operation.getCallCount()); + assertEquals(4.4 * cycle, lastTimestamp); + // Should not exceed upper bound. + clock.tick(0.9 * cycle); + // Reruns again at upper bound interval. + assertEquals(8, operation.getCallCount()); + assertEquals(5.3 * cycle, lastTimestamp); + // Assert proactive refresh is running. + assertTrue(proactiveRefresh.isRunning()); + // Simulate success on next run. + forceRetryError = false; + // Next rerun at upper bound interval should succeed. + clock.tick(0.9 * cycle); + assertEquals(9, operation.getCallCount()); + assertEquals(6.2 * cycle, lastTimestamp); + // Next one should run at normal cycles. + clock.tick(cycle); + assertEquals(10, operation.getCallCount()); + assertEquals(7.2 * cycle, lastTimestamp); + // Assert proactive refresh is running. + assertTrue(proactiveRefresh.isRunning()); + // Stop proactive refresh. + proactiveRefresh.stop(); + // Assert proactive refresh is not running. + assertFalse(proactiveRefresh.isRunning()); +} + + +function testProactiveRefresh_equalBounds() { + // Check when upper and lower bounds are equal that the same wait is applied + // each time an error occurs. + runsInBackground = true; + // Use same upper/lower bound. + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, lowerBound, + runsInBackground); + // Simulate error that meets retry policy. + forceRetryError = true; + // Start proactive refresh. + proactiveRefresh.start(); + // Operation should run after one cycle with an error occurring. + clock.tick(1 * cycle); + // Should run once. + assertEquals(1, operation.getCallCount()); + assertEquals(cycle, lastTimestamp); + // Simulate one lower bound duration. + clock.tick(lowerBound); + // Should run again. + assertEquals(2, operation.getCallCount()); + assertEquals(cycle + lowerBound, lastTimestamp); + // Simulate another lower bound duration. + clock.tick(lowerBound); + // Should run again. + assertEquals(3, operation.getCallCount()); + assertEquals(cycle + 2 * lowerBound, lastTimestamp); + // Simulate another lower bound duration. + clock.tick(lowerBound); + // Should run again. + assertEquals(4, operation.getCallCount()); + assertEquals(cycle + 3 * lowerBound, lastTimestamp); + // Stop proactive refresh. + proactiveRefresh.stop(); +} + + +function testProactiveRefresh_cannotRunInBackground_retryPolicy() { + // Test exponential backoff after a network error when the refresh cannot run + // in the background. + // Can run in background. + runsInBackground = false; + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, upperBound, + runsInBackground); + // Simulate error that meets retry policy. + forceRetryError = true; + // Start proactive refresh. + proactiveRefresh.start(); + // Operation should run after one cycle if app is visible. + clock.tick(1 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Should run once. + assertEquals(1, operation.getCallCount()); + assertEquals(cycle, lastTimestamp); + // Simulate 3 cycles before app is visible again. + clock.tick(3 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Even though an error occurred, it won't run until app is visible again. + assertEquals(2, operation.getCallCount()); + assertRoundedEqual(4 * cycle, lastTimestamp); + // Simulate 3 cycles before app is visible again. + clock.tick(3 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Even though an error occurred, it won't run until app is visible again. + assertEquals(3, operation.getCallCount()); + assertRoundedEqual(7 * cycle, lastTimestamp); + // Simulate 0.4 cycles before app is visible again. This is the expected time + // to rerun if the app is visible and 2 errors already occurred. + clock.tick(0.4 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Next run occurs at expected time as app is visible. + assertEquals(4, operation.getCallCount()); + assertRoundedEqual(7.4 * cycle, lastTimestamp); + // Simulate 0.8 cycles and app is visible. The is the expected next run after + // 3 errors. + clock.tick(0.8 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Next run occurs at expected time as app is visible. + assertEquals(5, operation.getCallCount()); + assertRoundedEqual(8.2 * cycle, lastTimestamp); + // Simulate next operation succeeds after expected retry interval. + forceRetryError = false; + clock.tick(0.9 * cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + // Should have succeeded after 0.9 cycles. + assertEquals(6, operation.getCallCount()); + assertRoundedEqual(9.1 * cycle, lastTimestamp); + // Next run should occur as scheduled after a successful refresh. + clock.tick(cycle); + // Simulate the app becoming visible. + simulateAppIsVisible(); + assertEquals(7, operation.getCallCount()); + assertRoundedEqual(10.1 * cycle, lastTimestamp); + // Stop proactive refresh. + proactiveRefresh.stop(); +} + + +function testProactiveRefresh_runsInBackground_retryPolicy_stopAndRestart() { + // Test exponential backoff after a network error when the refresh can run in + // the background. Test that restart resets the previous error. + // Can run in background. + runsInBackground = true; + proactiveRefresh = new fireauth.ProactiveRefresh( + operation, retryPolicy, getWaitDuration, lowerBound, upperBound, + runsInBackground); + // Simulate error that meets retry policy. + forceRetryError = true; + // Start proactive refresh. + proactiveRefresh.start(); + // After once cycle, operation should run and error detected. + clock.tick(cycle); + assertEquals(1, operation.getCallCount()); + assertEquals(cycle, lastTimestamp); + // Should rerun at lower bound. + clock.tick(0.1 * cycle); + assertEquals(2, operation.getCallCount()); + assertEquals(1.1 * cycle, lastTimestamp); + // Stop proactive refresh. + proactiveRefresh.stop(); + // Restart proactive refresh. + proactiveRefresh.start(); + // Confirm next run is at regular interval regardless of previous error. + clock.tick(cycle); + assertEquals(3, operation.getCallCount()); + assertEquals(2.1 * cycle, lastTimestamp); + // Stop proactive refresh. + proactiveRefresh.stop(); +} diff --git a/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js b/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js new file mode 100644 index 00000000000..82d1bc1c82d --- /dev/null +++ b/packages/auth/test/recaptchaverifier/recaptchaverifier_test.js @@ -0,0 +1,1816 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for recaptchaverifier.js. + */ + +goog.provide('fireauth.RecaptchaVerifierTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.BaseRecaptchaVerifier'); +goog.require('fireauth.RecaptchaVerifier'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.constants'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.Uri'); +goog.require('goog.dom'); +goog.require('goog.html.TrustedResourceUrl'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.TestCase'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.mockmatchers'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.RecaptchaVerifierTest'); + + +var mockControl; +var stubs = new goog.testing.PropertyReplacer(); +var app; +var grecaptcha; +var myElement, myElement2; +var ignoreArgument; +var loaderInstance; + + +/** + * Initialize reCAPTCHA mocks. This mocks the grecaptcha library. + */ +function initializeRecaptchaMocks() { + var recaptcha = { + // Recaptcha challenge ID. + 'challengesId': 0, + // Recaptcha array of instances. + 'instances': [], + // Render reCAPTCHA instance. + 'render': function(container, parameters) { + // New widget ID. + var id = recaptcha.instances.length; + // Element container. + var ele = goog.dom.getElement(container); + // Store new reCAPTCHA instance and its parameters. + recaptcha.instances.push({ + 'container': container, + 'response': 'response-' + recaptcha.challengesId, + 'userResponse': '', + 'parameters': parameters, + 'callback': parameters['callback'] || null, + 'expired-callback': parameters['expired-callback'] || null, + 'execute': false + }); + // Increment challenges ID. + recaptcha.challengesId++; + if (parameters.size !== 'invisible') { + // recaptcha can only be rendered on an empty element. + assertFalse(goog.dom.getChildren(ele).length > 0); + // Fill container with some HTML to simulate rendered widget. + ele.innerHTML = '
'; + } + // Return the reCAPTCHA widget ID. + return id; + }, + // Reset reCAPTCHA instance + 'reset': function(opt_id) { + // If widget ID not provided, use last created one. + var id = typeof opt_id !== 'undefined' ? + opt_id : recaptcha.instances.length - 1; + // Assert instance exists. + assertNotNullNorUndefined(recaptcha.instances[id]); + var parameters = recaptcha.instances[id]['parameters']; + // Reset instance challenge and its other properties. + recaptcha.instances[id] = { + 'container': parameters['container'], + 'response': 'response-' + recaptcha.challengesId, + 'userResponse': '', + 'parameters': parameters, + 'callback': parameters['callback'], + 'expired-callback': parameters['expired-callback'], + 'execute': false + }; + // Increment challenges ID. + recaptcha.challengesId++; + }, + // Returns reCAPTCHA instance's user response. + 'getResponse': function(opt_id) { + // If widget ID not provided, use last created one. + var id = typeof opt_id !== 'undefined' ? + opt_id : recaptcha.instances.length - 1; + // Assert instance exists. + assertNotNullNorUndefined(recaptcha.instances[id]); + // Return user response. + return recaptcha.instances[id]['userResponse'] || ''; + }, + // Executes the invisible reCAPTCHA. This will either force a reCAPTCHA + // visible challenge or resolve immediately. For testing, the former + // scenario is used. + 'execute': function(opt_id) { + // If widget id not provided, use last created one. + var id = typeof opt_id !== 'undefined' ? + opt_id : recaptcha.instances.length - 1; + // Assert instance exists. + assertNotNullNorUndefined(recaptcha.instances[id]); + var instance = recaptcha.instances[id]; + var parameters = instance['parameters']; + // execute should not be called on a visible reCAPTCHA. + if (parameters['size'] !== 'invisible') { + throw new Error('execute called on visible reCAPTCHA!'); + } + // Mark execute flag as true. + instance['execute'] = true; + }, + // For internal testing, simulates the reCAPTCHA corresponding to ID passed + // is solved. + 'solveResponse': function(opt_id) { + // If widget ID not provided, use last created one. + var id = typeof opt_id !== 'undefined' ? + opt_id : recaptcha.instances.length - 1; + // Assert instance exists. + assertNotNullNorUndefined(recaptcha.instances[id]); + var instance = recaptcha.instances[id]; + var parameters = instance['parameters']; + // Updated user response with the solve response. + instance['userResponse'] = instance['response']; + // execute must have been called on invisible reCAPTCHA. + if (!instance['execute'] && parameters['size'] === 'invisible') { + throw new Error('execute needs to be called before solving response!'); + } + // Trigger reCAPTCHA callback. + if (instance['callback'] && + typeof instance['callback'] == 'function') { + instance['callback'](instance['response']); + } + // Update next challenge response. + instance['response'] = 'response-' + recaptcha.challengesId; + recaptcha.challengesId++; + }, + // For internal testing, simulates the reCAPTCHA token corresponding to ID + // passed is expired. + 'expireResponse': function(opt_id) { + // If widget ID not provided, use last created one. + var id = typeof opt_id !== 'undefined' ? + opt_id : recaptcha.instances.length - 1; + // Assert instance exists. + assertNotNullNorUndefined(recaptcha.instances[id]); + var instance = recaptcha.instances[id]; + // Reset user response. + instance['userResponse'] = ''; + // Trigger expired callback. + if (instance['expired-callback'] && + typeof instance['expired-callback'] == 'function') { + instance['expired-callback'](); + } + // Reset execute. + instance['execute'] = false; + } + }; + // Fake the Recaptcha global object. + goog.global['grecaptcha'] = recaptcha; +} + + +/** + * Asserts the expected parameters used to initialize the reCAPTCHA. + * @param {number} widgetId The reCAPTCHA widget ID. + * @param {!Element|string} expectedContainer The expected reCAPTCHA container + * parameter. + * @param {!Object} expectedParams The expected parameters used to initialize + * the reCAPTCHA. + */ +function assertRecaptchaParams(widgetId, expectedContainer, expectedParams) { + // Confirm all expected parameters passed to the specified reCAPTCHA. + // This check excludes callbacks. + var instance = grecaptcha.instances[widgetId]; + var actualParameters = instance['parameters']; + for (var key in expectedParams) { + if (expectedParams.hasOwnProperty(key) && + key != 'callback' && + key != 'expired-callback') { + assertEquals(expectedParams[key], actualParameters[key]); + } + } + // Confirm the reCAPTCHA initialized on the expected container. + if (expectedParams.size !== 'invisible') { + // For visible reCAPTCHA, confirm expectedContainer element matches the + // parent of the actual container. + assertEquals( + goog.dom.getElement(expectedContainer), + goog.dom.getParentElement(instance.container)); + } else { + assertEquals(expectedContainer, instance.container); + } +} + + +function setUp() { + mockControl = new goog.testing.MockControl(); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl.$resetAll(); + app = null; + // Create DIV test element and add to document. + myElement = goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'recaptcha'}); + document.body.appendChild(myElement); + // Create another DIV test element and add to document. + myElement2 = goog.dom.createDom(goog.dom.TagName.DIV, {'id': 'recaptcha2'}); + document.body.appendChild(myElement2); + // Bypass singleton for tests so loaders are not shared among different tests. + loaderInstance = new fireauth.BaseRecaptchaVerifier.Loader(); + stubs.replace( + fireauth.BaseRecaptchaVerifier.Loader, + 'getInstance', + function() { + return loaderInstance; + }); +} + + +function tearDown() { + // Destroy both elements. + if (myElement) { + goog.dom.removeNode(myElement); + myElement = null; + } + if (myElement2) { + goog.dom.removeNode(myElement2); + myElement2 = null; + } + // Reset global grecaptcha. + grecaptcha = null; + delete grecaptcha; + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } + delete goog.global['devCallback']; + delete goog.global['devExpiredCallback']; + stubs.reset(); +} + + +/** + * Sets the Auth service on the provided app instance if not already set. + * @param {!firebase.app.App} app The Firebase app instance on which to + * initialize the Auth service if not already available. + */ +function initializeAuthServiceOnApp(app) { + // Do nothing if auth() already exists. + if (typeof app.auth !== 'function') { + // Use set as Auth doesn't exist on the App instance. + stubs.set(app, 'auth', function() { + if (!this.auth_) { + this.auth_ = {}; + } + return this.auth_; + }); + } +} + + +/** + * Simulates the current Auth language on the specified App instance. + * @param {!firebase.app.App} app The expected Firebase App instance. + * @param {?string} languageCode The default Auth language. + */ +function simulateAuthLanguage(app, languageCode) { + initializeAuthServiceOnApp(app); + app.auth().getLanguageCode = function() { + return languageCode; + }; +} + + +/** + * Simulates the current Auth frameworks on the specified App instance. + * @param {!firebase.app.App} app The expected Firebase app instance. + * @param {!Array} frameworks The current frameworks set on the Auth + * instance. + */ +function simulateAuthFramework(app, frameworks) { + initializeAuthServiceOnApp(app); + app.auth().getFramework = function() { + return frameworks; + }; +} + + +/** + * Install the test to run and runs it. + * @param {string} id The test identifier. + * @param {function():!goog.Promise} func The test function to run. + * @return {!goog.Promise} The result of the test. + */ +function installAndRunTest(id, func) { + var testCase = new goog.testing.TestCase(); + testCase.addNewTest(id, func); + var error = null; + return testCase.runTestsReturningPromise().then(function(result) { + assertTrue(result.complete); + // Display error detected. + if (result.errors.length) { + fail(result.errors.join('\n')); + } + assertEquals(1, result.totalCount); + assertEquals(1, result.runCount); + assertEquals(1, result.successCount); + assertEquals(0, result.errors.length); + // Delete app before resolving. + if (app) { + return app.delete(); + } + }).thenCatch(function(err) { + error = err; + // Delete app before resolving. + if (app) { + return app.delete(); + } + }).then(function() { + if (error) { + throw error; + } + }); +} + + +function testBaseRecaptchaVerifier_noHttpOrHttps() { + return installAndRunTest('testBaseAppVerifier_noHttpOrHttps', function() { + var isHttpOrHttps = mockControl.createMethodMock( + fireauth.util, 'isHttpOrHttps'); + isHttpOrHttps().$returns(false).$once(); + mockControl.$replayAll(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.OPERATION_NOT_SUPPORTED, + 'RecaptchaVerifier is only supported in a browser HTTP/HTTPS ' + + 'environment.'); + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + return recaptchaVerifier.render().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + }); +} + + +function testBaseRecaptchaVerifier_withSitekey() { + return installAndRunTest('testBaseAppVerifier_withSitekey', function() { + var options = { + sitekey: 'MY_SITE_KEY' + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'sitekey should not be provided for reCAPTCHA as one is ' + + 'automatically provisioned for the current project.'); + var error = assertThrows(function() { + new fireauth.BaseRecaptchaVerifier('API_KEY', myElement, options); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +function testBaseRecaptchaVerifier_visible_nonEmpty() { + return installAndRunTest('testBaseAppVerifier_visible_nonEmpty', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'reCAPTCHA container is either not found or already contains inner ' + + 'elements!'); + myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); + var error = assertThrows(function() { + new fireauth.BaseRecaptchaVerifier('API_KEY', myElement); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +function testBaseRecaptchaVerifier_invisible_nonEmpty() { + return installAndRunTest( + 'testBaseAppVerifier_invisible_nonEmpty', function() { + myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); + var returnValue = assertNotThrows(function() { + return new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, {'size': 'invisible'}); + }); + assertTrue(returnValue instanceof fireauth.BaseRecaptchaVerifier); + }); +} + + +function testBaseRecaptchaVerifier_invalidContainer() { + return installAndRunTest('testBaseAppVerifier_invalidContainer', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'reCAPTCHA container is either not found or already contains inner ' + + 'elements!'); + var error = assertThrows(function() { + new fireauth.BaseRecaptchaVerifier('API_KEY', 'invalidId'); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +function testBaseRecaptchaVerifier_validContainerId() { + return installAndRunTest('testBaseAppVerifier_validContainerId', function() { + app = firebase.initializeApp({ + apiKey: 'API_KEY' + }, 'test'); + var returnValue = assertNotThrows(function() { + return new fireauth.BaseRecaptchaVerifier( + 'API_KEY', 'recaptcha', {'size': 'compact'}); + }); + assertTrue(returnValue instanceof fireauth.BaseRecaptchaVerifier); + }); +} + + +function testBaseRecaptchaVerifier_render() { + return installAndRunTest('testBaseAppVerifier_render', function() { + // Confirm expected endpoint config and version passed to underlying RPC + // handler. + var version = '1.2.3'; + var endpoint = fireauth.constants.Endpoint.STAGING; + var endpointConfig = { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', endpointConfig, version) + .$returns(rpcHandler); + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, undefined, function() {return null;}, version, + endpointConfig); + assertEquals('recaptcha', recaptchaVerifier['type']); + // Confirm property is readonly. + recaptchaVerifier['type'] = 'modified'; + assertEquals('recaptcha', recaptchaVerifier['type']); + return recaptchaVerifier.render().then(function(widgetId) { + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + grecaptcha.solveResponse(0); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Already rendered. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + // Same unexpired response returned. + assertEquals('response-0', recaptchaToken); + // Expire response. + grecaptcha.expireResponse(0); + var resp = recaptchaVerifier.verify(); + // Solve response after expiration. New reCAPTCHA token should be + // returned. + grecaptcha.solveResponse(0); + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_render_offline() { + return installAndRunTest('testBaseAppVerifier_render_offline', function() { + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var isOnline = mockControl.createMethodMock(fireauth.util, 'isOnline'); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Simulate first attempt fails due to network connection not being + // available. + isOnline().$returns(false); + // Simulate second attempt succeeding. + isOnline().$returns(true); + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image', + // Invalid callback names should be ignored. + 'callback': 'invalid', + 'expired-callback': 'invalid' + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + return recaptchaVerifier.render().thenCatch(function(error) { + // Initial attempt fails due to network connection. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Try again. It should be successful now. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + grecaptcha.solveResponse(0); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Already rendered. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + grecaptcha.expireResponse(0); + var resp = recaptchaVerifier.verify(); + grecaptcha.solveResponse(0); + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_render_grecaptchaLoaded() { + return installAndRunTest('testBaseAppVerifier_recaptchaLoaded', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY' + }; + // In addition, test when the developer passes their own callbacks. + var devCallback = goog.testing.recordFunction(); + var devExpiredCallback = goog.testing.recordFunction(); + var params = { + 'callback': devCallback, + 'expired-callback': devExpiredCallback + }; + var resp = null; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, params); + return recaptchaVerifier.render().then(function(widgetId) { + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + // Simulate the reCAPTCHA challenge solved. + grecaptcha.solveResponse(0); + // Developer callback should be called with the expected token. + assertEquals(1, devCallback.getCallCount()); + assertEquals('response-0', devCallback.getLastCall().getArgument(0)); + // verify should resolve with the same token. + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Already rendered. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Cached response returned. + assertEquals(0, widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Expire the response. + grecaptcha.expireResponse(0); + // Developer expired callback should be triggered. + assertEquals(1, devExpiredCallback.getCallCount()); + // Try to verify again. + resp = recaptchaVerifier.verify(); + // Break thread to allow the verification to pick up the new response. + return goog.Promise.resolve(); + }).then(function() { + // Solve reCAPTCHA. Ths should be picked up. + grecaptcha.solveResponse(0); + // Developer token callback triggered with new token. + assertEquals(2, devCallback.getCallCount()); + assertEquals('response-1',devCallback.getLastCall().getArgument(0)); + assertEquals('response-1', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Expire token. + grecaptcha.expireResponse(0); + // Expired callback triggered. + assertEquals(2, devExpiredCallback.getCallCount()); + // Solve reCAPTCHA. + grecaptcha.solveResponse(0); + // Developer callback triggered with the new token. + assertEquals(3, devCallback.getCallCount()); + assertEquals('response-2', devCallback.getLastCall().getArgument(0)); + assertEquals('response-2', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Confirm the first resolved token picked up earlier. + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_render_stringCallbacks() { + return installAndRunTest('testBaseAppVerifier_stringCallbacks', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY' + }; + // Test when the developer passes callback function names instead of the + // function references directly. + goog.global['devCallback'] = goog.testing.recordFunction(); + goog.global['devExpiredCallback'] = goog.testing.recordFunction(); + var params = { + 'callback': 'devCallback', + 'expired-callback': 'devExpiredCallback' + }; + var resp = null; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, params); + return recaptchaVerifier.render().then(function(widgetId) { + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + // Simulate the reCAPTCHA challenge solved. + grecaptcha.solveResponse(0); + // Developer callback should be called with the expected token. + assertEquals(1, goog.global['devCallback'].getCallCount()); + assertEquals( + 'response-0', + goog.global['devCallback'].getLastCall().getArgument(0)); + // verify should resolve with the same token. + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Already rendered. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Cached response returned. + assertEquals(0, widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Expire the response. + grecaptcha.expireResponse(0); + // Developer expired callback should be triggered. + assertEquals(1, goog.global['devExpiredCallback'].getCallCount()); + // Try to verify again. + resp = recaptchaVerifier.verify(); + // Break thread to allow the verification to pick up the new response. + return goog.Promise.resolve(); + }).then(function() { + // Solve reCAPTCHA. Ths should be picked up. + grecaptcha.solveResponse(0); + // Developer token callback triggered with new token. + assertEquals(2, goog.global['devCallback'].getCallCount()); + assertEquals( + 'response-1', + goog.global['devCallback'].getLastCall().getArgument(0)); + assertEquals('response-1', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Expire token. + grecaptcha.expireResponse(0); + // Expired callback triggered. + assertEquals(2, goog.global['devExpiredCallback'].getCallCount()); + // Solve reCAPTCHA. + grecaptcha.solveResponse(0); + // Developer callback triggered with the new token. + assertEquals(3, goog.global['devCallback'].getCallCount()); + assertEquals( + 'response-2', + goog.global['devCallback'].getLastCall().getArgument(0)); + assertEquals('response-2', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Confirm the first resolved token picked up earlier. + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_getRecaptchaParamError() { + return installAndRunTest( + 'testBaseAppVerifier__recaptchaParamError', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'Something unexpected happened.'); + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + // Simulate first attempt fails for some unknown reason. + rpcHandler.getRecaptchaParam().$once().$does(function() { + return goog.Promise.reject(expectedError); + }); + // Allow second attempt to succeed. + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + return recaptchaVerifier.render().thenCatch(function(error) { + // First attempt fails with the same expected underlying error. + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + // Try again. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Resolves with the expected widget ID. + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Same cached response. All other behavior should be the same. + assertEquals(0, widgetId); + }); + }); +} + + +function testBaseRecaptchaVerifier_verify_newToken() { + return installAndRunTest('testBaseAppVerifier_verify_newToken', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var resp = null; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + // Simulate challenge solved after calling verify. + setTimeout(function() { + grecaptcha.solveResponse(0); + }, 10); + // This should render the reCAPTCHA and then after challenge is solve, + // resolve with the expected reCAPTCHA token. + return recaptchaVerifier.verify().then(function(recaptchaToken) { + assertRecaptchaParams(0, myElement, expectedParams); + assertEquals('response-0', recaptchaToken); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Expire the token response. + grecaptcha.expireResponse(0); + // Verify again. + resp = recaptchaVerifier.verify(); + return goog.Promise.resolve(); + }).then(function() { + // New response should be picked up. + grecaptcha.solveResponse(0); + assertEquals('response-1', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Expire and then solve again. + grecaptcha.expireResponse(0); + grecaptcha.solveResponse(0); + assertEquals('response-2', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Verify should resolve with the first expected response. + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_idElement() { + return installAndRunTest('testBaseAppVerifier_idElement', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + // Pass the element ID instead of the element itself for the container + // argument. + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', 'recaptcha'); + setTimeout(function() { + grecaptcha.solveResponse(0); + }, 10); + return recaptchaVerifier.verify().then(function(recaptchaToken) { + // reCAPTCHA initialized with the expected parameters. + assertRecaptchaParams(0, 'recaptcha', expectedParams); + // verify resolves with the expected response. + assertEquals('response-0', recaptchaToken); + // Same response cached until expiration. + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_verify_reset() { + return installAndRunTest('testBaseAppVerifier_verify_reset', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + var widgetIdReturned = null; + return recaptchaVerifier.render().then(function(widgetId) { + assertEquals(0, widgetId); + widgetIdReturned = widgetId; + grecaptcha.solveResponse(widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // After reset, the cached response is forgotten and the new one is used. + assertEquals('response-0', grecaptcha.getResponse(widgetIdReturned)); + recaptchaVerifier.reset(); + assertEquals('', grecaptcha.getResponse(widgetIdReturned)); + setTimeout(function() { + // The new solved challenge will be used as the old response is reset. + grecaptcha.solveResponse(0); + }, 10); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + // Since reCAPTCHA is reset, the new response should be used. + assertEquals('response-2', recaptchaToken); + assertEquals('response-2', grecaptcha.getResponse(0)); + }); + }); +} + + +function testBaseRecaptchaVerifier_multipleVerifiers() { + return installAndRunTest('testBaseAppVerifier_multipleVerifiers', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig1 = { + 'recaptchaSiteKey': 'SITE_KEY1' + }; + var recaptchaConfig2 = { + 'recaptchaSiteKey': 'SITE_KEY2' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandler2 = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument) + .$returns(rpcHandler); + rpcHandlerConstructor('API_KEY', null, ignoreArgument) + .$returns(rpcHandler2); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig1); + rpcHandler2.getRecaptchaParam().$once().$returns(recaptchaConfig2); + mockControl.$replayAll(); + + var expectedParams1 = { + 'sitekey': 'SITE_KEY1', + 'theme': 'dark ', + 'type': 'audio', + 'size': 'compact' + }; + var params1 = { + 'theme': 'dark ', + 'type': 'audio', + 'size': 'compact' + }; + var expectedParams2 = { + 'sitekey': 'SITE_KEY2', + 'theme': 'light', + 'type': 'image', + 'tabindex': 1 + }; + var params2 = { + 'theme': 'light', + 'type': 'image', + 'tabindex': 1 + }; + // Initialize 2 reCAPTCHA verifiers. + var recaptchaVerifier1 = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, params1); + var recaptchaVerifier2 = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement2, params2); + // Render first reCAPTCHA. + var p1 = recaptchaVerifier1.render().then(function(widgetId) { + // Expected widget ID returned. + assertEquals(0, widgetId); + // reCAPTCHA initialized with the expected parameters. + assertRecaptchaParams(widgetId, myElement, expectedParams1); + // Solve the challenge for the first reCAPTCHA. + grecaptcha.solveResponse(widgetId); + // verify should be resolved with the expected response. + return recaptchaVerifier1.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + }); + // Render second reCAPTCHA. + var p2 = recaptchaVerifier2.render().then(function(widgetId) { + // Expected widget ID returned. + assertEquals(1, widgetId); + // reCAPTCHA initialized with the expected parameters. + assertRecaptchaParams(widgetId, myElement2, expectedParams2); + // Solve the challenge for the second reCAPTCHA. + grecaptcha.solveResponse(widgetId); + // verify should be resolved with the expected response. + return recaptchaVerifier2.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + return goog.Promise.all([p1, p2]); + }); +} + + +function testBaseRecaptchaVerifier_verify_invisibleRecaptcha() { + return installAndRunTest('testBaseAppVerifier_verify_invisible', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'size': 'invisible' + }; + var resp = null; + // Initialize invisible reCAPTCHA. + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, {'size': 'invisible'}); + // Simulate invisible reCAPTCHA solved after some delay. + setTimeout(function() { + grecaptcha.execute(0); + grecaptcha.solveResponse(0); + }, 10); + // This should render and resolve with the expected response. + return recaptchaVerifier.verify().then(function(recaptchaToken) { + assertRecaptchaParams(0, myElement, expectedParams); + assertEquals('response-0', recaptchaToken); + // Same cached response returned. + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Expire response. + grecaptcha.expireResponse(0); + // verify again. + resp = recaptchaVerifier.verify(); + return goog.Promise.resolve(); + }).then(function() { + // After this is solved, verify should resolve with it. + // Even though execute is not call here, it will not throw an error as + // verify calls it underneath. + grecaptcha.solveResponse(0); + assertEquals('response-1', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + // Expire and solve again. This should be ignored by verify. + grecaptcha.expireResponse(0); + grecaptcha.execute(0); + grecaptcha.solveResponse(0); + assertEquals('response-2', grecaptcha.getResponse()); + return goog.Promise.resolve(); + }).then(function() { + return resp; + }).then(function(recaptchaToken) { + // Expected first response returned. + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testBaseRecaptchaVerifier_visible_clear() { + return installAndRunTest('testBaseAppVerifier_visible_clear', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'RecaptchaVerifier instance has been destroyed.'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + return recaptchaVerifier.render().then(function(widgetId) { + // After rendering a visible reCAPTCHA, confirm reCAPTCHA HTML content + // added to that container. + assertEquals( + 1, + goog.dom.getChildren(goog.dom.getElement(myElement)).length); + // Clear reCAPTCHA. + recaptchaVerifier.clear(); + // reCAPTCHA content should be cleared. + assertEquals( + 0, + goog.dom.getChildren(goog.dom.getElement(myElement)).length); + // Calling verify will throw the expected error. + var error = assertThrows(function() { + recaptchaVerifier.verify(); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + }); +} + + +function testBaseRecaptchaVerifier_pendingPromises_clear() { + return installAndRunTest( + 'testBaseAppVerifier_pendingPromises_clear', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + mockControl.$replayAll(); + + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement); + var p = new goog.Promise(function(resolve, reject) { + // This will remain pending until it is cancelled. + recaptchaVerifier.verify().then(function(token) { + reject('reCAPTCHA verify pending promise should be cancelled!'); + }).thenCatch(function(error) { + assertFalse(recaptchaVerifier.hasPendingPromises()); + // Cancellation error should trigger. + assertEquals( + 'RecaptchaVerifier instance has been destroyed.', error.message); + resolve(); + }); + // recaptchaVerifier verify promise should be pending. + assertTrue(recaptchaVerifier.hasPendingPromises()); + }); + // Clear reCAPTCHA. This should cancel the above pending promise. + recaptchaVerifier.clear(); + return p; + }); +} + + +function testBaseRecaptchaVerifier_invisible_clear() { + return installAndRunTest('testBaseAppVerifier_invisible_clear', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'RecaptchaVerifier instance has been destroyed.'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + // Append 2 DIVs to the container. This is fine for invisible reCAPTCHAs. + myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); + myElement.appendChild(goog.dom.createDom(goog.dom.TagName.DIV)); + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, {'size': 'invisible'}); + return recaptchaVerifier.render().then(function(widgetId) { + // After rendering, no new content added to the container. + assertEquals( + 2, + goog.dom.getChildren(goog.dom.getElement(myElement)).length); + // Clear reCAPTCHA. + recaptchaVerifier.clear(); + // No changes to container. + assertEquals( + 2, + goog.dom.getChildren(goog.dom.getElement(myElement)).length); + // Calling any reCAPTCHA verifier API will throw the expected error. + var error = assertThrows(function() { + recaptchaVerifier.render(); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); + }); +} + + +function testBaseRecaptchaVerifier_localization() { + return installAndRunTest('testBaseAppVerifier_localization', function() { + var currentLanguageCode = null; + var appLanguageCode = null; + var getLanguageCode = function() {return appLanguageCode;}; + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + // First instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'fr'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('fr', uri.getParameterValue('hl')); + currentLanguageCode = 'fr'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Second instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'de'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('de', uri.getParameterValue('hl')); + currentLanguageCode = 'de'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Third instance. As same language is used, no dependency reload. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Fourth instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with null language code. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + currentLanguageCode = null; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Fifth instance. As existing reCAPTCHA instance available, no dependency + // reload occurs. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Sixth instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'ru'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('ru', uri.getParameterValue('hl')); + currentLanguageCode = 'ru'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Seventh instance. As existing reCAPTCHA instance available, no dependency + // reload occurs. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + // Set Auth language code to 'fr'. + appLanguageCode = 'fr'; + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + var recaptchaVerifier2; + // First instance rendered with 'fr' language code. + return recaptchaVerifier.render().then(function(widgetId) { + assertEquals('fr', currentLanguageCode); + // Change Auth language code to 'de'. + appLanguageCode = 'de'; + // Clear existing reCAPTCHA verifier. + recaptchaVerifier.clear(); + // Render new instance which should use 'de' language code. + recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals('de', currentLanguageCode); + // Clear existing instance. + recaptchaVerifier.clear(); + // As same language is used, no dependency reload. + recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals('de', currentLanguageCode); + // Language reset should force reload of dependency. + appLanguageCode = null; + // Clear existing reCAPTCHA instance. + recaptchaVerifier.clear(); + // Render new instance. It should force a dependency reload with null + // language code. + recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertNull(currentLanguageCode); + // Simulate reCAPTCHA is not cleared and language is changed. + appLanguageCode = 'ru'; + // No dependency reload should occur as it could break existing instance. + recaptchaVerifier2 = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement2, null, getLanguageCode); + return recaptchaVerifier2.render(); + }).then(function(widgetId) { + assertNull(currentLanguageCode); + // Clear both. + recaptchaVerifier.clear(); + recaptchaVerifier2.clear(); + // This should reload dependency with last language code. + recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Last loaded language code. + assertEquals('ru', currentLanguageCode); + // Clear existing instance. + recaptchaVerifier.clear(); + // Assume developer rendered external instance. + grecaptcha.render(myElement, expectedParams); + // Changing language will not reload dependencies. + appLanguageCode = 'ar'; + recaptchaVerifier2 = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement2, null, getLanguageCode); + return recaptchaVerifier2.render(); + }).then(function(widgetId) { + // Previous loaded language code remains. + assertEquals('ru', currentLanguageCode); + recaptchaVerifier2.clear(); + }); + }); +} + + +function testBaseRecaptchaVerifier_localization_alreadyLoaded() { + return installAndRunTest( + 'testBaseAppVerifier_locale_alreadyLoaded', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + // Initialize after simulated reCAPTCHA dependency is loaded to make sure + // internal counter knows about the existence of grecaptcha. + loaderInstance = new fireauth.BaseRecaptchaVerifier.Loader(); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var appLanguageCode = 'de'; + var getLanguageCode = function() {return appLanguageCode;}; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // No language code reload. + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + // Even though this is the first instance of reCAPTCHA verifier, we can't + // know for sure that there is no other external instance. We shouldn't + // reload the reCAPTCHA dependencies. + var recaptchaVerifier = new fireauth.BaseRecaptchaVerifier( + 'API_KEY', myElement, null, getLanguageCode); + return recaptchaVerifier.render().then(function(widgetId) { + recaptchaVerifier.clear(); + }); + }); +} + + +function testBaseRecaptchaVerifierLoader() { + return installAndRunTest('testBaseAppVerifier_loader', function() { + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var currentLanguageCode = null; + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + // First load. Dependency loaded with null language code. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + currentLanguageCode = null; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + // Third load. Dependency loaded with 'fr' language code. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('fr', uri.getParameterValue('hl')); + currentLanguageCode = 'fr'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + // Seventh load. Dependency loaded with 'ar' language code. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('ar', uri.getParameterValue('hl')); + currentLanguageCode = 'ar'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + mockControl.$replayAll(); + var loader = new fireauth.BaseRecaptchaVerifier.Loader(); + // First load with empty string hl. + return loader.loadRecaptchaDeps(null).then(function() { + assertNull(currentLanguageCode); + // No load needed. + return loader.loadRecaptchaDeps(null); + }).then(function() { + assertNull(currentLanguageCode); + // Will load. + return loader.loadRecaptchaDeps('fr'); + }).then(function() { + assertEquals('fr', currentLanguageCode); + // Simulate reCAPTCHAs rendered. + grecaptcha.render(myElement, expectedParams); + grecaptcha.render(myElement2, expectedParams); + // This will do nothing. + return loader.loadRecaptchaDeps('de'); + }).then(function() { + assertEquals('fr', currentLanguageCode); + loader.clearSingleRecaptcha(); + // Still no load as one instance remains. + return loader.loadRecaptchaDeps('ru'); + }).then(function() { + assertEquals('fr', currentLanguageCode); + loader.clearSingleRecaptcha(); + // Language still loaded. No reload needed. + return loader.loadRecaptchaDeps('fr'); + }).then(function() { + assertEquals('fr', currentLanguageCode); + // This will load reCAPTCHA dependencies for specified language. + return loader.loadRecaptchaDeps('ar'); + }).then(function() { + assertEquals('ar', currentLanguageCode); + }); + }); +} + + +function testBaseRecaptchaVerifierLoader_alreadyLoaded() { + return installAndRunTest( + 'testBaseAppVerifier_loader_alreadyLoaded', function() { + // Simulate grecaptcha loaded. + initializeRecaptchaMocks(); + // No depedency load. + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + safeLoad(ignoreArgument).$times(0); + mockControl.$replayAll(); + + // Initialize after simulated reCAPTCHA dependency is loaded. + var loader = new fireauth.BaseRecaptchaVerifier.Loader(); + return loader.loadRecaptchaDeps('de'); + }); +} + + +function testRecaptchaVerifier_noApp() { + return installAndRunTest('testAppVerifier_noApp', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.ARGUMENT_ERROR, + 'No firebase.app.App instance is currently initialized.'); + var error = assertThrows(function() { + new fireauth.RecaptchaVerifier(myElement); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +function testRecaptchaVerifier_noApiKey() { + return installAndRunTest('testAppVerifier_noApiKey', function() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_API_KEY); + app = firebase.initializeApp({}, 'test'); + var error = assertThrows(function() { + new fireauth.RecaptchaVerifier(myElement, undefined, app); + }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + }); +} + + +function testRecaptchaVerifier_defaultApp() { + return installAndRunTest('testAppVerifier_defaultApp', function() { + app = firebase.initializeApp({ + apiKey: 'API_KEY' + }, 'test'); + stubs.set(firebase, 'app', function() { + return app; + }); + var returnValue = assertNotThrows(function() { + // Do not pass the App instance explicitly. + return new fireauth.RecaptchaVerifier('recaptcha', {'size': 'compact'}); + }); + assertTrue(returnValue instanceof fireauth.RecaptchaVerifier); + // Should be a subclass of fireauth.BaseRecaptchaVerifier. + assertTrue(returnValue instanceof fireauth.BaseRecaptchaVerifier); + }); +} + + +function testRecaptchaVerifier_render() { + return installAndRunTest('testAppVerifier_render', function() { + // Confirm expected endpoint config passed to underlying RPC handler. + var endpoint = fireauth.constants.Endpoint.STAGING; + var endpointConfig = { + 'firebaseEndpoint': endpoint.firebaseAuthEndpoint, + 'secureTokenEndpoint': endpoint.secureTokenEndpoint + }; + stubs.replace( + fireauth.constants, + 'getEndpointConfig', + function(opt_id) { + return endpointConfig; + }); + // Confirm expected client version with the expected frameworks. + var expectedClientFullVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebase.SDK_VERSION, + [fireauth.util.Framework.FIREBASEUI]); + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + // RpcHandler should be initialized with expected API key, endpoint config + // and client version. + rpcHandlerConstructor('API_KEY', endpointConfig, expectedClientFullVersion) + .$returns(rpcHandler); + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + app = firebase.initializeApp({ + apiKey: 'API_KEY' + }, 'test'); + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + // Simulate FirebaseUI logging. + simulateAuthFramework(app, [fireauth.util.Framework.FIREBASEUI]); + var recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + assertEquals('recaptcha', recaptchaVerifier['type']); + // Confirm property is readonly. + recaptchaVerifier['type'] = 'modified'; + assertEquals('recaptcha', recaptchaVerifier['type']); + return recaptchaVerifier.render().then(function(widgetId) { + assertRecaptchaParams(widgetId, myElement, expectedParams); + assertEquals(0, widgetId); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + grecaptcha.solveResponse(0); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + assertEquals('response-0', recaptchaToken); + // Already rendered. + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals(0, widgetId); + return recaptchaVerifier.verify(); + }).then(function(recaptchaToken) { + // Same unexpired response returned. + assertEquals('response-0', recaptchaToken); + // Expire response. + grecaptcha.expireResponse(0); + var resp = recaptchaVerifier.verify(); + // Solve response after expiration. New reCAPTCHA token should be + // returned. + grecaptcha.solveResponse(0); + return resp; + }).then(function(recaptchaToken) { + assertEquals('response-1', recaptchaToken); + }); + }); +} + + +function testRecaptchaVerifier_localization() { + return installAndRunTest('testAppVerifier_localization', function() { + var currentLanguageCode = null; + var safeLoad = mockControl.createMethodMock(goog.net.jsloader, 'safeLoad'); + var recaptchaConfig = { + 'recaptchaSiteKey': 'SITE_KEY' + }; + var rpcHandler = mockControl.createStrictMock(fireauth.RpcHandler); + var rpcHandlerConstructor = mockControl.createConstructorMock( + fireauth, 'RpcHandler'); + // First instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'fr'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('fr', uri.getParameterValue('hl')); + currentLanguageCode = 'fr'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Second instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'de'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('de', uri.getParameterValue('hl')); + currentLanguageCode = 'de'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Third instance. As same language is used, no dependency reload. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Fourth instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with null language code. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('', uri.getParameterValue('hl')); + currentLanguageCode = null; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Fifth instance. As existing reCAPTCHA instance available, no dependency + // reload occurs. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Sixth instance. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + // Dependency loaded with language code 'ru'. + safeLoad(ignoreArgument) + .$does(function(url) { + var uri = goog.Uri.parse(goog.html.TrustedResourceUrl.unwrap(url)); + var callback = uri.getParameterValue('onload'); + assertEquals('ru', uri.getParameterValue('hl')); + currentLanguageCode = 'ru'; + initializeRecaptchaMocks(); + goog.global[callback](); + }) + .$once(); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + // Seventh instance. As existing reCAPTCHA instance available, no dependency + // reload occurs. + rpcHandlerConstructor('API_KEY', null, ignoreArgument).$returns(rpcHandler); + rpcHandler.getRecaptchaParam().$once().$returns(recaptchaConfig); + mockControl.$replayAll(); + + app = firebase.initializeApp({ + apiKey: 'API_KEY' + }, 'test'); + // Set Auth language code to 'fr'. + simulateAuthLanguage(app, 'fr'); + var expectedParams = { + 'sitekey': 'SITE_KEY', + 'theme': 'light', + 'type': 'image' + }; + var recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + var recaptchaVerifier2; + // First instance rendered with 'fr' language code. + return recaptchaVerifier.render().then(function(widgetId) { + assertEquals('fr', currentLanguageCode); + // Change Auth language code to 'de'. + simulateAuthLanguage(app, 'de'); + // Clear existing reCAPTCHA verifier. + recaptchaVerifier.clear(); + // Render new instance which should use 'de' language code. + recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals('de', currentLanguageCode); + // Clear existing instance. + recaptchaVerifier.clear(); + // As same language is used, no dependency reload. + recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertEquals('de', currentLanguageCode); + // Language reset should force reload of dependency. + simulateAuthLanguage(app, null); + // Clear existing reCAPTCHA instance. + recaptchaVerifier.clear(); + // Render new instance. It should force a dependency reload with null + // language code. + recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + assertNull(currentLanguageCode); + // Simulate reCAPTCHA is not cleared and language is changed. + simulateAuthLanguage(app, 'ru'); + // No dependency reload should occur as it could break existing instance. + recaptchaVerifier2 = new fireauth.RecaptchaVerifier( + myElement2, undefined, app); + return recaptchaVerifier2.render(); + }).then(function(widgetId) { + assertNull(currentLanguageCode); + // Clear both. + recaptchaVerifier.clear(); + recaptchaVerifier2.clear(); + // This should reload dependency with last language code. + recaptchaVerifier = new fireauth.RecaptchaVerifier( + myElement, undefined, app); + return recaptchaVerifier.render(); + }).then(function(widgetId) { + // Last loaded language code. + assertEquals('ru', currentLanguageCode); + // Clear existing instance. + recaptchaVerifier.clear(); + // Assume developer rendered external instance. + grecaptcha.render(myElement, expectedParams); + // Changing language will not reload dependencies. + simulateAuthLanguage(app, 'ar'); + recaptchaVerifier2 = new fireauth.RecaptchaVerifier( + myElement2, undefined, app); + return recaptchaVerifier2.render(); + }).then(function(widgetId) { + // Previous loaded language code remains. + assertEquals('ru', currentLanguageCode); + recaptchaVerifier2.clear(); + }); + }); +} diff --git a/packages/auth/test/rpchandler_test.js b/packages/auth/test/rpchandler_test.js new file mode 100644 index 00000000000..e3a81ee35d9 --- /dev/null +++ b/packages/auth/test/rpchandler_test.js @@ -0,0 +1,5085 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for rpchandler.js. + */ + +goog.provide('fireauth.RpcHandlerTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.AuthErrorWithCredential'); +goog.require('fireauth.AuthProvider'); +goog.require('fireauth.FacebookAuthProvider'); +goog.require('fireauth.GoogleAuthProvider'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.json'); +goog.require('goog.net.XhrIo'); +goog.require('goog.net.XhrLike'); +goog.require('goog.object'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.RpcHandlerTest'); + + +var ignoreArgument; +var gapi = gapi || {}; +var stubs = new goog.testing.PropertyReplacer(); +var rpcHandler = null; +var expectedResponse = { + 'resp1': 'val1', + 'resp2': 'val2' +}; +var expectedStsTokenResponse = { + 'idToken': 'accessToken', + 'access_token': 'accessToken', + 'refresh_token': 'refreshToken', + 'expires_in': '3600' +}; +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +var CURRENT_URL = 'http://www.example.com:8080/foo.htm'; +var clock; +var mockControl; +var delay = 30000; + + +function setUp() { + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + stubs.replace( + goog.net.XhrIo.prototype, + 'send', + goog.testing.recordFunction()); + stubs.replace( + goog.net.XhrIo.prototype, + 'listen', + goog.testing.recordFunction()); + stubs.replace( + goog.net.XhrIo.prototype, + 'listenOnce', + goog.testing.recordFunction()); + stubs.replace( + goog.net.XhrIo.prototype, + 'setTimeoutInterval', + goog.testing.recordFunction()); + stubs.replace(fireauth.util, 'getCurrentUrl', function() { + return CURRENT_URL; + }); + rpcHandler = new fireauth.RpcHandler('apiKey'); + ignoreArgument = goog.testing.mockmatchers.ignoreArgument; + mockControl = new goog.testing.MockControl(); + mockControl.$resetAll(); +} + + +/** + * @param {string} url The URL to make a request to. + * @param {string} method The HTTP send method. + * @param {?ArrayBuffer|?ArrayBufferView|?Blob|?Document|?FormData|string} + * data The request content. + * @param {?Object} headers The request content headers. + * @param {number} timeout The request timeout. + * @param {?Object} response The response to return. + */ +function assertSendXhrAndRunCallback( + url, method, data, headers, timeout, response) { + stubs.replace( + fireauth.RpcHandler.prototype, + 'sendXhr_', + function(actualUrl, callback, actualMethod, actualData, actualHeaders, + actualTimeout) { + assertEquals(url, actualUrl); + assertEquals(method, actualMethod); + assertEquals(data, actualData); + assertObjectEquals(headers, actualHeaders); + assertEquals(timeout, actualTimeout); + callback(response); + }); +} + + +/** + * Asserts that server errors are handled correctly. + * @param {function() : !goog.Promise} methodToTest The method that we are + * testing, which returns a Promise that we expect to reject with an error. + * @param {!Object} errorMap A map from server errors to the + * errors we expect from the method under test. + * @param {string} url The expected URL to which a request is made. + * @param {!Object} body The expected body of the request. + */ +function assertServerErrorsAreHandled(methodToTest, errorMap, url, body) { + errorMap = goog.object.clone(errorMap); + + asyncTestCase.waitForSignals(goog.object.getKeys(errorMap).length); + var promise = goog.Promise.resolve(); + goog.object.forEach(errorMap, function(expectedError, serverErrorCode) { + promise = promise.then(function() { + assertSendXhrAndRunCallback( + url, + 'POST', + goog.json.serialize(body), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + { + 'error': { + 'message': serverErrorCode + } + }); + return methodToTest().thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(expectedError), error); + asyncTestCase.signal(); + }); + }); + }); +} + + +function tearDown() { + stubs.reset(); + rpcHandler = null; + fireauth.RpcHandler.loadGApi_ = null; + goog.dispose(clock); + try { + mockControl.$verifyAll(); + } finally { + mockControl.$tearDown(); + } +} + + +function testGetApiKey() { + assertEquals('apiKey', rpcHandler.getApiKey()); +} + + +function testRpcHandler_XMLHttpRequest_notSupported() { + stubs.replace( + fireauth.util, + 'getXMLHttpRequest', + function() {return undefined;}); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'The XMLHttpRequest compatibility library was not found.'); + var error = assertThrows(function() { new fireauth.RpcHandler('apiKey'); }); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); +} + + +function testRpcHandler_XMLHttpRequest_node() { + // Test node environment that Node.js implementation is used in xhrfactory. + // Install mock clock. + clock = new goog.testing.MockClock(true); + var xhrInstance = mockControl.createStrictMock(goog.net.XhrLike); + var xhrConstructor = mockControl.createConstructorMock( + goog.net, 'XhrLike'); + stubs.reset(); + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + // Return mock XHR constructor. In a Node.js environment the polyfill library + // would be used. + stubs.replace( + fireauth.util, + 'getXMLHttpRequest', + function() {return xhrConstructor;}); + // Node.js environment. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() {return fireauth.util.Env.NODE;}); + // Confirm RPC handler calls XHR instance from factory XHR. + xhrConstructor().$returns(xhrInstance); + xhrInstance.open(ignoreArgument, ignoreArgument, ignoreArgument).$once(); + xhrInstance.setRequestHeader(ignoreArgument, ignoreArgument).$once(); + xhrInstance.send(ignoreArgument).$once(); + xhrInstance.abort().$once(); + asyncTestCase.waitForSignals(1); + mockControl.$replayAll(); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Simulate RPC and then timeout. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + asyncTestCase.signal(); + }); + // Timeout XHR request. + clock.tick(delay * 2); +} + + +/** + * Asserts and applies the goog.net.XhrIo send call. + * @param {string} url The expected XHR URL. + * @param {string} method The XHR expected HTTP method. + * @param {?ArrayBuffer|?ArrayBufferView|?Blob|?Document|?FormData|string=} data + * The expected request data. + * @param {?Object|undefined} headers The expected HTTP headers. + * @param {number} timeout The expected timeout. + * @param {?Object|undefined} resp The expected response to return. + */ +function assertXhrIoAndRunCallback(url, method, data, headers, timeout, resp) { + // Confirm correct parameters passed to goog.net.XhrIo.send. + assertEquals( + 1, + goog.net.XhrIo.prototype.send.getCallCount()); + assertEquals( + url, + goog.net.XhrIo.prototype.send.getLastCall().getArgument(0)); + assertEquals( + method, + goog.net.XhrIo.prototype.send.getLastCall().getArgument(1)); + assertEquals( + data, + goog.net.XhrIo.prototype.send.getLastCall().getArgument(2)); + assertObjectEquals( + headers, + goog.net.XhrIo.prototype.send.getLastCall().getArgument(3)); + assertEquals( + 1, + goog.net.XhrIo.prototype.setTimeoutInterval.getCallCount()); + assertEquals( + timeout, + goog.net.XhrIo.prototype.setTimeoutInterval.getLastCall().getArgument(0)); + // Get on complete callback. + var callback = goog.net.XhrIo.prototype.listen.getLastCall().getArgument(1); + // Returned expected response. + var self = { + // Return the response text. + getResponseText: function() { + return goog.json.serialize(resp); + } + }; + // Run on complete callback, pass self as this to return expected response. + callback.apply(self); +} + + +function testSendXhr_post() { + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Check response is passed to provided callback. + var responseRecorded = null; + var func = function(response) { + responseRecorded = response; + }; + var data = 'key1=value1&key2=value2'; + var headers = { + 'Content-Type': 'application/json' + }; + // Send XHR with test parameters. + rpcHandler.sendXhr_( + 'url1', + func, + 'POST', + data, + headers, + 5000); + // Confirm correct parameters passed and run on complete. + assertXhrIoAndRunCallback( + 'url1', + 'POST', + data, + headers, + 5000, + expectedResponse); + // Confirm callback called with expected response. + assertObjectEquals(expectedResponse, responseRecorded); +} + + +/** + * Tests client version being correctly sent with requests to Firebase Auth + * server. + */ +function testSendFirebaseBackendRequest_clientVersion() { + var clientVersion = 'Chrome/JsCore/3.0.0'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + // Pass client version in constructor. + var rpcHandler = new fireauth.RpcHandler( + 'apiKey', null, clientVersion); + var expectedDomains = [ + 'domain.com', + 'www.mydomain.com' + ]; + var serverResponse = { + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + // The client version should be passed to header. + var expectedHeaders = { + 'Content-Type': 'application/json', + 'X-Client-Version': clientVersion + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50', + 'GET', + undefined, + expectedHeaders, + delay, + serverResponse); + rpcHandler.getAuthorizedDomains().then(function(domains) { + assertArrayEquals(expectedDomains, domains); + asyncTestCase.signal(); + }); +} + + +function testSendFirebaseBackendRequest_timeout() { + // Test network timeout error for Firebase backend request. + var actualError; + // Allow xhrIo requests. + stubs.reset(); + // Simulate CORS support. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + // Expected timeout error. + var timeoutError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + // Install mock clock. + clock = new goog.testing.MockClock(true); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Send request for backend API. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + // Record error. + actualError = error; + }); + // Timeout XHR request. + clock.tick(delay * 2); + // Timeout error should have been returned. + fireauth.common.testHelper.assertErrorEquals(timeoutError, actualError); +} + + +function testSendFirebaseBackendRequest_offline() { + // Test network timeout error for offline Firebase backend request. + asyncTestCase.waitForSignals(1); + // Allow xhrIo requests. + stubs.reset(); + // Simulate app offline. + stubs.replace( + fireauth.util, + 'isOnline', + function() {return false;}); + // Install mock clock. + clock = new goog.testing.MockClock(true); + // Expected timeout error. + var timeoutError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Send request for backend API. + rpcHandler.fetchProvidersForIdentifier('user@example.com') + .thenCatch(function(error) { + // Timeout error event without any wait (no tick in mockclock). + fireauth.common.testHelper.assertErrorEquals(timeoutError, error); + asyncTestCase.signal(); + }); +} + + +function testSendStsTokenBackendRequest_timeout() { + // Test network timeout error for STS token backend request. + var actualError; + // Allow xhrIo requests. + stubs.reset(); + // Simulate CORS support. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return true;}); + // Expected timeout error. + var timeoutError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + // Install mock clock. + clock = new goog.testing.MockClock(true); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Send request for backend API. + rpcHandler.requestStsToken({ + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).thenCatch(function(error) { + // Record error. + actualError = error; + }); + // Timeout XHR request. + clock.tick(delay * 2); + // Timeout error should have been returned. + fireauth.common.testHelper.assertErrorEquals(timeoutError, actualError); +} + + +function testSendStsTokenBackendRequest_offline() { + // Test network timeout error for offline STS token backend request. + asyncTestCase.waitForSignals(1); + // Allow xhrIo requests. + stubs.reset(); + // Simulate app offline. + stubs.replace( + fireauth.util, + 'isOnline', + function() {return false;}); + // Install mock clock. + clock = new goog.testing.MockClock(true); + // Expected timeout error. + var timeoutError = new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + rpcHandler = new fireauth.RpcHandler('apiKey'); + // Send request for backend API. + rpcHandler.requestStsToken({ + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).thenCatch(function(error) { + // Timeout error event without any wait (no tick in mockclock). + fireauth.common.testHelper.assertErrorEquals(timeoutError, error); + asyncTestCase.signal(); + }); +} + + +function testSendXhr_corsUnsupported() { + var expectedResponse = { + 'key1': 'value1', + 'key2': 'value2' + }; + var recordedToken = 'token'; + gapi.auth = gapi.auth || {}; + gapi.client = gapi.client || {}; + stubs.reset(); + // Simulate GApi loaded. + stubs.set( + gapi.auth, + 'getToken', + function() { + return recordedToken; + }); + stubs.set( + gapi.auth, + 'setToken', + function(token) { + recordedToken = token; + }); + stubs.set( + gapi.client, + 'request', + function(request) { + assertEquals('none', request['authType']); + assertEquals('url1', request['path']); + assertEquals('GET', request['method']); + assertEquals(data, request['body']); + assertObjectEquals(headers, request['headers']); + request['callback'](expectedResponse); + asyncTestCase.signal(); + }); + stubs.set( + gapi.client, + 'setApiKey', + function(apiKey) { + assertEquals('apiKey', apiKey); + asyncTestCase.signal(); + }); + // Simulate browser that does not support CORS. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return false;}); + var func = function(response) { + assertObjectEquals(expectedResponse, response); + // Verify token updated. + assertEquals('token', gapi.auth.getToken()); + asyncTestCase.signal(); + }; + var data = 'key1=value1&key2=value2'; + var headers = { + 'Content-Type': 'application/json' + }; + asyncTestCase.waitForSignals(3); + // Simulate GApi dependencies loaded. + fireauth.RpcHandler.loadGApi_ = goog.Promise.resolve(); + rpcHandler.sendXhr_( + 'url1', + func, + 'GET', + data, + headers, + 5000); +} + + +function testSendXhr_corsUnsupported_error() { + var expectedResponse = { + 'error': { + 'message': fireauth.RpcHandler.ServerError.CORS_UNSUPPORTED + } + }; + // Simulate browser that does not support CORS. + stubs.replace( + fireauth.util, + 'supportsCors', + function() {return false;}); + var func = function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }; + var data = 'key1=value1&key2=value2'; + var headers = { + 'Content-Type': 'application/json' + }; + asyncTestCase.waitForSignals(1); + fireauth.RpcHandler.loadGApi_ = goog.Promise.reject(); + rpcHandler.sendXhr_( + 'url1', + func, + 'GET', + data, + headers, + 5000); +} + + +function testSendSecureTokenBackendRequest_clientVersion() { + var clientVersion = 'Chrome/JsCore/3.0.0'; + // The client version should be passed to header. + var expectedHeaders = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Client-Version': clientVersion + }; + // Pass client version in constructor. + var rpcHandler = new fireauth.RpcHandler( + 'apiKey', null, clientVersion); + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + expectedHeaders, + delay, + expectedStsTokenResponse); + // Send STS token request, default config will be used. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then(function(response) { + assertObjectEquals( + expectedStsTokenResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_updateClientVersion() { + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-Client-Version': 'Chrome/JsCore/3.0.0/FirebaseCore-web' + }, + delay, + expectedStsTokenResponse); + // Update client version. + rpcHandler.updateClientVersion('Chrome/JsCore/3.0.0/FirebaseCore-web'); + // Send STS token request. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then(function(response) { + assertObjectEquals( + expectedStsTokenResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_removeClientVersion() { + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + delay, + expectedStsTokenResponse); + // Remove client version. + rpcHandler.updateClientVersion(null); + // Send STS token request. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then(function(response) { + assertObjectEquals( + expectedStsTokenResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_default() { + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_HEADERS_, + delay, + expectedStsTokenResponse); + // Send STS token request, default config will be used. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then(function(response) { + assertObjectEquals( + expectedStsTokenResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_custom() { + asyncTestCase.waitForSignals(1); + // Reinitialize RPC handler using custom config. + rpcHandler = new fireauth.RpcHandler( + 'apiKey', + { + 'secureTokenEndpoint': 'http://localhost/token', + 'secureTokenTimeout': new fireauth.util.Delay(5000, 5000), + 'secureTokenHeaders': {'Content-Type': 'application/json'} + }); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'http://localhost/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + { + 'Content-Type': 'application/json' + }, + 5000, + expectedStsTokenResponse); + // Send STS token request, custom config will be used. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then(function(response) { + assertObjectEquals( + expectedStsTokenResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_invalidRequest() { + asyncTestCase.waitForSignals(1); + // Reinitialize RPC handler. + rpcHandler = new fireauth.RpcHandler( + 'apiKey'); + // Send STS token request, no XHR, invalid request. + rpcHandler.requestStsToken( + { + 'invalid': 'authorization_code', + 'code': 'idToken' + }).then( + function(response) {}, + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_unknownServerResponse() { + var serverResponse = {'error': 'INTERNAL_SERVER_ERROR'}; + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_HEADERS_, + delay, + serverResponse); + // Send STS token request, default config will be used. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then( + function(response) {}, + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR, + goog.json.serialize(serverResponse)), + error); + asyncTestCase.signal(); + }); +} + + +function testRequestStsToken_specificErrorResponse() { + // Server error response when token is expired. + var serverResponse = { + "error": { + "code": 400, + "message": "TOKEN_EXPIRED", + "status": "INVALID_ARGUMENT" + } + }; + asyncTestCase.waitForSignals(1); + // Confirm correct parameters passed and run on complete. + assertSendXhrAndRunCallback( + 'https://securetoken.googleapis.com/v1/token?key=apiKey', + 'POST', + 'grant_type=authorization_code&code=idToken', + fireauth.RpcHandler.DEFAULT_SECURE_TOKEN_HEADERS_, + delay, + serverResponse); + // Send STS token request, default config will be used. + rpcHandler.requestStsToken( + { + 'grant_type': 'authorization_code', + 'code': 'idToken' + }).then( + function(response) {}, + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED), + error); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_success() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_updateClientVersion() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + { + 'Content-Type': 'application/json', + 'X-Client-Version': 'Chrome/JsCore/3.0.0/FirebaseCore-web' + }, + delay, + expectedResponse); + // Update client version. + rpcHandler.updateClientVersion('Chrome/JsCore/3.0.0/FirebaseCore-web'); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_removeClientVersion() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + { + 'Content-Type': 'application/json' + }, + delay, + expectedResponse); + // Remove client version. + rpcHandler.updateClientVersion(null); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_setCustomLocaleHeader_success() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'fr' + }, + delay, + expectedResponse); + // Set French as custom Firebase locale header. + rpcHandler.updateCustomLocaleHeader('fr'); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_updateCustomLocaleHeader_success() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'de' + }, + delay, + expectedResponse); + // Set French as custom Firebase locale header. + rpcHandler.updateCustomLocaleHeader('fr'); + // Change to German. + rpcHandler.updateCustomLocaleHeader('de'); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_removeCustomLocaleHeader_success() { + var expectedResponse = { + 'status': 'success' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + { + 'Content-Type': 'application/json' + }, + delay, + expectedResponse); + // Set French as custom Firebase locale header. + rpcHandler.updateCustomLocaleHeader('fr'); + // Remove custom locale header. + rpcHandler.updateCustomLocaleHeader(null); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_error() { + // Error case. + var errorResponse = { + 'error': { + 'message': 'ERROR_CODE' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + errorResponse); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }).then( + function(response) {}, + function(e) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError('internal-error', + goog.json.serialize(errorResponse)), + e); + asyncTestCase.signal(); + }); +} + + +function testRequestFirebaseEndpoint_networkError() { + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({'key1': 'value1'}), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + null); + rpcHandler.requestFirebaseEndpoint('method1', 'POST', {'key1': 'value1'}) + .then(null, function(e) { + fireauth.common.testHelper.assertErrorEquals(new fireauth.AuthError( + fireauth.authenum.Error.NETWORK_REQUEST_FAILED), e); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testRequestFirebaseEndpoint_keyInvalid() { + // Error case. + var errorResponse = { + 'error': { + 'errors': [ + { + 'domain': 'usageLimits', + 'reason': 'keyInvalid', + 'message': 'Bad Request' + } + ], + 'code': 400, + 'message': 'Bad Request' + } + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + '{}', + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + errorResponse); + rpcHandler.requestFirebaseEndpoint('method1', 'POST', {}) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_API_KEY), + error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + + +function testRequestFirebaseEndpoint_notAuthorized() { + // Error case. + var errorResponse = { + 'error': { + 'errors': [ + { + 'domain': 'usageLimits', + 'reason': 'ipRefererBlocked', + 'message': 'There is a per-IP or per-Referer restriction ' + + 'configured on your API key and the request does not match ' + + 'these restrictions. Please use the Google Developers Console ' + + 'to update your API key configuration if request from this IP ' + + 'or referer should be allowed.', + 'extendedHelp': 'https://console.developers.google.com' + } + ], + 'code': 403, + 'message': 'There is a per-IP or per-Referer restriction configured on ' + + 'your API key and the request does not match these restrictions. ' + + 'Please use the Google Developers Console to update your API key ' + + 'configuration if request from this IP or referer should be allowed.' + } + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + '{}', + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + errorResponse); + rpcHandler.requestFirebaseEndpoint('method1', 'POST', {}) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.APP_NOT_AUTHORIZED), + error); + asyncTestCase.signal(); + }); + asyncTestCase.waitForSignals(1); +} + + +function testRequestFirebaseEndpoint_customError() { + // Error case. + var errorResponse = { + 'error': { + 'message': 'ERROR_CODE' + } + }; + var errorMap = { + 'ERROR_CODE': fireauth.authenum.Error.INVALID_PASSWORD + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/method1?key' + + '=apiKey', + 'POST', + goog.json.serialize({ + 'key1': 'value1', + 'key2': 'value2' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + errorResponse); + rpcHandler.requestFirebaseEndpoint( + 'method1', + 'POST', + { + 'key1': 'value1', + 'key2': 'value2' + }, + errorMap).then( + function(response) {}, + function(e) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_PASSWORD), + e); + asyncTestCase.signal(); + }); +} + + +function testGetAuthorizedDomains() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedResponse = [ + 'domain.com', + 'www.mydomain.com' + ]; + var serverResponse = { + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getAuthorizedDomains() + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testGetRecaptchaParam_success() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedResponse = { + 'recaptchaSiteKey': 'RECAPTCHA_SITE_KEY' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getRecaptchaParam?key=apiKey&cb=50', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.getRecaptchaParam() + .then(function(response) { + assertEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testGetRecaptchaParam_invalidResponse_missingSitekey() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + // If for some reason, sitekey is not returned. + var serverResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getRecaptchaParam?key=apiKey&cb=50', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getRecaptchaParam() + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testGetDynamicLinkDomain_success() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ], + 'dynamicLinksDomain': 'example.app.goog.gl' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&returnDynamicLink=true', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getDynamicLinkDomain() + .then(function(dynamicLinksDomain) { + assertEquals('example.app.goog.gl', dynamicLinksDomain); + asyncTestCase.signal(); + }); +} + + +function testGetDynamicLinkDomain_internalError() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR); + // This should not happen in reality but need to confirm that logic checks + // for presence of dynamic links domain. + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&returnDynamicLink=true', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getDynamicLinkDomain() + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testGetDynamicLinkDomain_notActivated() { + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.DYNAMIC_LINK_NOT_ACTIVATED); + var serverResponse = { + 'error': { + 'errors': [ + { + 'domain': 'global', + 'reason': 'invalid', + 'message': 'DYNAMIC_LINK_NOT_ACTIVATED' + } + ], + 'code': 400, + 'message': 'DYNAMIC_LINK_NOT_ACTIVATED' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&returnDynamicLink=true', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getDynamicLinkDomain() + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIsIosBundleIdValid_success() { + var iosBundleId = 'com.example.app'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&iosBundleId=' + + encodeURIComponent(iosBundleId), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isIosBundleIdValid(iosBundleId) + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testIsIosBundleIdValid_error() { + var iosBundleId = 'com.example.app'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_APP_ID); + var serverResponse = { + 'error': { + 'errors': [ + { + 'message': 'INVALID_APP_ID' + } + ], + 'code': 400, + 'message': 'INVALID_APP_ID' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&iosBundleId=' + + encodeURIComponent(iosBundleId), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isIosBundleIdValid(iosBundleId) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIsAndroidPackageNameValid_success_noSha1Cert() { + var androidPackageName = 'com.example.app'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&androidPackageName=' + + encodeURIComponent(androidPackageName), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isAndroidPackageNameValid(androidPackageName) + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testIsAndroidPackageNameValid_error_noSha1Cert() { + var androidPackageName = 'com.example.app'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_APP_ID); + var serverResponse = { + 'error': { + 'errors': [ + { + 'message': 'INVALID_APP_ID' + } + ], + 'code': 400, + 'message': 'INVALID_APP_ID' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&androidPackageName=' + + encodeURIComponent(androidPackageName), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isAndroidPackageNameValid(androidPackageName) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIsAndroidPackageNameValid_success_sha1Cert() { + var androidPackageName = 'com.example.app'; + var sha1Cert = 'SHA_1_ANDROID_CERT'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&androidPackageName=' + + encodeURIComponent(androidPackageName) + + '&sha1Cert=' + encodeURIComponent(sha1Cert), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isAndroidPackageNameValid(androidPackageName, sha1Cert) + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testIsAndroidPackageNameValid_error_sha1Cert() { + var androidPackageName = 'com.example.app'; + var sha1Cert = 'INVALID_SHA_1_ANDROID_CERT'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CERT_HASH); + var serverResponse = { + 'error': { + 'errors': [ + { + 'message': 'INVALID_CERT_HASH' + } + ], + 'code': 400, + 'message': 'INVALID_CERT_HASH' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&androidPackageName=' + + encodeURIComponent(androidPackageName) + + '&sha1Cert=' + encodeURIComponent(sha1Cert), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isAndroidPackageNameValid(androidPackageName, sha1Cert) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testIsOAuthClientIdValid_success() { + var clientId = '123456.apps.googleusercontent.com'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var serverResponse = { + 'projectId': '12345678', + 'authorizedDomains': [ + 'domain.com', + 'www.mydomain.com' + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&clientId=' + + encodeURIComponent(clientId), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isOAuthClientIdValid(clientId) + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testIsOAuthClientIdValid_error() { + var clientId = '123456.apps.googleusercontent.com'; + // Simulate clock. + clock = new goog.testing.MockClock(); + clock.install(); + clock.tick(50); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_OAUTH_CLIENT_ID); + var serverResponse = { + 'error': { + 'errors': [ + { + 'message': 'INVALID_OAUTH_CLIENT_ID' + } + ], + 'code': 400, + 'message': 'INVALID_OAUTH_CLIENT_ID' + } + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'getProjectConfig?key=apiKey&cb=50&clientId=' + + encodeURIComponent(clientId), + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.isOAuthClientIdValid(clientId) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testFetchProvidersForIdentifier() { + var expectedResponse = [ + 'google.com', + 'myauthprovider.com' + ]; + var serverResponse = { + 'kind': 'identitytoolkit#CreateAuthUriResponse', + 'authUri': 'https://accounts.google.com/o/oauth2/auth?foo=bar', + 'providerId': 'google.com', + 'allProviders': [ + 'google.com', + 'myauthprovider.com' + ], + 'registered': true, + 'forExistingProvider': true, + 'sessionId': 'MY_SESSION_ID' + }; + var identifier = 'MY_ID'; + + asyncTestCase.waitForSignals(1); + var request = { + 'identifier': identifier, + 'continueUri': CURRENT_URL + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'createAuthUri?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.fetchProvidersForIdentifier(identifier) + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testFetchProvidersForIdentifier_nonHttpOrHttps() { + // Simulate non http or https current URL. + stubs.replace(fireauth.util, 'getCurrentUrl', function() { + return 'chrome-extension://234567890/index.html'; + }); + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() { + return 'chrome-extension:'; + }); + var expectedResponse = [ + 'google.com', + 'myauthprovider.com' + ]; + var serverResponse = { + 'kind': 'identitytoolkit#CreateAuthUriResponse', + 'authUri': 'https://accounts.google.com/o/oauth2/auth?foo=bar', + 'providerId': 'google.com', + 'allProviders': [ + 'google.com', + 'myauthprovider.com' + ], + 'registered': true, + 'forExistingProvider': true, + 'sessionId': 'MY_SESSION_ID' + }; + var identifier = 'MY_ID'; + + asyncTestCase.waitForSignals(1); + var request = { + 'identifier': identifier, + // A fallback HTTP URL should be used. + 'continueUri': 'http://localhost' + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'createAuthUri?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.fetchProvidersForIdentifier(identifier) + .then(function(response) { + assertArrayEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testFetchProvidersForIdentifier_serverCaughtError() { + var identifier = 'MY_IDENTIFIER'; + var requestBody = { + 'identifier': identifier, + 'continueUri': CURRENT_URL + }; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/createAuthUri?key=apiKey'; + + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDENTIFIER] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CONTINUE_URI] = + fireauth.authenum.Error.INTERNAL_ERROR; + + assertServerErrorsAreHandled(function() { + return rpcHandler.fetchProvidersForIdentifier(identifier); + }, errorMap, expectedUrl, requestBody); +} + + +function testGetAccountInfoByIdToken() { + var expectedResponse = { + 'users': [{ + 'localId': '14584746072031976743', + 'email': 'uid123@fake.com', + 'emailVerified': true, + 'displayName': 'John Doe', + 'providerUserInfo': [ + { + 'providerId': 'google.com', + 'displayName': 'John Doe', + 'photoUrl': 'https://lh5.googleusercontent.com/123456789/photo.jpg', + 'federatedId': 'https://accounts.google.com/123456789' + }, + { + 'providerId': 'twitter.com', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default_profile_images/def' + + 'ault_profile_3_normal.png', + 'federatedId': 'http://twitter.com/987654321' + } + ], + 'photoUrl': 'http://abs.twimg.com/sticky/photo.png', + 'passwordUpdatedAt': 0.0, + 'disabled': false + }] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountI' + + 'nfo?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.getAccountInfoByIdToken('ID_TOKEN').then(function(response) { + assertObjectEquals( + expectedResponse, + response); + asyncTestCase.signal(); + }); +} + + +function testVerifyCustomToken_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCusto' + + 'mToken?key=apiKey', + 'POST', + goog.json.serialize({ + 'token': 'CUSTOM_TOKEN', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyCustomToken('CUSTOM_TOKEN').then( + function(response) { + assertEquals('ID_TOKEN', response['idToken']); + asyncTestCase.signal(); + }); +} + + +function testVerifyCustomToken_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/verifyCustomToken?key=apiKey'; + var token = 'CUSTOM_TOKEN'; + var requestBody = { + 'token': token, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CUSTOM_TOKEN] = + fireauth.authenum.Error.INTERNAL_ERROR; + errorMap[fireauth.RpcHandler.ServerError.INVALID_CUSTOM_TOKEN] = + fireauth.authenum.Error.INVALID_CUSTOM_TOKEN; + errorMap[fireauth.RpcHandler.ServerError.CREDENTIAL_MISMATCH] = + fireauth.authenum.Error.CREDENTIAL_MISMATCH; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyCustomToken(token); + }, errorMap, expectedUrl, requestBody); +} + + +function testServerProvidedErrorMessage_knownErrorCode() { + // Test when server returns an error message with the details appended: + // INVALID_CUSTOM_TOKEN : [error detail here] + // The above error message should generate an Auth error with code + // client equivalent of INVALID_CUSTOM_TOKEN and the message: + // [error detail here] + asyncTestCase.waitForSignals(1); + // Expected client side error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_CUSTOM_TOKEN, + 'Some specific reason.'); + // Server response. + var serverResponse = { + 'error': { + 'errors': [ + { + 'domain': 'global', + 'reason': 'invalid', + 'message': 'INVALID_CUSTOM_TOKEN : Some specific reason.' + } + ], + 'code': 400, + 'message': 'INVALID_CUSTOM_TOKEN : Some specific reason.' + } + }; + // Simulate invalid custom token. + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCusto' + + 'mToken?key=apiKey', + 'POST', + goog.json.serialize({ + 'token': 'CUSTOM_TOKEN', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyCustomToken('CUSTOM_TOKEN').thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testServerProvidedErrorMessage_unknownErrorCode() { + // Test when server returns an error message with the details appended: + // UNKNOWN_CODE : [error detail here] + // The above error message should generate an Auth error with internal-error + // ccode and the message: + // [error detail here] + asyncTestCase.waitForSignals(1); + // Expected client side error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'Something strange happened.'); + // Server response. + var serverResponse = { + 'error': { + 'errors': [ + { + 'domain': 'global', + 'reason': 'invalid', + 'message': 'WHAAAAAT?: Something strange happened.' + } + ], + 'code': 400, + 'message': 'WHAAAAAT?: Something strange happened.' + } + }; + // Simulate unknown backend error. + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCusto' + + 'mToken?key=apiKey', + 'POST', + goog.json.serialize({ + 'token': 'CUSTOM_TOKEN', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyCustomToken('CUSTOM_TOKEN').thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testServerProvidedErrorMessage_noErrorCode() { + // Test when server returns an unexpected error message with a colon in the + // message field that it does not treat the string after the colon as the + // detailed error message. Instead the whole response should be serialized. + var errorMessage = 'Error getting access token from FACEBOOK, response: OA' + + 'uth2TokenResponse{params: %7B%22error%22:%7B%22message%22:%22This+IP+' + + 'can\'t+make+requests+for+that+application.%22,%22type%22:%22OAuthExce' + + 'ption%22,%22code%22:5,%22fbtrace_id%22:%22AHHaoO5cS1K%22%7D%7D&error=' + + 'OAuthException&error_description=This+IP+can\'t+make+requests+for+tha' + + 't+application., httpMetadata: HttpMetadata{status=400, cachePolicy=NO' + + '_CACHE, cacheDuration=null, staleWhileRevalidate=null, filename=null,' + + 'lastModified=null, headers=HTTP/1.1 200 OK\r\n\r\n, cookieList=[]}}, ' + + 'OAuth2 redirect uri is: https://example12345.firebaseapp.com/__/auth/' + + 'handler'; + asyncTestCase.waitForSignals(1); + // Server response. + var serverResponse = { + 'error': { + 'errors': [ + { + 'domain': 'global', + 'reason': 'invalid', + 'message': errorMessage + } + ], + 'code': 400, + 'message': errorMessage + } + }; + // Expected client side error (should contain the serialized response). + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + fireauth.util.stringifyJSON(serverResponse)); + // Simulate unknown backend error. + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyAssertion({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testUnexpectedApiaryError() { + // Test when an unexpected Apiary error is returned that serialized server + // response is used as the client facing error message. + asyncTestCase.waitForSignals(1); + // Server response. + var serverResponse = { + 'error': { + 'errors': [ + { + 'domain': 'usageLimits', + 'reason': 'keyExpired', + 'message': 'Bad Request' + } + ], + 'code': 400, + 'message': 'Bad Request' + } + }; + // Expected client side error. + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + goog.json.serialize(serverResponse)); + // Simulate unexpected Apiary error. + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCusto' + + 'mToken?key=apiKey', + 'POST', + goog.json.serialize({ + 'token': 'CUSTOM_TOKEN', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.verifyCustomToken('CUSTOM_TOKEN').thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + expectedError, + error); + asyncTestCase.signal(); + }); +} + + +function testGetErrorCodeDetails() { + // No error message. + assertUndefined( + fireauth.RpcHandler.getErrorCodeDetails('OPERATION_NOT_ALLOWED')); + // Error messages with variation of spaces before and after colon. + assertEquals( + 'Provider Id is not enabled in configuration.', + fireauth.RpcHandler.getErrorCodeDetails('OPERATION_NOT_ALLOWED : Provi' + + 'der Id is not enabled in configuration.')); + assertEquals( + 'Provider Id is not enabled in configuration.', + fireauth.RpcHandler.getErrorCodeDetails('OPERATION_NOT_ALLOWED:Provide' + + 'r Id is not enabled in configuration.')); + // Error message that contains colons. + assertEquals( + 'blabla:bla:::bla: something:', + fireauth.RpcHandler.getErrorCodeDetails('OPERATION_NOT_ALLOWED: blabl' + + 'a:bla:::bla: something:')); +} + + +function testVerifyCustomToken_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCusto' + + 'mToken?key=apiKey', + 'POST', + goog.json.serialize({ + 'token': 'CUSTOM_TOKEN', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.verifyCustomToken('CUSTOM_TOKEN').thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testVerifyPassword_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassw' + + 'ord?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'uid123@fake.com', + 'password': 'mysupersecretpassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyPassword('uid123@fake.com', 'mysupersecretpassword') + .then(function(response) { + assertEquals('ID_TOKEN', response['idToken']); + asyncTestCase.signal(); + }); +} + + +function testVerifyPassword_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/verifyPassword?key=apiKey'; + var email = 'uid123@fake.com'; + var password = 'mysupersecretpassword'; + var requestBody = { + 'email': email, + 'password': password, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_EMAIL] = + fireauth.authenum.Error.INVALID_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_PASSWORD] = + fireauth.authenum.Error.INVALID_PASSWORD; + errorMap[fireauth.RpcHandler.ServerError.TOO_MANY_ATTEMPTS_TRY_LATER] = + fireauth.authenum.Error.TOO_MANY_ATTEMPTS_TRY_LATER; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyPassword(email, password); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests invalid server response verifyPassword error. + */ +function testVerifyPassword_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassw' + + 'ord?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'uid123@fake.com', + 'password': 'mysupersecretpassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.verifyPassword('uid123@fake.com', 'mysupersecretpassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPassword error. + */ +function testVerifyPassword_invalidPasswordError() { + // Test when request is invalid. + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPassword('uid123@fake.com', '') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_PASSWORD), + error); + asyncTestCase.signal(); + }); +} + + +function testVerifyPassword_invalidEmailError() { + // Test when invalid email is passed in verifyPassword request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.verifyPassword('uid123@invalid', 'mysupersecretpassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +function testCreateAccount_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'signupNewUser?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'uid123@fake.com', + 'password': 'mysupersecretpassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.createAccount('uid123@fake.com', 'mysupersecretpassword') + .then(function(response) { + assertEquals('ID_TOKEN', response['idToken']); + asyncTestCase.signal(); + }); +} + + +function testCreateAccount_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/signupNewUser?key=apiKey'; + var email = 'uid123@fake.com'; + var password = 'mysupersecretpassword'; + var requestBody = { + 'email': email, + 'password': password, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EMAIL_EXISTS] = + fireauth.authenum.Error.EMAIL_EXISTS; + errorMap[fireauth.RpcHandler.ServerError.PASSWORD_LOGIN_DISABLED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + errorMap[fireauth.RpcHandler.ServerError.OPERATION_NOT_ALLOWED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + errorMap[fireauth.RpcHandler.ServerError.WEAK_PASSWORD] = + fireauth.authenum.Error.WEAK_PASSWORD; + + assertServerErrorsAreHandled(function() { + return rpcHandler.createAccount(email, password); + }, errorMap, expectedUrl, requestBody); +} + + +function testCreateAccount_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'signupNewUser?key=apiKey', + 'POST', + goog.json.serialize({ + 'email': 'uid123@fake.com', + 'password': 'mysupersecretpassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.createAccount('uid123@fake.com', 'mysupersecretpassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testCreateAccount_noPasswordError() { + asyncTestCase.waitForSignals(1); + rpcHandler.createAccount('uid123@fake.com', '') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.WEAK_PASSWORD), + error); + asyncTestCase.signal(); + }); +} + + +function testCreateAccount_invalidEmailError() { + // Test when invalid email is passed in setAccountInfo request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.createAccount('uid123@invalid', 'mysupersecretpassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +function testDeleteAccount_success() { + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'deleteAccount?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + '{}'); + rpcHandler.deleteAccount('ID_TOKEN') + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testDeleteAccount_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/deleteAccount?key=apiKey'; + var requestBody = { + 'idToken': 'ID_TOKEN' + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.CREDENTIAL_TOO_OLD_LOGIN_AGAIN] = + fireauth.authenum.Error.CREDENTIAL_TOO_OLD_LOGIN_AGAIN; + errorMap[fireauth.RpcHandler.ServerError.INVALID_ID_TOKEN] = + fireauth.authenum.Error.INVALID_AUTH; + errorMap[fireauth.RpcHandler.ServerError.USER_NOT_FOUND] = + fireauth.authenum.Error.TOKEN_EXPIRED; + errorMap[fireauth.RpcHandler.ServerError.TOKEN_EXPIRED] = + fireauth.authenum.Error.TOKEN_EXPIRED; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.deleteAccount('ID_TOKEN'); + }, errorMap, expectedUrl, requestBody); +} + + +function testDeleteAccount_invalidRequestError() { + asyncTestCase.waitForSignals(1); + rpcHandler.deleteAccount().thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +function testSignInAnonymously_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'signupNewUser?key=apiKey', + 'POST', + goog.json.serialize({ + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.signInAnonymously() + .then(function(response) { + assertEquals('ID_TOKEN', response['idToken']); + asyncTestCase.signal(); + }); +} + + +function testSignInAnonymously_unknownServerResponse() { + // Test when server returns unexpected response with no error message. + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'signupNewUser?key=apiKey', + 'POST', + goog.json.serialize({ + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.signInAnonymously() + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyAssertion RPC call. + */ +function testVerifyAssertion_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).then( + function(response) { + assertEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyAssertion RPC call with no sessionId passed. + */ +function testVerifyAssertion_error() { + asyncTestCase.waitForSignals(1); + rpcHandler.verifyAssertion({ + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyAssertion for linking RPC call. + */ +function testVerifyAssertionForLinking_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'existingIdToken', + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertionForLinking({ + 'idToken': 'existingIdToken', + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).then( + function(response) { + assertEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyAssertion for linking RPC call with no idToken passed. + */ +function testVerifyAssertionForLinking_error() { + asyncTestCase.waitForSignals(1); + rpcHandler.verifyAssertionForLinking({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server caught verifyAssertion errors. + */ +function testVerifyAssertion_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/verifyAssertion?key=apiKey'; + var requestBody = { + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id=' + + 'google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDP_RESPONSE] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + errorMap[fireauth.RpcHandler.ServerError.FEDERATED_USER_ID_ALREADY_LINKED] = + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE; + errorMap[fireauth.RpcHandler.ServerError.OPERATION_NOT_ALLOWED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + errorMap[fireauth.RpcHandler.ServerError.USER_CANCELLED] = + fireauth.authenum.Error.USER_CANCELLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyAssertion(requestBody); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests invalid request verifyAssertionForIdToken error. + */ +function testVerifyAssertion_invalidRequestError() { + // Test when request is invalid. + asyncTestCase.waitForSignals(1); + rpcHandler.verifyAssertion({'postBody': '....'}).thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests need confirmation verifyAssertionForIdToken Auth linking error with + * OAuth response. + */ +function testVerifyAssertion_needConfirmationError_oauthResponseAndEmail() { + // Test Auth linking error when need confirmation flag is returned. + var credential = fireauth.GoogleAuthProvider.credential(null, + 'googleAccessToken'); + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + { + email: 'user@example.com', + credential: credential + }); + var expectedResponse = { + 'needConfirmation': true, + 'idToken': 'PENDING_TOKEN', + 'email': 'user@example.com', + 'oauthAccessToken': 'googleAccessToken', + 'providerId': 'google.com' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provide' + + 'r_id=google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id' + + '=google.com', + 'requestUri': 'http://localhost' + }).thenCatch( + function(error) { + assertTrue(error instanceof fireauth.AuthErrorWithCredential); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests need confirmation verifyAssertionForIdToken Auth linking error without + * OAuth response. + */ +function testVerifyAssertion_needConfirmationError_emailResponseOnly() { + // Test Auth linking error when need confirmation flag is returned. + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.NEED_CONFIRMATION, + {email: 'user@example.com'}); + var expectedResponse = { + 'needConfirmation': true, + 'idToken': 'PENDING_TOKEN', + 'email': 'user@example.com' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provide' + + 'r_id=google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id' + + '=google.com', + 'requestUri': 'http://localhost' + }).thenCatch( + function(error) { + assertTrue(error instanceof fireauth.AuthErrorWithCredential); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests need confirmation verifyAssertionForIdToken with no additional info in + * response. + */ +function testVerifyAssertion_needConfirmationError_noExtraInfo() { + // Test Auth error when need confirmation flag is returned but OAuth response + // missing. + var expectedResponse = { + 'needConfirmation': true + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provide' + + 'r_id=google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id' + + '=google.com', + 'requestUri': 'http://localhost' + }).thenCatch( + function(error) { + assertTrue(error instanceof fireauth.AuthError); + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.NEED_CONFIRMATION), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyAssertionForIdToken when FEDERATED_USER_ID_ALREADY_LINKED error + * is returned by the server. + */ +function testVerifyAssertion_credAlreadyInUseError_oauthResponseAndEmail() { + // Test Auth linking error when FEDERATED_USER_ID_ALREADY_LINKED errorMessage + // is returned. + var credential = fireauth.GoogleAuthProvider.credential(null, + 'googleAccessToken'); + // Credential already in use error returned. + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + email: 'user@example.com', + credential: credential + }); + var expectedResponse = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'errorMessage': 'FEDERATED_USER_ID_ALREADY_LINKED', + 'email': 'user@example.com', + 'oauthAccessToken': 'googleAccessToken', + 'oauthExpireIn': 5183999, + 'providerId': 'google.com' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provide' + + 'r_id=google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id' + + '=google.com', + 'requestUri': 'http://localhost' + }).thenCatch( + function(error) { + assertTrue(error instanceof fireauth.AuthErrorWithCredential); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyAssertionForIdToken when EMAIL_EXISTS error + * is returned by the server. + */ +function testVerifyAssertion_emailExistsError_oauthResponseAndEmail() { + // Test Auth linking error when EMAIL_EXISTS errorMessage is returned. + var credential = fireauth.FacebookAuthProvider.credential( + 'facebookAccessToken'); + // Email exists error returned. + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.EMAIL_EXISTS, + { + email: 'user@example.com', + credential: credential + }); + var expectedResponse = { + 'kind': 'identitytoolkit#VerifyAssertionResponse', + 'errorMessage': 'EMAIL_EXISTS', + 'email': 'user@example.com', + 'oauthAccessToken': 'facebookAccessToken', + 'oauthExpireIn': 5183999, + 'providerId': 'facebook.com' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'postBody': 'access_token=accessToken&provider_id=facebook.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertion({ + 'postBody': 'access_token=accessToken&provider_id=facebook.com', + 'requestUri': 'http://localhost' + }).thenCatch( + function(error) { + assertTrue(error instanceof fireauth.AuthErrorWithCredential); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyAssertionForExisting RPC call. + */ +function testVerifyAssertionForExisting_success() { + var expectedResponse = { + 'idToken': 'ID_TOKEN', + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + // autoCreate flag should be passed and set to false. + 'autoCreate': false, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertionForExisting({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).then( + function(response) { + assertEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests verifyAssertionForExisting RPC call with no sessionId passed. + */ +function testVerifyAssertionForExisting_error() { + asyncTestCase.waitForSignals(1); + // Same client side validation as verifyAssertion. + rpcHandler.verifyAssertionForExisting({ + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests USER_NOT_FOUND verifyAssertionForExisting response. + */ +function testVerifyAssertionForExisting_error_userNotFound() { + // No user is found. No idToken returned. + var expectedResponse = { + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE', + 'errorMessage': 'USER_NOT_FOUND' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + // autoCreate flag should be passed and set to false. + 'autoCreate': false, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertionForExisting({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.USER_DELETED), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests no idToken verifyAssertionForExisting response. + */ +function testVerifyAssertionForExisting_error_userNotFound() { + // No idToken returned for whatever reason. + var expectedResponse = { + 'oauthAccessToken': 'ACCESS_TOKEN', + 'oauthExpireIn': 3600, + 'oauthAuthorizationCode': 'AUTHORIZATION_CODE' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAsse' + + 'rtion?key=apiKey', + 'POST', + goog.json.serialize({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse', + 'returnIdpCredential': true, + // autoCreate flag should be passed and set to false. + 'autoCreate': false, + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyAssertionForExisting({ + 'sessionId': 'SESSION_ID', + 'requestUri': 'http://localhost/callback#oauthResponse' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyAssertionForExisting error. + */ +function testVerifyAssertionForExisting_invalidRequestError() { + // Test when request is invalid. + asyncTestCase.waitForSignals(1); + rpcHandler.verifyAssertionForExisting({'postBody': '....'}) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server caught verifyAssertionForExisting errors. + */ +function testVerifyAssertionForExisting_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/' + + 'relyingparty/verifyAssertion?key=apiKey'; + var requestBody = { + 'postBody': 'id_token=googleIdToken&access_token=accessToken&provider_id=' + + 'google.com', + 'requestUri': 'http://localhost', + 'returnIdpCredential': true, + // autoCreate flag should be passed and set to false. + 'autoCreate': false, + 'returnSecureToken': true + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.INVALID_IDP_RESPONSE] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + errorMap[fireauth.RpcHandler.ServerError.OPERATION_NOT_ALLOWED] = + fireauth.authenum.Error.OPERATION_NOT_ALLOWED; + errorMap[fireauth.RpcHandler.ServerError.USER_CANCELLED] = + fireauth.authenum.Error.USER_CANCELLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyAssertionForExisting(requestBody); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful sendPasswordResetEmail RPC call with action code settings. + */ +function testSendPasswordResetEmail_success_actionCodeSettings() { + var userEmail = 'user@example.com'; + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': userEmail, + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendPasswordResetEmail('user@example.com', additionalRequestData) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendPasswordResetEmail RPC call with no action code + * settings. + */ +function testSendPasswordResetEmail_success_noActionCodeSettings() { + var userEmail = 'user@example.com'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': userEmail + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendPasswordResetEmail('user@example.com', {}) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendPasswordResetEmail RPC call with custom locale and no + * action code settings. + */ +function testSendPasswordResetEmail_success_customLocale_noActionCode() { + var userEmail = 'user@example.com'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': userEmail + }), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'es' + }, + delay, + expectedResponse); + rpcHandler.updateCustomLocaleHeader('es'); + rpcHandler.sendPasswordResetEmail('user@example.com', {}) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid email sendPasswordResetEmail error. + */ +function testSendPasswordResetEmail_invalidEmailError() { + // Test when invalid email is passed in getOobCode request. + asyncTestCase.waitForSignals(1); + // Test when request is invalid. + rpcHandler.sendPasswordResetEmail('user@exampl', {}) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response sendPasswordResetEmail error. + */ +function testSendPasswordResetEmail_unknownServerResponse() { + var userEmail = 'user@example.com'; + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': userEmail + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendPasswordResetEmail(userEmail, {}).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side sendPasswordResetEmail error. + */ +function testSendPasswordResetEmail_caughtServerError() { + var userEmail = 'user@example.com'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/getOobConfirmationCode?key=apiKey'; + var requestBody = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.PASSWORD_RESET, + 'email': userEmail + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EMAIL_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + errorMap[fireauth.RpcHandler.ServerError.INVALID_RECIPIENT_EMAIL] = + fireauth.authenum.Error.INVALID_RECIPIENT_EMAIL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SENDER] = + fireauth.authenum.Error.INVALID_SENDER; + errorMap[fireauth.RpcHandler.ServerError.INVALID_MESSAGE_PAYLOAD] = + fireauth.authenum.Error.INVALID_MESSAGE_PAYLOAD; + + // Action code settings related errors. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CONTINUE_URI] = + fireauth.authenum.Error.INVALID_CONTINUE_URI; + errorMap[fireauth.RpcHandler.ServerError.MISSING_ANDROID_PACKAGE_NAME] = + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME; + errorMap[fireauth.RpcHandler.ServerError.MISSING_IOS_BUNDLE_ID] = + fireauth.authenum.Error.MISSING_IOS_BUNDLE_ID; + errorMap[fireauth.RpcHandler.ServerError.UNAUTHORIZED_DOMAIN] = + fireauth.authenum.Error.UNAUTHORIZED_DOMAIN; + + assertServerErrorsAreHandled(function() { + return rpcHandler.sendPasswordResetEmail(userEmail, {}); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful sendEmailVerification RPC call with action code settings. + */ +function testSendEmailVerification_success_actionCodeSettings() { + var idToken = 'ID_TOKEN'; + var userEmail = 'user@example.com'; + var expectedResponse = { + 'email': userEmail + }; + var additionalRequestData = { + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken, + 'continueUrl': 'https://www.example.com/?state=abc', + 'iOSBundleId': 'com.example.ios', + 'androidPackageName': 'com.example.android', + 'androidInstallApp': true, + 'androidMinimumVersion': '12', + 'canHandleCodeInApp': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendEmailVerification(idToken, additionalRequestData) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendEmailVerification RPC call with no action code settings. + */ +function testSendEmailVerification_success_noActionCodeSettings() { + var idToken = 'ID_TOKEN'; + var userEmail = 'user@example.com'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendEmailVerification(idToken, {}) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendEmailVerification RPC call with custom locale and no + * action code settings. + */ +function testSendEmailVerification_success_customLocale_noActionCodeSettings() { + var idToken = 'ID_TOKEN'; + var userEmail = 'user@example.com'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken + }), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'ar' + }, + delay, + expectedResponse); + rpcHandler.updateCustomLocaleHeader('ar'); + rpcHandler.sendEmailVerification(idToken, {}) + .then(function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response sendEmailVerification error. + */ +function testSendEmailVerification_unknownServerResponse() { + var idToken = 'ID_TOKEN'; + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobCon' + + 'firmationCode?key=apiKey', + 'POST', + goog.json.serialize({ + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendEmailVerification(idToken, {}).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side sendEmailVerification error. + */ +function testSendEmailVerification_caughtServerError() { + var idToken = 'ID_TOKEN'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/getOobConfirmationCode?key=apiKey'; + var requestBody = { + 'requestType': fireauth.RpcHandler.GetOobCodeRequestType.VERIFY_EMAIL, + 'idToken': idToken + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EMAIL_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + + // Action code settings related errors. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CONTINUE_URI] = + fireauth.authenum.Error.INVALID_CONTINUE_URI; + errorMap[fireauth.RpcHandler.ServerError.MISSING_ANDROID_PACKAGE_NAME] = + fireauth.authenum.Error.MISSING_ANDROID_PACKAGE_NAME; + errorMap[fireauth.RpcHandler.ServerError.MISSING_IOS_BUNDLE_ID] = + fireauth.authenum.Error.MISSING_IOS_BUNDLE_ID; + errorMap[fireauth.RpcHandler.ServerError.UNAUTHORIZED_DOMAIN] = + fireauth.authenum.Error.UNAUTHORIZED_DOMAIN; + + assertServerErrorsAreHandled(function() { + return rpcHandler.sendEmailVerification(idToken, {}); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful confirmPasswordReset RPC call. + */ +function testConfirmPasswordReset_success() { + var userEmail = 'user@example.com'; + var newPassword = 'newPass'; + var code = 'PASSWORD_RESET_OOB_CODE'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPass' + + 'word?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code, + 'newPassword': newPassword + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.confirmPasswordReset(code, newPassword).then( + function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +function testConfirmPasswordReset_missingCode() { + asyncTestCase.waitForSignals(1); + rpcHandler.confirmPasswordReset('', 'myPassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OOB_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response confirmPasswordReset error. + */ +function testConfirmPasswordReset_unknownServerResponse() { + var newPassword = 'newPass'; + var code = 'PASSWORD_RESET_OOB_CODE'; + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPass' + + 'word?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code, + 'newPassword': newPassword + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.confirmPasswordReset(code, newPassword).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side confirmPasswordReset error. + */ +function testConfirmPasswordReset_caughtServerError() { + var newPassword = 'newPass'; + var code = 'PASSWORD_RESET_OOB_CODE'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/resetPassword?key=apiKey'; + var requestBody = { + 'oobCode': code, + 'newPassword': newPassword + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EXPIRED_OOB_CODE] = + fireauth.authenum.Error.EXPIRED_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_OOB_CODE] = + fireauth.authenum.Error.INVALID_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_OOB_CODE] = + fireauth.authenum.Error.INTERNAL_ERROR; + + assertServerErrorsAreHandled(function() { + return rpcHandler.confirmPasswordReset(code, newPassword); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful checkActionCode RPC call. + */ +function testCheckActionCode_success() { + var code = 'REVOKE_EMAIL_OOB_CODE'; + var expectedResponse = { + 'email': 'user@example.com', + 'newEmail': 'fake@example.com', + 'requestType': 'PASSWORD_RESET' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPass' + + 'word?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.checkActionCode(code).then( + function(info) { + assertObjectEquals(expectedResponse, info); + asyncTestCase.signal(); + }); +} + + +function testCheckActionCode_missingCode() { + asyncTestCase.waitForSignals(1); + rpcHandler.checkActionCode('') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OOB_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response checkActionCode error (empty response). + */ +function testCheckActionCode_uncaughtServerError() { + var code = 'REVOKE_EMAIL_OOB_CODE'; + // Required fields missing in response. + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPass' + + 'word?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.checkActionCode(code).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response checkActionCode error (email only returned). + */ +function testCheckActionCode_uncaughtServerError() { + var code = 'REVOKE_EMAIL_OOB_CODE'; + // Required requestType field missing in response. + var expectedResponse = { + 'email': 'user@example.com', + 'newEmail': 'fake@example.com' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/resetPass' + + 'word?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.checkActionCode(code).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side checkActionCode error. + */ +function testCheckActionCode_caughtServerError() { + var code = 'REVOKE_EMAIL_OOB_CODE'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/resetPassword?key=apiKey'; + var requestBody = { + 'oobCode': code + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EXPIRED_OOB_CODE] = + fireauth.authenum.Error.EXPIRED_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_OOB_CODE] = + fireauth.authenum.Error.INVALID_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_OOB_CODE] = + fireauth.authenum.Error.INTERNAL_ERROR; + + assertServerErrorsAreHandled(function() { + return rpcHandler.checkActionCode(code); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful applyActionCode RPC call. + */ +function testApplyActionCode_success() { + var userEmail = 'user@example.com'; + var code = 'EMAIL_VERIFICATION_OOB_CODE'; + var expectedResponse = { + 'email': userEmail + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'setAccountInfo?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.applyActionCode(code).then( + function(email) { + assertEquals(userEmail, email); + asyncTestCase.signal(); + }); +} + + +function testApplyActionCode_missingCode() { + asyncTestCase.waitForSignals(1); + rpcHandler.applyActionCode('') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_OOB_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response applyActionCode error. + */ +function testApplyActionCode_unknownServerResponse() { + var code = 'EMAIL_VERIFICATION_OOB_CODE'; + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'setAccountInfo?key=apiKey', + 'POST', + goog.json.serialize({ + 'oobCode': code + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.applyActionCode(code).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side applyActionCode error. + */ +function testApplyActionCode_caughtServerError() { + var code = 'EMAIL_VERIFICATION_OOB_CODE'; + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/setAccountInfo?key=apiKey'; + var requestBody = { + 'oobCode': code + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.EXPIRED_OOB_CODE] = + fireauth.authenum.Error.EXPIRED_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.EMAIL_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + errorMap[fireauth.RpcHandler.ServerError.INVALID_OOB_CODE] = + fireauth.authenum.Error.INVALID_OOB_CODE; + errorMap[fireauth.RpcHandler.ServerError.USER_DISABLED] = + fireauth.authenum.Error.USER_DISABLED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.applyActionCode(code); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests serialize_ method. + */ +function testSerialize() { + var obj1 = { + 'a': 1, + 'b': 'gegg', + 'c': [1, 2, 4] + }; + assertEquals(goog.json.serialize(obj1), fireauth.RpcHandler.serialize_(obj1)); + var obj2 = { + 'a': 1, + 'b': null, + 'c': undefined, + 'd': '', + 'e': 0, + 'f': false + }; + // null and undefined should be removed. + assertEquals( + goog.json.serialize({'a': 1, 'd': '', 'e': 0, 'f': false}), + fireauth.RpcHandler.serialize_(obj2)); +} + + +/** + * Tests successful deleteLinkedAccounts RPC call. + */ +function testDeleteLinkedAccounts_success() { + var expectedResponse = { + 'email': 'user@example.com', + 'providerUserInfo': [ + {'providerId': 'google.com'} + ] + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'deleteProvider': ['github.com', 'facebook.com'] + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.deleteLinkedAccounts('ID_TOKEN', ['github.com', 'facebook.com']) + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request deleteLinkedAccounts error. + */ +function testDeleteLinkedAccounts_invalidRequestError() { + // Test when request is invalid. + asyncTestCase.waitForSignals(1); + rpcHandler.deleteLinkedAccounts('ID_TOKEN', 'google.com').thenCatch( + function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server caught deleteLinkedAccounts errors. + */ +function testDeleteLinkedAccounts_serverCaughtError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/setAccountInfo?key=apiKey'; + var requestBody = { + 'idToken': 'ID_TOKEN', + 'deleteProvider': ['github.com', 'facebook.com'] + }; + var errorMap = {}; + errorMap[fireauth.RpcHandler.ServerError.USER_NOT_FOUND] = + fireauth.authenum.Error.TOKEN_EXPIRED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.deleteLinkedAccounts( + 'ID_TOKEN', ['github.com', 'facebook.com']); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful updateProfile request. + */ +function testUpdateProfile_success() { + var expectedResponse = { + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updateProfile('ID_TOKEN', { + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png' + }).then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_blankFields() { + var expectedResponse = { + // We test here that a response without email is a valid response. + 'email': '', + 'displayName': '' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'displayName': '', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updateProfile('ID_TOKEN', { + 'displayName': '' + }).then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_omittedFields() { + var expectedResponse = { + 'email': 'uid123@fake.com', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'displayName': 'John Doe', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updateProfile('ID_TOKEN', { + 'displayName': 'John Doe' + }).then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testUpdateProfile_deleteFields() { + var expectedResponse = { + 'email': 'uid123@fake.com', + 'displayName': 'John Doe' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'displayName': 'John Doe', + 'deleteAttribute': ['PHOTO_URL'], + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updateProfile('ID_TOKEN', { + 'displayName': 'John Doe', + 'photoUrl': null + }).then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server caught updateProfile error. + */ +function testUpdateProfile_error() { + var serverResponse = { + 'error': {'message': fireauth.RpcHandler.ServerError.INTERNAL_ERROR} + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + goog.json.serialize(serverResponse)); + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.updateProfile('ID_TOKEN', { + 'displayName': 'John Doe', + 'photoUrl': 'http://abs.twimg.com/sticky/default.png' + }).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmail_success() { + var expectedResponse = { + 'email': 'newuser@example.com' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.updateEmail('ID_TOKEN', 'newuser@example.com') + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'email': 'newuser@example.com', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); +} + + +function testUpdateEmail_customLocale_success() { + var expectedResponse = { + 'email': 'newuser@example.com' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.updateCustomLocaleHeader('tr'); + rpcHandler.updateEmail('ID_TOKEN', 'newuser@example.com') + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'email': 'newuser@example.com', + 'returnSecureToken': true + }), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'tr' + }, + delay, + expectedResponse); +} + + +function testUpdateEmail_invalidEmail() { + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INVALID_EMAIL); + asyncTestCase.waitForSignals(1); + rpcHandler.updateEmail('ID_TOKEN', 'newuser@exam') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +function testUpdatePassword_success() { + var expectedResponse = { + 'email': 'user@example.com', + 'idToken': 'idToken' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'password': 'newPassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updatePassword('ID_TOKEN', 'newPassword') + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testUpdatePassword_noPassword() { + asyncTestCase.waitForSignals(1); + rpcHandler.updatePassword('ID_TOKEN', '') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.WEAK_PASSWORD), + error); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmailAndPassword_success() { + var expectedResponse = { + 'email': 'user@example.com', + 'idToken': 'idToken' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccount' + + 'Info?key=apiKey', + 'POST', + goog.json.serialize({ + 'idToken': 'ID_TOKEN', + 'email': 'me@gmail.com', + 'password': 'newPassword', + 'returnSecureToken': true + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.updateEmailAndPassword('ID_TOKEN', 'me@gmail.com', 'newPassword') + .then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmailAndPassword_noEmail() { + asyncTestCase.waitForSignals(1); + rpcHandler.updateEmailAndPassword('ID_TOKEN', '', 'newPassword') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INVALID_EMAIL), + error); + asyncTestCase.signal(); + }); +} + + +function testUpdateEmailAndPassword_noPassword() { + asyncTestCase.waitForSignals(1); + rpcHandler.updateEmailAndPassword('ID_TOKEN', 'me@gmail.com', '') + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.WEAK_PASSWORD), + error); + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc() { + asyncTestCase.waitForSignals(3); + var request = { + 'myRequestKey': 'myRequestValue', + 'myOtherRequestKey': 'myOtherRequestValue' + }; + var response = { + 'myResponseKey': 'myResponseValue', + 'myOtherResponseKey': 'myOtherResponseValue' + }; + + var rpcMethod = { + endpoint: 'myEndpoint', + requestRequiredFields: ['myRequestKey'], + requestValidator: function(actualRequest) { + assertObjectEquals(request, actualRequest); + asyncTestCase.signal(); + }, + responseValidator: function(actualResponse) { + assertObjectEquals(response, actualResponse); + asyncTestCase.signal(); + } + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'myEndpoint?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + response); + rpcHandler.invokeRpc_(rpcMethod, request).then(function(actualResponse) { + assertObjectEquals(response, actualResponse); + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc_httpMethod() { + asyncTestCase.waitForSignals(1); + var request = {}; + var rpcMethod = { + endpoint: 'myEndpoint', + httpMethod: fireauth.RpcHandler.HttpMethod.GET + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'myEndpoint?key=apiKey', + 'GET', + undefined, + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + {}); + rpcHandler.invokeRpc_(rpcMethod, request) + .then(function() { + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc_requiredFields() { + // The XHR should not be sent if there was a problem with the request. + stubs.replace(fireauth.RpcHandler.prototype, 'sendXhr_', fail); + asyncTestCase.waitForSignals(1); + var request = { + 'myRequestKey': 'myRequestValue', + 'myOtherRequestKey': 'myOtherRequestValue' + }; + var rpcMethod = { + endpoint: 'myEndpoint', + requestRequiredFields: ['myRequestKey', 'keyThatIsNotThere'], + requestValidator: function() {} + }; + rpcHandler.invokeRpc_(rpcMethod, request).then(fail, function(actualError) { + fireauth.common.testHelper.assertErrorEquals(new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR), actualError); + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc_requestError() { + // The XHR should not be sent if there was a problem with the request. + stubs.replace(fireauth.RpcHandler.prototype, 'sendXhr_', fail); + asyncTestCase.waitForSignals(2); + var request = { + 'myRequestKey': 'myRequestValue', + 'myOtherRequestKey': 'myOtherRequestValue' + }; + + var error = {'name': 'myRequestError'}; + var rpcMethod = { + endpoint: 'myEndpoint', + requestValidator: function(actualRequest) { + assertObjectEquals(request, actualRequest); + asyncTestCase.signal(); + throw error; + } + }; + rpcHandler.invokeRpc_(rpcMethod, request).then(fail, function(actualError) { + assertObjectEquals(error, actualError); + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc_responseError() { + asyncTestCase.waitForSignals(2); + var request = {}; + var response = { + 'myResponseKey': 'myResponseValue', + 'myOtherResponseKey': 'myOtherResponseValue' + }; + var error = {'name': 'myResponseError'}; + var rpcMethod = { + endpoint: 'myEndpoint', + responseValidator: function(actualResponse) { + assertObjectEquals(response, actualResponse); + asyncTestCase.signal(); + throw error; + } + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'myEndpoint?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + response); + rpcHandler.invokeRpc_(rpcMethod, request).then(fail, function(actualError) { + assertObjectEquals(error, actualError); + asyncTestCase.signal(); + }); +} + + +function testInvokeRpc_responseField() { + asyncTestCase.waitForSignals(1); + var request = {}; + var response = { + 'someOtherField': 'unimportantInfo', + 'theFieldWeWant': 'importantInfo' + }; + var rpcMethod = { + endpoint: 'myEndpoint', + responseField: 'theFieldWeWant' + }; + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/' + + 'myEndpoint?key=apiKey', + 'POST', + goog.json.serialize(request), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + response); + rpcHandler.invokeRpc_(rpcMethod, request).then(function(actualValue) { + assertObjectEquals('importantInfo', actualValue); + asyncTestCase.signal(); + }); +} + + +/** + * Test getAdditionalScopes_ for passing scopes in createAuthUri request. + */ +function testGetAdditionalScopes() { + var scopes = fireauth.RpcHandler.getAdditionalScopes_('google.com'); + assertNull(scopes); + scopes = fireauth.RpcHandler.getAdditionalScopes_( + 'google.com', ['scope1', 'scope2', 'scope3']); + assertEquals( + goog.json.serialize({ + 'google.com': 'scope1,scope2,scope3' + }), + scopes); +} + + +/** + * Tests successful getAuthUri request. + */ +function testGetAuthUri_success() { + var expectedCustomParameters = { + 'hd': 'example.com', + 'login_hint': 'user@example.com' + }; + var expectedResponse = { + 'authUri': 'https://accounts.google.com', + 'providerId': 'google.com', + 'registered': true, + 'forExistingProvider': true, + 'sessionId': 'SESSION_ID' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuth' + + 'Uri?key=apiKey', + 'POST', + goog.json.serialize({ + 'identifier': 'user@example.com', + 'providerId': 'google.com', + 'continueUri': 'http://localhost/widget', + 'customParameter': expectedCustomParameters, + 'oauthScope': goog.json.serialize({ + 'google.com': 'scope1,scope2,scope3' + }) + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.getAuthUri( + 'google.com', + 'http://localhost/widget', + expectedCustomParameters, + ['scope1', 'scope2', 'scope3'], + 'user@example.com').then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful getAuthUri request with Google provider and sessionId. + */ +function testGetAuthUri_googleProvider_withSessionId_success() { + var expectedCustomParameters = { + 'hd': 'example.com', + 'login_hint': 'user@example.com' + }; + var expectedResponse = { + 'authUri': 'https://accounts.google.com', + 'providerId': 'google.com', + 'registered': true, + 'forExistingProvider': true, + 'sessionId': 'SESSION_ID' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuth' + + 'Uri?key=apiKey', + 'POST', + goog.json.serialize({ + 'identifier': 'user@example.com', + 'providerId': 'google.com', + 'continueUri': 'http://localhost/widget', + 'customParameter': expectedCustomParameters, + 'oauthScope': goog.json.serialize({ + 'google.com': 'scope1,scope2,scope3' + }), + 'sessionId': 'SESSION_ID', + 'authFlowType': 'CODE_FLOW' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.getAuthUri( + 'google.com', + 'http://localhost/widget', + expectedCustomParameters, + ['scope1', 'scope2', 'scope3'], + 'user@example.com', + 'SESSION_ID').then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful getAuthUri request with other provider and sessionId. + */ +function testGetAuthUri_otherProvider_withSessionId_success() { + var expectedResponse = { + 'authUri': 'https://facebook.com/login', + 'providerId': 'facebook.com', + 'registered': true, + 'sessionId': 'SESSION_ID' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuth' + + 'Uri?key=apiKey', + 'POST', + goog.json.serialize({ + 'providerId': 'facebook.com', + 'continueUri': 'http://localhost/widget', + 'customParameter': {}, + 'oauthScope': goog.json.serialize({ + 'facebook.com': 'scope1,scope2,scope3' + }), + 'sessionId': 'SESSION_ID' + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.getAuthUri( + 'facebook.com', + 'http://localhost/widget', + undefined, + ['scope1', 'scope2', 'scope3'], + undefined, + 'SESSION_ID').then(function(response) { + assertObjectEquals(expectedResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server caught getAuthUri error. + */ +function testGetAuthUri_error() { + var serverResponse = { + 'error': {'message': fireauth.RpcHandler.ServerError.INTERNAL_ERROR} + }; + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + goog.json.serialize(serverResponse)); + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/createAuth' + + 'Uri?key=apiKey', + 'POST', + goog.json.serialize({ + 'providerId': 'google.com', + 'continueUri': 'http://localhost/widget', + 'customParameter': {} + }), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + serverResponse); + rpcHandler.getAuthUri( + 'google.com', + 'http://localhost/widget').thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful sendVerificationCode RPC call. + */ +function testSendVerificationCode_success() { + var expectedRequest = { + 'phoneNumber': '+15551234567', + 'recaptchaToken': 'RECAPTCHA_TOKEN' + }; + var expectedResponse = { + 'sessionInfo': 'SESSION_INFO' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerifi' + + 'cationCode?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendVerificationCode(expectedRequest).then(function(sessionInfo) { + assertEquals(expectedResponse['sessionInfo'], sessionInfo); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request sendVerificationCode error for a missing phone number. + */ +function testSendVerificationCode_invalidRequest_missingPhoneNumber() { + var expectedRequest = { + 'recaptchaToken': 'RECAPTCHA_TOKEN' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.sendVerificationCode(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request sendVerificationCode error for a missing reCAPTCHA + * token. + */ +function testSendVerificationCode_invalidRequest_missingRecaptchaToken() { + var expectedRequest = { + 'phoneNumber': '+15551234567' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.sendVerificationCode(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response sendVerificationCode error. + */ +function testSendVerificationCode_unknownServerResponse() { + var expectedRequest = { + 'phoneNumber': '+15551234567', + 'recaptchaToken': 'RECAPTCHA_TOKEN' + }; + // No sessionInfo returned. + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerifi' + + 'cationCode?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.sendVerificationCode(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side sendVerificationCode error. + */ +function testSendVerificationCode_caughtServerError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/sendVerificationCode?key=apiKey'; + var requestBody = { + 'phoneNumber': '+15551234567', + 'recaptchaToken': 'RECAPTCHA_TOKEN' + }; + var errorMap = {}; + // All related server errors for sendVerificationCode. + errorMap[fireauth.RpcHandler.ServerError.CAPTCHA_CHECK_FAILED] = + fireauth.authenum.Error.CAPTCHA_CHECK_FAILED; + errorMap[fireauth.RpcHandler.ServerError.INVALID_APP_CREDENTIAL] = + fireauth.authenum.Error.INVALID_APP_CREDENTIAL; + errorMap[fireauth.RpcHandler.ServerError.INVALID_PHONE_NUMBER] = + fireauth.authenum.Error.INVALID_PHONE_NUMBER; + errorMap[fireauth.RpcHandler.ServerError.MISSING_APP_CREDENTIAL] = + fireauth.authenum.Error.MISSING_APP_CREDENTIAL; + errorMap[fireauth.RpcHandler.ServerError.MISSING_PHONE_NUMBER] = + fireauth.authenum.Error.MISSING_PHONE_NUMBER; + errorMap[fireauth.RpcHandler.ServerError.QUOTA_EXCEEDED] = + fireauth.authenum.Error.QUOTA_EXCEEDED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.sendVerificationCode(requestBody); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful verifyPhoneNumber RPC call using an SMS code. + */ +function testVerifyPhoneNumber_success_usingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedStsTokenResponse); + rpcHandler.verifyPhoneNumber(expectedRequest).then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyPhoneNumber RPC call using an SMS code and passing + * custom locale. + */ +function testVerifyPhoneNumber_success_customLocale_usingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + { + 'Content-Type': 'application/json', + 'X-Firebase-Locale': 'ru' + }, + delay, + expectedStsTokenResponse); + rpcHandler.updateCustomLocaleHeader('ru'); + rpcHandler.verifyPhoneNumber(expectedRequest).then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyPhoneNumber RPC call using a temporary proof. + */ +function testVerifyPhoneNumber_success_usingTemporaryProof() { + var expectedRequest = { + 'phoneNumber': '+16505550101', + 'temporaryProof': 'TEMPORARY_PROOF' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedStsTokenResponse); + rpcHandler.verifyPhoneNumber(expectedRequest).then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests a verifyPhoneNumber RPC call using a temporary proof without a + * phone number. + */ +function testVerifyPhoneNumber_error_noPhoneNumber() { + var expectedRequest = { + 'temporaryProof': 'TEMPORARY_PROOF' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumber(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests a verifyPhoneNumber RPC call using a phone number without a + * temporary proof. + */ +function testVerifyPhoneNumber_error_noTemporaryProof() { + var expectedRequest = { + 'phoneNumber': '+16505550101' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumber(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumber error for a missing sessionInfo. + */ +function testVerifyPhoneNumber_invalidRequest_missingSessionInfo() { + var expectedRequest = { + 'code': '123456' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumber(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_SESSION_INFO), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumber error for a missing code. + */ +function testVerifyPhoneNumber_invalidRequest_missingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumber(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response verifyPhoneNumber error. + */ +function testVerifyPhoneNumber_unknownServerResponse() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456' + }; + // No idToken returned. + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyPhoneNumber(expectedRequest).thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side verifyPhoneNumber error. + */ +function testVerifyPhoneNumber_caughtServerError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/verifyPhoneNumber?key=apiKey'; + var requestBody = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456' + }; + var errorMap = {}; + // All related server errors for verifyPhoneNumber. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CODE] = + fireauth.authenum.Error.INVALID_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SESSION_INFO] = + fireauth.authenum.Error.INVALID_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.INVALID_TEMPORARY_PROOF] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CODE] = + fireauth.authenum.Error.MISSING_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_SESSION_INFO] = + fireauth.authenum.Error.MISSING_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.SESSION_EXPIRED] = + fireauth.authenum.Error.CODE_EXPIRED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyPhoneNumber(requestBody); + }, errorMap, expectedUrl, requestBody); +} + + +/** + * Tests successful verifyPhoneNumberForLinking RPC call using an SMS code. + */ +function testVerifyPhoneNumberForLinking_success_usingCode() { + // Temporary proof not used for linking as it corresponds to an existing + // credential that will fail when being linked or updated on another account. + // No need to test for it. + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedStsTokenResponse); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForLinking error for a missing + * sessionInfo. + */ +function testVerifyPhoneNumberForLinking_invalidRequest_missingSessionInfo() { + var expectedRequest = { + 'code': '123456', + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError( + fireauth.authenum.Error.MISSING_SESSION_INFO), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForLinking error for a missing code. + */ +function testVerifyPhoneNumberForLinking_invalidRequest_missingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'idToken': 'ID_TOKEN' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForLinking error for a missing ID + * token. + */ +function testVerifyPhoneNumberForLinking_invalidRequest_missingIdToken() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response verifyPhoneNumberForLinking error. + */ +function testVerifyPhoneNumberForLinking_unknownServerResponse() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'idToken': 'ID_TOKEN' + }; + // No idToken returned. + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side verifyPhoneNumber error. + */ +function testVerifyPhoneNumberForLinking_caughtServerError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/verifyPhoneNumber?key=apiKey'; + var requestBody = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'idToken': 'ID_TOKEN' + }; + var errorMap = {}; + // All related server errors for verifyPhoneNumberForLinking. + errorMap[fireauth.RpcHandler.ServerError.INVALID_CODE] = + fireauth.authenum.Error.INVALID_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SESSION_INFO] = + fireauth.authenum.Error.INVALID_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.INVALID_TEMPORARY_PROOF] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CODE] = + fireauth.authenum.Error.MISSING_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_SESSION_INFO] = + fireauth.authenum.Error.MISSING_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.SESSION_EXPIRED] = + fireauth.authenum.Error.CODE_EXPIRED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyPhoneNumberForLinking(requestBody); + }, errorMap, expectedUrl, requestBody); +} + + + +/** + * Tests that when verifyPhoneNumber returns a temporaryProof, an appropriate + * credential object is created and attached to the error. + */ +function testVerifyPhoneNumberForLinking_credentialAlreadyInUseError() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'idToken': 'ID_TOKEN' + }; + var expectedResponse = { + 'temporaryProof': 'theTempProof', + 'phoneNumber': '+16505550101' + }; + + var credential = fireauth.AuthProvider.getCredentialFromResponse( + expectedResponse); + var expectedError = new fireauth.AuthErrorWithCredential( + fireauth.authenum.Error.CREDENTIAL_ALREADY_IN_USE, + { + phoneNumber: '+16505550101', + credential: credential + }); + + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyPhoneNumberForLinking(expectedRequest) + .then(fail, function(error) { + assertTrue(error instanceof fireauth.AuthErrorWithCredential); + fireauth.common.testHelper.assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyPhoneNumberForExisting RPC call using an SMS code. + */ +function testVerifyPhoneNumberForExisting_success_usingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedStsTokenResponse); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests successful verifyPhoneNumberForExisting RPC call using a temporary + * proof. + */ +function testVerifyPhoneNumberForExisting_success_usingTemporaryProof() { + var expectedRequest = { + 'phoneNumber': '+16505550101', + 'temporaryProof': 'TEMPORARY_PROOF', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedStsTokenResponse); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .then(function(response) { + assertEquals(expectedStsTokenResponse, response); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForExisting error for a missing + * sessionInfo. + */ +function testVerifyPhoneNumberForExisting_invalidRequest_missingSessionInfo() { + var expectedRequest = { + 'code': '123456', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError( + fireauth.authenum.Error.MISSING_SESSION_INFO), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForExisting error for a missing code. + */ +function testVerifyPhoneNumberForExisting_invalidRequest_missingCode() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.MISSING_CODE), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForExisting error for a missing + * phoneNumber. + */ +function testVerifyPhoneNumberForExisting_invalidRequest_missingPhoneNumber() { + var expectedRequest = { + 'temporaryProof': 'TEMPORARY_PROOF', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid request verifyPhoneNumberForExisting error for a missing + * temporary proof. + */ +function testVerifyPhoneNumberForExisting_invalidRequest_missingTempProof() { + var expectedRequest = { + 'phoneNumber': '+16505550101', + 'operation': 'REAUTH' + }; + asyncTestCase.waitForSignals(1); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests invalid response verifyPhoneNumberForExisting error. + */ +function testVerifyPhoneNumberForExisting_unknownServerResponse() { + var expectedRequest = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'operation': 'REAUTH' + }; + // No idToken returned. + var expectedResponse = {}; + asyncTestCase.waitForSignals(1); + assertSendXhrAndRunCallback( + 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhon' + + 'eNumber?key=apiKey', + 'POST', + goog.json.serialize(expectedRequest), + fireauth.RpcHandler.DEFAULT_FIREBASE_HEADERS_, + delay, + expectedResponse); + rpcHandler.verifyPhoneNumberForExisting(expectedRequest) + .thenCatch(function(error) { + fireauth.common.testHelper.assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR), + error); + asyncTestCase.signal(); + }); +} + + +/** + * Tests server side verifyPhoneNumberForExisting error. + */ +function testVerifyPhoneNumberForExisting_caughtServerError() { + var expectedUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyin' + + 'gparty/verifyPhoneNumber?key=apiKey'; + var requestBody = { + 'sessionInfo': 'SESSION_INFO', + 'code': '123456', + 'operation': 'REAUTH' + }; + var errorMap = {}; + // All related server errors for verifyPhoneNumberForExisting. + + // This should be overridden from the default error mapping. + errorMap[fireauth.RpcHandler.ServerError.USER_NOT_FOUND] = + fireauth.authenum.Error.USER_DELETED; + + errorMap[fireauth.RpcHandler.ServerError.INVALID_CODE] = + fireauth.authenum.Error.INVALID_CODE; + errorMap[fireauth.RpcHandler.ServerError.INVALID_SESSION_INFO] = + fireauth.authenum.Error.INVALID_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.INVALID_TEMPORARY_PROOF] = + fireauth.authenum.Error.INVALID_IDP_RESPONSE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_CODE] = + fireauth.authenum.Error.MISSING_CODE; + errorMap[fireauth.RpcHandler.ServerError.MISSING_SESSION_INFO] = + fireauth.authenum.Error.MISSING_SESSION_INFO; + errorMap[fireauth.RpcHandler.ServerError.SESSION_EXPIRED] = + fireauth.authenum.Error.CODE_EXPIRED; + + assertServerErrorsAreHandled(function() { + return rpcHandler.verifyPhoneNumberForExisting(requestBody); + }, errorMap, expectedUrl, requestBody); +} diff --git a/packages/auth/test/storage/asyncstorage_test.js b/packages/auth/test/storage/asyncstorage_test.js new file mode 100644 index 00000000000..ac3f8d200dd --- /dev/null +++ b/packages/auth/test/storage/asyncstorage_test.js @@ -0,0 +1,59 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.AsyncStorageTest'); + +goog.require('fireauth.storage.AsyncStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('fireauth.storage.testHelper.FakeAsyncStorage'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.AsyncStorageTest'); + + +var stubs = new goog.testing.PropertyReplacer(); +var storage; + + +function setUp() { + storage = new fireauth.storage.AsyncStorage( + new fireauth.storage.testHelper.FakeAsyncStorage()); +} + + +function tearDown() { + storage = null; + stubs.reset(); +} + + +function testBasicStorageOperations() { + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes() { + return assertDifferentTypes(storage); +} + + +function testNotAvailable() { + stubs.replace(firebase.INTERNAL, 'reactNative', {}); + var error = assertThrows(function() { new fireauth.storage.AsyncStorage(); }); + assertEquals('auth/internal-error', error.code); +} diff --git a/packages/auth/test/storage/factory_test.js b/packages/auth/test/storage/factory_test.js new file mode 100644 index 00000000000..2a441a12232 --- /dev/null +++ b/packages/auth/test/storage/factory_test.js @@ -0,0 +1,130 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.FactoryTest'); + +goog.require('fireauth.storage.AsyncStorage'); +goog.require('fireauth.storage.Factory'); +goog.require('fireauth.storage.Factory.EnvConfig'); +goog.require('fireauth.storage.InMemoryStorage'); +goog.require('fireauth.storage.IndexedDB'); +goog.require('fireauth.storage.LocalStorage'); +goog.require('fireauth.storage.NullStorage'); +goog.require('fireauth.storage.SessionStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.FactoryTest'); + + +var stubs = new goog.testing.PropertyReplacer(); + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); +} + + +function tearDown() { + stubs.reset(); +} + + +function testGetStorage_browser_temporary() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.BROWSER); + assertTrue(factory.makeTemporaryStorage() instanceof + fireauth.storage.SessionStorage); +} + + +function testGetStorage_browser_persistent() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.BROWSER); + assertTrue(factory.makePersistentStorage() instanceof + fireauth.storage.LocalStorage); +} + + +function testGetStorage_browser_persistent_isLocalStorageNotSynchronized() { + // Simulate browser to force usage of indexedDB storage. + var mock = { + type: 'indexedDB' + }; + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return true; + }); + stubs.replace( + fireauth.storage.IndexedDB, + 'getFireauthManager', + function() { + return mock; + }); + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.BROWSER); + assertEquals('indexedDB', factory.makePersistentStorage().type); +} + + +function testGetStorage_node_temporary() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.NODE); + assertTrue(factory.makeTemporaryStorage() instanceof + fireauth.storage.SessionStorage); +} + + +function testGetStorage_node_persistent() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.NODE); + assertTrue(factory.makePersistentStorage() instanceof + fireauth.storage.LocalStorage); +} + + +function testGetStorage_reactnative_temporary() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.REACT_NATIVE); + assertTrue(factory.makeTemporaryStorage() instanceof + fireauth.storage.NullStorage); +} + + +function testGetStorage_reactnative_persistent() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.REACT_NATIVE); + assertTrue(factory.makePersistentStorage() instanceof + fireauth.storage.AsyncStorage); +} + + +function testGetStorage_inMemory() { + var factory = new fireauth.storage.Factory( + fireauth.storage.Factory.EnvConfig.BROWSER); + assertTrue(factory.makeInMemoryStorage() instanceof + fireauth.storage.InMemoryStorage); +} diff --git a/packages/auth/test/storage/indexeddb_test.js b/packages/auth/test/storage/indexeddb_test.js new file mode 100644 index 00000000000..9f0e1958f2f --- /dev/null +++ b/packages/auth/test/storage/indexeddb_test.js @@ -0,0 +1,328 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.IndexedDBTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.IndexedDB'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.storage.IndexedDBTest'); + + +var stubs = new goog.testing.PropertyReplacer(); +var db = null; +var manager; +var clock; +var indexedDBMock; + + +function setUp() { + // IndexedDB not supported in IE9. + stubs.replace( + fireauth.storage.IndexedDB, + 'isAvailable', + function() { + return true; + }); + clock = new goog.testing.MockClock(true); + indexedDBMock = { + open: function(dbName, version) { + assertEquals('firebaseLocalStorageDb', dbName); + assertEquals(1, version); + var dbRequest = {}; + goog.Promise.resolve().then(function() { + db = { + key: null, + store: {}, + createObjectStore: function(objectStoreName, keyPath) { + var request = { + 'transaction': { + } + }; + assertEquals( + 'firebaseLocalStorage', + objectStoreName); + assertObjectEquals( + { + 'keyPath': 'fbase_key' + }, + keyPath); + db.key = keyPath['keyPath']; + db.store[objectStoreName] = {}; + goog.Promise.resolve().then(function() { + var event = { + 'target': { + 'result': db + } + }; + dbRequest.onsuccess(event); + }); + return request; + }, + transaction: function(objectStores, type) { + for (var i = 0; i < objectStores.length; i++) { + if (!db.store[objectStores[i]]) { + fail('Object store does not exist!'); + } + } + return { + objectStore: function(objectStoreName) { + if (!db.store[objectStoreName]) { + fail('Object store does not exist!'); + } + return { + add: function(data) { + var request = {}; + if (type != 'readwrite') { + fail('Invalid write operation!'); + } + if (db.store[objectStoreName][data[db.key]]) { + fail('Unable to add. Key already exists!'); + } + goog.Promise.resolve().then(function() { + db.store[objectStoreName][data[db.key]] = data; + request.onsuccess(); + }); + return request; + }, + put: function(data) { + var request = {}; + if (type != 'readwrite') { + fail('Invalid write operation!'); + } + if (!db.store[objectStoreName][data[db.key]]) { + fail('Unable to put. Key does not exist!'); + } + for (var subKey in data) { + db.store[objectStoreName][data[db.key]][subKey] = + data[subKey]; + } + goog.Promise.resolve().then(function() { + request.onsuccess(); + }); + return request; + }, + delete: function(keyToRemove) { + var request = {}; + if (type != 'readwrite') { + fail('Invalid write operation!'); + } + if (db.store[objectStoreName][keyToRemove]) { + delete db.store[objectStoreName][keyToRemove]; + } + goog.Promise.resolve().then(function() { + request.onsuccess(); + }); + return request; + }, + get: function(keyToGet, callback) { + var request = {}; + var data = db.store[objectStoreName][keyToGet] || null; + goog.Promise.resolve().then(function() { + var event = { + 'target': { + 'result': data + } + }; + request.onsuccess(event); + }); + return request; + }, + getAll: function() { + var request = {}; + var results = []; + for (var key in db.store[objectStoreName]) { + results.push(db.store[objectStoreName][key]); + } + goog.Promise.resolve().then(function() { + var event = { + 'target': { + 'result': results + } + }; + request.onsuccess(event); + }); + return request; + } + }; + } + }; + } + }; + var event = { + 'target': { + 'result': db + } + }; + dbRequest.onupgradeneeded(event); + }); + return dbRequest; + } + }; +} + + +function tearDown() { + if (manager) { + manager.removeAllStorageListeners(); + } + manager = null; + db = null; + indexedDBMock = null; + stubs.reset(); + goog.dispose(clock); +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +function assertErrorEquals(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +} + + +/** + * @return {!fireauth.storage.IndexedDB} The default indexedDB + * local storage manager to be used for testing. + */ +function getDefaultFireauthManager() { + return new fireauth.storage.IndexedDB( + 'firebaseLocalStorageDb', + 'firebaseLocalStorage', + 'fbase_key', + 'value', + 1, + indexedDBMock); +} + + +function testIndexedDb_notSupported() { + // Test when indexedDB is not supported. + stubs.replace( + fireauth.storage.IndexedDB, + 'isAvailable', + function() { + return false; + }); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED); + try { + getDefaultFireauthManager(); + fail('Should fail when indexedDB is not supported.'); + } catch (error) { + assertErrorEquals(expectedError, error); + } +} + + +function testIndexedDb_null() { + manager = getDefaultFireauthManager(); + return manager.get('key1') + .then(function(data) { + assertNull(data); + }); +} + + +function testIndexedDb_setGetRemove() { + manager = getDefaultFireauthManager(); + manager.addStorageListener(function() { + fail('Storage should not be triggered for local changes!'); + }); + return goog.Promise.resolve() + .then(function() { + return manager.set('key1', 'value1'); + }) + .then(function() { + return manager.get('key1'); + }) + .then(function(data) { + assertEquals('value1', data); + }) + .then(function() { + return manager.remove('key1'); + }) + .then(function() { + return manager.get('key1'); + }) + .then(function(data) { + assertNull(data); + }); +} + + +function testStartListeners() { + manager = getDefaultFireauthManager(); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + + // Add listeners. + manager.addStorageListener(listener1); + manager.addStorageListener(listener2); + clock.tick(800); + clock.tick(800); + clock.tick(800); + db.store['firebaseLocalStorage'] = { + 'key1': {'fbase_key': 'key1', 'value': 1}, + 'key2': {'fbase_key': 'key2', 'value': 2} + }; + clock.tick(800); + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener2.getCallCount()); + assertArrayEquals( + ['key1', 'key2'], listener1.getLastCall().getArgument(0)); + assertArrayEquals( + ['key1', 'key2'], listener2.getLastCall().getArgument(0)); +} + + +function testStopListeners() { + manager = getDefaultFireauthManager(); + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + // Add listeners. + manager.addStorageListener(listener1); + manager.addStorageListener(listener2); + manager.addStorageListener(listener3); + clock.tick(800); + clock.tick(800); + clock.tick(800); + // Remove all but listener3. + manager.removeStorageListener(listener1); + manager.removeStorageListener(listener2); + db.store['firebaseLocalStorage'] = { + 'key1': {'fbase_key': 'key1', 'value': 1}, + 'key2': {'fbase_key': 'key2', 'value': 2} + }; + clock.tick(800); + // Only listener3 should be called. + assertEquals(0, listener1.getCallCount()); + assertEquals(0, listener2.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertArrayEquals( + ['key1', 'key2'], listener3.getLastCall().getArgument(0)); +} diff --git a/packages/auth/test/storage/inmemorystorage_test.js b/packages/auth/test/storage/inmemorystorage_test.js new file mode 100644 index 00000000000..348f678dd6a --- /dev/null +++ b/packages/auth/test/storage/inmemorystorage_test.js @@ -0,0 +1,47 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.InMemoryStorageTest'); + +goog.require('fireauth.storage.InMemoryStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.InMemoryStorageTest'); + + +var storage; + + +function setUp() { + storage = new fireauth.storage.InMemoryStorage(); +} + + +function tearDown() { + storage = null; +} + + +function testBasicStorageOperations() { + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes() { + return assertDifferentTypes(storage); +} diff --git a/packages/auth/test/storage/localstorage_test.js b/packages/auth/test/storage/localstorage_test.js new file mode 100644 index 00000000000..c050117bb56 --- /dev/null +++ b/packages/auth/test/storage/localstorage_test.js @@ -0,0 +1,145 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.LocalStorageTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.LocalStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('fireauth.util'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.storage.LocalStorageTest'); + + +var stubs = new goog.testing.PropertyReplacer(); +var storage; + + +function setUp() { + storage = new fireauth.storage.LocalStorage(); +} + + +function tearDown() { + storage = null; + stubs.reset(); + localStorage.clear(); +} + + +/** Simulates a Node.js environment. */ +function simulateNodeEnvironment() { + // Node.js environment. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() {return fireauth.util.Env.NODE;}); + // No window.localStorage. + stubs.replace( + fireauth.storage.LocalStorage, + 'getGlobalStorage', + function() {return null;}); +} + + +function testBasicStorageOperations() { + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes() { + return assertDifferentTypes(storage); +} + + +function testListeners() { + var storageEvent; + + var listener1 = goog.testing.recordFunction(); + var listener2 = goog.testing.recordFunction(); + var listener3 = goog.testing.recordFunction(); + + storage.addStorageListener(listener1); + storage.addStorageListener(listener3); + + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'myKey'; + goog.testing.events.fireBrowserEvent(storageEvent); + + assertEquals(1, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); + + storage.removeStorageListener(listener3); + + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'myKey2'; + goog.testing.events.fireBrowserEvent(storageEvent); + + assertEquals(2, listener1.getCallCount()); + assertEquals(1, listener3.getCallCount()); + assertEquals(0, listener2.getCallCount()); +} + + +function testNotAvailable() { + stubs.replace( + fireauth.storage.LocalStorage, 'isAvailable', + function() { return false; }); + var error = assertThrows(function() { new fireauth.storage.LocalStorage(); }); + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED), + error); +} + + +function testBasicStorageOperations_node() { + simulateNodeEnvironment(); + storage = new fireauth.storage.LocalStorage(); + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes_node() { + simulateNodeEnvironment(); + storage = new fireauth.storage.LocalStorage(); + return assertDifferentTypes(storage); +} + + +function testNotAvailable_node() { + // Compatibility libraries not included. + stubs.replace(firebase.INTERNAL, 'node', {}); + // Simulate Node.js environment. + simulateNodeEnvironment(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'The LocalStorage compatibility library was not found.'); + var error = assertThrows(function() { new fireauth.storage.LocalStorage(); }); + assertErrorEquals( + expectedError, + error); +} diff --git a/packages/auth/test/storage/nullstorage_test.js b/packages/auth/test/storage/nullstorage_test.js new file mode 100644 index 00000000000..84f1bd3486c --- /dev/null +++ b/packages/auth/test/storage/nullstorage_test.js @@ -0,0 +1,39 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.NullStorageTest'); + +goog.require('fireauth.storage.NullStorage'); +goog.require('goog.Promise'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.NullStorageTest'); + + + +function testNullStorage() { + var storage = new fireauth.storage.NullStorage(); + var listener = function() {}; + storage.addStorageListener(listener); + storage.removeStorageListener(listener); + return goog.Promise.resolve() + .then(function() { return storage.set('foo', 'bar'); }) + .then(function() { return storage.get('foo'); }) + .then(function(value) { + assertNull(value); + return storage.remove('foo'); + }); +} diff --git a/packages/auth/test/storage/sessionstorage_test.js b/packages/auth/test/storage/sessionstorage_test.js new file mode 100644 index 00000000000..061c39d02b2 --- /dev/null +++ b/packages/auth/test/storage/sessionstorage_test.js @@ -0,0 +1,113 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +goog.provide('fireauth.storage.SessionStorageTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.authenum.Error'); +goog.require('fireauth.storage.SessionStorage'); +/** @suppress {extraRequire} */ +goog.require('fireauth.storage.testHelper'); +goog.require('fireauth.util'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.SessionStorageTest'); + + +var stubs = new goog.testing.PropertyReplacer(); +var storage; + + +function setUp() { + storage = new fireauth.storage.SessionStorage(); +} + + +function tearDown() { + storage = null; + stubs.reset(); + sessionStorage.clear(); +} + + +/** Simulates a Node.js environment. */ +function simulateNodeEnvironment() { + // Node.js environment. + stubs.replace( + fireauth.util, + 'getEnvironment', + function() {return fireauth.util.Env.NODE;}); + // No window.sessionStorage. + stubs.replace( + fireauth.storage.SessionStorage, + 'getGlobalStorage', + function() {return null;}); +} + + +function testBasicStorageOperations() { + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes() { + return assertDifferentTypes(storage); +} + + +function testNotAvailable() { + stubs.replace( + fireauth.storage.SessionStorage, 'isAvailable', + function() { return false; }); + var error = assertThrows(function() { + new fireauth.storage.SessionStorage(); + }); + assertErrorEquals( + new fireauth.AuthError(fireauth.authenum.Error.WEB_STORAGE_UNSUPPORTED), + error); +} + + +function testBasicStorageOperations_node() { + simulateNodeEnvironment(); + storage = new fireauth.storage.SessionStorage(); + return assertBasicStorageOperations(storage); +} + + +function testDifferentTypes_node() { + simulateNodeEnvironment(); + storage = new fireauth.storage.SessionStorage(); + return assertDifferentTypes(storage); +} + + +function testNotAvailable_node() { + // Compatibility libraries not included. + stubs.replace(firebase.INTERNAL, 'node', {}); + // Simulate Node.js environment. + simulateNodeEnvironment(); + var expectedError = new fireauth.AuthError( + fireauth.authenum.Error.INTERNAL_ERROR, + 'The SessionStorage compatibility library was not found.'); + var error = assertThrows(function() { + new fireauth.storage.SessionStorage(); + }); + assertErrorEquals( + expectedError, + error); +} diff --git a/packages/auth/test/storage/testhelper.js b/packages/auth/test/storage/testhelper.js new file mode 100644 index 00000000000..35530fd6609 --- /dev/null +++ b/packages/auth/test/storage/testhelper.js @@ -0,0 +1,152 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Provides utilities for testing storage. + */ +goog.provide('fireauth.storage.testHelper'); +goog.provide('fireauth.storage.testHelper.FakeAsyncStorage'); +goog.setTestOnly('fireauth.storage.testHelper'); + +goog.require('goog.Promise'); + + +/** + * Provides a fake implementation of the React Native AsyncStorage API. It + * currently does not implement all of the APIs. + * @constructor + * @see https://facebook.github.io/react-native/docs/asyncstorage.html + */ +fireauth.storage.testHelper.FakeAsyncStorage = function() { + /** @private {!Object} */ + this.storage_ = {}; +}; + + +/** + * @param {string} key + * @return {!goog.Promise} + */ +fireauth.storage.testHelper.FakeAsyncStorage.prototype.getItem = + function(key) { + return goog.Promise.resolve(this.storage_[key]); +}; + + +/** + * @param {string} key + * @param {string} value + * @return {!goog.Promise} + */ +fireauth.storage.testHelper.FakeAsyncStorage.prototype.setItem = + function(key, value) { + this.storage_[key] = value; + return goog.Promise.resolve(); +}; + + +/** + * @param {string} key + * @return {!goog.Promise} + */ +fireauth.storage.testHelper.FakeAsyncStorage.prototype.removeItem = + function(key) { + delete this.storage_[key]; + return goog.Promise.resolve(); +}; + + +var storageFirebaseExtension = { + 'INTERNAL': { + 'reactNative': { + 'AsyncStorage': new fireauth.storage.testHelper.FakeAsyncStorage() + }, + 'node': { + 'localStorage': window.localStorage, + 'sessionStorage': window.sessionStorage, + } + } +}; + + +if (!goog.global['firebase'] || !goog.global['firebase']['INTERNAL']) { + goog.global['firebase'] = storageFirebaseExtension; +} else { + goog.global['firebase']['INTERNAL']['extendNamespace']( + storageFirebaseExtension); +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +function assertErrorEquals(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +} + + +/** + * @param {!fireauth.storage.Storage} storage + * @return {!goog.Promise} + */ +function assertBasicStorageOperations(storage) { + return goog.Promise.resolve() + .then(function() { return storage.get('foo'); }) + .then(function(value) { + assertUndefined(value); + return storage.set('foo', 'bar'); + }) + .then(function() { return storage.get('foo'); }) + .then(function(value) { + assertEquals('bar', value); + return storage.remove('foo'); + }) + .then(function() { return storage.get('foo'); }) + .then(function(value) { assertUndefined(value); }); +} + + +/** + * @param {!fireauth.storage.Storage} storage + * @return {!goog.Promise} + */ +function assertDifferentTypes(storage) { + var obj = {'a': 1.2, 'b': 'foo'}; + var num = 54; + var bool = true; + return goog.Promise.resolve() + .then(function() { + return goog.Promise.all([ + storage.set('obj', obj), storage.set('num', num), + storage.set('bool', bool), storage.set('null', null) + ]); + }) + .then(function() { + return goog.Promise.all([ + storage.get('obj'), storage.get('num'), storage.get('bool'), + storage.get('null'), storage.get('undefined') + ]); + }) + .then(function(values) { + assertObjectEquals(obj, values[0]); + assertEquals(num, values[1]); + assertEquals(bool, values[2]); + assertNull(values[3]); + }); +} diff --git a/packages/auth/test/storageautheventmanager_test.js b/packages/auth/test/storageautheventmanager_test.js new file mode 100644 index 00000000000..323b9b1b7c9 --- /dev/null +++ b/packages/auth/test/storageautheventmanager_test.js @@ -0,0 +1,178 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for storageautheventmanager.js + */ + +goog.provide('fireauth.storage.AuthEventManagerTest'); + +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.storage.AuthEventManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.storage.AuthEventManagerTest'); + + +var appId = 'appId1'; +var stubs = new goog.testing.PropertyReplacer(); + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + window.localStorage.clear(); + window.sessionStorage.clear(); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultStorageManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testGetSetRemoveAuthEvent() { + var storageManager = getDefaultStorageManagerInstance(); + var authEventManager = + new fireauth.storage.AuthEventManager(appId, storageManager); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaPopup', + '1234', + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + // Set expected Auth event in localStorage. + window.localStorage.setItem( + 'firebase:authEvent:appId1', + JSON.stringify(expectedAuthEvent.toPlainObject())); + var storageKey = 'firebase:authEvent:appId1'; + return goog.Promise.resolve() + .then(function() { + return authEventManager.getAuthEvent(); + }) + .then(function(authEvent) { + assertObjectEquals(expectedAuthEvent, authEvent); + }) + .then(function() { + return authEventManager.removeAuthEvent(); + }) + .then(function() { + assertNull(window.localStorage.getItem(storageKey)); + return authEventManager.getAuthEvent(); + }) + .then(function(authEvent) { + assertNull(authEvent); + }); +} + + +function testGetSetRemoveRedirectEvent() { + var storageManager = getDefaultStorageManagerInstance(); + var authEventManager = + new fireauth.storage.AuthEventManager(appId, storageManager); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaRedirect', + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + // Set expected Auth event in sessionStorage. + window.sessionStorage.setItem( + 'firebase:redirectEvent:appId1', + JSON.stringify(expectedAuthEvent.toPlainObject())); + var storageKey = 'firebase:redirectEvent:appId1'; + return goog.Promise.resolve() + .then(function() { + return authEventManager.getRedirectEvent(); + }) + .then(function(authEvent) { + assertObjectEquals(expectedAuthEvent, authEvent); + }) + .then(function() { + return authEventManager.removeRedirectEvent(); + }) + .then(function() { + assertNull(window.sessionStorage.getItem(storageKey)); + return authEventManager.getRedirectEvent(); + }) + .then(function(authEvent) { + assertNull(authEvent); + }); +} + + +function testAddRemoveAuthEventListener() { + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaRedirect', + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + var storageManager = getDefaultStorageManagerInstance(); + var authEventManager = + new fireauth.storage.AuthEventManager('appId1', storageManager); + var listener = goog.testing.recordFunction(); + // Save existing Auth events for appId1 and appId2. + window.localStorage.setItem( + 'firebase:authEvent:appId1', + JSON.stringify(expectedAuthEvent.toPlainObject())); + window.localStorage.setItem( + 'firebase:authEvent:appId2', + JSON.stringify(expectedAuthEvent.toPlainObject())); + authEventManager.addAuthEventListener(listener); + // Simulate appId1 event deletion. + storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authEvent:appId1'; + storageEvent.newValue = null; + window.localStorage.removeItem('firebase:authEvent:appId1'); + // This should trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + // Simulate appId2 event deletion. + storageEvent.key = 'firebase:authEvent:appId2'; + window.localStorage.removeItem('firebase:authEvent:appId2'); + // This should not trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + // Remove listener. + authEventManager.removeAuthEventListener(listener); + // Simulate new event saved for appId1. + // This should not trigger listener anymore. + storageEvent.key = 'firebase:authEvent:appId1'; + storageEvent.oldValue = null; + storageEvent.newValue = JSON.stringify(expectedAuthEvent.toPlainObject()); + window.localStorage.setItem( + 'firebase:authEvent:appId1', + JSON.stringify(expectedAuthEvent.toPlainObject())); + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); +} diff --git a/packages/auth/test/storageoauthhandlermanager_test.js b/packages/auth/test/storageoauthhandlermanager_test.js new file mode 100644 index 00000000000..d8552c19077 --- /dev/null +++ b/packages/auth/test/storageoauthhandlermanager_test.js @@ -0,0 +1,170 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for storageoauthhandlermanager.js + */ + +goog.provide('fireauth.storage.OAuthHandlerManagerTest'); + +goog.require('fireauth.AuthEvent'); +goog.require('fireauth.OAuthHelperState'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.storage.OAuthHandlerManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.OAuthHandlerManagerTest'); + + +var appId = 'appId1'; +var stubs = new goog.testing.PropertyReplacer(); + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + window.localStorage.clear(); + window.sessionStorage.clear(); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultStorageManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testGetSetRemoveSessionId() { + var storageManager = getDefaultStorageManagerInstance(); + var oauthHandlerManager = + new fireauth.storage.OAuthHandlerManager(storageManager); + var expectedSessionId = 'g43g4ngh4hhk4hn042rj290rg4g4'; + var storageKey = 'firebase:sessionId:appId1'; + return goog.Promise.resolve() + .then(function() { + return oauthHandlerManager.setSessionId(appId, expectedSessionId); + }) + .then(function() { + return oauthHandlerManager.getSessionId(appId); + }) + .then(function(sessionId) { + assertEquals( + window.sessionStorage.getItem(storageKey), + JSON.stringify(expectedSessionId)); + assertObjectEquals(expectedSessionId, sessionId); + }) + .then(function() { + return oauthHandlerManager.removeSessionId(appId); + }) + .then(function() { + assertNull(window.sessionStorage.getItem(storageKey)); + return oauthHandlerManager.getSessionId(appId); + }) + .then(function(sessionId) { + assertUndefined(sessionId); + }); +} + + +function testSetAuthEvent() { + var storageManager = getDefaultStorageManagerInstance(); + var oauthHandlerManager = + new fireauth.storage.OAuthHandlerManager(storageManager); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaPopup', + '1234', + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + return goog.Promise.resolve() + .then(function() { + return oauthHandlerManager.setAuthEvent(appId, expectedAuthEvent); + }) + .then(function() { + assertEquals( + JSON.stringify(expectedAuthEvent.toPlainObject()), + window.localStorage.getItem( + 'firebase:authEvent:appId1')); + }); +} + + +function testSetRedirectEvent() { + var storageManager = getDefaultStorageManagerInstance(); + var oauthHandlerManager = + new fireauth.storage.OAuthHandlerManager(storageManager); + var expectedAuthEvent = new fireauth.AuthEvent( + 'signInViaRedirect', + null, + 'http://www.example.com/#oauthResponse', + 'SESSION_ID'); + return goog.Promise.resolve() + .then(function() { + return oauthHandlerManager.setRedirectEvent(appId, expectedAuthEvent); + }) + .then(function() { + assertEquals( + JSON.stringify(expectedAuthEvent.toPlainObject()), + window.sessionStorage.getItem( + 'firebase:redirectEvent:appId1')); + }); +} + + +function testGetSetRemoveOAuthHelperState() { + var storageManager = getDefaultStorageManagerInstance(); + var oauthHandlerManager = + new fireauth.storage.OAuthHandlerManager(storageManager); + var expectedState = new fireauth.OAuthHelperState( + 'API_KEY', + fireauth.AuthEvent.Type.SIGN_IN_VIA_POPUP, + '12345678', + 'http://www.example.com/redirect'); + var storageKey = 'firebase:oauthHelperState'; + return goog.Promise.resolve() + .then(function() { + return oauthHandlerManager.setOAuthHelperState(expectedState); + }) + .then(function() { + return oauthHandlerManager.getOAuthHelperState(); + }) + .then(function(state) { + assertEquals( + window.sessionStorage.getItem(storageKey), + JSON.stringify(expectedState.toPlainObject())); + assertObjectEquals(expectedState, state); + }) + .then(function() { + return oauthHandlerManager.removeOAuthHelperState(); + }) + .then(function() { + assertNull(window.sessionStorage.getItem(storageKey)); + return oauthHandlerManager.getOAuthHelperState(); + }) + .then(function(state) { + assertNull(state); + }); +} diff --git a/packages/auth/test/storagependingredirectmanager_test.js b/packages/auth/test/storagependingredirectmanager_test.js new file mode 100644 index 00000000000..8ef41d4dbf5 --- /dev/null +++ b/packages/auth/test/storagependingredirectmanager_test.js @@ -0,0 +1,85 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for storagependingredirectmanager.js + */ + +goog.provide('fireauth.storage.PendingRedirectManagerTest'); + +goog.require('fireauth.authStorage'); +goog.require('fireauth.storage.PendingRedirectManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.PendingRedirectManagerTest'); + + +var appId = 'appId1'; +var stubs = new goog.testing.PropertyReplacer(); + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + window.localStorage.clear(); + window.sessionStorage.clear(); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultStorageManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testGetSetPendingStatus() { + var storageManager = getDefaultStorageManagerInstance(); + var pendingRedirectManager = + new fireauth.storage.PendingRedirectManager(appId, storageManager); + var storageKey = 'firebase:pendingRedirect:appId1'; + return goog.Promise.resolve() + .then(function() { + return pendingRedirectManager.setPendingStatus(); + }) + .then(function() { + return pendingRedirectManager.getPendingStatus(); + }) + .then(function(status) { + assertEquals( + window.sessionStorage.getItem(storageKey), + JSON.stringify('pending')); + assertTrue(status); + return pendingRedirectManager.removePendingStatus(); + }) + .then(function() { + assertNull(window.sessionStorage.getItem(storageKey)); + return pendingRedirectManager.getPendingStatus(); + }) + .then(function(status) { + assertFalse(status); + }); +} diff --git a/packages/auth/test/storageredirectusermanager_test.js b/packages/auth/test/storageredirectusermanager_test.js new file mode 100644 index 00000000000..40c0242cd95 --- /dev/null +++ b/packages/auth/test/storageredirectusermanager_test.js @@ -0,0 +1,137 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for storageredirectusermanager.js + */ + +goog.provide('fireauth.storage.RedirectUserManagerTest'); + +goog.require('fireauth.AuthUser'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.storage.RedirectUserManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.storage.RedirectUserManagerTest'); + + +var config = { + apiKey: 'apiKey1' +}; +var appId = 'appId1'; +var clock; +var expectedUser; +var expectedUserWithAuthDomain; +var stubs = new goog.testing.PropertyReplacer(); + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + clock = new goog.testing.MockClock(true); + window.localStorage.clear(); + window.sessionStorage.clear(); +} + + +function tearDown() { + if (expectedUser) { + expectedUser.destroy(); + } + if (expectedUserWithAuthDomain) { + expectedUserWithAuthDomain.destroy(); + } + goog.dispose(clock); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultStorageManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testGetSetRemoveRedirectUser() { + // Avoid triggering getProjectConfig RPC. + fireauth.AuthEventManager.ENABLED = false; + var storageManager = getDefaultStorageManagerInstance(); + var redirectUserManager = + new fireauth.storage.RedirectUserManager(appId, storageManager); + var config = { + 'apiKey': 'API_KEY', + 'appName': 'appId1' + }; + var configWithAuthDomain = { + 'apiKey': 'API_KEY', + 'appName': 'appId1', + 'authDomain': 'project.firebaseapp.com' + }; + var accountInfo = { + 'uid': 'defaultUserId', + 'email': 'user@default.com', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'emailVerified': true + }; + var tokenResponse = { + 'idToken': 'accessToken', + 'refreshToken': 'refreshToken', + 'expiresIn': 3600 + }; + expectedUser = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Expected user with authDomain. + expectedUserWithAuthDomain = + new fireauth.AuthUser(configWithAuthDomain, tokenResponse, accountInfo); + var storageKey = 'firebase:redirectUser:appId1'; + return goog.Promise.resolve() + .then(function() { + return redirectUserManager.setRedirectUser(expectedUser); + }) + .then(function() { + return redirectUserManager.getRedirectUser(); + }) + .then(function(user) { + assertEquals( + window.sessionStorage.getItem(storageKey), + JSON.stringify(expectedUser.toPlainObject())); + assertObjectEquals(expectedUser, user); + // Get user with authDomain. + return redirectUserManager.getRedirectUser('project.firebaseapp.com'); + }) + .then(function(user) { + assertObjectEquals(expectedUserWithAuthDomain, user); + return redirectUserManager.removeRedirectUser(); + }) + .then(function() { + assertNull(window.sessionStorage.getItem(storageKey)); + return redirectUserManager.getRedirectUser(); + }) + .then(function(user) { + assertNull(user); + }); +} diff --git a/packages/auth/test/storageusermanager_test.js b/packages/auth/test/storageusermanager_test.js new file mode 100644 index 00000000000..9d9998bd692 --- /dev/null +++ b/packages/auth/test/storageusermanager_test.js @@ -0,0 +1,622 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for storageusermanager.js + */ + +goog.provide('fireauth.storage.UserManagerTest'); + +goog.require('fireauth.AuthUser'); +goog.require('fireauth.authStorage'); +goog.require('fireauth.common.testHelper'); +goog.require('fireauth.storage.UserManager'); +goog.require('fireauth.util'); +goog.require('goog.Promise'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.testing.MockClock'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.events'); +goog.require('goog.testing.events.Event'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly('fireauth.storage.UserManagerTest'); + + +var config = { + apiKey: 'apiKey1' +}; +var appId = 'appId1'; +var clock; +var expectedUser; +var expectedUserWithAuthDomain; +var stubs = new goog.testing.PropertyReplacer(); +var testUser; +var testUser2; + + +function setUp() { + // Simulate browser that synchronizes between and iframe and a popup. + stubs.replace( + fireauth.util, + 'isLocalStorageNotSynchronized', + function() { + return false; + }); + clock = new goog.testing.MockClock(true); + window.localStorage.clear(); + window.sessionStorage.clear(); + var config = { + 'apiKey': 'API_KEY', + 'appName': 'appId1' + }; + var accountInfo = { + 'uid': 'defaultUserId', + 'email': 'user@default.com', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'emailVerified': true + }; + var accountInfo2 = { + 'uid': 'defaultUserId2', + 'email': 'user2@default.com', + 'displayName': 'defaultDisplayName2', + 'photoURL': 'https://www.default.com/default/default2.png', + 'emailVerified': false + }; + var tokenResponse = { + 'idToken': 'accessToken', + 'refreshToken': 'refreshToken', + 'expiresIn': 3600 + }; + testUser = new fireauth.AuthUser(config, tokenResponse, accountInfo); + testUser2 = new fireauth.AuthUser(config, tokenResponse, accountInfo2); +} + + +function tearDown() { + if (expectedUser) { + expectedUser.destroy(); + } + if (expectedUserWithAuthDomain) { + expectedUserWithAuthDomain.destroy(); + } + if (testUser) { + testUser.destroy(); + } + if (testUser2) { + testUser2.destroy(); + } + goog.dispose(clock); +} + + +/** + * @return {!fireauth.authStorage.Manager} The default local storage + * synchronized manager instance used for testing. + */ +function getDefaultStorageManagerInstance() { + return new fireauth.authStorage.Manager('firebase', ':', false, true); +} + + +function testGetSetRemoveCurrentUser() { + // Avoid triggering getProjectConfig RPC. + fireauth.AuthEventManager.ENABLED = false; + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager(appId, storageManager); + var config = { + 'apiKey': 'API_KEY', + 'appName': 'appId1' + }; + var configWithAuthDomain = { + 'apiKey': 'API_KEY', + 'appName': 'appId1', + 'authDomain': 'project.firebaseapp.com' + }; + var accountInfo = { + 'uid': 'defaultUserId', + 'email': 'user@default.com', + 'displayName': 'defaultDisplayName', + 'photoURL': 'https://www.default.com/default/default.png', + 'emailVerified': true + }; + var tokenResponse = { + 'idToken': 'accessToken', + 'refreshToken': 'refreshToken', + 'expiresIn': 3600 + }; + expectedUser = new fireauth.AuthUser(config, tokenResponse, accountInfo); + // Expected user with authDomain. + expectedUserWithAuthDomain = + new fireauth.AuthUser(configWithAuthDomain, tokenResponse, accountInfo); + var storageKey = 'firebase:authUser:appId1'; + return goog.Promise.resolve() + .then(function() { + return userManager.setCurrentUser(expectedUser); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertEquals( + window.localStorage.getItem(storageKey), + JSON.stringify(expectedUser.toPlainObject())); + assertObjectEquals(expectedUser, user); + // Get user with authDomain. + return userManager.getCurrentUser('project.firebaseapp.com'); + }) + .then(function(user) { + assertObjectEquals(expectedUserWithAuthDomain, user); + return userManager.removeCurrentUser(); + }) + .then(function() { + assertNull(window.localStorage.getItem(storageKey)); + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertNull(user); + }); +} + + +function testAddRemoveCurrentUserChangeListener() { + var calls = 0; + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + var listener = function() { + calls++; + if (calls > 1) { + fail('Listener should be called once.'); + } + }; + // Save existing Auth users for appId1 and appId2. + window.localStorage.setItem( + 'firebase:authUser:appId1', + JSON.stringify(testUser.toPlainObject())); + window.localStorage.setItem( + 'firebase:authUser:appId2', + JSON.stringify(testUser.toPlainObject())); + userManager.addCurrentUserChangeListener(listener); + // Simulate appId1 user deletion. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = 'firebase:authUser:appId1'; + storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.newValue = null; + window.localStorage.removeItem('firebase:authUser:appId1'); + // This should trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, calls); + // Simulate appId2 user deletion. + storageEvent.key = 'firebase:authUser:appId2'; + storageEvent.oldValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.newValue = null; + window.localStorage.removeItem('firebase:authUser:appId2'); + // This should not trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, calls); + // Remove listener. + userManager.removeCurrentUserChangeListener(listener); + // Simulate new user saved for appId1. + // This should not trigger listener. + storageEvent.key = 'firebase:authUser:appId1'; + storageEvent.newValue = JSON.stringify(testUser.toPlainObject()); + storageEvent.oldValue = null; + window.localStorage.setItem( + 'firebase:authUser:appId1', + JSON.stringify(testUser.toPlainObject())); + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, calls); +} + + +function testUserManager_initializedWithSession() { + // Save state in session storage. + var storageKey = 'firebase:authUser:appId1'; + window.sessionStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser, user); + // User should be saved in session storage only with everything else + // cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser, storageManager); + }).then(function() { + // Should be saved in session storage. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithSession_duplicateStorage() { + // Confirm any duplicate storage is cleared on initialization. + var storageManager = getDefaultStorageManagerInstance(); + var userManager; + // Save state in session storage. + var storageKey = 'firebase:authUser:appId1'; + window.sessionStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + // Add state to other types of storage. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser2.toPlainObject())); + // Set redirect persistence to none. + window.sessionStorage.setItem( + 'firebase:persistence:appId1', JSON.stringify('none')); + // Save state using in memory storage. + return storageManager.set( + {name: 'authUser', persistent: 'none'}, + testUser.toPlainObject(), + 'appId1') + .then(function() { + userManager = new fireauth.storage.UserManager( + 'appId1', storageManager); + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser, user); + // User should be saved in session storage only with everything else + // cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser, storageManager); + }).then(function() { + // Should be saved in session storage. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithInMemory() { + // Save state in in-memory storage. + var storageManager = getDefaultStorageManagerInstance(); + var userManager; + return storageManager.set( + {name: 'authUser', persistent: 'none'}, + testUser.toPlainObject(), + 'appId1') + .then(function() { + userManager = new fireauth.storage.UserManager( + 'appId1', storageManager); + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser, user); + // User should be saved in memory only with everything else + // cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'none', testUser, storageManager); + }).then(function() { + // Should be saved using in memory storage only. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'none', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithLocal() { + // Save state in local storage. + var storageKey = 'firebase:authUser:appId1'; + window.localStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser, user); + // User should be saved in local storage only with everything else + // cleared. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser, storageManager); + }).then(function() { + // Should be saved in local storage only. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithDefault() { + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertNull(user); + // Should be saved in default local storage. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }); +} + + +function testUserManager_initializedWithSavedPersistence() { + // Save redirect persistence. + window.sessionStorage.setItem( + 'firebase:persistence:appId1', JSON.stringify('session')); + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.getCurrentUser() + .then(function(user) { + assertNull(user); + // Should be saved in session storage as specified in redirect + // persistence. + return userManager.setCurrentUser(testUser2); + }) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser2, storageManager); + }); +} + + +function testUserManager_savePersistenceForRedirect_default() { + // Confirm savePersistenceForRedirect behavior. + var storageKey = 'firebase:persistence:appId1'; + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + return userManager.savePersistenceForRedirect() + .then(function() { + // Should store persistence value in session storage. + assertEquals( + window.sessionStorage.getItem(storageKey), + // Should apply the current default persistence. + JSON.stringify('local')); + }); +} + + +function testUserManager_savePersistenceForRedirect_modifed() { + var storageKey = 'firebase:persistence:appId1'; + var storageManager = getDefaultStorageManagerInstance(); + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + // Update persistence. + userManager.setPersistence('session'); + return userManager.savePersistenceForRedirect() + .then(function() { + // Should store persistence value in session storage. + assertEquals( + window.sessionStorage.getItem(storageKey), + // The latest modified persistence value should be used. + JSON.stringify('session')); + }); +} + + +function testUserManager_clearState_setPersistence() { + // Test setPersistence behavior with initially no saved stated. + var storageManager = getDefaultStorageManagerInstance(); + // As no existing state, the default is local. + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + // Switch to session persistence. + userManager.setPersistence('session'); + // Should be saved in session. + return userManager.setCurrentUser(testUser2) + .then(function() { + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser2, storageManager); + }) + .then(function() { + // Move to in memory. + return userManager.setPersistence('none'); + }) + .then(function() { + // User should be switched to in-memory storage. + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'none', testUser2, storageManager); + }) + .then(function() { + // This should match. + return userManager.getCurrentUser(); + }) + .then(function(user) { + assertObjectEquals(testUser2, user); + // Internally switches to local storage. + userManager.setPersistence('local'); + // Internally switches back to session storage. + userManager.setPersistence('session'); + // This error should not affect last state change. + assertThrows(function() { + userManager.setPersistence('bla'); + }); + // Clears user (storage should be empty after). + userManager.removeCurrentUser(); + // This should be saved in sessionStorage. + userManager.setCurrentUser(testUser); + return userManager.getCurrentUser(); + }) + .then(function(user) { + // Should only be saved in session storage. + assertObjectEquals(testUser, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser, storageManager); + }); +} + + +function testUserManager_existingState_setPersistence() { + // Test setPersistence behavior with some initial saved persistence state. + var storageKey = 'firebase:authUser:appId1'; + // Save initial data in local storage. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser2.toPlainObject())); + var storageManager = getDefaultStorageManagerInstance(); + // As no existing state, the default is local. + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + // Switch persistence to session. + userManager.setPersistence('session'); + // Should be switched to session. + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser2, storageManager); + }) + .then(function() { + // Simulate some state duplication due to some unexpected error. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + // Should switch state from session to none and clear everything else. + userManager.setPersistence('none'); + return userManager.getCurrentUser(); + }) + .then(function(user) { + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'none', testUser2, storageManager); + }); +} + + +function testUserManager_switchToLocalOnExternalEvents_noExistingUser() { + // Test when external storage event is detected with no existing user that + // persistence is switched to local. + var storageKey = 'firebase:authUser:appId1'; + var listener = goog.testing.recordFunction(); + // Fake storage event. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = storageKey; + storageEvent.newValue = null; + + var storageManager = getDefaultStorageManagerInstance(); + // As no existing state, the default is local. + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + userManager.addCurrentUserChangeListener(listener); + // Should switch to session. + return userManager.setPersistence('session') + .then(function() { + // Simulate user signed in in another tab. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser2.toPlainObject())); + storageEvent.newValue = JSON.stringify(testUser2.toPlainObject()); + // This should trigger listener and switch storage from session to + // local. + goog.testing.events.fireBrowserEvent(storageEvent); + // Listener should be called. + assertEquals(1, listener.getCallCount()); + return userManager.getCurrentUser(); + }) + .then(function(user) { + // User should be save in local storage. + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }) + .then(function() { + userManager.removeCurrentUserChangeListener(listener); + // This should not trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + }); +} + + +function testUserManager_switchToLocalOnExternalEvents_existingUser() { + // Test when external storage event is detected with an existing user stored + // in a non-local storage that persistence is switched to local. + var storageKey = 'firebase:authUser:appId1'; + var listener = goog.testing.recordFunction(); + // Fake storage event. + var storageEvent = + new goog.testing.events.Event(goog.events.EventType.STORAGE, window); + storageEvent.key = storageKey; + storageEvent.newValue = null; + // Existing user in session storage. + window.sessionStorage.setItem( + storageKey, JSON.stringify(testUser.toPlainObject())); + + var storageManager = getDefaultStorageManagerInstance(); + // Due to existing state in session storage, the initial state is session. + var userManager = new fireauth.storage.UserManager('appId1', storageManager); + userManager.addCurrentUserChangeListener(listener); + // Should switch to session. + return userManager.getCurrentUser() + .then(function(user) { + assertObjectEquals(testUser, user); + // Confirm user stored in session + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'session', testUser, storageManager); + }) + .then(function(user) { + // Simulate user signed in in another tab. + window.localStorage.setItem( + storageKey, JSON.stringify(testUser2.toPlainObject())); + storageEvent.newValue = JSON.stringify(testUser2.toPlainObject()); + // This should trigger listener and switch storage to local. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + return userManager.getCurrentUser(); + }) + .then(function(user) { + // New user should be stored in local storage. + assertObjectEquals(testUser2, user); + return fireauth.common.testHelper.assertUserStorage( + 'appId1', 'local', testUser2, storageManager); + }) + .then(function() { + userManager.removeCurrentUserChangeListener(listener); + // This should not trigger listener. + goog.testing.events.fireBrowserEvent(storageEvent); + assertEquals(1, listener.getCallCount()); + }); +} diff --git a/packages/auth/test/testhelper.js b/packages/auth/test/testhelper.js new file mode 100644 index 00000000000..163f005a916 --- /dev/null +++ b/packages/auth/test/testhelper.js @@ -0,0 +1,156 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Helper functions for testing Firebase Auth common + * functionalities. + */ + +goog.provide('fireauth.common.testHelper'); + +goog.setTestOnly('fireauth.common.testHelper'); + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!fireauth.AuthError} expected + * @param {!fireauth.AuthError} actual + */ +fireauth.common.testHelper.assertErrorEquals = function(expected, actual) { + assertObjectEquals(expected.toPlainObject(), actual.toPlainObject()); +}; + + +/** + * Asserts that a user credential response matches the expected values. + * @param {?fireauth.AuthUser} expectedUser The user to expect. + * @param {?fireauth.AuthCredential} expectedCred The credential to expect. + * @param {?fireauth.AdditionalUserInfo} expectedAdditionalData The additional + * user info to expect. + * @param {?string|undefined} expectedOperationType The operation type to + * expect. + * @param {!fireauth.AuthEventManager.Result} response The actual response. + */ +fireauth.common.testHelper.assertUserCredentialResponse = function(expectedUser, + expectedCred, expectedAdditionalData, expectedOperationType, response) { + if (!expectedCred) { + assertTrue( + response['credential'] === null || + response['credential'] === undefined); + } else { + // Confirm property is read-only. + response['credential'] = 'should not modify property'; + assertObjectEquals(expectedCred, response['credential']); + } + if (!expectedAdditionalData) { + assertTrue( + response['additionalUserInfo'] === null || + response['additionalUserInfo'] === undefined); + } else { + // Confirm property is read-only. + response['additionalUserInfo'] = 'should not modify property'; + assertObjectEquals(expectedAdditionalData, response['additionalUserInfo']); + } + if (!expectedOperationType) { + assertTrue( + response['operationType'] === null || + response['operationType'] === undefined); + } else { + // Confirm property is read-only. + response['operationType'] = 'should not modify property'; + assertEquals(expectedOperationType, response['operationType']); + } + if (!expectedUser) { + assertNull(response['user']); + } else { + // Confirm property is read-only. + response['user'] = 'should not modify property'; + assertEquals(expectedUser, response['user']); + } +}; + + +/** + * Asserts that a popup and redirect response matches the expected values. + * @param {?fireauth.User} expectedUser The user to expect. + * @param {?fireauth.AuthCredential} expectedCred The credential to expect. + * @param {!fireauth.Auth.PopupAndRedirectResult} response The actual response. + */ +fireauth.common.testHelper.assertDeprecatedUserCredentialResponse = function( + expectedUser, expectedCred, response) { + if (!expectedCred) { + assertTrue( + response['credential'] === null || + response['credential'] === undefined); + } else { + // Confirm property is read-only. + response['credential'] = 'should not modify property'; + assertObjectEquals(expectedCred, response['credential']); + } + if (!expectedUser) { + assertNull(response['user']); + } else { + // Confirm property is read-only. + response['user'] = 'should not modify property'; + assertEquals(expectedUser, response['user']); + } + assertUndefined(response['operationType']); + assertUndefined(response['additionalUserInfo']); +}; + + +/** + * Asserts that the specified user is stored in the specified persistence and + * no where else. + * @param {string} appId The app ID used for the storage key. + * @param {?fireauth.authStorage.Persistence} persistence The persistence to + * check for existence. If null is passed, the check will ensure no user is + * saved in storage. + * @param {?fireauth.AuthUser} expectedUser The expected Auth user to test for. + * @param {?fireauth.authStorage.Manager=} opt_manager The underlying storage + * manager to use. If none is provided, the default global instance is used. + * @return {!goog.Promise} A promise that resolves when the check completes. + */ +fireauth.common.testHelper.assertUserStorage = + function(appId, persistence, expectedUser, opt_manager) { + // Get storage manager. + var storage = opt_manager || fireauth.authStorage.Manager.getInstance(); + var promises = []; + // All supported persistence types. + var types = ['local', 'session', 'none']; + // For each persistence type. + for (var i = 0; i < types.length; i++) { + // Get the current user if stored in current persistence. + var p = storage.get({name: 'authUser', persistent: types[i]}, appId); + if (persistence === types[i]) { + // If matching specified persistence, ensure value matches the specified + // user. + promises.push(p.then(function(user) { + assertObjectEquals( + expectedUser && expectedUser.toPlainObject(), + user); + })); + } else { + // All other persistence types, should not have any value stored. + promises.push(p.then(function(user) { + assertUndefined(user); + })); + } + } + // Wait for all checks to complete before resolving. + return goog.Promise.all(promises); +}; diff --git a/packages/auth/test/token_test.js b/packages/auth/test/token_test.js new file mode 100644 index 00000000000..60ad24072fc --- /dev/null +++ b/packages/auth/test/token_test.js @@ -0,0 +1,409 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for token.js + */ + +goog.provide('fireauth.StsTokenManagerTest'); + +goog.require('fireauth.AuthError'); +goog.require('fireauth.RpcHandler'); +goog.require('fireauth.StsTokenManager'); +goog.require('fireauth.authenum.Error'); +goog.require('goog.Promise'); +goog.require('goog.testing.AsyncTestCase'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly('fireauth.StsTokenManagerTest'); + + +var token = null; +var rpcHandler = null; +var stubs = new goog.testing.PropertyReplacer(); +var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(); +var now = 1449534145526; + + +function setUp() { + // Override goog.now(). + stubs.replace( + goog, + 'now', + function() { + return now; + }); + // Initialize RPC handler and token. + rpcHandler = new fireauth.RpcHandler( + 'apiKey', + { + 'tokenEndpoint': 'https://securetoken.googleapis.com/v1/token', + 'tokenTimeout': 10000, + 'tokenHeaders': { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + token = new fireauth.StsTokenManager(rpcHandler); +} + + +function tearDown() { + // Reset property replacer, token and RPC handler. + token = null; + rpcHandler = null; + stubs.reset(); +} + + +/** + * Helper function to check RPC requestStsToken parameters and simulate a + * returned response. + * @param {?Object|string} expectedData The expected body data. + * @param {?Object} xhrResponse The returned response when no error is returned. + * @param {?fireauth.AuthError=} opt_error The specific error returned. + */ +function assertRpcHandler( + expectedData, + xhrResponse, + opt_error) { + stubs.replace( + fireauth.RpcHandler.prototype, + 'requestStsToken', + function(data) { + return new goog.Promise(function(resolve, reject) { + // Confirm expected data sent. + assertObjectEquals(expectedData, data); + if (xhrResponse) { + // Return expected response. + resolve(xhrResponse); + } else if (opt_error) { + reject(opt_error); + } else { + reject( + new fireauth.AuthError(fireauth.authenum.Error.INTERNAL_ERROR)); + } + }); + }); +} + + +/** + * Asserts that two errors are equivalent. Plain assertObjectEquals cannot be + * used as Internet Explorer adds the stack trace as a property of the object. + * @param {!Error} expected + * @param {!Error} actual + */ +function assertErrorEquals(expected, actual) { + assertEquals(expected.code, actual.code); + assertEquals(expected.message, actual.message); +} + + +function testStsTokenManager_gettersSetters() { + var expirationTime = goog.now() + 3600 * 1000; + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', expirationTime); + assertEquals('refreshToken', token.getRefreshToken()); + assertEquals(expirationTime, token.getExpirationTime()); +} + + +function testParseServerResponse() { + var serverResponse = { + 'idToken': 'myStsAccessToken', + 'refreshToken': 'myStsRefreshToken', + 'expiresIn': '3600' + }; + var accessToken = token.parseServerResponse(serverResponse); + assertEquals('myStsAccessToken', accessToken); + assertEquals('myStsRefreshToken', token.getRefreshToken()); + assertEquals(now + 3600 * 1000, token.getExpirationTime()); +} + + +function testStsTokenManager_getToken_noToken() { + // No token. + asyncTestCase.waitForSignals(1); + token.getToken().then(function(token) { + assertNull(token); + asyncTestCase.signal(); + }); +} + + +function testStsTokenManager_getToken_invalidResponse() { + // Test when an network error is returned and then called again successfully + // that the network error is not cached. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.NETWORK_REQUEST_FAILED); + token.setRefreshToken('myRefreshToken'); + // Simulate invalid response from server. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'myRefreshToken' + }, + null, + expectedError); + asyncTestCase.waitForSignals(1); + token.getToken().then(fail, function(error) { + // Invalid response error should be triggered. + assertErrorEquals(expectedError, error); + // Since this is not an expired token error, another call should still + // ping the backend. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'myRefreshToken' + }, + { + 'access_token': 'accessToken2', + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' + }); + token.getToken().then(function(response) { + // Confirm all properties updated. + assertEquals('accessToken2', response['accessToken']); + assertEquals('refreshToken2', response['refreshToken']); + assertEquals( + goog.now() + 3600 * 1000, response['expirationTime']); + asyncTestCase.signal(); + }); + }); +} + + +function testStsTokenManager_getToken_tokenExpiredError() { + // Test when expired refresh token error is returned. + // Simulate Id token is expired to force refresh. + var expirationTime = goog.now() - 3600 * 1000; + // Expected token expired error. + var expectedError = + new fireauth.AuthError(fireauth.authenum.Error.TOKEN_EXPIRED); + token.setAccessToken('accessToken', expirationTime); + token.setRefreshToken('myRefreshToken'); + assertFalse(token.isRefreshTokenExpired()); + // Simulate token expired error from server. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'myRefreshToken' + }, + null, + expectedError); + asyncTestCase.waitForSignals(4); + // This call will return token expired error. + token.getToken().then(fail, function(error) { + assertTrue(token.isRefreshTokenExpired()); + // If another RPC is sent, it will resolve with valid STS token. + // This should not happen since the token expired error is cached. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'refreshToken2' + }, + { + 'access_token': 'accessToken2', + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' + }); + // Token expired error should be thrown. + assertErrorEquals(expectedError, error); + // Try again, cached expired token error should be triggered. + token.getToken(false).thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Try again with forced refresh, cached expired token error should be + // triggered. + token.getToken(true).thenCatch(function(error) { + assertErrorEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Plain object should have refresh token reset. + assertObjectEquals( + { + 'apiKey': 'apiKey', + 'refreshToken': null, + 'accessToken': 'accessToken', + 'expirationTime': expirationTime + }, + token.toPlainObject()); + // If the refresh token is manually updated, the cached error should be + // cleared. + token.setRefreshToken('refreshToken2'); + // This should now resolve. + token.getToken().then(function(response) { + assertFalse(token.isRefreshTokenExpired()); + // Confirm all properties updated. + assertEquals('accessToken2', response['accessToken']); + assertEquals('refreshToken2', response['refreshToken']); + assertEquals( + goog.now() + 3600 * 1000, response['expirationTime']); + // Plain object should have the new refresh token set. + assertObjectEquals( + { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken2', + 'accessToken': 'accessToken2', + 'expirationTime': goog.now() + 3600 * 1000 + }, + token.toPlainObject()); + asyncTestCase.signal(); + }); + asyncTestCase.signal(); + }); +} + + +function testStsTokenManager_getToken_exchangeRefreshToken() { + // Set a previously cached access token that is expired. + var expirationTime = goog.now() - 3600; + token.setRefreshToken('refreshToken'); + // Expired access token. + token.setAccessToken('expiredAccessToken', expirationTime); + // It will attempt to exchange refresh token for STS token. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'refreshToken' + }, + { + 'access_token': 'accessToken2', + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' + }); + asyncTestCase.waitForSignals(1); + token.getToken().then(function(response) { + // Confirm all properties updated. + assertEquals('accessToken2', token.accessToken_); + assertEquals('refreshToken2', token.getRefreshToken()); + assertEquals( + goog.now() + 3600 * 1000, token.getExpirationTime()); + // Confirm correct STS response. + assertObjectEquals( + { + 'accessToken': 'accessToken2', + 'expirationTime': goog.now() + 3600 * 1000, + 'refreshToken': 'refreshToken2' + }, + response); + asyncTestCase.signal(); + }); +} + + +function testStsTokenManager_getToken_cached() { + // Set a previously cached access token that hasn't expired yet. + var expirationTime = goog.now() + 60 * 1000; + // Set refresh token and unexpired access token. + // No XHR request needed. + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', expirationTime); + asyncTestCase.waitForSignals(1); + token.getToken().then(function(response) { + // Confirm all properties updated. + assertEquals('accessToken', token.accessToken_); + assertEquals('refreshToken', token.getRefreshToken()); + assertEquals( + expirationTime, token.getExpirationTime()); + // Confirm correct STS response. + assertObjectEquals( + { + 'accessToken': 'accessToken', + 'expirationTime': expirationTime, + 'refreshToken': 'refreshToken' + }, + response); + asyncTestCase.signal(); + }); +} + + +function testStsTokenManager_getToken_forceRefresh() { + // Set a previously cached access token that hasn't expired yet. + var expirationTime = goog.now() + 1000; + // Set ID token, refresh token and unexpired access token. + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', expirationTime); + // Even though unexpired access token, it will attempt to exchange for refresh + // token since force refresh is set to true. + assertRpcHandler( + { + 'grant_type': 'refresh_token', + 'refresh_token': 'refreshToken' + }, + { + 'access_token': 'accessToken2', + 'refresh_token': 'refreshToken2', + 'expires_in': '3600' + }); + asyncTestCase.waitForSignals(1); + token.getToken(true).then(function(response) { + // Confirm all properties updated. + assertEquals('accessToken2', token.accessToken_); + assertEquals('refreshToken2', token.getRefreshToken()); + assertEquals( + goog.now() + 3600 * 1000, token.getExpirationTime()); + // Confirm correct STS response. + assertObjectEquals( + { + 'accessToken': 'accessToken2', + 'expirationTime': goog.now() + 3600 * 1000, + 'refreshToken': 'refreshToken2' + }, + response); + asyncTestCase.signal(); + }); +} + + +function testToPlainObject() { + var expirationTime = goog.now() + 3600 * 1000; + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', expirationTime); + assertObjectEquals( + { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': expirationTime + }, + token.toPlainObject()); +} + + +function testFromPlainObject() { + var expirationTime = goog.now() + 3600 * 1000; + assertNull( + fireauth.StsTokenManager.fromPlainObject( + new fireauth.RpcHandler('apiKey'), + {})); + + token.setRefreshToken('refreshToken'); + token.setAccessToken('accessToken', expirationTime); + assertObjectEquals( + token, + fireauth.StsTokenManager.fromPlainObject( + rpcHandler, + { + 'apiKey': 'apiKey', + 'refreshToken': 'refreshToken', + 'accessToken': 'accessToken', + 'expirationTime': expirationTime + })); +} diff --git a/packages/auth/test/utils_test.js b/packages/auth/test/utils_test.js new file mode 100644 index 00000000000..0d92f0f6e7d --- /dev/null +++ b/packages/auth/test/utils_test.js @@ -0,0 +1,1612 @@ +/** + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Tests for utils.js + */ + +goog.provide('fireauth.utilTest'); + +goog.require('fireauth.util'); +goog.require('goog.Timer'); +goog.require('goog.testing.MockControl'); +goog.require('goog.testing.PropertyReplacer'); +goog.require('goog.testing.TestCase'); +goog.require('goog.testing.jsunit'); +goog.require('goog.userAgent'); + +goog.setTestOnly('fireauth.utilTest'); + + +var mockControl; +var stubs = new goog.testing.PropertyReplacer(); + +var operaUA = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHT' + + 'ML, like Gecko) Chrome/49.0.2623.110 Safari/537.36 OPR/36.0.2130.74'; +var ieUA = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;' + + ' SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; ' + + 'Media Center PC 6.0; .NET4.0C)'; +var edgeUA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240'; +var firefoxUA = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:46.0) Gecko/201' + + '00101 Firefox/46.0'; +var silkUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, li' + + 'ke Gecko) Silk/44.1.54 like Chrome/44.0.2403.63 Safari/537.36'; +var safariUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11-4) AppleWebKit' + + '/601.5.17 (KHTML, like Gecko) Version/9.1 Safari/601.5.17'; +var chromeUA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, ' + + 'like Gecko) Chrome/50.0.2661.94 Safari/537.36'; +var iOS8iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) A' + + 'ppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12A366 Safar' + + 'i/600.1.4'; +var iOS7iPodUA = 'Mozilla/5.0 (iPod touch; CPU iPhone OS 7_0_3 like Mac OS ' + + 'X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B511 ' + + 'Safari/9537.53'; +var iOS7iPadUA = 'Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/' + + '537.51.1 (KHTML, like Gecko) CriOS/30.0.1599.12 Mobile/11A465 Safari/8' + + '536.25 (3B92C18B-D9DE-4CB7-A02A-22FD2AF17C8F)'; +var iOS7iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0_4 like Mac OS X)' + + 'AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Sa' + + 'fari/9537.53'; +var androidUA = 'Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Buil' + + 'd/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Sa' + + 'fari/534.30'; +var blackberryUA = 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleW' + + 'ebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.' + + '11+'; +var webOSUA = 'Mozilla/5.0 (webOS/1.3; U; en-US) AppleWebKit/525.27.1 (KHTM' + + 'L, like Gecko) Version/1.0 Safari/525.27.1 Desktop/1.0'; +var windowsPhoneUA = 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0' + + ';Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 920)'; +var chriosUA = 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_1_1 like Mac OS X; ' + + 'en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/19.0.1084.60 Mobile' + + '/9B206 Safari/7534.48.3'; +var iOS9iPhoneUA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) A' + + 'ppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13C75 Safar' + + 'i/601.1'; +// This user agent is manually constructed and not copied from a production +// user agent. +var chrome55iOS10UA = 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 10_2_0 like Ma' + + 'c OS X; en) AppleWebKit/534.46.0 (KHTML, like Gecko) CriOS/55.0.2883.7' + + '9 Mobile/9B206 Safari/7534.48.3'; + + +var jsonString = '{"a":2,"b":["Hello","World"],"c":{"someKeyName":true,' + + '"someOtherKeyName":false}}'; +var parsedJSON = { + 'a': 2, + 'b': ['Hello', 'World'], + 'c': { + 'someKeyName': true, + 'someOtherKeyName': false + } +}; + + +function setUp() { + mockControl = new goog.testing.MockControl(); +} + + +function tearDown() { + mockControl.$tearDown(); + angular = undefined; + stubs.reset(); +} + + +if (goog.global['window'] && + typeof goog.global['window'].CustomEvent !== 'function') { + var doc = goog.global.document; + /** + * CustomEvent polyfill for IE 9, 10 and 11. + * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent + * @param {string} event The event type. + * @param {Object=} opt_params The optional event parameters. + * @return {!Event} The generated custom event. + */ + var CustomEvent = function(event, opt_params) { + var params = opt_params || { + bubbles: false, cancelable: false, detail: undefined + }; + var evt = doc.createEvent('CustomEvent'); + evt.initCustomEvent( + event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + CustomEvent.prototype = goog.global['window'].Event.prototype; + goog.global['window'].CustomEvent = CustomEvent; +} + + +/** + * Install the test to run and runs it. + * @param {string} id The test identifier. + * @param {function():!goog.Promise} func The test function to run. + * @return {!goog.Promise} The result of the test. + */ +function installAndRunTest(id, func) { + var testCase = new goog.testing.TestCase(); + testCase.addNewTest(id, func); + return testCase.runTestsReturningPromise().then(function(result) { + assertTrue(result.complete); + // Display error detected. + if (result.errors.length) { + fail(result.errors.join('\n')); + } + assertEquals(1, result.totalCount); + assertEquals(1, result.runCount); + assertEquals(1, result.successCount); + assertEquals(0, result.errors.length); + }); +} + + +function testIsIe11() { + if (goog.userAgent.IE && + !!goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE == 11) { + assertTrue(fireauth.util.isIe11()); + } else { + assertFalse(fireauth.util.isIe11()); + } +} + + +function testIsIe10() { + if (goog.userAgent.IE && + !!goog.userAgent.DOCUMENT_MODE && + goog.userAgent.DOCUMENT_MODE == 10) { + assertTrue(fireauth.util.isIe10()); + } else { + assertFalse(fireauth.util.isIe10()); + } +} + + +function testIsEdge() { + var EDGE_UA = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.10240'; + var CHROME_DESKTOP_UA = + 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/27.0.1453.15 Safari/537.36'; + assertTrue(fireauth.util.isEdge(EDGE_UA)); + assertFalse(fireauth.util.isEdge(CHROME_DESKTOP_UA)); +} + + +function testGetCurrentUrl() { + assertEquals(window.location.href, fireauth.util.getCurrentUrl()); +} + + +function testSanitizeRequestUri() { + // Simulate AngularJS defined. + angular = {}; + assertEquals( + 'http://localhost/path/#abc', + fireauth.util.sanitizeRequestUri( + 'http://localhost/path/#abc')); + assertEquals( + 'http://localhost/path/?query1=value1&query2=value2#abc', + fireauth.util.sanitizeRequestUri( + 'http://localhost/path/?query1=value1&query2=value2#abc')); + // Modified url with #/, should be replace with #. + assertEquals( + 'http://localhost/path#abc', + fireauth.util.sanitizeRequestUri( + 'http://localhost/path#/abc')); + // Modified url with #!/, should be replace with #. + assertEquals( + 'http://localhost/path#abc', + fireauth.util.sanitizeRequestUri( + 'http://localhost/path#!/abc')); +} + + +function testGoTo() { + var fakeWindow = { + location: { + href: '' + } + }; + fireauth.util.goTo('http://www.google.com', fakeWindow); + assertEquals('http://www.google.com', fakeWindow.location.href); + fireauth.util.goTo('http://www.google.com/some?complicated=path', fakeWindow); + assertEquals('http://www.google.com/some?complicated=path', + fakeWindow.location.href); +} + + +function testGetKeyDiff() { + var a = {'key1': 'a', 'key2': 'b'}; + var b = {'key2': 'b', 'key3': 'c'}; + assertArrayEquals( + ['key1', 'key3'], + fireauth.util.getKeyDiff(a, b)); + var c = {'key1': {'c': 3, 'd': 4}, 'key2': [5, 6], 'key3': {'a': 1, 'b': 2}}; + var d = {'key1': {'c': 3, 'd': 4}, 'key2': [5, 6], 'key3': {'a': 1, 'b': 3}}; + assertArrayEquals( + ['key3'], + fireauth.util.getKeyDiff(c, d)); + var e = {'key1': {'c': 3, 'd': 4}, 'key2': [5, 6], 'key3': {'a': 1, 'b': 2}}; + var f = {'key1': {'c': 3, 'd': 4}, 'key2': [5, 7], 'key3': {'a': 1, 'b': 3}}; + assertArrayEquals( + ['key2', 'key3'], + fireauth.util.getKeyDiff(e, f)); + var g = {'key1': null, 'key2': null}; + var h = {'key1': null, 'key2': null}; + assertArrayEquals([], fireauth.util.getKeyDiff(g, h)); + var i = {'key1': null, 'key2': null, 'key3': {}}; + var j = {'key1': null, 'key2': null, 'key3': null}; + assertArrayEquals(['key3'], fireauth.util.getKeyDiff(i, j)); +} + + +function testOnPopupClose() { + return installAndRunTest('onPopupClose', function() { + var win = {}; + + // Simulate close after 50ms. + goog.Timer.promise(10).then(function() { + assertFalse(!!win.closed); + win.closed = true; + }); + assertFalse(!!win.closed); + // Check every 10ms that popup is closed. + return fireauth.util.onPopupClose(win, 2).then(function() { + assertTrue(win.closed); + }); + }); +} + + +function testIsAuthorizedDomain() { + assertFalse( + fireauth.util.isAuthorizedDomain( + [], + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456/popup.html')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['chrome-extension://abcdefghijklmnopqrstuvwxyz123456'], + 'http://aihpiglmnhnhijdnjghpfnlledckkhja/abc?a=1#b=2')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['chrome-extension://abcdefghijklmnopqrstuvwxyz123456'], + 'file://aihpiglmnhnhijdnjghpfnlledckkhja/abc?a=1#b=2')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['chrome-extenion://abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456_suffix', + 'chrome-extension://prefix_abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension://prefix_abcdefghijklmnopqrstuvwxyz123456_suffix', + 'abcdefghijklmnopqrstuvwxyz123456'], + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456/popup.html')); + assertTrue( + fireauth.util.isAuthorizedDomain( + ['chrome-extension://abcdefghijklmnopqrstuvwxyz123456'], + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456/popup.html')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['abcdefghijklmnopqrstuvwxyz123456'], + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456/popup.html')); + assertFalse(fireauth.util.isAuthorizedDomain([], 'http://www.domain.com')); + assertTrue( + fireauth.util.isAuthorizedDomain( + ['other.com', 'domain.com'], 'http://www.domain.com/abc?a=1#b=2')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['other.com', 'example.com'], 'http://www.domain.com/abc?a=1#b=2')); + assertTrue( + fireauth.util.isAuthorizedDomain( + ['domain.com', 'domain.com.lb'], + 'http://www.domain.com.lb:8080/abc?a=1#b=2')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['domain.com', 'domain.com.mx'], + 'http://www.domain.com.lb/abc?a=1#b=2')); + // Check for suffix matching. + assertFalse( + fireauth.util.isAuthorizedDomain( + ['site.example.com'], + 'http://prefix-site.example.com')); + assertTrue( + fireauth.util.isAuthorizedDomain( + ['site.example.com'], 'https://www.site.example.com')); + // Check for IP addresses. + assertTrue( + fireauth.util.isAuthorizedDomain( + ['127.0.0.1'], 'http://127.0.0.1:8080/?redirect=132.0.0.1')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['132.0.0.1'], 'http://127.0.0.1:8080/?redirect=132.0.0.1')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['127.0.0.1'], 'http://127.0.0.1.appdomain.com/?redirect=127.0.0.1')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['127.0.0.1'], 'http://a127.0.0.1/?redirect=127.0.0.1')); + // Other schemes. + assertFalse( + fireauth.util.isAuthorizedDomain( + ['domain.com'], 'file://www.domain.com')); + assertFalse( + fireauth.util.isAuthorizedDomain( + ['domain.com'], 'other://www.domain.com')); +} + + +function testMatchDomain_chromeExtensionPattern() { + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456', + 'abcdefghijklmnopqrstuvwxyz123456', + 'http')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456', + 'abcdefghijklmnopqrstuvwxyz123456', + 'file')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://prefix-abcdefghijklmnopqrstuvwxyz123456', + 'abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456-suffix', + 'abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://prefix-abcdefghijklmnopqrstuvwxyz123456-suffix', + 'abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456', + 'www.abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); + assertFalse(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456', + 'abcdefghijklmnopqrstuvwxyz123456.com', + 'chrome-extension')); + assertTrue(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456', + 'abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); + assertTrue(fireauth.util.matchDomain( + 'chrome-extension://abcdefghijklmnopqrstuvwxyz123456/popup.html', + 'abcdefghijklmnopqrstuvwxyz123456', + 'chrome-extension')); +} + + +function testMatchDomain_unsupportedScheme() { + assertFalse(fireauth.util.matchDomain('127.0.0.1', '127.0.0.1', 'file')); + assertFalse(fireauth.util.matchDomain('domain.com', 'domain.com', 'file')); +} + + +function testMatchDomain_ipAddressPattern() { + assertTrue(fireauth.util.matchDomain('127.0.0.1', '127.0.0.1', 'http')); + assertTrue(fireauth.util.matchDomain('127.0.0.1', '127.0.0.1', 'https')); + assertFalse(fireauth.util.matchDomain('127.0.0.1', 'a127.0.0.1', 'http')); + assertFalse(fireauth.util.matchDomain('127.0.0.1', 'abc.domain.com', 'http')); + assertFalse(fireauth.util.matchDomain( + '127.0.0.1', '127.0.0.1', 'chrome-extension')); +} + + +function testMatchDomain_ipAddressDomain() { + assertFalse(fireauth.util.matchDomain('domain.com', '127.0.0.1', 'http')); + assertFalse(fireauth.util.matchDomain('a127.0.0.1', '127.0.0.1', 'http')); +} + + +function testMatchDomain_caseInsensitiveMatch() { + assertTrue(fireauth.util.matchDomain('localhost', 'localhost', 'http')); + assertTrue(fireauth.util.matchDomain('domain.com', 'DOMAIN.COM', 'http')); + assertTrue(fireauth.util.matchDomain( + 'doMAin.com', 'abC.domain.COM', 'http')); + assertTrue(fireauth.util.matchDomain('localhost', 'localhost', 'https')); + assertTrue(fireauth.util.matchDomain('domain.com', 'DOMAIN.COM', 'https')); + assertTrue(fireauth.util.matchDomain( + 'doMAin.com', 'abC.domain.COM', 'https')); + assertFalse(fireauth.util.matchDomain( + 'doMAin.com', 'abC.domain.COM', 'chrome-extension')); +} + + +function testMatchDomain_domainMismatch() { + assertFalse(fireauth.util.matchDomain('domain.com', 'domain.com.lb', 'http')); + assertFalse(fireauth.util.matchDomain( + 'domain.com', 'abc.domain.com.lb', 'http')); + assertFalse(fireauth.util.matchDomain( + 'domain2.com', 'abc.domain.com', 'http')); +} + + +function testMatchDomain_subdomainComparison() { + assertTrue(fireauth.util.matchDomain('domain.com', 'abc.domain.com', 'http')); + assertTrue(fireauth.util.matchDomain( + 'domain.com', 'abc.domain.com', 'https')); + assertFalse(fireauth.util.matchDomain( + 'other.domain.com', 'abc.domain.com', 'http')); + assertTrue(fireauth.util.matchDomain( + 'domain.com', 'abc.ef.gh.domain.com', 'http')); + assertTrue(fireauth.util.matchDomain( + 'domain.com', 'abc.ef.gh.domain.com', 'https')); + assertFalse(fireauth.util.matchDomain( + 'domain.com', 'abc.ef.gh.domain.com', 'chrome-extension')); +} + + +function testMatchDomain_dotsInPatternEscaped() { + // Dots should be escaped. + assertFalse(fireauth.util.matchDomain( + 'domain.com', 'abc.domainacom', 'http')); + assertFalse(fireauth.util.matchDomain( + 'abc.def.com', 'abczdefzcom', 'http')); +} + + +function testOnDomReady() { + return installAndRunTest('onDomReady', function() { + // Should resolve immediately. + return fireauth.util.onDomReady(); + }); +} + + +function testCreateStorageKey() { + assertEquals( + 'apiKey:appName', + fireauth.util.createStorageKey('apiKey', 'appName')); +} + + +function testGetEnvironment_browser() { + assertEquals(fireauth.util.Env.BROWSER, + fireauth.util.getEnvironment('Gecko')); +} + + +function testGetEnvironment_reactNative() { + stubs.set(firebase.INTERNAL, 'reactNative', {}); + assertEquals(fireauth.util.Env.REACT_NATIVE, + fireauth.util.getEnvironment()); +} + + +function testGetEnvironment_node() { + // Simulate Node.js environment. + stubs.set(firebase.INTERNAL, 'node', {}); + assertEquals(fireauth.util.Env.NODE, fireauth.util.getEnvironment()); +} + + +function testGetBrowserName_opera() { + assertEquals('Opera', fireauth.util.getBrowserName(operaUA)); +} + + +function testGetBrowserName_ie() { + assertEquals('IE', fireauth.util.getBrowserName(ieUA)); +} + + +function testGetBrowserName_edge() { + assertEquals('Edge', fireauth.util.getBrowserName(edgeUA)); +} + + +function testGetBrowserName_firefox() { + assertEquals('Firefox', fireauth.util.getBrowserName(firefoxUA)); +} + + +function testGetBrowserName_silk() { + assertEquals('Silk', fireauth.util.getBrowserName(silkUA)); +} + + +function testGetBrowserName_safari() { + assertEquals('Safari', fireauth.util.getBrowserName(safariUA)); +} + + +function testGetBrowserName_chrome() { + assertEquals('Chrome', fireauth.util.getBrowserName(chromeUA)); +} + + +function testGetBrowserName_android() { + assertEquals('Android', fireauth.util.getBrowserName(androidUA)); +} + + +function testGetBrowserName_blackberry() { + assertEquals('Blackberry', fireauth.util.getBrowserName(blackberryUA)); +} + + +function testGetBrowserName_iemobile() { + assertEquals('IEMobile', fireauth.util.getBrowserName(windowsPhoneUA)); +} + + +function testGetBrowserName_webos() { + assertEquals('Webos', fireauth.util.getBrowserName(webOSUA)); +} + + +function testGetBrowserName_recognizable() { + var ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + + 'Gecko) Awesome/2.0.012'; + assertEquals('Awesome', fireauth.util.getBrowserName(ua)); +} + + +function testGetBrowserName_other() { + var ua = 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKi' + + 't/600.1.4 (KHTML, like Gecko) Mobile/12D508 [FBAN/FBIOS;FBAV/27.0.0.1' + + '0.12;FBBV/8291884;FBDV/iPhone7,1;FBMD/iPhone;FBSN/iPhone OS;FBSV/8.2;' + + 'FBSS/3; FBCR/vodafoneIE;FBID/phone;FBLC/en_US;FBOP/5]'; + assertEquals('Other', fireauth.util.getBrowserName(ua)); +} + + +function testGetClientVersion() { + var ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + + 'Gecko) Chrome/50.0.2661.94 Safari/537.36'; + var firebaseSdkVersion = '3.0.0'; + assertEquals( + 'Chrome/JsCore/3.0.0/FirebaseCore-web', + fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, firebaseSdkVersion, + null, ua)); +} + + +function testGetClientVersion_reactNative() { + stubs.set(firebase.INTERNAL, 'reactNative', {}); + var firebaseSdkVersion = '3.0.0'; + var navigatorProduct = 'ReactNative'; + var clientVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + firebaseSdkVersion, + '', + navigatorProduct); + assertEquals('ReactNative/JsCore/3.0.0/FirebaseCore-web', clientVersion); +} + + +function testGetClientVersion_node() { + var firebaseSdkVersion = '3.0.0'; + // Simulate Node.js environment. + stubs.set(firebase.INTERNAL, 'node', {}); + var clientVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + firebaseSdkVersion); + assertEquals('Node/JsCore/3.0.0/FirebaseCore-web', clientVersion); +} + + +function testGetFrameworkIds() { + assertArrayEquals([], fireauth.util.getFrameworkIds([])); + assertArrayEquals([], fireauth.util.getFrameworkIds(['bla'])); + assertArrayEquals( + ['FirebaseUI-web'], fireauth.util.getFrameworkIds(['FirebaseUI-web'])); + assertArrayEquals( + ['FirebaseCore-web', 'FirebaseUI-web'], + fireauth.util.getFrameworkIds( + ['foo', 'FirebaseCore-web', 'bar', 'FirebaseCore-web', + 'FirebaseUI-web'])); + // Test frameworks IDs are sorted. + assertArrayEquals( + ['FirebaseCore-web', 'FirebaseUI-web'], + fireauth.util.getFrameworkIds(['FirebaseUI-web', 'FirebaseCore-web'])); +} + + +function testGetClientVersion_frameworkVersion_single() { + var ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + + 'Gecko) Chrome/50.0.2661.94 Safari/537.36'; + var firebaseSdkVersion = '3.0.0'; + var clientVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + firebaseSdkVersion, + ['FirebaseUI-web'], + ua); + assertEquals( + 'Chrome/JsCore/3.0.0/FirebaseUI-web', clientVersion); +} + + +function testGetClientVersion_frameworkVersion_multiple() { + var ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like ' + + 'Gecko) Chrome/50.0.2661.94 Safari/537.36'; + var firebaseSdkVersion = '3.0.0'; + var clientVersion = fireauth.util.getClientVersion( + fireauth.util.ClientImplementation.JSCORE, + firebaseSdkVersion, + ['foo', 'FirebaseCore-web', 'bar', 'FirebaseCore-web', 'FirebaseUI-web'], + ua); + assertEquals( + 'Chrome/JsCore/3.0.0/FirebaseCore-web,FirebaseUI-web', clientVersion); +} + + +function testGetObjectRef() { + var scope = { + 'a': false, + 'b': { + 'c': { + 'd': 123 + }, + 'e': null, + 'f': '', + 'g': 'hello', + 'h': true, + 'i': false, + 'j': undefined, + 'k': null + } + }; + assertUndefined(fireauth.util.getObjectRef('', scope)); + assertUndefined(fireauth.util.getObjectRef(' ', scope)); + assertUndefined(fireauth.util.getObjectRef('e', scope)); + assertUndefined(fireauth.util.getObjectRef('.a', scope)); + assertUndefined(fireauth.util.getObjectRef('a.', scope)); + assertEquals(false, fireauth.util.getObjectRef('a', scope)); + assertUndefined(fireauth.util.getObjectRef('a.b', scope)); + assertEquals(123, fireauth.util.getObjectRef('b.c.d', scope)); + assertNull(fireauth.util.getObjectRef('b.e', scope)); + assertEquals('', fireauth.util.getObjectRef('b.f', scope)); + assertEquals('hello', fireauth.util.getObjectRef('b.g', scope)); + assertEquals(true, fireauth.util.getObjectRef('b.h', scope)); + assertEquals(false, fireauth.util.getObjectRef('b.i', scope)); + assertUndefined(fireauth.util.getObjectRef('b.j', scope)); + assertNull(fireauth.util.getObjectRef('b.k', scope)); + assertUndefined(fireauth.util.getObjectRef('b.e.k', scope)); + assertObjectEquals({'d': 123}, fireauth.util.getObjectRef('b.c', scope)); +} + + +function testRunsInBackground_canRunInBackground() { + assertTrue(fireauth.util.runsInBackground(operaUA)); + assertTrue(fireauth.util.runsInBackground(ieUA)); + assertTrue(fireauth.util.runsInBackground(edgeUA)); + assertTrue(fireauth.util.runsInBackground(silkUA)); + assertTrue(fireauth.util.runsInBackground(safariUA)); + assertTrue(fireauth.util.runsInBackground(chromeUA)); +} + + +function testChromeVersion() { + // Should return null for non Chrome browsers. + assertNull(fireauth.util.getChromeVersion(operaUA)); + assertNull(fireauth.util.getChromeVersion(ieUA)); + assertNull(fireauth.util.getChromeVersion(edgeUA)); + assertNull(fireauth.util.getChromeVersion(firefoxUA)); + assertNull(fireauth.util.getChromeVersion(silkUA)); + assertNull(fireauth.util.getChromeVersion(safariUA)); + assertNull(fireauth.util.getChromeVersion(iOS8iPhoneUA)); + // Should return correct version for Chrome. + assertEquals(50, fireauth.util.getChromeVersion(chromeUA)); +} + + +function testRunsInBackground_cannotRunInBackground() { + assertFalse(fireauth.util.runsInBackground(iOS7iPodUA)); + assertFalse(fireauth.util.runsInBackground(iOS7iPhoneUA)); + assertFalse(fireauth.util.runsInBackground(iOS7iPadUA)); + assertFalse(fireauth.util.runsInBackground(iOS8iPhoneUA)); + assertFalse(fireauth.util.runsInBackground(iOS9iPhoneUA)); + assertFalse(fireauth.util.runsInBackground(firefoxUA)); + assertFalse(fireauth.util.runsInBackground(androidUA)); + assertFalse(fireauth.util.runsInBackground(blackberryUA)); + assertFalse(fireauth.util.runsInBackground(webOSUA)); + assertFalse(fireauth.util.runsInBackground(windowsPhoneUA)); +} + + +function testIsMobileBrowser() { + // Mobile OS. + assertTrue(fireauth.util.isMobileBrowser(iOS7iPodUA)); + assertTrue(fireauth.util.isMobileBrowser(iOS7iPhoneUA)); + assertTrue(fireauth.util.isMobileBrowser(iOS7iPadUA)); + assertTrue(fireauth.util.isMobileBrowser(iOS9iPhoneUA)); + assertTrue(fireauth.util.isMobileBrowser(iOS8iPhoneUA)); + assertTrue(fireauth.util.isMobileBrowser(androidUA)); + assertTrue(fireauth.util.isMobileBrowser(blackberryUA)); + assertTrue(fireauth.util.isMobileBrowser(webOSUA)); + assertTrue(fireauth.util.isMobileBrowser(windowsPhoneUA)); + // Desktop OS. + assertFalse(fireauth.util.isMobileBrowser(firefoxUA)); + assertFalse(fireauth.util.isMobileBrowser(operaUA)); + assertFalse(fireauth.util.isMobileBrowser(ieUA)); + assertFalse(fireauth.util.isMobileBrowser(edgeUA)); + assertFalse(fireauth.util.isMobileBrowser(firefoxUA)); + assertFalse(fireauth.util.isMobileBrowser(silkUA)); + assertFalse(fireauth.util.isMobileBrowser(safariUA)); +} + + +function testIframeCanSyncWebStorage() { + // Safari iOS. + assertFalse(fireauth.util.iframeCanSyncWebStorage(iOS7iPodUA)); + assertFalse(fireauth.util.iframeCanSyncWebStorage(iOS7iPhoneUA)); + assertFalse(fireauth.util.iframeCanSyncWebStorage(iOS7iPadUA)); + assertFalse(fireauth.util.iframeCanSyncWebStorage(iOS8iPhoneUA)); + // Desktop Safari. + assertFalse(fireauth.util.iframeCanSyncWebStorage(safariUA)); + // Chrome iOS. + assertFalse(fireauth.util.iframeCanSyncWebStorage(chriosUA)); + // Other Mobile OS. + assertTrue(fireauth.util.iframeCanSyncWebStorage(androidUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(blackberryUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(webOSUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(windowsPhoneUA)); + // Desktop OS. + assertTrue(fireauth.util.iframeCanSyncWebStorage(firefoxUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(operaUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(ieUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(edgeUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(firefoxUA)); + assertTrue(fireauth.util.iframeCanSyncWebStorage(silkUA)); +} + + +function testStringifyJSON() { + assertObjectEquals(jsonString, fireauth.util.stringifyJSON(parsedJSON)); +} + + +function testStringifyJSON_undefined() { + assertNull(fireauth.util.stringifyJSON(undefined)); +} + + +function testParseJSON() { + assertObjectEquals(parsedJSON, fireauth.util.parseJSON(jsonString)); +} + + +function testParseJSON_null() { + assertUndefined(fireauth.util.parseJSON(null)); +} + + +function testParseJSON_noEval() { + stubs.replace(window, 'eval', function() { + throw 'eval() is not allowed in this context.'; + }); + assertObjectEquals(parsedJSON, fireauth.util.parseJSON(jsonString)); +} + + +function testParseJSON_syntaxError() { + assertThrows(function() { fireauth.util.parseJSON('{"a":2'); }); + assertThrows(function() { fireauth.util.parseJSON('b:"hello"}'); }); +} + + +function testGetWindowDimensions() { + var myWin = { + 'innerWidth': 1985.5, + 'innerHeight': 500.5 + }; + assertNull(fireauth.util.getWindowDimensions({})); + assertObjectEquals( + {'width': 1985.5, 'height': 500.5}, + fireauth.util.getWindowDimensions(myWin)); +} + + +function testIsPopupRedirectSupported_webStorageNotSupported() { + assertTrue(fireauth.util.isPopupRedirectSupported()); + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return false; + }); + assertFalse(fireauth.util.isPopupRedirectSupported()); +} + + +function testIsPopupRedirectSupported_isAndroidOrIosFileEnvironment() { + fireauth.util.isCordovaEnabled = false; + assertTrue(fireauth.util.isPopupRedirectSupported()); + // Web storage supported. + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return true; + }); + // File scheme. + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'file:'; + }); + // iOS or Android file environment. + stubs.replace(fireauth.util, 'isAndroidOrIosFileEnvironment', function() { + return true; + }); + assertTrue(fireauth.util.isPopupRedirectSupported()); +} + + +function testIsPopupRedirectSupported_isChromeExtension() { + fireauth.util.isCordovaEnabled = false; + assertTrue(fireauth.util.isPopupRedirectSupported()); + // Web storage supported. + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return true; + }); + // Chrome extension scheme. + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'chrome-extension:'; + }); + // Chrome extension. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return true; + }); + assertTrue(fireauth.util.isPopupRedirectSupported()); +} + + +function testIsPopupRedirectSupported_unsupportedFileEnvironment() { + fireauth.util.isCordovaEnabled = false; + assertTrue(fireauth.util.isPopupRedirectSupported()); + // Web storage supported. + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return true; + }); + // File scheme. + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'file:'; + }); + // Neither iOS, nor Android file environment. + stubs.replace(fireauth.util, 'isAndroidOrIosFileEnvironment', function() { + return false; + }); + assertFalse(fireauth.util.isPopupRedirectSupported()); +} + + +function testIsPopupRedirectSupported_unsupportedNativeEnvironment() { + fireauth.util.isCordovaEnabled = false; + assertTrue(fireauth.util.isPopupRedirectSupported()); + // Web storage supported. + stubs.replace(fireauth.util, 'isWebStorageSupported', function() { + return true; + }); + // https scheme. + stubs.replace(fireauth.util, 'getCurrentScheme', function() { + return 'https:'; + }); + // Neither iOS, nor Android file environment. + stubs.replace(fireauth.util, 'isAndroidOrIosFileEnvironment', function() { + return false; + }); + // Native environment. + stubs.replace(fireauth.util, 'isNativeEnvironment', function() { + return true; + }); + assertFalse(fireauth.util.isPopupRedirectSupported()); +} + + +function testIsChromeExtension() { + // Test https environment. + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() { + return 'https:'; + }); + assertFalse(fireauth.util.isChromeExtension()); + // Test Chrome extension environment. + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() { + return 'chrome-extension:'; + }); + assertTrue(fireauth.util.isChromeExtension()); +} + + +function testIsIOS() { + assertFalse(fireauth.util.isIOS(operaUA)); + assertFalse(fireauth.util.isIOS(ieUA)); + assertFalse(fireauth.util.isIOS(edgeUA)); + assertFalse(fireauth.util.isIOS(firefoxUA)); + assertFalse(fireauth.util.isIOS(silkUA)); + assertFalse(fireauth.util.isIOS(safariUA)); + assertFalse(fireauth.util.isIOS(chromeUA)); + assertFalse(fireauth.util.isIOS(androidUA)); + assertFalse(fireauth.util.isIOS(blackberryUA)); + assertFalse(fireauth.util.isIOS(webOSUA)); + assertFalse(fireauth.util.isIOS(windowsPhoneUA)); + assertTrue(fireauth.util.isIOS(iOS9iPhoneUA)); + assertTrue(fireauth.util.isIOS(iOS8iPhoneUA)); + assertTrue(fireauth.util.isIOS(iOS7iPodUA)); + assertTrue(fireauth.util.isIOS(iOS7iPadUA)); + assertTrue(fireauth.util.isIOS(iOS7iPhoneUA)); + assertTrue(fireauth.util.isIOS(chriosUA)); +} + + +function testIsAndroid() { + assertFalse(fireauth.util.isAndroid(operaUA)); + assertFalse(fireauth.util.isAndroid(ieUA)); + assertFalse(fireauth.util.isAndroid(edgeUA)); + assertFalse(fireauth.util.isAndroid(firefoxUA)); + assertFalse(fireauth.util.isAndroid(silkUA)); + assertFalse(fireauth.util.isAndroid(safariUA)); + assertFalse(fireauth.util.isAndroid(chromeUA)); + assertFalse(fireauth.util.isAndroid(blackberryUA)); + assertFalse(fireauth.util.isAndroid(webOSUA)); + assertFalse(fireauth.util.isAndroid(windowsPhoneUA)); + assertFalse(fireauth.util.isAndroid(iOS8iPhoneUA)); + assertFalse(fireauth.util.isAndroid(iOS7iPodUA)); + assertFalse(fireauth.util.isAndroid(iOS7iPadUA)); + assertFalse(fireauth.util.isAndroid(iOS7iPhoneUA)); + assertFalse(fireauth.util.isAndroid(iOS9iPhoneUA)); + assertFalse(fireauth.util.isAndroid(chriosUA)); + assertTrue(fireauth.util.isAndroid(androidUA)); +} + + +function testIsAndroidOrIosFileEnvironment() { + // Test https environment. + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() { + return 'https:'; + }); + // Non file environment. + assertFalse(fireauth.util.isAndroidOrIosFileEnvironment(iOS8iPhoneUA)); + // Test https environment. + stubs.replace( + fireauth.util, + 'getCurrentScheme', + function() { + return 'file:'; + }); + // iOS file environment. + assertTrue(fireauth.util.isAndroidOrIosFileEnvironment(iOS8iPhoneUA)); + // Android file environment. + assertTrue(fireauth.util.isAndroidOrIosFileEnvironment(androidUA)); + // Desktop file environment. + assertFalse(fireauth.util.isAndroidOrIosFileEnvironment(firefoxUA)); +} + + +function testIsIOS7Or8() { + assertTrue(fireauth.util.isIOS7Or8(iOS7iPodUA)); + assertTrue(fireauth.util.isIOS7Or8(iOS7iPhoneUA)); + assertTrue(fireauth.util.isIOS7Or8(iOS7iPadUA)); + assertTrue(fireauth.util.isIOS7Or8(iOS8iPhoneUA)); + assertFalse(fireauth.util.isIOS7Or8(iOS9iPhoneUA)); + assertFalse(fireauth.util.isIOS7Or8(firefoxUA)); + assertFalse(fireauth.util.isIOS7Or8(androidUA)); + assertFalse(fireauth.util.isIOS7Or8(blackberryUA)); + assertFalse(fireauth.util.isIOS7Or8(webOSUA)); + assertFalse(fireauth.util.isIOS7Or8(windowsPhoneUA)); +} + + +function testRequiresPopupDelay() { + assertFalse(fireauth.util.requiresPopupDelay(iOS7iPodUA)); + assertFalse(fireauth.util.requiresPopupDelay(iOS7iPhoneUA)); + assertFalse(fireauth.util.requiresPopupDelay(iOS7iPadUA)); + assertFalse(fireauth.util.requiresPopupDelay(iOS8iPhoneUA)); + assertFalse(fireauth.util.requiresPopupDelay(iOS9iPhoneUA)); + assertFalse(fireauth.util.requiresPopupDelay(firefoxUA)); + assertFalse(fireauth.util.requiresPopupDelay(androidUA)); + assertFalse(fireauth.util.requiresPopupDelay(blackberryUA)); + assertFalse(fireauth.util.requiresPopupDelay(webOSUA)); + assertFalse(fireauth.util.requiresPopupDelay(windowsPhoneUA)); + assertTrue(fireauth.util.requiresPopupDelay(chrome55iOS10UA)); +} + + +function testCheckIfCordova_incorrectFileEnvironment() { + return installAndRunTest('checkIfCordova_incorrectFileEnv', function() { + stubs.replace( + fireauth.util, + 'isAndroidOrIosFileEnvironment', + function() { + return false; + }); + return fireauth.util.checkIfCordova(null, 10).then(function() { + throw new Error('Unexpected success!'); + }).thenCatch(function(error) { + assertEquals( + 'Cordova must run in an Android or iOS file scheme.', + error.message); + }); + }); +} + + +function testCheckIfCordova_deviceReadyTimeout() { + return installAndRunTest('checkIfCordova_deviceReadyTimeout', function() { + stubs.replace( + fireauth.util, + 'isAndroidOrIosFileEnvironment', + function() { + return true; + }); + return fireauth.util.checkIfCordova(null, 10).then(function() { + throw new Error('Unexpected success!'); + }).thenCatch(function(error) { + assertEquals( + 'Cordova framework is not ready.', + error.message); + }); + }); +} + + +function testCheckIfCordova_success() { + return installAndRunTest('checkIfCordova_success', function() { + stubs.replace( + fireauth.util, + 'isAndroidOrIosFileEnvironment', + function() { + return true; + }); + var doc = goog.global.document; + // Create deviceready custom event. + var deviceReadyEvent = new CustomEvent('deviceready'); + var checkIfCordova = fireauth.util.checkIfCordova(null, 10); + // Trigger deviceready event on DOM ready. + fireauth.util.onDomReady().then(function() { + doc.dispatchEvent(deviceReadyEvent); + }); + return checkIfCordova; + }); +} + + +function testRemoveEntriesWithKeys() { + var obj = { + 'a': false, + 'b': undefined, + 'c': 'abc', + 'd': 1, + 'e': 0, + 'f': '', + 'g': 0.5, + 'h': null + }; + // Remove nothing from an empty object. + assertObjectEquals( + {}, + fireauth.util.removeEntriesWithKeys({}, [])); + + // Remove keys from an empty object. + assertObjectEquals( + {}, + fireauth.util.removeEntriesWithKeys({}, ['a', 'b'])); + + // Remove everything. + var filteredObj1 = {}; + var filter1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; + assertObjectEquals( + filteredObj1, + fireauth.util.removeEntriesWithKeys(obj, filter1)); + var filteredObj2 = obj; + + // Remove keys that do not exist. + var filter2 = ['i', 'j']; + assertObjectEquals( + filteredObj2, + fireauth.util.removeEntriesWithKeys(obj, filter2)); + + // Remove nothing. + assertObjectEquals( + filteredObj2, + fireauth.util.removeEntriesWithKeys(obj, [])); + + // Remove keys with values that are not true; if(obj[key]) resolves to false. + var filteredObj3 = { + 'c': 'abc', + 'd': 1, + 'g': 0.5 + }; + var filter3 = ['a', 'b', 'e', 'f', 'h', 'i', 'j']; + assertObjectEquals( + filteredObj3, + fireauth.util.removeEntriesWithKeys(obj, filter3)); + + // Keep keys with values that are not true. + var filteredObj4 = { + 'a': false, + 'b': undefined, + 'e': 0, + 'h': null + }; + var filter4 = ['c', 'd', 'f', 'g', 'i', 'j']; + assertObjectEquals( + filteredObj4, + fireauth.util.removeEntriesWithKeys(obj, filter4)); +} + + +function testCopyWithoutNullsOrUndefined() { + var obj = { + 'a': false, + 'b': undefined, + 'c': 'abc', + 'd': 1, + 'e': 0, + 'f': '', + 'g': 0.5, + 'h': null + }; + var filteredObj = { + 'a': false, + 'c': 'abc', + 'd': 1, + 'e': 0, + 'f': '', + 'g': 0.5, + }; + // All nulls and undefined removed. + assertObjectEquals( + filteredObj, + fireauth.util.copyWithoutNullsOrUndefined(obj)); + var obj2 = { + 'a': 1, + 'b': 2 + }; + // No nulls or undefined. + assertObjectEquals( + obj2, + fireauth.util.copyWithoutNullsOrUndefined(obj2)); + // Empty object. + assertObjectEquals( + {}, + fireauth.util.copyWithoutNullsOrUndefined({})); + // Object with undefined and nulls only. + assertObjectEquals( + {}, + fireauth.util.copyWithoutNullsOrUndefined({'b': undefined, 'c': null})); +} + + +function testIsMobileDevice() { + // Mobile devices. + assertTrue( + fireauth.util.isMobileDevice(chriosUA, fireauth.util.Env.BROWSER)); + assertTrue( + fireauth.util.isMobileDevice(null, fireauth.util.Env.REACT_NATIVE)); + // Desktop devices. + assertFalse( + fireauth.util.isMobileDevice(chromeUA, fireauth.util.Env.BROWSER)); + assertFalse( + fireauth.util.isMobileDevice(null, fireauth.util.Env.NODE)); +} + + +function testIsMobileDevice_desktopBrowser_default() { + // Simulate desktop browser. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return false; + }); + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.BROWSER; + }); + assertFalse(fireauth.util.isMobileDevice()); +} + + +function testIsMobileDevice_mobileBrowser_default() { + // Simulate mobile browser. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return true; + }); + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.BROWSER; + }); + assertTrue(fireauth.util.isMobileDevice()); +} + + +function testIsMobileDevice_desktopEnv_default() { + // Simulate desktop Node.js environment. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return false; + }); + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.NODE; + }); + assertFalse(fireauth.util.isMobileDevice()); +} + + +function testIsMobileDevice_mobileEnv_default() { + // Simulate mobile React Native environment. + stubs.replace(fireauth.util, 'isMobileBrowser', function(ua) { + return false; + }); + stubs.replace(fireauth.util, 'getEnvironment', function() { + return fireauth.util.Env.REACT_NATIVE; + }); + assertTrue(fireauth.util.isMobileDevice()); +} + + +function testIsOnline_httpOrHttps_online() { + // HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return true; + }); + // Non-Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return false; + }); + // navigator.onLine resolves to true. + // fireauth.util.isOnline() should return true. + assertTrue(fireauth.util.isOnline({onLine: true})); +} + + +function testIsOnline_httpOrHttps_offline() { + // HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return true; + }); + // Non-Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return false; + }); + // navigator.onLine resolves to false. + // fireauth.util.isOnline() should return false. + assertFalse(fireauth.util.isOnline({onLine: false})); +} + + +function testIsOnline_chromeExtension_online() { + // Non-HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return false; + }); + // Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return true; + }); + // navigator.onLine resolves to true. + // fireauth.util.isOnline() should return true. + assertTrue(fireauth.util.isOnline({onLine: true})); +} + + +function testIsOnline_chromeExtension_offline() { + // Non-HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return false; + }); + // Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return true; + }); + // navigator.onLine resolves to false. + // fireauth.util.isOnline() should return false. + assertFalse(fireauth.util.isOnline({onLine: false})); +} + + +function testIsOnline_other_navigatorConnection_online() { + // Non-HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return false; + }); + // Non-Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return false; + }); + // cordova-plugin-network-information installed. + // navigator.onLine resolves to true. + // fireauth.util.isOnline() should return true. + assertTrue(fireauth.util.isOnline({onLine: true, connection: {}})); +} + + +function testIsOnline_other_navigatorConnection_offline() { + // Non-HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return false; + }); + // Non-Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return false; + }); + // cordova-plugin-network-information installed. + // navigator.onLine resolves to false. + // fireauth.util.isOnline() should return false. + assertFalse(fireauth.util.isOnline({onLine: false, connection: {}})); +} + + +function testIsOnline_notSupported() { + // Non-HTTP/HTTPS environment. + stubs.replace(fireauth.util, 'isHttpOrHttps', function(ua) { + return false; + }); + // Non-Chrome extension environment. + stubs.replace(fireauth.util, 'isChromeExtension', function() { + return false; + }); + // Evaluates to true even though navigator.onLine is false. + assertTrue(fireauth.util.isOnline({onLine: false})); +} + + +function testDelay_invalid() { + assertThrows(function() { + new fireauth.util.Delay(50, 10); + }); +} + + +function testDelay_mobileDevice_default() { + // Simulate mobile browser. + stubs.replace(fireauth.util, 'isMobileDevice', function(ua) { + return true; + }); + var delay = new fireauth.util.Delay(10, 50); + assertEquals(50, delay.get()); +} + + +function testDelay_desktopDevice_default() { + // Simulate desktop Node.js environment. + stubs.replace(fireauth.util, 'isMobileDevice', function(ua) { + return false; + }); + var delay = new fireauth.util.Delay(10, 50); + assertEquals(10, delay.get()); +} + + +function testDelay_desktopBrowser() { + var delay = + new fireauth.util.Delay(10, 50, chromeUA, fireauth.util.Env.BROWSER); + assertEquals(10, delay.get()); +} + + +function testDelay_mobileBrowser() { + var delay = + new fireauth.util.Delay(10, 50, chriosUA, fireauth.util.Env.BROWSER); + assertEquals(50, delay.get()); +} + + +function testDelay_node() { + var delay = new fireauth.util.Delay(10, 50, null, fireauth.util.Env.NODE); + assertEquals(10, delay.get()); +} + + +function testDelay_reactNative() { + stubs.set(firebase.INTERNAL, 'reactNative', {}); + var delay = + new fireauth.util.Delay(10, 50, null, fireauth.util.Env.REACT_NATIVE); + assertEquals(50, delay.get()); +} + + +function testGetUserLanguage() { + // Simulate modern browser. + assertEquals('de', fireauth.util.getUserLanguage({ + 'languages': ['de', 'en'], + 'language': 'en' + })); + + // Simulate non-Chrome/Firefox modern browser. + assertEquals('en', fireauth.util.getUserLanguage({ + 'language': 'en' + })); + + // Simulate older IE. + assertEquals('fr', fireauth.util.getUserLanguage({ + 'userLanguage': 'fr' + })); + + // Simulate other environment. + assertNull(fireauth.util.getUserLanguage({})); +} + + +function testIsIframe_iframe() { + // Mock window. + var win = { + name: 'windowA' + }; + // Set top window to another window. + win.top = { + name: 'windowB' + }; + // WindowB is the top window. + win.top.top = win.top; + assertTrue(fireauth.util.isIframe(win)); +} + + +function testIsIframe_notIframe() { + // Mock window. + var win = { + name: 'windowA' + }; + // Set top window to current window. + win.top = win; + assertFalse(fireauth.util.isIframe(win)); +} + + +function testIsOpenerAnIframe_openerIframe() { + // Simulate opener is an iframe. + // Mock opener window. + var win = { + name: 'windowA' + }; + // Set top window to another window. + win.top = { + name: 'windowB' + }; + // WindowB is the top window. + win.top.top = win.top; + // Current mock window with opener set to above window. + var currentWindow = { + name: 'windowC', + opener: win + }; + assertTrue(fireauth.util.isOpenerAnIframe(currentWindow)); +} + + +function testIsOpenerAnIframe_openerNotIframe() { + // Simulate opener is a top window. + // Mock window. + var win = { + name: 'windowA' + }; + // Set top window to current window. + win.top = win; + // Current mock window with opener set to above window. + var currentWindow = { + name: 'windowC', + opener: win + }; + assertFalse(fireauth.util.isOpenerAnIframe(currentWindow)); +} + + +function testIsOpenerAnIframe_noOpener() { + // Current window has no opener. + var currentWindow = { + name: 'windowC', + opener: null + }; + assertFalse(fireauth.util.isOpenerAnIframe(currentWindow)); +} + + +function testOnAppVisible_initiallyVisible() { + return installAndRunTest('onAppVisible_initiallyVisible', function() { + // Simulate app visible initially. + stubs.replace( + fireauth.util, + 'isAppVisible', + function() { + return true; + }); + // Should resolve quickly. + return fireauth.util.onAppVisible(); + }); +} + + +function testOnAppVisible_initiallyHidden() { + return installAndRunTest('onAppVisible_initiallyHidden', function() { + // Initially, simulate app not visible. + stubs.replace( + fireauth.util, + 'isAppVisible', + function() { + return false; + }); + // Reset event triggered flag. + var eventTriggered = false; + var doc = goog.global.document; + // Create custom visibility change event. + var visibilitychangeEvent = new CustomEvent('visibilitychange'); + // Run onAppVisible and save returned promise. + var p = fireauth.util.onAppVisible(); + // Simulate visibility change event after a short period of time. + setTimeout(function() { + // Simulate app visible after short period of time. + stubs.replace( + fireauth.util, + 'isAppVisible', + function() { + return true; + }); + // Trigger visibility change. + doc.dispatchEvent(visibilitychangeEvent); + // Track event being triggered. + eventTriggered = true; + }, 10); + return p.then(function() { + // Visibility change should have been triggered. + assertTrue(eventTriggered); + }); + }); +} + + +function testConsoleWarn() { + if (typeof console === 'undefined') { + // Ignore browsers that don't support console. The test + // testConsoleWarn_doesntBreakIE tests that this function doesn't break + // those browsers. + return; + } + var consoleWarn = mockControl.createMethodMock(goog.global.console, 'warn'); + var message = 'This is my message.'; + consoleWarn(message).$once(); + + mockControl.$replayAll(); + + fireauth.util.consoleWarn(message); +} + + +function testConsoleWarn_doesntBreakIE() { + fireauth.util.consoleWarn('This should not trigger an error in IE.'); +} + + +function testUtcTimestampToDateString() { + var actual; + // Null. + assertNull(fireauth.util.utcTimestampToDateString(null)); + + // Invalid. + assertNull(fireauth.util.utcTimestampToDateString('bla')); + + // Null should be returned when a non numeric timestamp is provided. + assertNull( + fireauth.util.utcTimestampToDateString('22 Sep 2017 01:49:58 GMT')); + + // UTC timestamp string. + actual = fireauth.util.utcTimestampToDateString('1506044998000'); + assertTrue( + // All non IE10 browsers. + actual == 'Fri, 22 Sep 2017 01:49:58 GMT' || + // IE 10 browser. + actual == 'Fri, 22 Sep 2017 01:49:58 UTC'); + + // UTC timestamp number. + actual = fireauth.util.utcTimestampToDateString(1506046529000); + assertTrue( + // All non IE10 browsers. + actual == 'Fri, 22 Sep 2017 02:15:29 GMT' || + // IE 10 browser. + actual == 'Fri, 22 Sep 2017 02:15:29 UTC'); + assertEquals( + new Date(1506046529000).toUTCString(), + fireauth.util.utcTimestampToDateString(1506046529000)); +} diff --git a/yarn.lock b/yarn.lock index 5f7bae29576..f58fbe8d2f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -98,10 +98,22 @@ version "2.2.44" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.44.tgz#1d4a798e53f35212fd5ad4d04050620171cd5b5e" +"@types/node@^6.0.46": + version "6.0.90" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.90.tgz#0ed74833fa1b73dcdb9409dcb1c97ec0a8b13b02" + "@types/node@^8.0.47": version "8.0.47" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.47.tgz#968e596f91acd59069054558a00708c445ca30c2" +"@types/q@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" + +"@types/selenium-webdriver@^2.53.35", "@types/selenium-webdriver@~2.53.39": + version "2.53.42" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-2.53.42.tgz#74cb77fb6052edaff2a8984ddafd88d419f25cac" + "@types/sinon@^2.3.7": version "2.3.7" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.7.tgz#e92c2fed3297eae078d78d1da032b26788b4af86" @@ -146,14 +158,18 @@ acorn@4.X, acorn@^4.0.3, acorn@^4.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" acorn@^5.0.0, acorn@^5.0.3: - version "5.1.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" + version "5.2.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" -adm-zip@0.4.7, adm-zip@~0.4.3: +adm-zip@0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.4.tgz#a61ed5ae6905c3aea58b3a657d25033091052736" + +adm-zip@0.4.7, adm-zip@^0.4.7, adm-zip@~0.4.3: version "0.4.7" resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.7.tgz#8606c2cbf1c426ce8c8ec00174447fd49b6eafc1" @@ -169,8 +185,8 @@ agent-base@2: semver "~5.0.1" ajv-keywords@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" ajv@5.2.2: version "5.2.2" @@ -673,6 +689,12 @@ block-stream@*: dependencies: inherits "~2.0.0" +blocking-proxy@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-0.0.5.tgz#462905e0dcfbea970f41aa37223dda9c07b1912b" + dependencies: + minimist "^1.2.0" + bluebird@3.4.6: version "3.4.6" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.6.tgz#01da8d821d87813d158967e743d5fe6c62cf8c0f" @@ -762,7 +784,7 @@ boxen@^0.6.0: string-width "^1.0.1" widest-line "^1.0.0" -brace-expansion@^1.1.7: +brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" dependencies: @@ -1256,6 +1278,10 @@ clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + clone@^1.0.0, clone@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149" @@ -1272,7 +1298,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^1.0.6" through2 "^2.0.1" -closure-builder@^2.2.23: +closure-builder@^2.0.17, closure-builder@^2.2.23: version "2.2.27" resolved "https://registry.yarnpkg.com/closure-builder/-/closure-builder-2.2.27.tgz#2514342bfc8aabad91e3953aa5e3817c1910f773" dependencies: @@ -2049,7 +2075,7 @@ default-resolution@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" -defaults@^1.0.3: +defaults@^1.0.0, defaults@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" dependencies: @@ -2066,6 +2092,18 @@ defined@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" +del@^2.2.0, del@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + del@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" @@ -2089,6 +2127,10 @@ depd@1.1.1, depd@~1.1.0, depd@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" +deprecated@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" + deps-sort@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5" @@ -2293,6 +2335,12 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +end-of-stream@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" + dependencies: + once "~1.3.0" + engine.io-client@1.8.3: version "1.8.3" resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-1.8.3.tgz#1798ed93451246453d4c6f635d7a201fe940d5ab" @@ -2576,6 +2624,10 @@ exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + expand-braces@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" @@ -2648,7 +2700,7 @@ express@4.15.4: utils-merge "1.0.0" vary "~1.1.1" -express@^4.13.3, express@^4.15.4: +express@^4.13.3, express@^4.15.4, express@^4.16.2: version "4.16.2" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" dependencies: @@ -2838,6 +2890,10 @@ finalhandler@1.1.0: statuses "~1.3.1" unpipe "~1.0.0" +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -2877,8 +2933,8 @@ fined@^1.0.1: parse-filepath "^1.0.1" firebase-tools@^3.10.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-3.13.1.tgz#2ca82a5cdd5e887c5fe961cd8019e4a987625f5a" + version "3.14.0" + resolved "https://registry.yarnpkg.com/firebase-tools/-/firebase-tools-3.14.0.tgz#708d0e5fedab329d9cb1485714d1bbdeabf1a2b5" dependencies: JSONStream "^1.2.1" archiver "^0.16.0" @@ -3101,6 +3157,12 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gaze@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" + dependencies: + globule "~0.1.0" + gcp-metadata@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.3.1.tgz#313814456e7c3d0eeb8f8b084b33579e886f829a" @@ -3252,6 +3314,17 @@ glob-slasher@^1.0.1: lodash.isobject "^2.4.1" toxic "^1.0.0" +glob-stream@^3.1.5: + version "3.1.18" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" + dependencies: + glob "^4.3.1" + glob2base "^0.0.12" + minimatch "^2.0.1" + ordered-read-streams "^0.1.0" + through2 "^0.6.1" + unique-stream "^1.0.0" + glob-stream@^5.3.2: version "5.3.5" resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22" @@ -3265,6 +3338,12 @@ glob-stream@^5.3.2: to-absolute-glob "^0.1.1" unique-stream "^2.0.2" +glob-watcher@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" + dependencies: + gaze "^0.5.1" + glob-watcher@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-3.2.0.tgz#ffc1a2d3d07783b672f5e21799a4d0b3fed92daf" @@ -3274,7 +3353,13 @@ glob-watcher@^3.0.0: lodash.debounce "^4.0.6" object.defaults "^1.0.0" -glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2: +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + dependencies: + find-index "^0.1.1" + +glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3285,7 +3370,16 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glo once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + +glob@^5.0.14, glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" dependencies: @@ -3295,6 +3389,14 @@ glob@^5.0.15, glob@^5.0.3, glob@~5.0.0: once "^1.3.0" path-is-absolute "^1.0.0" +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + global-modules@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-0.2.3.tgz#ea5a3bed42c6d6ce995a4f8a1269b5dae223828d" @@ -3311,6 +3413,17 @@ global-prefix@^0.1.4: is-windows "^0.2.0" which "^1.2.12" +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -3321,6 +3434,14 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globule@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" + dependencies: + glob "~3.1.21" + lodash "~1.0.1" + minimatch "~0.2.11" + glogg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.0.tgz#7fe0f199f57ac906cf512feead8f90ee4a284fc5" @@ -3345,6 +3466,27 @@ google-auto-auth@^0.7.1, google-auto-auth@^0.7.2: google-auth-library "^0.10.0" request "^2.79.0" +google-closure-compiler@^20151015.0.0: + version "20151015.7.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20151015.7.0.tgz#a494909eb33ec5b6aed1ffb712f0557ff596ba6f" + dependencies: + chalk "^1.0.0" + gulp-util "^3.0.7" + through2 "^2.0.0" + vinyl-sourcemaps-apply "^0.2.0" + +google-closure-compiler@^20170910.0.0: + version "20170910.0.0" + resolved "https://registry.yarnpkg.com/google-closure-compiler/-/google-closure-compiler-20170910.0.0.tgz#7a7cf5111d7376b376ce7461137e1b039303f1ea" + dependencies: + chalk "^1.0.0" + vinyl "^2.0.1" + vinyl-sourcemaps-apply "^0.2.0" + +google-closure-library@^20170910.0.0: + version "20170910.0.0" + resolved "https://registry.yarnpkg.com/google-closure-library/-/google-closure-library-20170910.0.0.tgz#d6122ec24472672ee1347b19e8c4a43d18f57bef" + google-p12-pem@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-0.1.2.tgz#33c46ab021aa734fa0332b3960a9a3ffcb2f3177" @@ -3442,6 +3584,16 @@ graceful-fs@4.1.11, graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.0, gra version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graceful-fs@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + dependencies: + natives "^1.1.0" + +graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" @@ -3502,6 +3654,19 @@ gulp-cli@^1.0.0: wreck "^6.3.0" yargs "^3.28.0" +gulp-closure-compiler@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/gulp-closure-compiler/-/gulp-closure-compiler-0.4.0.tgz#c4726edb1b44cb758e00d5b1522e1bdcd4e1a49a" + dependencies: + glob "^5.0.14" + google-closure-compiler "^20151015.0.0" + graceful-fs "^4.1.2" + gulp-util "^3.0.0" + mkdirp "^0.5.0" + temp-write "^1.0.0" + through "^2.3.4" + uuid "^2.0.1" + gulp-concat@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/gulp-concat/-/gulp-concat-2.6.1.tgz#633d16c95d88504628ad02665663cee5a4793353" @@ -3585,7 +3750,7 @@ gulp-util@3.0.7: through2 "^2.0.0" vinyl "^0.5.0" -gulp-util@^3.0.6, gulp-util@^3.0.7, gulp-util@~3.0.7: +gulp-util@^3.0.0, gulp-util@^3.0.6, gulp-util@^3.0.7, gulp-util@~3.0.7: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -3608,6 +3773,24 @@ gulp-util@^3.0.6, gulp-util@^3.0.7, gulp-util@~3.0.7: through2 "^2.0.0" vinyl "^0.5.0" +gulp@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" + dependencies: + archy "^1.0.0" + chalk "^1.0.0" + deprecated "^0.0.1" + gulp-util "^3.0.0" + interpret "^1.0.0" + liftoff "^2.1.0" + minimist "^1.1.0" + orchestrator "^0.3.0" + pretty-hrtime "^1.0.0" + semver "^4.1.0" + tildify "^1.0.0" + v8flags "^2.0.2" + vinyl-fs "^0.3.0" + gulp@gulpjs/gulp#4.0: version "4.0.0-alpha.2" resolved "https://codeload.github.com/gulpjs/gulp/tar.gz/6d71a658c61edb3090221579d8f97dbe086ba2ed" @@ -3938,6 +4121,10 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" @@ -4349,6 +4536,22 @@ isurl@^1.0.0-alpha5: has-to-string-tag-x "^1.2.0" is-object "^1.0.1" +jasmine-core@~2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" + +jasmine@^2.5.3: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.8.0" + +jasminewd2@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" + jju@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jju/-/jju-1.3.0.tgz#dadd9ef01924bc728b03f2f7979bdbd62f7a2aaa" @@ -4382,8 +4585,8 @@ jsbn@~0.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" jschardet@^1.4.2: - version "1.5.1" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + version "1.6.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.6.0.tgz#c7d1a71edcff2839db2f9ec30fc5d5ebd3c1a678" json-loader@^0.5.4: version "0.5.7" @@ -4791,7 +4994,7 @@ lie@~3.1.0: dependencies: immediate "~3.0.5" -liftoff@^2.3.0: +liftoff@^2.1.0, liftoff@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.3.0.tgz#a98f2ff67183d8ba7cfaca10548bd7ff0550b385" dependencies: @@ -5067,6 +5270,10 @@ lodash@^3.10.0, lodash@^3.10.1, lodash@^3.8.0, lodash@~3.10.0, lodash@~3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" +lodash@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" + log-driver@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056" @@ -5123,6 +5330,10 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" @@ -5340,6 +5551,19 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" +minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + dependencies: + brace-expansion "^1.0.0" + +minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" + dependencies: + lru-cache "2" + sigmund "~1.0.0" + minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" @@ -5512,6 +5736,10 @@ nash@^2.0.4: lodash "^3.10.0" minimist "^1.1.0" +natives@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.0.tgz#e9ff841418a6b2ec7a495e939984f78f163e6e31" + ncp@1.0.x: version "1.0.1" resolved "https://registry.yarnpkg.com/ncp/-/ncp-1.0.1.tgz#d15367e5cb87432ba117d2bf80fdf45aecfb4246" @@ -5774,6 +6002,12 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.3.3, once@^1.4.0: dependencies: wrappy "1" +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + dependencies: + wrappy "1" + onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" @@ -5794,7 +6028,7 @@ opn@^5.1.0: dependencies: is-wsl "^1.1.0" -optimist@^0.6.1: +optimist@^0.6.1, optimist@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" dependencies: @@ -5838,6 +6072,18 @@ ora@^1.3.0: cli-spinners "^1.0.0" log-symbols "^1.0.2" +orchestrator@^0.3.0: + version "0.3.8" + resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" + dependencies: + end-of-stream "~0.1.5" + sequencify "~0.0.7" + stream-consume "~0.1.0" + +ordered-read-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" + ordered-read-streams@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b" @@ -6237,6 +6483,26 @@ protochain@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/protochain/-/protochain-1.0.5.tgz#991c407e99de264aadf8f81504b5e7faf7bfa260" +protractor@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/protractor/-/protractor-5.2.0.tgz#d3f39b195e85f3539ad9d8cb6560a9d2b63297c4" + dependencies: + "@types/node" "^6.0.46" + "@types/q" "^0.0.32" + "@types/selenium-webdriver" "~2.53.39" + blocking-proxy "0.0.5" + chalk "^1.1.3" + glob "^7.0.3" + jasmine "^2.5.3" + jasminewd2 "^2.1.0" + optimist "~0.6.0" + q "1.4.1" + saucelabs "~1.3.0" + selenium-webdriver "3.6.0" + source-map-support "~0.4.0" + webdriver-js-extender "^1.0.0" + webdriver-manager "^12.0.6" + proxy-addr@~1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918" @@ -6636,7 +6902,7 @@ request@2.81.0: tunnel-agent "^0.6.0" uuid "^3.0.0" -request@2.x, request@^2.58.0, request@^2.72.0, request@^2.74.0, request@^2.79.0, request@^2.81.0, request@^2.83.0: +request@2.x, request@^2.58.0, request@^2.72.0, request@^2.74.0, request@^2.78.0, request@^2.79.0, request@^2.81.0, request@^2.83.0: version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" dependencies: @@ -6737,7 +7003,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -6811,8 +7077,8 @@ samsam@1.x: resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.3.0.tgz#8d1d9350e25622da30de3e44ba692b5221ab7c50" sauce-connect-launcher@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.2.2.tgz#7346cc8fbdc443191323439b0733451f5f3521f2" + version "1.2.3" + resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.2.3.tgz#d2f931ad7ae8fdabf1968a440e7b20417aca7f86" dependencies: adm-zip "~0.4.3" async "^2.1.2" @@ -6826,6 +7092,16 @@ saucelabs@^1.4.0: dependencies: https-proxy-agent "^1.0.0" +saucelabs@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.3.0.tgz#d240e8009df7fa87306ec4578a69ba3b5c424fee" + dependencies: + https-proxy-agent "^1.0.0" + +sax@0.6.x: + version "0.6.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-0.6.1.tgz#563b19c7c1de892e09bfc4f2fc30e3c27f0952b9" + sax@>=0.6.0: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -6866,6 +7142,16 @@ selenium-webdriver@3.6.0: tmp "0.0.30" xml2js "^0.4.17" +selenium-webdriver@^2.53.2: + version "2.53.3" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz#d29ff5a957dff1a1b49dc457756e4e4bfbdce085" + dependencies: + adm-zip "0.4.4" + rimraf "^2.2.8" + tmp "0.0.24" + ws "^1.0.1" + xml2js "0.4.4" + selfsigned@^1.9.1: version "1.10.1" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52" @@ -6888,7 +7174,7 @@ semver-greatest-satisfied-range@^1.0.0: version "5.4.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" -semver@~4.3.3: +semver@^4.1.0, semver@~4.3.3: version "4.3.6" resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" @@ -6932,6 +7218,10 @@ send@0.16.1: range-parser "~1.2.0" statuses "~1.3.1" +sequencify@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" + serializerr@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/serializerr/-/serializerr-1.0.3.tgz#12d4c5aa1c3ffb8f6d1dc5f395aa9455569c3f91" @@ -7029,6 +7319,10 @@ shelljs@0.7.7: interpret "^1.0.0" rechoir "^0.6.2" +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7156,7 +7450,7 @@ source-map-resolve@^0.3.0: source-map-url "~0.3.0" urix "~0.1.0" -source-map-support@^0.4.0: +source-map-support@^0.4.0, source-map-support@~0.4.0: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" dependencies: @@ -7316,6 +7610,10 @@ stream-combiner@~0.0.4: dependencies: duplexer "~0.1.1" +stream-consume@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" + stream-events@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.2.tgz#abf39f66c0890a4eb795bc8d5e859b2615b590b2" @@ -7436,6 +7734,13 @@ strip-bom-string@1.X: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" +strip-bom@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" + dependencies: + first-chunk-stream "^1.0.0" + is-utf8 "^0.2.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -7627,6 +7932,15 @@ temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" +temp-write@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-1.1.2.tgz#75b57a3cd9f802beaae3762b11e66ab1f4afd947" + dependencies: + graceful-fs "^4.1.2" + mkdirp "^0.5.0" + os-tmpdir "^1.0.0" + uuid "^2.0.1" + temp-write@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.3.0.tgz#c1a96de2b36061342eae81f44ff001aec8f615a9" @@ -7690,7 +8004,7 @@ through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@^2.0.3 readable-stream "^2.1.5" xtend "~4.0.1" -through2@^0.6.0: +through2@^0.6.0, through2@^0.6.1: version "0.6.5" resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" dependencies: @@ -7750,6 +8064,10 @@ timers-ext@^0.1.2: es5-ext "~0.10.14" next-tick "1" +tmp@0.0.24: + version "0.0.24" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.24.tgz#d6a5e198d14a9835cc6f2d7c3d9e302428c8cf12" + tmp@0.0.27: version "0.0.27" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.27.tgz#6aaf42a2d7664150ab528287068ecbc27139a013" @@ -7831,8 +8149,8 @@ try-require@^1.0.0: resolved "https://registry.yarnpkg.com/try-require/-/try-require-1.2.1.tgz#34489a2cac0c09c1cc10ed91ba011594d4333be2" ts-loader@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.1.0.tgz#d59bed512ee1dfc2636184f437714baad08c7ca1" + version "3.1.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-3.1.1.tgz#602d93c12029eaf8fa1ee478a90785d40c5f6658" dependencies: chalk "^2.3.0" enhanced-resolve "^3.0.0" @@ -7977,6 +8295,10 @@ undertaker@^1.0.0: object.reduce "^1.0.0" undertaker-registry "^1.0.0" +unique-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" + unique-stream@^2.0.2: version "2.2.1" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369" @@ -8129,7 +8451,7 @@ uuid@^2.0.1, uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" -v8flags@^2.0.9: +v8flags@^2.0.2, v8flags@^2.0.9: version "2.1.1" resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" dependencies: @@ -8176,6 +8498,19 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vinyl-fs@^0.3.0: + version "0.3.14" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-0.3.14.tgz#9a6851ce1cac1c1cea5fe86c0931d620c2cfa9e6" + dependencies: + defaults "^1.0.0" + glob-stream "^3.1.5" + glob-watcher "^0.0.6" + graceful-fs "^3.0.0" + mkdirp "^0.5.0" + strip-bom "^1.0.0" + through2 "^0.6.1" + vinyl "^0.4.0" + vinyl-fs@^2.0.0, vinyl-fs@~2.4.3: version "2.4.4" resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-2.4.4.tgz#be6ff3270cb55dfd7d3063640de81f25d7532239" @@ -8198,6 +8533,12 @@ vinyl-fs@^2.0.0, vinyl-fs@~2.4.3: vali-date "^1.0.0" vinyl "^1.0.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@1.X, vinyl@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" @@ -8206,6 +8547,13 @@ vinyl@1.X, vinyl@^1.0.0: clone-stats "^0.0.1" replace-ext "0.0.1" +vinyl@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + vinyl@^0.5.0: version "0.5.3" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" @@ -8214,7 +8562,7 @@ vinyl@^0.5.0: clone-stats "^0.0.1" replace-ext "0.0.1" -vinyl@^2.0.0, vinyl@^2.1.0: +vinyl@^2.0.0, vinyl@^2.0.1, vinyl@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" dependencies: @@ -8283,6 +8631,29 @@ wd@^1.4.0: underscore.string "3.3.4" vargs "0.1.0" +webdriver-js-extender@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-1.0.0.tgz#81c533a9e33d5bfb597b4e63e2cdb25b54777515" + dependencies: + "@types/selenium-webdriver" "^2.53.35" + selenium-webdriver "^2.53.2" + +webdriver-manager@^12.0.6: + version "12.0.6" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.0.6.tgz#3df1a481977010b4cbf8c9d85c7a577828c0e70b" + dependencies: + adm-zip "^0.4.7" + chalk "^1.1.1" + del "^2.2.0" + glob "^7.0.3" + ini "^1.3.4" + minimist "^1.2.0" + q "^1.4.1" + request "^2.78.0" + rimraf "^2.5.2" + semver "^5.3.0" + xml2js "^0.4.17" + webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0: version "1.12.0" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709" @@ -8527,6 +8898,13 @@ ws@1.1.2: options ">=0.0.5" ultron "1.0.x" +ws@^1.0.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.4.tgz#57f40d036832e5f5055662a397c4de76ed66bf61" + dependencies: + options ">=0.0.5" + ultron "1.0.x" + wtf-8@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wtf-8/-/wtf-8-1.0.0.tgz#392d8ba2d0f1c34d1ee2d630f15d0efb68e1048a" @@ -8541,6 +8919,13 @@ xdg-basedir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" +xml2js@0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.4.tgz#3111010003008ae19240eba17497b57c729c555d" + dependencies: + sax "0.6.x" + xmlbuilder ">=1.0.0" + xml2js@^0.4.17: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" @@ -8548,7 +8933,7 @@ xml2js@^0.4.17: sax ">=0.6.0" xmlbuilder "~9.0.1" -xmlbuilder@~9.0.1: +xmlbuilder@>=1.0.0, xmlbuilder@~9.0.1: version "9.0.4" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" @@ -8675,8 +9060,8 @@ yauzl@2.4.1: fd-slicer "~1.0.1" yauzl@^2.4.2, yauzl@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.8.0.tgz#79450aff22b2a9c5a41ef54e02db907ccfbf9ee2" + version "2.9.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f" dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1"