├── .github
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── test.yaml
├── .gitignore
├── .npmignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── scripts
├── publish-container
│ ├── Dockerfile
│ └── cloudbuild.yaml
├── publish.sh
├── publish
│ ├── cloudbuild.yaml
│ ├── deploy_key.enc
│ ├── hub.enc
│ ├── npmrc.enc
│ └── twitter.json.enc
└── tweet.js
├── spec
├── app.spec.ts
├── cloudevent
│ └── generate.ts
├── index.spec.ts
├── integration
│ └── providers
│ │ └── firestore.spec.ts
├── lifecycle.spec.ts
├── main.spec.ts
├── providers
│ ├── database.spec.ts
│ ├── firestore.spec.ts
│ ├── https.spec.ts
│ └── scheduled.spec.ts
├── secretmanager.spec.ts
└── v2.spec.ts
├── src
├── app.ts
├── cloudevent
│ ├── generate.ts
│ ├── mocks
│ │ ├── alerts
│ │ │ ├── alerts-on-alert-published.ts
│ │ │ ├── app-distribution-on-new-tester-ios-device-published.ts
│ │ │ ├── billing-on-plan-automated-update-published.ts
│ │ │ ├── billing-on-plan-update-published.ts
│ │ │ ├── crashlytics-on-new-anr-issue-published.ts
│ │ │ ├── crashlytics-on-new-fatal-issue-published.ts
│ │ │ ├── crashlytics-on-new-nonfatal-issue-published.ts
│ │ │ ├── crashlytics-on-regression-alert-published.ts
│ │ │ ├── crashlytics-on-stability-digest-published.ts
│ │ │ ├── crashlytics-on-velocity-alert-published.ts
│ │ │ └── performance-on-threshold-alert-published.ts
│ │ ├── database
│ │ │ ├── database-on-value-created.ts
│ │ │ ├── database-on-value-deleted.ts
│ │ │ ├── database-on-value-updated.ts
│ │ │ ├── database-on-value-written.ts
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ ├── eventarc
│ │ │ └── eventarc-on-custom-event-published.ts
│ │ ├── firestore
│ │ │ ├── firestore-on-document-created-with-auth-context.ts
│ │ │ ├── firestore-on-document-created.ts
│ │ │ ├── firestore-on-document-deleted-with-auth-context.ts
│ │ │ ├── firestore-on-document-deleted.ts
│ │ │ ├── firestore-on-document-updated-with-auth-context.ts
│ │ │ ├── firestore-on-document-updated.ts
│ │ │ ├── firestore-on-document-written-with-auth-context.ts
│ │ │ ├── firestore-on-document-written.ts
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ ├── helpers.ts
│ │ ├── partials.ts
│ │ ├── pubsub
│ │ │ └── pubsub-on-message-published.ts
│ │ ├── remoteconfig
│ │ │ └── remote-config-on-config-updated.ts
│ │ ├── storage
│ │ │ ├── index.ts
│ │ │ └── storage-data.ts
│ │ └── testlab
│ │ │ └── test-lab-on-test-matrix-completed.ts
│ └── types.ts
├── features.ts
├── index.ts
├── lifecycle.ts
├── main.ts
├── providers
│ ├── analytics.ts
│ ├── auth.ts
│ ├── database.ts
│ ├── firestore.ts
│ ├── pubsub.ts
│ └── storage.ts
├── secretManager.ts
├── v1.ts
└── v2.ts
├── tsconfig.json
├── tsconfig.release.json
└── tslint.json
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more
22 | information on using pull requests.
23 |
24 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
31 |
32 | ### Version info
33 |
34 |
36 |
37 | **firebase-functions-test:**
38 |
39 | **firebase-functions:**
40 |
41 | **firebase-admin:**
42 |
43 | ### Test case
44 |
45 |
46 |
47 |
48 | ### Steps to reproduce
49 |
50 |
51 |
52 |
53 | ### Expected behavior
54 |
55 |
56 |
57 |
58 | ### Actual behavior
59 |
60 |
61 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
22 |
23 | ### Description
24 |
25 |
27 |
28 | ### Code sample
29 |
30 |
31 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: CI Tests
2 |
3 | on:
4 | - pull_request
5 | - push
6 |
7 | env:
8 | CI: true
9 |
10 | jobs:
11 | unit:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node-version:
16 | - 20.x
17 | - 22.x
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: ${{ matrix.node-version }}
23 |
24 | - name: Cache npm
25 | uses: actions/cache@v4
26 | with:
27 | path: ~/.npm
28 | key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
29 |
30 | - run: npm ci
31 | - run: npm run lint
32 | - run: npm run format
33 | - run: npm run test
34 | integration:
35 | runs-on: ubuntu-latest
36 | strategy:
37 | matrix:
38 | node-version:
39 | - 20.x
40 | - 22.x
41 | steps:
42 | - uses: actions/checkout@v3
43 | - uses: actions/setup-node@v3
44 | with:
45 | node-version: ${{ matrix.node-version }}
46 |
47 | - name: Cache npm
48 | uses: actions/cache@v4
49 | with:
50 | path: ~/.npm
51 | key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
52 |
53 | - run: npm install
54 | - run: npm run integrationTest
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | lib
4 | .npmrc
5 | .vscode
6 | *.tgz
7 | .tmp
8 | *.log
9 | .idea
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .tmp
2 | coverage
3 | .vscode
4 | .idea
5 | tsconfig.*
6 | tslint.*
7 | .travis.yml
8 | .github
9 |
10 | # Don't include the raw typescript
11 | src
12 | spec
13 | *.tgz
14 | scripts
15 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "singleQuote": true,
4 | "trailingComma": "es5",
5 | }
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-test/fb6cd5ce738231306f9184430f051286f6869dba/CHANGELOG.md
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Firebase
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Firebase Test SDK for Cloud Functions
2 |
3 | The `firebase-functions-test` is unit testing library for Cloud Functions for Firebase. It is a companion to [firebase-functions](https://github.com/Firebase/firebase-functions).
4 |
5 | _NOTE: This library can only be used with `firebase-functions` v3.20.1 or above._
6 |
7 | ## Usage
8 |
9 | 1. Write some Firebase Functions
10 | 2. With your testing framework of choice, write a unit-test that imports your Firebase Functions.
11 | 3. `wrap` your Firebase Functions. You can invoke the Firebase Function's handler by invoking the `wrap` call.
12 |
13 | Eg.
14 |
15 | ```typescript
16 | import {myFirebaseFunction} from "../index"; // Your Firebase Functions
17 | import firebaseFunctionsTest from "firebase-functions-test";
18 |
19 | // Extracting `wrap` out of the lazy-loaded features
20 | const {wrap} = firebaseFunctionsTest();
21 |
22 | // `jest-ts` example
23 | test('my unit test', () => {
24 | const wrappedFirebaseFunction = wrap(myFirebaseFunction);
25 |
26 | // Invoke the firebase function
27 | wrappedFirebaseFunction();
28 |
29 | // Invoke the firebase function with CloudEvent overrides
30 | wrappedFirebaseFunction({data: {arbitrary: 'values'}});
31 | });
32 | ```
33 |
34 | ## Examples
35 |
36 | * [Unit Testing Gen-1 Cloud Functions using Mocha](https://github.com/firebase/functions-samples/tree/main/Node-1st-gen/quickstarts/uppercase-rtdb/functions)
37 | * [Unit Testing Gen-2 Cloud Functions using Mocha](https://github.com/firebase/functions-samples/tree/main/Node/test-functions-mocha/functions)
38 | * [Unit Testing Gen-2 Cloud Functions using Jest](https://github.com/firebase/functions-samples/tree/main/Node/test-functions-jest/functions)
39 | * [Unit Testing Gen-2 Cloud Functions using Jest-Ts](https://github.com/firebase/functions-samples/tree/main/Node/test-functions-jest-ts/functions)
40 |
41 | ## Learn more
42 |
43 | Learn more about unit testing Cloud Functions [here](https://firebase.google.com/docs/functions/unit-testing).
44 |
45 | ## Contributing
46 |
47 | To contribute a change, [check out the contributing guide](.github/CONTRIBUTING.md).
48 |
49 | ## License
50 |
51 | © Google, 2018. Licensed under [The MIT License](LICENSE).
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "firebase-functions-test",
3 | "version": "3.4.1",
4 | "description": "A testing companion to firebase-functions.",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "build": "npm i && node_modules/.bin/tsc -p tsconfig.release.json",
8 | "build:pack": "npm prune --production && rm -rf lib && npm install && node_modules/.bin/tsc -p tsconfig.release.json && npm pack && npm install",
9 | "build:release": "npm ci --production && npm install --no-save typescript firebase-functions firebase-admin && node_modules/.bin/tsc -p tsconfig.release.json",
10 | "lint": "node_modules/.bin/tslint src/{**/*,*}.ts spec/{**/*,*}.ts",
11 | "pretest": "node_modules/.bin/tsc",
12 | "test": "mocha .tmp/spec/index.spec.js",
13 | "posttest": "npm run lint && rm -rf .tmp",
14 | "preintegrationTest": "node_modules/.bin/tsc",
15 | "integrationTest": "firebase emulators:exec --project=not-a-project --only firestore 'mocha .tmp/spec/integration/**/*.spec.js'",
16 | "postintegrationTest": "rm -rf .tmp",
17 | "format": "prettier --check '**/*.{json,ts,yml,yaml}'",
18 | "format:fix": "prettier --write '**/*.{json,ts,yml,yaml}'"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/firebase/firebase-functions-test.git"
23 | },
24 | "keywords": [
25 | "firebase",
26 | "functions",
27 | "google",
28 | "cloud",
29 | "test"
30 | ],
31 | "author": "Firebase Team",
32 | "license": "MIT",
33 | "bugs": {
34 | "url": "https://github.com/firebase/firebase-functions-test/issues"
35 | },
36 | "publishConfig": {
37 | "registry": "https://wombat-dressing-room.appspot.com"
38 | },
39 | "homepage": "https://github.com/firebase/firebase-functions-test#readme",
40 | "dependencies": {
41 | "@types/lodash": "^4.14.104",
42 | "lodash": "^4.17.5",
43 | "ts-deepmerge": "^2.0.1"
44 | },
45 | "devDependencies": {
46 | "@types/chai": "~4.2.4",
47 | "@types/express": "4.17.8",
48 | "@types/mocha": "^5.2.7",
49 | "chai": "^4.2.0",
50 | "firebase-admin": "^12.0.0",
51 | "firebase-functions": "^4.9.0",
52 | "firebase-tools": "^8.9.2",
53 | "mocha": "^6.2.2",
54 | "prettier": "^1.19.1",
55 | "sinon": "^7.5.0",
56 | "tslint": "^5.20.0",
57 | "typescript": "^4.2.5"
58 | },
59 | "peerDependencies": {
60 | "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0",
61 | "firebase-functions": ">=4.9.0",
62 | "jest": ">=28.0.0"
63 | },
64 | "engines": {
65 | "node": ">=14.0.0"
66 | },
67 | "typings": "lib/index.d.ts"
68 | }
69 |
--------------------------------------------------------------------------------
/scripts/publish-container/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 |
3 | # Install dependencies
4 | RUN apt-get update && \
5 | apt-get install -y curl git jq
6 |
7 | # Install npm at latest.
8 | RUN npm install --global npm@latest
9 |
10 | # Install hub
11 | RUN curl -fsSL --output hub.tgz https://github.com/github/hub/releases/download/v2.13.0/hub-linux-amd64-2.13.0.tgz
12 | RUN tar --strip-components=2 -C /usr/bin -xf hub.tgz hub-linux-amd64-2.13.0/bin/hub
13 |
14 |
--------------------------------------------------------------------------------
/scripts/publish-container/cloudbuild.yaml:
--------------------------------------------------------------------------------
1 | steps:
2 | - name: 'gcr.io/cloud-builders/docker'
3 | args: ['build', '-t', 'gcr.io/$PROJECT_ID/fft-package-builder', '.']
4 | images: ['gcr.io/$PROJECT_ID/fft-package-builder']
5 |
--------------------------------------------------------------------------------
/scripts/publish.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | printusage() {
5 | echo "publish.sh "
6 | echo "REPOSITORY_ORG and REPOSITORY_NAME should be set in the environment."
7 | echo "e.g. REPOSITORY_ORG=user, REPOSITORY_NAME=repo"
8 | echo ""
9 | echo "Arguments:"
10 | echo " version: 'patch', 'minor', or 'major'."
11 | }
12 |
13 | VERSION=$1
14 | if [[ $VERSION == "" ]]; then
15 | printusage
16 | exit 1
17 | elif [[ ! ($VERSION == "patch" || $VERSION == "minor" || $VERSION == "major") ]]; then
18 | printusage
19 | exit 1
20 | fi
21 |
22 | if [[ $REPOSITORY_ORG == "" ]]; then
23 | printusage
24 | exit 1
25 | fi
26 | if [[ $REPOSITORY_NAME == "" ]]; then
27 | printusage
28 | exit 1
29 | fi
30 |
31 | WDIR=$(pwd)
32 |
33 | echo "Checking for commands..."
34 | trap "echo 'Missing hub.'; exit 1" ERR
35 | which hub &> /dev/null
36 | trap - ERR
37 |
38 | trap "echo 'Missing node.'; exit 1" ERR
39 | which node &> /dev/null
40 | trap - ERR
41 |
42 | trap "echo 'Missing jq.'; exit 1" ERR
43 | which jq &> /dev/null
44 | trap - ERR
45 | echo "Checked for commands."
46 |
47 | echo "Checking for Twitter credentials..."
48 | trap "echo 'Missing Twitter credentials.'; exit 1" ERR
49 | test -f "${WDIR}/scripts/twitter.json"
50 | trap - ERR
51 | echo "Checked for Twitter credentials..."
52 |
53 | echo "Checking for logged-in npm user..."
54 | trap "echo 'Please login to npm using \`npm login --registry https://wombat-dressing-room.appspot.com\`'; exit 1" ERR
55 | npm whoami --registry https://wombat-dressing-room.appspot.com
56 | trap - ERR
57 | echo "Checked for logged-in npm user."
58 |
59 | echo "Moving to temporary directory.."
60 | TEMPDIR=$(mktemp -d)
61 | echo "[DEBUG] ${TEMPDIR}"
62 | cd "${TEMPDIR}"
63 | echo "Moved to temporary directory."
64 |
65 | echo "Cloning repository..."
66 | git clone "git@github.com:${REPOSITORY_ORG}/${REPOSITORY_NAME}.git"
67 | cd "${REPOSITORY_NAME}"
68 | echo "Cloned repository."
69 |
70 | echo "Making sure there is a changelog..."
71 | if [ ! -s CHANGELOG.md ]; then
72 | echo "CHANGELOG.md is empty. aborting."
73 | exit 1
74 | fi
75 | echo "Made sure there is a changelog."
76 |
77 | echo "Running npm ci..."
78 | npm ci
79 | echo "Ran npm ci."
80 |
81 | echo "Running tests..."
82 | npm test
83 | echo "Ran tests."
84 |
85 | echo "Running publish build..."
86 | npm run build:release
87 | echo "Ran publish build."
88 |
89 | echo "Making a $VERSION version..."
90 | npm version $VERSION
91 | NEW_VERSION=$(jq -r ".version" package.json)
92 | echo "Made a $VERSION version."
93 |
94 | echo "Making the release notes..."
95 | RELEASE_NOTES_FILE=$(mktemp)
96 | echo "[DEBUG] ${RELEASE_NOTES_FILE}"
97 | echo "v${NEW_VERSION}" >> "${RELEASE_NOTES_FILE}"
98 | echo "" >> "${RELEASE_NOTES_FILE}"
99 | cat CHANGELOG.md >> "${RELEASE_NOTES_FILE}"
100 | echo "Made the release notes."
101 |
102 | echo "Publishing to npm..."
103 | if [[ $DRY_RUN == "" ]]; then
104 | npm publish
105 | else
106 | echo "DRY RUN: running publish with --dry-run"
107 | npm publish --dry-run
108 | fi
109 | echo "Published to npm."
110 |
111 | if [[ $DRY_RUN != "" ]]; then
112 | echo "All other commands are mutations, and we are doing a dry run."
113 | echo "Terminating."
114 | exit
115 | fi
116 |
117 | echo "Cleaning up release notes..."
118 | rm CHANGELOG.md
119 | touch CHANGELOG.md
120 | git commit -m "[firebase-release] Removed change log and reset repo after ${NEW_VERSION} release" CHANGELOG.md
121 | echo "Cleaned up release notes."
122 |
123 | echo "Pushing to GitHub..."
124 | git push origin master --tags
125 | echo "Pushed to GitHub."
126 |
127 | echo "Publishing release notes..."
128 | hub release create --file "${RELEASE_NOTES_FILE}" "v${NEW_VERSION}"
129 | echo "Published release notes."
130 |
131 | echo "Making the tweet..."
132 | npm install --no-save twitter@1.7.1
133 | cp -v "${WDIR}/scripts/twitter.json" "${TEMPDIR}/${REPOSITORY_NAME}/scripts/"
134 | node ./scripts/tweet.js ${NEW_VERSION}
135 | echo "Made the tweet."
136 |
137 |
--------------------------------------------------------------------------------
/scripts/publish/cloudbuild.yaml:
--------------------------------------------------------------------------------
1 | steps:
2 | # Decrypt the SSH key.
3 | - name: 'gcr.io/cloud-builders/gcloud'
4 | args:
5 | [
6 | 'kms',
7 | 'decrypt',
8 | '--ciphertext-file=deploy_key.enc',
9 | '--plaintext-file=/root/.ssh/id_rsa',
10 | '--location=global',
11 | '--keyring=${_KEY_RING}',
12 | '--key=${_KEY_NAME}',
13 | ]
14 |
15 | # Decrypt the Twitter credentials.
16 | - name: 'gcr.io/cloud-builders/gcloud'
17 | args:
18 | [
19 | 'kms',
20 | 'decrypt',
21 | '--ciphertext-file=twitter.json.enc',
22 | '--plaintext-file=twitter.json',
23 | '--location=global',
24 | '--keyring=${_KEY_RING}',
25 | '--key=${_KEY_NAME}',
26 | ]
27 |
28 | # Decrypt the npm credentials.
29 | - name: 'gcr.io/cloud-builders/gcloud'
30 | args:
31 | [
32 | 'kms',
33 | 'decrypt',
34 | '--ciphertext-file=npmrc.enc',
35 | '--plaintext-file=npmrc',
36 | '--location=global',
37 | '--keyring=${_KEY_RING}',
38 | '--key=${_KEY_NAME}',
39 | ]
40 |
41 | # Decrypt the hub (GitHub) credentials.
42 | - name: 'gcr.io/cloud-builders/gcloud'
43 | args:
44 | [
45 | 'kms',
46 | 'decrypt',
47 | '--ciphertext-file=hub.enc',
48 | '--plaintext-file=hub',
49 | '--location=global',
50 | '--keyring=${_KEY_RING}',
51 | '--key=${_KEY_NAME}',
52 | ]
53 |
54 | # Set up git with key and domain.
55 | - name: 'gcr.io/cloud-builders/git'
56 | entrypoint: 'bash'
57 | args:
58 | - '-c'
59 | - |
60 | chmod 600 /root/.ssh/id_rsa
61 | cat </root/.ssh/config
62 | Hostname github.com
63 | IdentityFile /root/.ssh/id_rsa
64 | EOF
65 | ssh-keyscan github.com >> /root/.ssh/known_hosts
66 | # Clone the repository.
67 | - name: 'gcr.io/cloud-builders/git'
68 | args: ['clone', 'git@github.com:${_REPOSITORY_ORG}/${_REPOSITORY_NAME}']
69 |
70 | # Set up the Git configuration.
71 | - name: 'gcr.io/cloud-builders/git'
72 | dir: '${_REPOSITORY_NAME}'
73 | args: ['config', '--global', 'user.email', 'firebase-oss-bot@google.com']
74 | - name: 'gcr.io/cloud-builders/git'
75 | dir: '${_REPOSITORY_NAME}'
76 | args: ['config', '--global', 'user.name', 'Google Open Source Bot']
77 |
78 | # Set up the Twitter credentials.
79 | - name: 'gcr.io/$PROJECT_ID/fft-package-builder'
80 | entrypoint: 'cp'
81 | args: ['-v', 'twitter.json', '${_REPOSITORY_NAME}/scripts/twitter.json']
82 |
83 | # Set up the npm credentials.
84 | - name: 'gcr.io/$PROJECT_ID/fft-package-builder'
85 | entrypoint: 'bash'
86 | args: ['-c', 'cp -v npmrc ~/.npmrc']
87 |
88 | # Set up the hub credentials for fft-package-builder.
89 | - name: 'gcr.io/$PROJECT_ID/fft-package-builder'
90 | entrypoint: 'bash'
91 | args: ['-c', 'mkdir -vp ~/.config && cp -v hub ~/.config/hub']
92 |
93 | # Publish the package.
94 | - name: 'gcr.io/$PROJECT_ID/fft-package-builder'
95 | dir: '${_REPOSITORY_NAME}'
96 | args: ['bash', './scripts/publish.sh', '${_VERSION}']
97 | env:
98 | - 'REPOSITORY_ORG=${_REPOSITORY_ORG}'
99 | - 'REPOSITORY_NAME=${_REPOSITORY_NAME}'
100 | - 'DRY_RUN=${_DRY_RUN}'
101 |
102 | options:
103 | volumes:
104 | - name: 'ssh'
105 | path: /root/.ssh
106 |
107 | substitutions:
108 | _VERSION: ''
109 | _DRY_RUN: ''
110 | _KEY_RING: 'npm-publish-keyring'
111 | _KEY_NAME: 'publish'
112 | _REPOSITORY_ORG: 'firebase'
113 | _REPOSITORY_NAME: 'firebase-functions-test'
114 |
--------------------------------------------------------------------------------
/scripts/publish/deploy_key.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-test/fb6cd5ce738231306f9184430f051286f6869dba/scripts/publish/deploy_key.enc
--------------------------------------------------------------------------------
/scripts/publish/hub.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-test/fb6cd5ce738231306f9184430f051286f6869dba/scripts/publish/hub.enc
--------------------------------------------------------------------------------
/scripts/publish/npmrc.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-test/fb6cd5ce738231306f9184430f051286f6869dba/scripts/publish/npmrc.enc
--------------------------------------------------------------------------------
/scripts/publish/twitter.json.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/firebase-functions-test/fb6cd5ce738231306f9184430f051286f6869dba/scripts/publish/twitter.json.enc
--------------------------------------------------------------------------------
/scripts/tweet.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const fs = require("fs");
4 | const Twitter = require("twitter");
5 |
6 | function printUsage() {
7 | console.error(
8 | `
9 | Usage: tweet.js
10 | Credentials must be stored in "twitter.json" in this directory.
11 | Arguments:
12 | - version: Version of module that was released. e.g. "1.2.3"
13 | `
14 | );
15 | process.exit(1);
16 | }
17 |
18 | function getUrl(version) {
19 | return `https://github.com/firebase/firebase-functions-test/releases/tag/v${version}`;
20 | }
21 |
22 | if (process.argv.length !== 3) {
23 | console.error("Missing arguments.");
24 | printUsage();
25 | }
26 |
27 | const version = process.argv.pop();
28 | if (!version.match(/^\d+\.\d+\.\d+$/)) {
29 | console.error(`Version "${version}" not a version number.`);
30 | printUsage();
31 | }
32 |
33 | if (!fs.existsSync(`${__dirname}/twitter.json`)) {
34 | console.error("Missing credentials.");
35 | printUsage();
36 | }
37 | const creds = require("./twitter.json");
38 |
39 | const client = new Twitter(creds);
40 |
41 | client.post(
42 | "statuses/update",
43 | { status: `v${version} of @Firebase Test SDK for Cloud Functions is available. Release notes: ${getUrl(version)}` },
44 | (err) => {
45 | if (err) {
46 | console.error(err);
47 | process.exit(1);
48 | }
49 | }
50 | );
51 |
52 |
--------------------------------------------------------------------------------
/spec/app.spec.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { expect } from 'chai';
24 | import * as sinon from 'sinon';
25 | import * as firebase from 'firebase-admin';
26 |
27 | import { testApp } from '../src/app';
28 | import { FirebaseFunctionsTest } from '../src/lifecycle';
29 |
30 | describe('app', () => {
31 | let appInstance;
32 | let test;
33 |
34 | before(() => {
35 | test = new FirebaseFunctionsTest();
36 | test.init();
37 | appInstance = testApp();
38 | });
39 |
40 | after(() => {
41 | test.cleanup();
42 | });
43 |
44 | describe('#getApp', () => {
45 | const spy = sinon.spy(firebase, 'initializeApp');
46 |
47 | afterEach(() => {
48 | spy.resetHistory();
49 | appInstance.deleteApp();
50 | });
51 |
52 | it('should initialize a new app if appSingleton does not exist', () => {
53 | appInstance.getApp();
54 | expect(spy.called).to.be.true;
55 | });
56 |
57 | it('should only initialize app once', () => {
58 | appInstance.getApp();
59 | appInstance.getApp();
60 | expect(spy.calledOnce).to.be.true;
61 | });
62 | });
63 |
64 | describe('#deleteApp', () => {
65 | it('deletes appSingleton if it exists', () => {
66 | const spy = sinon.spy();
67 | appInstance.appSingleton = {
68 | delete: spy,
69 | };
70 | appInstance.deleteApp();
71 | expect(spy.called).to.be.true;
72 | expect(appInstance.appSingleton).to.equal(undefined);
73 | });
74 |
75 | it('does not throw an error if there are no apps to delete', () => {
76 | delete appInstance.appSingleton;
77 | expect(() => appInstance.deleteApp).to.not.throw(Error);
78 | });
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/spec/cloudevent/generate.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { expect } from 'chai';
24 |
25 | import { alerts, storage } from 'firebase-functions/v2';
26 | import { generateMockCloudEvent } from '../../src/cloudevent/generate';
27 |
28 | describe('generate (CloudEvent)', () => {
29 | describe('#generateMockCloudEvent', () => {
30 | describe('alerts.billing.onPlanAutomatedUpdatePublished()', () => {
31 | it('should create CloudEvent with appropriate fields', () => {
32 | const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(() => {});
33 | const cloudEvent = generateMockCloudEvent(cloudFn);
34 |
35 | expect(cloudEvent.type).equal(
36 | 'google.firebase.firebasealerts.alerts.v1.published'
37 | );
38 | expect(cloudEvent.source).equal(
39 | '//firebasealerts.googleapis.com/projects/42'
40 | );
41 | expect(cloudEvent.subject).equal(undefined);
42 | });
43 | });
44 | describe('storage.onObjectArchived', () => {
45 | it('should create CloudEvent with appropriate fields', () => {
46 | const bucketName = 'bucket_name';
47 | const cloudFn = storage.onObjectArchived(bucketName, () => {});
48 | const cloudEvent = generateMockCloudEvent(cloudFn);
49 |
50 | expect(cloudEvent.type).equal(
51 | 'google.cloud.storage.object.v1.archived'
52 | );
53 | expect(cloudEvent.source).equal(
54 | `//storage.googleapis.com/projects/_/buckets/${bucketName}`
55 | );
56 | expect(cloudEvent.subject).equal('objects/file_name');
57 | });
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/spec/index.spec.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import 'mocha';
24 | import { expect } from 'chai';
25 |
26 | describe('index', () => {
27 | /* tslint:disable-next-line:no-var-requires */
28 | const indexExport = require('../src')(
29 | { projectId: 'fakeProject' },
30 | 'fakeServiceAccount'
31 | );
32 | after(() => {
33 | // Call cleanup (handles case of cleanup function not existing)
34 | indexExport.cleanup && indexExport.cleanup();
35 | });
36 |
37 | it('should export the expected functions and namespaces', () => {
38 | expect(Object.getOwnPropertyNames(indexExport).sort()).to.deep.equal([
39 | 'analytics',
40 | 'auth',
41 | 'cleanup',
42 | 'database',
43 | 'firestore',
44 | 'makeChange',
45 | 'mockConfig',
46 | 'pubsub',
47 | 'storage',
48 | 'wrap',
49 | ]);
50 | });
51 |
52 | it('should set env variables based parameters SDK was initialized with', () => {
53 | expect(process.env.FIREBASE_CONFIG).to.equal(
54 | JSON.stringify({ projectId: 'fakeProject' })
55 | );
56 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.equal(
57 | 'fakeServiceAccount'
58 | );
59 | });
60 |
61 | it('should clean up env variables once cleanup is called', () => {
62 | indexExport.cleanup();
63 | expect(process.env.FIREBASE_CONFIG).to.equal(undefined);
64 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.equal(undefined);
65 | });
66 | });
67 |
68 | import './lifecycle.spec';
69 | import './main.spec';
70 | import './secretmanager.spec';
71 | import './v2.spec';
72 | import './cloudevent/generate';
73 | import './app.spec';
74 | import './providers/https.spec';
75 | import './providers/firestore.spec';
76 | import './providers/database.spec';
77 | import './providers/scheduled.spec';
78 | // import './providers/analytics.spec';
79 | // import './providers/auth.spec';
80 | // import './providers/https.spec';
81 | // import './providers/pubsub.spec';
82 | // import './providers/storage.spec';
83 |
--------------------------------------------------------------------------------
/spec/integration/providers/firestore.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { initializeApp } from 'firebase-admin/app';
3 | import { getFirestore } from 'firebase-admin/firestore';
4 | import fft = require('../../../src/index');
5 |
6 | describe('providers/firestore', () => {
7 | before(() => {
8 | initializeApp();
9 | });
10 |
11 | it('clears database with clearFirestoreData', async () => {
12 | const test = fft({ projectId: 'not-a-project' });
13 | const db = getFirestore();
14 |
15 | await Promise.all([
16 | db
17 | .collection('test')
18 | .doc('doc1')
19 | .set({}),
20 | db
21 | .collection('test')
22 | .doc('doc1')
23 | .collection('test')
24 | .doc('doc2')
25 | .set({}),
26 | ]);
27 |
28 | await test.firestore.clearFirestoreData({ projectId: 'not-a-project' });
29 |
30 | const docs = await Promise.all([
31 | db
32 | .collection('test')
33 | .doc('doc1')
34 | .get(),
35 | db
36 | .collection('test')
37 | .doc('doc1')
38 | .collection('test')
39 | .doc('doc2')
40 | .get(),
41 | ]);
42 | expect(docs[0].exists).to.be.false;
43 | expect(docs[1].exists).to.be.false;
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/spec/lifecycle.spec.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { expect } from 'chai';
24 |
25 | import { FirebaseFunctionsTest } from '../src/lifecycle';
26 | import { mockConfig } from '../src/main';
27 | import { afterEach } from 'mocha';
28 |
29 | describe('lifecycle', () => {
30 | describe('#init', () => {
31 | let test;
32 |
33 | before(() => {
34 | test = new FirebaseFunctionsTest();
35 | });
36 |
37 | afterEach(() => {
38 | test.cleanup();
39 | });
40 |
41 | it('sets env variables appropriately if SDK initialized without parameters', () => {
42 | test.init();
43 | expect(process.env.FIREBASE_CONFIG).to.equal(
44 | JSON.stringify({
45 | databaseURL: 'https://not-a-project.firebaseio.com',
46 | storageBucket: 'not-a-project.appspot.com',
47 | projectId: 'not-a-project',
48 | })
49 | );
50 | expect(process.env.GCLOUD_PROJECT).to.equal('not-a-project');
51 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.be.undefined;
52 | });
53 |
54 | it('sets env variables appropriately if SDK initialized with parameters', () => {
55 | let firebaseConfig = {
56 | databaseURL: 'https://my-project.firebaseio.com',
57 | storageBucket: 'my-project.appspot.com',
58 | projectId: 'my-project',
59 | };
60 | test.init(firebaseConfig, 'path/to/key.json');
61 |
62 | expect(process.env.FIREBASE_CONFIG).to.equal(
63 | JSON.stringify(firebaseConfig)
64 | );
65 | expect(process.env.GCLOUD_PROJECT).to.equal('my-project');
66 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.equal(
67 | 'path/to/key.json'
68 | );
69 | });
70 | });
71 |
72 | describe('#cleanUp', () => {
73 | beforeEach(() => {
74 | delete process.env.FIREBASE_CONFIG;
75 | delete process.env.GCLOUD_PROJECT;
76 | delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
77 | delete process.env.CLOUD_RUNTIME_CONFIG;
78 | });
79 |
80 | afterEach(() => {
81 | delete process.env.FIREBASE_CONFIG;
82 | delete process.env.GCLOUD_PROJECT;
83 | delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
84 | delete process.env.CLOUD_RUNTIME_CONFIG;
85 | });
86 |
87 | it('deletes all the env variables if they did not previously exist', () => {
88 | let test = new FirebaseFunctionsTest();
89 | test.init();
90 | mockConfig({ foo: { bar: 'faz ' } });
91 | test.cleanup();
92 | expect(process.env.FIREBASE_CONFIG).to.be.undefined;
93 | expect(process.env.GCLOUD_PROJECT).to.be.undefined;
94 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.be.undefined;
95 | expect(process.env.CLOUD_RUNTIME_CONFIG).to.be.undefined;
96 | });
97 |
98 | it('restores env variables if they had previous values', () => {
99 | process.env.FIREBASE_CONFIG = 'oldFb';
100 | process.env.GCLOUD_PROJECT = 'oldGc';
101 | process.env.GOOGLE_APPLICATION_CREDENTIALS = 'oldGac';
102 | process.env.CLOUD_RUNTIME_CONFIG = 'oldCrc';
103 | let test = new FirebaseFunctionsTest();
104 |
105 | test.init();
106 | mockConfig({ foo: { bar: 'faz ' } });
107 | test.cleanup();
108 |
109 | expect(process.env.FIREBASE_CONFIG).to.equal('oldFb');
110 | expect(process.env.GCLOUD_PROJECT).to.equal('oldGc');
111 | expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).to.equal('oldGac');
112 | expect(process.env.CLOUD_RUNTIME_CONFIG).to.equal('oldCrc');
113 | });
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/spec/main.spec.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { expect } from 'chai';
24 | import * as functions from 'firebase-functions/v1';
25 | import { set } from 'lodash';
26 |
27 | import { mockConfig, makeChange, wrap } from '../src/main';
28 | import { _makeResourceName, _extractParams } from '../src/v1';
29 | import { features } from '../src/features';
30 | import { FirebaseFunctionsTest } from '../src/lifecycle';
31 | import { alerts } from 'firebase-functions/v2';
32 | import { wrapV2 } from '../src/v2';
33 |
34 | describe('main', () => {
35 | describe('#wrap', () => {
36 | describe('background functions', () => {
37 | const constructBackgroundCF = (eventType?: string) => {
38 | const cloudFunction = (input) => input;
39 | set(cloudFunction, 'run', (data, context) => {
40 | return { data, context };
41 | });
42 | set(cloudFunction, '__endpoint', {
43 | eventTrigger: {
44 | eventFilters: {
45 | resource: 'ref/{wildcard}/nested/{anotherWildcard}',
46 | },
47 | eventType: eventType || 'event',
48 | retry: false,
49 | },
50 | });
51 | return cloudFunction as functions.CloudFunction;
52 | };
53 |
54 | it('should invoke the function with the supplied data', () => {
55 | const wrapped = wrap(constructBackgroundCF());
56 | expect(wrapped('data').data).to.equal('data');
57 | });
58 |
59 | it('should generate the appropriate context if no fields specified', () => {
60 | const context = wrap(constructBackgroundCF())('data').context;
61 | expect(typeof context.eventId).to.equal('string');
62 | expect(context.resource.service).to.equal(
63 | 'unknown-service.googleapis.com'
64 | );
65 | expect(
66 | /ref\/wildcard[1-9]\/nested\/anotherWildcard[1-9]/.test(
67 | context.resource.name
68 | )
69 | ).to.be.true;
70 | expect(context.eventType).to.equal('event');
71 | expect(Date.parse(context.timestamp)).to.be.greaterThan(0);
72 | expect(context.params).to.deep.equal({});
73 | expect(context.auth).to.be.undefined;
74 | expect(context.authType).to.be.undefined;
75 | });
76 |
77 | it('should allow specification of context fields', () => {
78 | const wrapped = wrap(constructBackgroundCF());
79 | const context = wrapped('data', {
80 | eventId: '111',
81 | timestamp: '2018-03-28T18:58:50.370Z',
82 | }).context;
83 | expect(context.eventId).to.equal('111');
84 | expect(context.timestamp).to.equal('2018-03-28T18:58:50.370Z');
85 | });
86 |
87 | describe('database functions', () => {
88 | let test;
89 | let change;
90 |
91 | beforeEach(() => {
92 | test = new FirebaseFunctionsTest();
93 | test.init();
94 | change = features.database.exampleDataSnapshotChange();
95 | });
96 |
97 | afterEach(() => {
98 | test.cleanup();
99 | });
100 |
101 | it('should generate auth and authType', () => {
102 | const wrapped = wrap(
103 | constructBackgroundCF('google.firebase.database.ref.write')
104 | );
105 | const context = wrapped(change).context;
106 | expect(context.auth).to.equal(null);
107 | expect(context.authType).to.equal('UNAUTHENTICATED');
108 | });
109 |
110 | it('should allow auth and authType to be specified', () => {
111 | const wrapped = wrap(
112 | constructBackgroundCF('google.firebase.database.ref.write')
113 | );
114 | const context = wrapped(change, {
115 | auth: { uid: 'abc' },
116 | authType: 'USER',
117 | }).context;
118 | expect(context.auth).to.deep.equal({ uid: 'abc' });
119 | expect(context.authType).to.equal('USER');
120 | });
121 | });
122 |
123 | it('should throw when passed invalid options', () => {
124 | const wrapped = wrap(constructBackgroundCF());
125 | expect(() =>
126 | wrapped('data', {
127 | auth: { uid: 'abc' },
128 | isInvalid: true,
129 | } as any)
130 | ).to.throw();
131 | });
132 |
133 | it('should generate the appropriate resource based on params', () => {
134 | const params = {
135 | wildcard: 'a',
136 | anotherWildcard: 'b',
137 | };
138 | const wrapped = wrap(constructBackgroundCF());
139 | const context = wrapped('data', { params }).context;
140 | expect(context.params).to.deep.equal(params);
141 | expect(context.resource.name).to.equal('ref/a/nested/b');
142 | });
143 |
144 | describe('Params extraction', () => {
145 | let test;
146 |
147 | beforeEach(() => {
148 | test = new FirebaseFunctionsTest();
149 | test.init();
150 | });
151 |
152 | afterEach(() => {
153 | test.cleanup();
154 | });
155 |
156 | it('should extract the appropriate params for database function trigger', () => {
157 | const cf = constructBackgroundCF(
158 | 'google.firebase.database.ref.create'
159 | );
160 | cf.__endpoint.eventTrigger.eventFilters.resource =
161 | 'companies/{company}/users/{user}';
162 | const wrapped = wrap(cf);
163 | const context = wrapped(
164 | features.database.makeDataSnapshot(
165 | { foo: 'bar' },
166 | 'companies/Google/users/Lauren'
167 | )
168 | ).context;
169 | expect(context.params).to.deep.equal({
170 | company: 'Google',
171 | user: 'Lauren',
172 | });
173 | expect(context.resource.name).to.equal(
174 | 'companies/Google/users/Lauren'
175 | );
176 | });
177 |
178 | it('should extract the appropriate params for Firestore function trigger', () => {
179 | const cf = constructBackgroundCF('google.firestore.document.create');
180 | cf.__endpoint.eventTrigger.eventFilters.resource =
181 | 'databases/(default)/documents/companies/{company}/users/{user}';
182 | const wrapped = wrap(cf);
183 | const context = wrapped(
184 | features.firestore.makeDocumentSnapshot(
185 | { foo: 'bar' },
186 | 'companies/Google/users/Lauren'
187 | )
188 | ).context;
189 | expect(context.params).to.deep.equal({
190 | company: 'Google',
191 | user: 'Lauren',
192 | });
193 | expect(context.resource.name).to.equal(
194 | 'databases/(default)/documents/companies/Google/users/Lauren'
195 | );
196 | });
197 |
198 | it('should prefer provided context.params over the extracted params', () => {
199 | const cf = constructBackgroundCF(
200 | 'google.firebase.database.ref.create'
201 | );
202 | cf.__endpoint.eventTrigger.eventFilters.resource =
203 | 'companies/{company}/users/{user}';
204 | const wrapped = wrap(cf);
205 | const context = wrapped(
206 | features.database.makeDataSnapshot(
207 | { foo: 'bar' },
208 | 'companies/Google/users/Lauren'
209 | ),
210 | {
211 | params: {
212 | company: 'Alphabet',
213 | user: 'Lauren',
214 | foo: 'bar',
215 | },
216 | }
217 | ).context;
218 | expect(context.params).to.deep.equal({
219 | company: 'Alphabet',
220 | user: 'Lauren',
221 | foo: 'bar',
222 | });
223 | expect(context.resource.name).to.equal(
224 | 'companies/Alphabet/users/Lauren'
225 | );
226 | });
227 | });
228 | });
229 |
230 | describe('v2 functions', () => {
231 | it('should invoke wrapV2 wrapper', () => {
232 | const handler = (cloudEvent) => ({ cloudEvent });
233 | const cloudFn = alerts.billing.onPlanAutomatedUpdatePublished(handler);
234 | const cloudFnWrap = wrapV2(cloudFn);
235 |
236 | const expectedType =
237 | 'google.firebase.firebasealerts.alerts.v1.published';
238 | expect(cloudFnWrap().cloudEvent).to.include({ type: expectedType });
239 | });
240 | });
241 |
242 | describe('callable functions', () => {
243 | let wrappedCF;
244 |
245 | before(() => {
246 | const cloudFunction = (input) => input;
247 | set(cloudFunction, 'run', (data, context) => {
248 | return { data, context };
249 | });
250 | set(cloudFunction, '__endpoint', {
251 | callableTrigger: {},
252 | });
253 | wrappedCF = wrap(cloudFunction as functions.CloudFunction);
254 | });
255 |
256 | it('should invoke the function with the supplied data', () => {
257 | expect(wrappedCF('data').data).to.equal('data');
258 | });
259 |
260 | it('should allow specification of context fields', () => {
261 | const context = wrappedCF('data', {
262 | auth: { uid: 'abc' },
263 | app: { appId: 'efg' },
264 | instanceIdToken: '123',
265 | rawRequest: { body: 'hello' },
266 | }).context;
267 | expect(context.auth).to.deep.equal({ uid: 'abc' });
268 | expect(context.app).to.deep.equal({ appId: 'efg' });
269 | expect(context.instanceIdToken).to.equal('123');
270 | expect(context.rawRequest).to.deep.equal({ body: 'hello' });
271 | });
272 |
273 | it('should throw when passed invalid options', () => {
274 | expect(() =>
275 | wrappedCF('data', {
276 | auth: { uid: 'abc' },
277 | isInvalid: true,
278 | } as any)
279 | ).to.throw();
280 | });
281 | });
282 | });
283 |
284 | describe('#_makeResourceName', () => {
285 | it('constructs the right resource name from params', () => {
286 | const resource = _makeResourceName('companies/{company}/users/{user}', {
287 | company: 'Google',
288 | user: 'Lauren',
289 | });
290 | expect(resource).to.equal('companies/Google/users/Lauren');
291 | });
292 | });
293 |
294 | describe('#_extractParams', () => {
295 | it('should not extract any params', () => {
296 | const params = _extractParams('users/foo', 'users/foo');
297 | expect(params).to.deep.equal({});
298 | });
299 |
300 | it('should extract params', () => {
301 | const params = _extractParams(
302 | 'companies/{company}/users/{user}',
303 | 'companies/Google/users/Lauren'
304 | );
305 | expect(params).to.deep.equal({
306 | company: 'Google',
307 | user: 'Lauren',
308 | });
309 | });
310 | });
311 |
312 | describe('#makeChange', () => {
313 | it('should make a Change object with the correct before and after', () => {
314 | const change = makeChange('before', 'after');
315 | expect(change instanceof functions.Change).to.be.true;
316 | expect(change.before).to.equal('before');
317 | expect(change.after).to.equal('after');
318 | });
319 | });
320 |
321 | describe('#mockConfig', () => {
322 | let config: Record;
323 |
324 | beforeEach(() => {
325 | config = { foo: { bar: 'faz ' } };
326 | });
327 |
328 | afterEach(() => {
329 | delete process.env.CLOUD_RUNTIME_CONFIG;
330 | });
331 |
332 | it('should mock functions.config()', () => {
333 | mockConfig(config);
334 | expect(functions.config()).to.deep.equal(config);
335 | });
336 |
337 | it('should purge singleton config object when it is present', () => {
338 | mockConfig(config);
339 | config.foo = { baz: 'qux' };
340 | mockConfig(config);
341 |
342 | expect(functions.config()).to.deep.equal(config);
343 | });
344 | });
345 | });
346 |
--------------------------------------------------------------------------------
/spec/providers/database.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { FirebaseFunctionsTest } from '../../src/lifecycle';
3 | import { makeDataSnapshot } from '../../src/providers/database';
4 |
5 | describe('providers/database', () => {
6 | let test;
7 |
8 | before(() => {
9 | test = new FirebaseFunctionsTest();
10 | test.init();
11 | });
12 |
13 | after(() => {
14 | test.cleanup();
15 | });
16 |
17 | it('produces the right snapshot with makeDataSnapshot', async () => {
18 | const snapshot = makeDataSnapshot(
19 | {
20 | foo: 'bar',
21 | },
22 | 'path'
23 | );
24 |
25 | expect(snapshot.val()).to.deep.equal({ foo: 'bar' });
26 | expect(snapshot.ref.key).to.equal('path');
27 | });
28 |
29 | it('should allow null value in makeDataSnapshot', async () => {
30 | const snapshot = makeDataSnapshot(null, 'path');
31 |
32 | expect(snapshot.val()).to.deep.equal(null);
33 | expect(snapshot.ref.key).to.equal('path');
34 | });
35 |
36 | it('should use the default test apps databaseURL if no instance is specified in makeDataSnapshot', async () => {
37 | const snapshot = makeDataSnapshot(null, 'path', null);
38 |
39 | expect(snapshot.ref.toString()).to.equal(
40 | 'https://not-a-project.firebaseio.com/path'
41 | );
42 | });
43 |
44 | it('should allow different DB instance to be specified in makeDataSnapshot', async () => {
45 | const snapshot = makeDataSnapshot(
46 | null,
47 | 'path',
48 | null,
49 | 'https://another-instance.firebaseio.com'
50 | );
51 |
52 | expect(snapshot.ref.toString()).to.equal(
53 | 'https://another-instance.firebaseio.com/path'
54 | );
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/spec/providers/firestore.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as firebase from 'firebase-admin';
3 | import * as sinon from 'sinon';
4 | import * as http from 'http';
5 | import { FeaturesList } from '../../src/features';
6 | import fft = require('../../src/index');
7 |
8 | describe('providers/firestore', () => {
9 | let test: FeaturesList;
10 | let fakeHttpRequestMethod;
11 | let fakeHttpResponse;
12 |
13 | beforeEach(() => {
14 | test = fft();
15 | fakeHttpResponse = {
16 | statusCode: 200,
17 | on: (event, cb) => cb(),
18 | };
19 | fakeHttpRequestMethod = sinon.fake((config, cb) => {
20 | cb(fakeHttpResponse);
21 | });
22 | sinon.replace(http, 'request', fakeHttpRequestMethod);
23 | });
24 |
25 | afterEach(() => {
26 | sinon.restore();
27 | });
28 |
29 | it('produces the right snapshot with makeDocumentSnapshot', async () => {
30 | const snapshot = test.firestore.makeDocumentSnapshot(
31 | {
32 | email_address: 'test@test.com',
33 | },
34 | 'collection/doc-id'
35 | );
36 |
37 | expect(snapshot.data()).to.deep.equal({
38 | email_address: 'test@test.com',
39 | });
40 | expect(snapshot.id).to.equal('doc-id');
41 | });
42 |
43 | it('should allow empty document in makeDocumentSnapshot', async () => {
44 | const snapshot = test.firestore.makeDocumentSnapshot(
45 | {},
46 | 'collection/doc-id'
47 | );
48 |
49 | expect(snapshot.data()).to.deep.equal(undefined);
50 | expect(snapshot.id).to.equal('doc-id');
51 | });
52 |
53 | it('should allow geopoints with makeDocumentSnapshot', () => {
54 | const hq = new firebase.firestore.GeoPoint(47.6703, 122.1971);
55 | const snapshot = test.firestore.makeDocumentSnapshot(
56 | { geopoint: hq },
57 | 'collection/doc-id'
58 | );
59 |
60 | expect(snapshot.data()).to.deep.equal({ geopoint: hq });
61 | });
62 |
63 | it('should allow timestamps with makeDocumentSnapshot', () => {
64 | const time = new Date();
65 | const snapshot = test.firestore.makeDocumentSnapshot(
66 | { time },
67 | 'collection/doc-id'
68 | );
69 |
70 | expect(snapshot.data().time).to.be.instanceof(firebase.firestore.Timestamp);
71 | expect(snapshot.data().time.toDate()).to.deep.equal(time);
72 | });
73 |
74 | it('should allow references with makeDocumentSnapshot', () => {
75 | firebase.initializeApp({
76 | projectId: 'not-a-project',
77 | });
78 |
79 | const ref = firebase.firestore().doc('collection/doc-id');
80 | const snapshot = test.firestore.makeDocumentSnapshot(
81 | { ref },
82 | 'collection/doc-id'
83 | );
84 |
85 | expect(snapshot.data().ref).to.be.instanceOf(
86 | firebase.firestore.DocumentReference
87 | );
88 | expect(snapshot.data().ref.toString()).to.equal(ref.toString());
89 | });
90 |
91 | it('should use host name from FIRESTORE_EMULATOR_HOST env in clearFirestoreData', async () => {
92 | process.env.FIRESTORE_EMULATOR_HOST = 'not-local-host:8080';
93 |
94 | await test.firestore.clearFirestoreData({ projectId: 'not-a-project' });
95 |
96 | expect(
97 | fakeHttpRequestMethod.calledOnceWith({
98 | hostname: 'not-local-host',
99 | method: 'DELETE',
100 | path:
101 | '/emulator/v1/projects/not-a-project/databases/(default)/documents',
102 | port: '8080',
103 | })
104 | ).to.be.true;
105 | });
106 |
107 | it('should use host name from FIREBASE_FIRESTORE_EMULATOR_ADDRESS env in clearFirestoreData', async () => {
108 | process.env.FIREBASE_FIRESTORE_EMULATOR_ADDRESS = 'custom-host:9090';
109 |
110 | await test.firestore.clearFirestoreData({ projectId: 'not-a-project' });
111 |
112 | expect(
113 | fakeHttpRequestMethod.calledOnceWith({
114 | hostname: 'custom-host',
115 | method: 'DELETE',
116 | path:
117 | '/emulator/v1/projects/not-a-project/databases/(default)/documents',
118 | port: '9090',
119 | })
120 | ).to.be.true;
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/spec/providers/https.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import * as functions from 'firebase-functions/v1';
3 | import fft = require('../../src/index');
4 |
5 | const cfToUpperCaseOnRequest = functions.https.onRequest((req, res) => {
6 | res.json({ msg: req.params.message.toUpperCase() });
7 | });
8 |
9 | const cfToUpperCaseOnCall = functions.https.onCall((data, context) => {
10 | const result: any = {
11 | msg: data.message.toUpperCase(),
12 | from: 'anonymous',
13 | };
14 |
15 | if (context.auth && context.auth.uid) {
16 | result.from = context.auth.uid;
17 | }
18 |
19 | if (context.rawRequest) {
20 | result.rawRequest = context.rawRequest;
21 | }
22 |
23 | return result;
24 | });
25 |
26 | describe('providers/https', () => {
27 | it('should not throw when passed onRequest function', async () => {
28 | const test = fft();
29 | /*
30 | Note that we must cast the function to any here because onRequst functions
31 | do not fulfill Runnable<>, so these checks are solely for usage of this lib
32 | in JavaScript test suites.
33 | */
34 | expect(() => test.wrap(cfToUpperCaseOnRequest as any)).to.throw();
35 | });
36 |
37 | it('should run the wrapped onCall function and return result', async () => {
38 | const test = fft();
39 |
40 | const result = await test.wrap(cfToUpperCaseOnCall)({
41 | message: 'lowercase',
42 | });
43 |
44 | expect(result).to.deep.equal({ msg: 'LOWERCASE', from: 'anonymous' });
45 | });
46 |
47 | it('should accept auth params', async () => {
48 | const test = fft();
49 | const options = { auth: { uid: 'abc' } };
50 |
51 | const result = await test.wrap(cfToUpperCaseOnCall)(
52 | { message: 'lowercase' },
53 | options
54 | );
55 |
56 | expect(result).to.deep.equal({ msg: 'LOWERCASE', from: 'abc' });
57 | });
58 |
59 | it('should accept raw request', async () => {
60 | const mockRequest: any = (sessionData) => {
61 | return {
62 | session: { data: sessionData },
63 | };
64 | };
65 | mockRequest.rawBody = Buffer.from('foobar');
66 | const test = fft();
67 | const options = {
68 | rawRequest: mockRequest,
69 | };
70 |
71 | const result = await test.wrap(cfToUpperCaseOnCall)(
72 | { message: 'lowercase' },
73 | options
74 | );
75 |
76 | expect(result).to.deep.equal({
77 | msg: 'LOWERCASE',
78 | from: 'anonymous',
79 | rawRequest: mockRequest,
80 | });
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/spec/providers/scheduled.spec.ts:
--------------------------------------------------------------------------------
1 | import * as sinon from 'sinon';
2 | import * as functions from 'firebase-functions/v1';
3 | import fft = require('../../src/index');
4 | import { WrappedScheduledFunction } from '../../src/main';
5 |
6 | describe('providers/scheduled', () => {
7 | const fakeFn = sinon.fake.resolves();
8 | const scheduledFunc = functions.pubsub
9 | .schedule('every 2 hours')
10 | .onRun(fakeFn);
11 |
12 | const emptyObjectMatcher = sinon.match(
13 | (v) => sinon.match.object.test(v) && Object.keys(v).length === 0
14 | );
15 |
16 | afterEach(() => {
17 | fakeFn.resetHistory();
18 | });
19 |
20 | it('should run the wrapped function with generated context', async () => {
21 | const test = fft();
22 | const fn: WrappedScheduledFunction = test.wrap(scheduledFunc);
23 | await fn();
24 | // Function should only be called with 1 argument
25 | sinon.assert.calledOnce(fakeFn);
26 | sinon.assert.calledWithExactly(
27 | fakeFn,
28 | sinon.match({
29 | eventType: sinon.match.string,
30 | timestamp: sinon.match.string,
31 | params: emptyObjectMatcher,
32 | })
33 | );
34 | });
35 |
36 | it('should run the wrapped function with provided context', async () => {
37 | const timestamp = new Date().toISOString();
38 | const test = fft();
39 | const fn: WrappedScheduledFunction = test.wrap(scheduledFunc);
40 | await fn({ timestamp });
41 | sinon.assert.calledOnce(fakeFn);
42 | sinon.assert.calledWithExactly(
43 | fakeFn,
44 | sinon.match({
45 | eventType: sinon.match.string,
46 | timestamp,
47 | params: emptyObjectMatcher,
48 | })
49 | );
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/spec/secretmanager.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 | import { mockSecretManager } from '../src/secretManager';
3 |
4 | describe('mockSecretManager', () => {
5 | let originalEnv;
6 |
7 | before(() => {
8 | // Capture the original environment variables
9 | originalEnv = { ...process.env };
10 | });
11 |
12 | afterEach(() => {
13 | // Reset any mutations made by the test run
14 | process.env = { ...originalEnv };
15 | });
16 |
17 | it('applies each key/value pair to process.env', () => {
18 | const conf = { FOO: 'bar', BAZ: 'qux' };
19 |
20 | mockSecretManager(conf);
21 |
22 | expect(process.env.FOO).to.equal('bar');
23 | expect(process.env.BAZ).to.equal('qux');
24 | });
25 |
26 | it('overwrites an existing variable with the new value', () => {
27 | process.env.EXISTING = 'old';
28 | const conf = { EXISTING: 'new' };
29 |
30 | mockSecretManager(conf);
31 |
32 | expect(process.env.EXISTING).to.equal('new');
33 | });
34 |
35 | it('supports non-string values (coerced to string)', () => {
36 | const conf: Record = {
37 | NUM_VALUE: '123',
38 | BOOL_VALUE: 'true',
39 | };
40 |
41 | mockSecretManager(conf);
42 |
43 | expect(process.env.NUM_VALUE).to.equal('123');
44 | expect(process.env.BOOL_VALUE).to.equal('true');
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import * as firebase from 'firebase-admin';
24 |
25 | /** @internal */
26 | export function testApp(): testApp.App {
27 | if (typeof testApp.singleton === 'undefined') {
28 | testApp.init();
29 | }
30 | return testApp.singleton;
31 | }
32 |
33 | /** @internal */
34 | export namespace testApp {
35 | export let singleton: testApp.App;
36 | export let init = () => (singleton = new testApp.App());
37 |
38 | export class App {
39 | appSingleton: firebase.app.App;
40 | constructor() {}
41 |
42 | getApp(): firebase.app.App {
43 | if (typeof this.appSingleton === 'undefined') {
44 | const config = process.env.FIREBASE_CONFIG
45 | ? JSON.parse(process.env.FIREBASE_CONFIG)
46 | : {};
47 | this.appSingleton = firebase.initializeApp(
48 | config,
49 | // Give this app a name so it does not conflict with apps that user initialized.
50 | 'firebase-functions-test'
51 | );
52 | }
53 | return this.appSingleton;
54 | }
55 |
56 | deleteApp() {
57 | if (this.appSingleton) {
58 | this.appSingleton.delete();
59 | delete this.appSingleton;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/cloudevent/generate.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CloudEvent,
3 | CloudFunction,
4 | database,
5 | pubsub,
6 | } from 'firebase-functions/v2';
7 | import {
8 | DocumentSnapshot,
9 | QueryDocumentSnapshot,
10 | } from 'firebase-admin/firestore';
11 | import { LIST_OF_MOCK_CLOUD_EVENT_PARTIALS } from './mocks/partials';
12 | import { DeepPartial } from './types';
13 | import { Change } from 'firebase-functions/v1';
14 | import merge from 'ts-deepmerge';
15 |
16 | /**
17 | * @return {CloudEvent} Generated Mock CloudEvent
18 | */
19 | export function generateCombinedCloudEvent<
20 | EventType extends CloudEvent
21 | >(
22 | cloudFunction: CloudFunction,
23 | cloudEventPartial?: DeepPartial
24 | ): EventType {
25 | const generatedCloudEvent = generateMockCloudEvent(
26 | cloudFunction,
27 | cloudEventPartial
28 | );
29 | return mergeCloudEvents(generatedCloudEvent, cloudEventPartial);
30 | }
31 |
32 | export function generateMockCloudEvent>(
33 | cloudFunction: CloudFunction,
34 | cloudEventPartial?: DeepPartial
35 | ): EventType {
36 | for (const mockCloudEventPartial of LIST_OF_MOCK_CLOUD_EVENT_PARTIALS) {
37 | if (mockCloudEventPartial.match(cloudFunction)) {
38 | return mockCloudEventPartial.generateMock(
39 | cloudFunction,
40 | cloudEventPartial
41 | );
42 | }
43 | }
44 | // No matches were found
45 | return null;
46 | }
47 |
48 | const IMMUTABLE_DATA_TYPES = [
49 | database.DataSnapshot,
50 | DocumentSnapshot,
51 | QueryDocumentSnapshot,
52 | Change,
53 | pubsub.Message,
54 | ];
55 |
56 | function mergeCloudEvents>(
57 | generatedCloudEvent: EventType,
58 | cloudEventPartial: DeepPartial
59 | ) {
60 | /**
61 | * There are several CloudEvent.data types that can not be overridden with json.
62 | * In these circumstances, we generate the CloudEvent.data given the user supplies
63 | * in the DeepPartial.
64 | *
65 | * Because we have already extracted the user supplied data, we don't want to overwrite
66 | * the CloudEvent.data with an incompatible type.
67 | *
68 | * An example of this is a user supplying JSON for the data of the DatabaseEvents.
69 | * The returned CloudEvent should be returning DataSnapshot that uses the supplied json,
70 | * NOT the supplied JSON.
71 | */
72 | if (shouldDeleteUserSuppliedData(generatedCloudEvent, cloudEventPartial)) {
73 | delete cloudEventPartial.data;
74 | }
75 | return cloudEventPartial
76 | ? (merge(generatedCloudEvent, cloudEventPartial) as EventType)
77 | : generatedCloudEvent;
78 | }
79 |
80 | function shouldDeleteUserSuppliedData>(
81 | generatedCloudEvent: EventType,
82 | cloudEventPartial: DeepPartial
83 | ) {
84 | // Don't attempt to delete the data if there is no data.
85 | if (cloudEventPartial?.data === undefined) {
86 | return false;
87 | }
88 | // If the user intentionally provides one of the IMMUTABLE DataTypes, DON'T delete it!
89 | if (
90 | IMMUTABLE_DATA_TYPES.some((type) => cloudEventPartial?.data instanceof type)
91 | ) {
92 | return false;
93 | }
94 |
95 | /** If the generated CloudEvent.data is an IMMUTABLE DataTypes, then use the generated data and
96 | * delete the user supplied CloudEvent.data.
97 | */
98 | if (
99 | IMMUTABLE_DATA_TYPES.some(
100 | (type) => generatedCloudEvent?.data instanceof type
101 | )
102 | ) {
103 | return true;
104 | }
105 |
106 | // Otherwise, don't delete the data and allow ts-merge to handle merging the data.
107 | return false;
108 | }
109 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/alerts-on-alert-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | APP_ID,
5 | getBaseCloudEvent,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import { FirebaseAlertData, AlertEvent } from 'firebase-functions/v2/alerts';
10 |
11 | export const alertsOnAlertPublished: MockCloudEventAbstractFactory> = {
14 | generateMock(
15 | cloudFunction: CloudFunction>
16 | ): AlertEvent {
17 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
18 |
19 | const alertType = 'appDistribution.newTesterIosDevice';
20 | const appId = APP_ID;
21 |
22 | return {
23 | // Spread common fields
24 | ...getBaseCloudEvent(cloudFunction),
25 | // Spread fields specific to this CloudEvent
26 |
27 | alertType,
28 | appId,
29 | data: getOnAlertPublishedData(),
30 | source,
31 | };
32 | },
33 | match(cloudFunction: CloudFunction>): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published'
37 | );
38 | },
39 | };
40 |
41 | /** Alert Published Data */
42 |
43 | function getOnAlertPublishedData(): FirebaseAlertData {
44 | const now = new Date().toISOString();
45 | return {
46 | createTime: now,
47 | endTime: now,
48 | payload: {},
49 | };
50 | }
51 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/app-distribution-on-new-tester-ios-device-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | AppDistributionEvent,
11 | NewTesterDevicePayload,
12 | } from 'firebase-functions/v2/alerts/appDistribution';
13 |
14 | export const alertsAppDistributionOnNewTesterIosDevicePublished: MockCloudEventAbstractFactory> = {
17 | generateMock(
18 | cloudFunction: CloudFunction>
19 | ): AppDistributionEvent {
20 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
21 | const now = new Date().toISOString();
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: {
29 | createTime: now,
30 | endTime: now,
31 | payload: {
32 | ['@type']:
33 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.AppDistroNewTesterIosDevicePayload',
34 | testerName: 'tester name',
35 | testerEmail: 'test@test.com',
36 | testerDeviceModelName: 'tester device model name',
37 | testerDeviceIdentifier: 'tester device identifier',
38 | },
39 | },
40 | };
41 | },
42 | match(
43 | cloudFunction: CloudFunction>
44 | ): boolean {
45 | return (
46 | getEventType(cloudFunction) ===
47 | 'google.firebase.firebasealerts.alerts.v1.published' &&
48 | getEventFilters(cloudFunction)?.alerttype ===
49 | 'appDistribution.newTesterIosDevice'
50 | );
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/billing-on-plan-automated-update-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
4 | import {
5 | BillingEvent,
6 | PlanAutomatedUpdatePayload,
7 | } from 'firebase-functions/v2/alerts/billing';
8 | import {
9 | getBaseCloudEvent,
10 | getEventFilters,
11 | getEventType,
12 | PROJECT_ID,
13 | } from '../helpers';
14 |
15 | export const alertsBillingOnPlanAutomatedUpdatePublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): BillingEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getBillingPlanAutomatedUpdateData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype ===
38 | 'billing.planAutomatedUpdate'
39 | );
40 | },
41 | };
42 |
43 | function getBillingPlanAutomatedUpdateData(): FirebaseAlertData<
44 | PlanAutomatedUpdatePayload
45 | > {
46 | const now = new Date().toISOString();
47 | return {
48 | createTime: now,
49 | endTime: now,
50 | payload: {
51 | ['@type']:
52 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.BillingPlanAutomatedUpdatePayload',
53 | billingPlan: 'flame',
54 | notificationType: 'upgrade',
55 | },
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/billing-on-plan-update-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
4 | import {
5 | BillingEvent,
6 | PlanUpdatePayload,
7 | } from 'firebase-functions/v2/alerts/billing';
8 | import {
9 | getBaseCloudEvent,
10 | getEventFilters,
11 | getEventType,
12 | PROJECT_ID,
13 | } from '../helpers';
14 |
15 | export const alertsBillingOnPlanUpdatePublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): BillingEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getBillingPlanUpdateData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype === 'billing.planUpdate'
38 | );
39 | },
40 | };
41 |
42 | /** Alert Billing Data */
43 | function getBillingPlanUpdateData(): FirebaseAlertData {
44 | const now = new Date().toISOString();
45 | return {
46 | createTime: now,
47 | endTime: now,
48 | payload: {
49 | ['@type']:
50 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.BillingPlanUpdatePayload',
51 | billingPlan: 'flame',
52 | principalEmail: 'test@test.com',
53 | notificationType: 'upgrade',
54 | },
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-new-anr-issue-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | NewAnrIssuePayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnNewAnrIssuePublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsNewAnrIssueData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype === 'crashlytics.newAnrIssue'
38 | );
39 | },
40 | };
41 |
42 | function getCrashlyticsNewAnrIssueData(): FirebaseAlertData<
43 | NewAnrIssuePayload
44 | > {
45 | const now = new Date().toISOString();
46 | return {
47 | createTime: now,
48 | endTime: now,
49 | payload: {
50 | ['@type']:
51 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsNewAnrIssuePayload',
52 | issue: {
53 | id: 'crashlytics_issue_id',
54 | title: 'crashlytics_issue_title',
55 | subtitle: 'crashlytics_issue_subtitle',
56 | appVersion: 'crashlytics_issue_app_version',
57 | },
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-new-fatal-issue-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | NewFatalIssuePayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnNewFatalIssuePublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsNewFatalIssueData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype === 'crashlytics.newFatalIssue'
38 | );
39 | },
40 | };
41 |
42 | function getCrashlyticsNewFatalIssueData(): FirebaseAlertData<
43 | NewFatalIssuePayload
44 | > {
45 | const now = new Date().toISOString();
46 | return {
47 | createTime: now,
48 | endTime: now,
49 | payload: {
50 | ['@type']:
51 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsNewFatalIssuePayload',
52 | issue: {
53 | id: 'crashlytics_issue_id',
54 | title: 'crashlytics_issue_title',
55 | subtitle: 'crashlytics_issue_subtitle',
56 | appVersion: 'crashlytics_issue_app_version',
57 | },
58 | },
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-new-nonfatal-issue-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | NewNonfatalIssuePayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnNewNonfatalIssuePublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsNewNonfatalIssueData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype ===
38 | 'crashlytics.newNonfatalIssue'
39 | );
40 | },
41 | };
42 |
43 | function getCrashlyticsNewNonfatalIssueData(): FirebaseAlertData<
44 | NewNonfatalIssuePayload
45 | > {
46 | const now = new Date().toISOString();
47 | return {
48 | createTime: now,
49 | endTime: now,
50 | payload: {
51 | ['@type']:
52 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsNewNonfatalIssuePayload',
53 | issue: {
54 | id: 'crashlytics_issue_id',
55 | title: 'crashlytics_issue_title',
56 | subtitle: 'crashlytics_issue_subtitle',
57 | appVersion: 'crashlytics_issue_app_version',
58 | },
59 | },
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-regression-alert-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | RegressionAlertPayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnRegressionAlertPublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsRegressionAlertPayload(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype === 'crashlytics.regression'
38 | );
39 | },
40 | };
41 |
42 | /** Alert Crashlytics Data */
43 | function getCrashlyticsRegressionAlertPayload(): FirebaseAlertData<
44 | RegressionAlertPayload
45 | > {
46 | const now = new Date().toISOString();
47 | return {
48 | createTime: now,
49 | endTime: now,
50 | payload: {
51 | ['@type']:
52 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsRegressionAlertPayload',
53 | issue: {
54 | id: 'crashlytics_issue_id',
55 | title: 'crashlytics_issue_title',
56 | subtitle: 'crashlytics_issue_subtitle',
57 | appVersion: 'crashlytics_issue_app_version',
58 | },
59 | type: 'test type',
60 | resolveTime: now,
61 | },
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-stability-digest-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | StabilityDigestPayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnStabilityDigestPublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsStabilityData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype ===
38 | 'crashlytics.stabilityDigest'
39 | );
40 | },
41 | };
42 |
43 | function getCrashlyticsStabilityData(): FirebaseAlertData<
44 | StabilityDigestPayload
45 | > {
46 | const now = new Date().toISOString();
47 | return {
48 | createTime: now,
49 | endTime: now,
50 | payload: {
51 | ['@type']:
52 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsStabilityDigestPayload',
53 | digestDate: new Date().toISOString(),
54 | trendingIssues: [
55 | {
56 | type: 'type',
57 | eventCount: 100,
58 | userCount: 100,
59 | issue: {
60 | id: 'crashlytics_issue_id',
61 | title: 'crashlytics_issue_title',
62 | subtitle: 'crashlytics_issue_subtitle',
63 | appVersion: 'crashlytics_issue_app_version',
64 | },
65 | },
66 | ],
67 | },
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/crashlytics-on-velocity-alert-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 | import {
10 | CrashlyticsEvent,
11 | VelocityAlertPayload,
12 | } from 'firebase-functions/v2/alerts/crashlytics';
13 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
14 |
15 | export const alertsCrashlyticsOnVelocityAlertPublished: MockCloudEventAbstractFactory> = {
18 | generateMock(
19 | cloudFunction: CloudFunction>
20 | ): CrashlyticsEvent {
21 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
22 |
23 | return {
24 | // Spread common fields
25 | ...getBaseCloudEvent(cloudFunction),
26 | // Spread fields specific to this CloudEvent
27 | source,
28 | data: getCrashlyticsVelocityAlertData(),
29 | };
30 | },
31 | match(
32 | cloudFunction: CloudFunction>
33 | ): boolean {
34 | return (
35 | getEventType(cloudFunction) ===
36 | 'google.firebase.firebasealerts.alerts.v1.published' &&
37 | getEventFilters(cloudFunction)?.alerttype === 'crashlytics.velocity'
38 | );
39 | },
40 | };
41 |
42 | function getCrashlyticsVelocityAlertData(): FirebaseAlertData<
43 | VelocityAlertPayload
44 | > {
45 | const now = new Date().toISOString();
46 | return {
47 | createTime: now,
48 | endTime: now,
49 | payload: {
50 | ['@type']:
51 | 'type.googleapis.com/google.events.firebase.firebasealerts.v1.CrashlyticsVelocityAlertPayload',
52 | crashCount: 100,
53 | issue: {
54 | id: 'crashlytics_issue_id',
55 | title: 'crashlytics_issue_title',
56 | subtitle: 'crashlytics_issue_subtitle',
57 | appVersion: 'crashlytics_issue_app_version',
58 | },
59 | createTime: now,
60 | firstVersion: '1.1',
61 | crashPercentage: 50.0,
62 | },
63 | };
64 | }
65 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/alerts/performance-on-threshold-alert-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction } from 'firebase-functions/v2';
3 | import {
4 | PerformanceEvent,
5 | ThresholdAlertPayload,
6 | } from 'firebase-functions/v2/alerts/performance';
7 | import {
8 | getBaseCloudEvent,
9 | getEventFilters,
10 | getEventType,
11 | PROJECT_ID,
12 | APP_ID,
13 | } from '../helpers';
14 | import { FirebaseAlertData } from 'firebase-functions/v2/alerts';
15 |
16 | export const performanceThresholdOnThresholdAlertPublished: MockCloudEventAbstractFactory> = {
19 | generateMock(
20 | cloudFunction: CloudFunction>
21 | ): PerformanceEvent {
22 | const source = `//firebasealerts.googleapis.com/projects/${PROJECT_ID}`;
23 | const alertType = 'performance.threshold';
24 | const appId = APP_ID;
25 | return {
26 | ...getBaseCloudEvent(cloudFunction),
27 | alertType,
28 | appId,
29 | source,
30 | data: getThresholdAlertPayload(),
31 | };
32 | },
33 | match(
34 | cloudFunction: CloudFunction>
35 | ): boolean {
36 | return (
37 | getEventType(cloudFunction) ===
38 | 'google.firebase.firebasealerts.alerts.v1.published' &&
39 | getEventFilters(cloudFunction)?.alerttype === 'performance.threshold'
40 | );
41 | },
42 | };
43 |
44 | function getThresholdAlertPayload(): FirebaseAlertData {
45 | const now = new Date().toISOString();
46 | return {
47 | createTime: now,
48 | endTime: now,
49 | payload: {
50 | eventName: 'test.com/api/123',
51 | eventType: 'network_request',
52 | metricType: 'duration',
53 | numSamples: 200,
54 | thresholdValue: 100,
55 | thresholdUnit: 'ms',
56 | violationValue: 200,
57 | violationUnit: 'ms',
58 | investigateUri: 'firebase.google.com/firebase/console',
59 | },
60 | };
61 | }
62 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/database-on-value-created.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, database } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { getDatabaseSnapshotCloudEvent } from './helpers';
5 |
6 | export const databaseOnValueCreated: MockCloudEventAbstractFactory> = {
9 | generateMock: getDatabaseSnapshotCloudEvent,
10 | match(cloudFunction: CloudFunction>): boolean {
11 | return (
12 | getEventType(cloudFunction) === 'google.firebase.database.ref.v1.created'
13 | );
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/database-on-value-deleted.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, database } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { getDatabaseSnapshotCloudEvent } from './helpers';
5 |
6 | export const databaseOnValueDeleted: MockCloudEventAbstractFactory> = {
9 | generateMock: getDatabaseSnapshotCloudEvent,
10 | match(cloudFunction: CloudFunction>): boolean {
11 | return (
12 | getEventType(cloudFunction) === 'google.firebase.database.ref.v1.deleted'
13 | );
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/database-on-value-updated.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, database } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { Change } from 'firebase-functions/v1';
5 | import { getDatabaseChangeSnapshotCloudEvent } from './helpers';
6 |
7 | export const databaseOnValueUpdated: MockCloudEventAbstractFactory
9 | >> = {
10 | generateMock: getDatabaseChangeSnapshotCloudEvent,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) === 'google.firebase.database.ref.v1.updated'
14 | );
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/database-on-value-written.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, database } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { Change } from 'firebase-functions/v1';
5 | import { getDatabaseChangeSnapshotCloudEvent } from './helpers';
6 |
7 | export const databaseOnValueWritten: MockCloudEventAbstractFactory
9 | >> = {
10 | generateMock: getDatabaseChangeSnapshotCloudEvent,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) === 'google.firebase.database.ref.v1.written'
14 | );
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/helpers.ts:
--------------------------------------------------------------------------------
1 | import { CloudFunction, database } from 'firebase-functions/v2';
2 | import { DeepPartial } from '../../types';
3 | import {
4 | exampleDataSnapshot,
5 | exampleDataSnapshotChange,
6 | } from '../../../providers/database';
7 | import {
8 | resolveStringExpression,
9 | getBaseCloudEvent,
10 | extractRef,
11 | } from '../helpers';
12 | import { Change } from 'firebase-functions/v1';
13 | import { makeDataSnapshot } from '../../../providers/database';
14 |
15 | type ChangeLike = {
16 | before: database.DataSnapshot | object;
17 | after: database.DataSnapshot | object;
18 | };
19 |
20 | function getOrCreateDataSnapshot(
21 | data: database.DataSnapshot | object,
22 | ref: string
23 | ) {
24 | if (data instanceof database.DataSnapshot) {
25 | return data;
26 | }
27 | if (data instanceof Object) {
28 | return makeDataSnapshot(data, ref);
29 | }
30 | return exampleDataSnapshot(ref);
31 | }
32 |
33 | function getOrCreateDataSnapshotChange(
34 | data: DeepPartial | ChangeLike>,
35 | ref: string
36 | ) {
37 | if (data instanceof Change) {
38 | return data;
39 | }
40 | if (data instanceof Object && data?.before && data?.after) {
41 | const beforeDataSnapshot = getOrCreateDataSnapshot(data!.before, ref);
42 | const afterDataSnapshot = getOrCreateDataSnapshot(data!.after, ref);
43 | return new Change(beforeDataSnapshot, afterDataSnapshot);
44 | }
45 | return exampleDataSnapshotChange(ref);
46 | }
47 |
48 | export function getDatabaseSnapshotCloudEvent(
49 | cloudFunction: CloudFunction>,
50 | cloudEventPartial?: DeepPartial<
51 | database.DatabaseEvent
52 | >
53 | ) {
54 | const {
55 | instance,
56 | firebaseDatabaseHost,
57 | ref,
58 | location,
59 | params,
60 | } = getCommonDatabaseFields(cloudFunction, cloudEventPartial);
61 |
62 | const data = getOrCreateDataSnapshot(cloudEventPartial?.data, ref);
63 |
64 | return {
65 | // Spread common fields
66 | ...getBaseCloudEvent(cloudFunction),
67 |
68 | // Update fields specific to this CloudEvent
69 | data,
70 |
71 | instance,
72 | firebaseDatabaseHost,
73 | ref,
74 | location,
75 | params,
76 | };
77 | }
78 |
79 | export function getDatabaseChangeSnapshotCloudEvent(
80 | cloudFunction: CloudFunction<
81 | database.DatabaseEvent>
82 | >,
83 | cloudEventPartial?: DeepPartial<
84 | database.DatabaseEvent | ChangeLike>
85 | >
86 | ): database.DatabaseEvent> {
87 | const {
88 | instance,
89 | firebaseDatabaseHost,
90 | ref,
91 | location,
92 | params,
93 | } = getCommonDatabaseFields(cloudFunction, cloudEventPartial);
94 |
95 | const data = getOrCreateDataSnapshotChange(cloudEventPartial?.data, ref);
96 |
97 | return {
98 | // Spread common fields
99 | ...getBaseCloudEvent(cloudFunction),
100 |
101 | // Update fields specific to this CloudEvent
102 | data,
103 |
104 | instance,
105 | firebaseDatabaseHost,
106 | ref,
107 | location,
108 | params,
109 | };
110 | }
111 |
112 | export function getCommonDatabaseFields(
113 | cloudFunction: CloudFunction<
114 | database.DatabaseEvent<
115 | database.DataSnapshot | Change
116 | >
117 | >,
118 | cloudEventPartial?: DeepPartial<
119 | database.DatabaseEvent<
120 | database.DataSnapshot | Change
121 | >
122 | >
123 | ) {
124 | const instanceOrExpression =
125 | (cloudEventPartial?.instance as string) ||
126 | cloudFunction.__endpoint?.eventTrigger?.eventFilterPathPatterns?.instance ||
127 | cloudFunction.__endpoint?.eventTrigger?.eventFilters?.instance ||
128 | 'instance-1';
129 | const instance = resolveStringExpression(instanceOrExpression);
130 | const firebaseDatabaseHost =
131 | (cloudEventPartial?.firebaseDatabaseHost as string) ||
132 | 'firebaseDatabaseHost';
133 | const rawRefOrExpression =
134 | (cloudEventPartial?.ref as string) ||
135 | cloudFunction?.__endpoint?.eventTrigger?.eventFilterPathPatterns?.ref ||
136 | '/foo/bar';
137 | const rawRef = resolveStringExpression(rawRefOrExpression);
138 | const location = (cloudEventPartial?.location as string) || 'us-central1';
139 | const params: Record = cloudEventPartial?.params || {};
140 | const ref = extractRef(rawRef, params);
141 |
142 | return {
143 | instance,
144 | firebaseDatabaseHost,
145 | ref,
146 | location,
147 | params,
148 | };
149 | }
150 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/database/index.ts:
--------------------------------------------------------------------------------
1 | export { databaseOnValueCreated } from './database-on-value-created';
2 | export { databaseOnValueDeleted } from './database-on-value-deleted';
3 | export { databaseOnValueUpdated } from './database-on-value-updated';
4 | export { databaseOnValueWritten } from './database-on-value-written';
5 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/eventarc/eventarc-on-custom-event-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction } from 'firebase-functions/v2';
3 | import { getBaseCloudEvent } from '../helpers';
4 |
5 | export const eventarcOnCustomEventPublished: MockCloudEventAbstractFactory = {
6 | generateMock(
7 | cloudFunction: CloudFunction>,
8 | cloudEventPartial?: DeepPartial>
9 | ): CloudEvent {
10 | const source = 'eventarc_source';
11 | const subject = 'eventarc_subject';
12 |
13 | return {
14 | // Spread common fields
15 | ...getBaseCloudEvent(cloudFunction),
16 | // Spread fields specific to this CloudEvent
17 | data: cloudEventPartial?.data || {},
18 | source,
19 | subject,
20 | };
21 | },
22 | match(_: CloudFunction>): boolean {
23 | return true;
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-created-with-auth-context.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, firestore } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
5 | import { getDocumentSnapshotCloudEventWithAuthContext } from './helpers';
6 |
7 | export const firestoreOnDocumentCreatedWithAuthContext: MockCloudEventAbstractFactory> = {
10 | generateMock: getDocumentSnapshotCloudEventWithAuthContext,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) ===
14 | 'google.cloud.firestore.document.v1.created.withAuthContext'
15 | );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-created.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, firestore } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
5 | import { getDocumentSnapshotCloudEvent } from './helpers';
6 |
7 | export const firestoreOnDocumentCreated: MockCloudEventAbstractFactory> = {
10 | generateMock: getDocumentSnapshotCloudEvent,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) ===
14 | 'google.cloud.firestore.document.v1.created'
15 | );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-deleted-with-auth-context.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, firestore } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
5 | import { getDocumentSnapshotCloudEventWithAuthContext } from './helpers';
6 |
7 | export const firestoreOnDocumentDeletedWithAuthContext: MockCloudEventAbstractFactory> = {
10 | generateMock: getDocumentSnapshotCloudEventWithAuthContext,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) ===
14 | 'google.cloud.firestore.document.v1.deleted.withAuthContext'
15 | );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-deleted.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, firestore } from 'firebase-functions/v2';
3 | import { getEventType } from '../helpers';
4 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
5 | import { getDocumentSnapshotCloudEvent } from './helpers';
6 |
7 | export const firestoreOnDocumentDeleted: MockCloudEventAbstractFactory> = {
10 | generateMock: getDocumentSnapshotCloudEvent,
11 | match(cloudFunction: CloudFunction>): boolean {
12 | return (
13 | getEventType(cloudFunction) ===
14 | 'google.cloud.firestore.document.v1.deleted'
15 | );
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-updated-with-auth-context.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import {
3 | Change,
4 | CloudEvent,
5 | CloudFunction,
6 | firestore,
7 | } from 'firebase-functions/v2';
8 | import { getEventType } from '../helpers';
9 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
10 | import { getDocumentSnapshotChangeCloudEventWithAuthContext } from './helpers';
11 |
12 | export const firestoreOnDocumentUpdatedWithAuthContext: MockCloudEventAbstractFactory
14 | >> = {
15 | generateMock: getDocumentSnapshotChangeCloudEventWithAuthContext,
16 | match(cloudFunction: CloudFunction>): boolean {
17 | return (
18 | getEventType(cloudFunction) ===
19 | 'google.cloud.firestore.document.v1.updated.withAuthContext'
20 | );
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-updated.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import {
3 | Change,
4 | CloudEvent,
5 | CloudFunction,
6 | firestore,
7 | } from 'firebase-functions/v2';
8 | import { getEventType } from '../helpers';
9 | import { QueryDocumentSnapshot } from 'firebase-admin/firestore';
10 | import { getDocumentSnapshotChangeCloudEvent } from './helpers';
11 |
12 | export const firestoreOnDocumentUpdated: MockCloudEventAbstractFactory
14 | >> = {
15 | generateMock: getDocumentSnapshotChangeCloudEvent,
16 | match(cloudFunction: CloudFunction>): boolean {
17 | return (
18 | getEventType(cloudFunction) ===
19 | 'google.cloud.firestore.document.v1.updated'
20 | );
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-written-with-auth-context.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import {
3 | Change,
4 | CloudEvent,
5 | CloudFunction,
6 | firestore,
7 | } from 'firebase-functions/v2';
8 | import { getEventType } from '../helpers';
9 | import { DocumentSnapshot } from 'firebase-admin/firestore';
10 | import { getDocumentSnapshotChangeCloudEventWithAuthContext } from './helpers';
11 |
12 | export const firestoreOnDocumentWrittenWithAuthContext: MockCloudEventAbstractFactory
14 | >> = {
15 | generateMock: getDocumentSnapshotChangeCloudEventWithAuthContext,
16 | match(cloudFunction: CloudFunction>): boolean {
17 | return (
18 | getEventType(cloudFunction) ===
19 | 'google.cloud.firestore.document.v1.written.withAuthContext'
20 | );
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/firestore-on-document-written.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../../types';
2 | import {
3 | Change,
4 | CloudEvent,
5 | CloudFunction,
6 | firestore,
7 | } from 'firebase-functions/v2';
8 | import { getEventType } from '../helpers';
9 | import { DocumentSnapshot } from 'firebase-admin/firestore';
10 | import { getDocumentSnapshotChangeCloudEvent } from './helpers';
11 |
12 | export const firestoreOnDocumentWritten: MockCloudEventAbstractFactory
14 | >> = {
15 | generateMock: getDocumentSnapshotChangeCloudEvent,
16 | match(cloudFunction: CloudFunction>): boolean {
17 | return (
18 | getEventType(cloudFunction) ===
19 | 'google.cloud.firestore.document.v1.written'
20 | );
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/helpers.ts:
--------------------------------------------------------------------------------
1 | import { DocumentSnapshot } from 'firebase-admin/firestore';
2 | import { Change, CloudFunction, firestore } from 'firebase-functions/v2';
3 | import {
4 | exampleDocumentSnapshot,
5 | exampleDocumentSnapshotChange,
6 | makeDocumentSnapshot,
7 | } from '../../../providers/firestore';
8 | import { DeepPartial } from '../../types';
9 | import {
10 | extractRef,
11 | getBaseCloudEvent,
12 | resolveStringExpression,
13 | } from '../helpers';
14 |
15 | type ChangeLike = {
16 | before: DocumentSnapshot | object;
17 | after: DocumentSnapshot | object;
18 | };
19 |
20 | /** Creates a mock CloudEvent that contains a DocumentSnapshot as its data. */
21 | export function getDocumentSnapshotCloudEvent(
22 | cloudFunction: CloudFunction>,
23 | cloudEventPartial?: DeepPartial<
24 | firestore.FirestoreEvent
25 | >
26 | ) {
27 | const {
28 | location,
29 | project,
30 | database,
31 | namespace,
32 | document,
33 | params,
34 | } = getFirestoreEventFields(cloudFunction, cloudEventPartial);
35 | const data = getOrCreateDocumentSnapshot(cloudEventPartial?.data, document);
36 | return {
37 | ...getBaseCloudEvent(cloudFunction),
38 |
39 | location,
40 | project,
41 | database,
42 | namespace,
43 | document,
44 | params,
45 |
46 | data,
47 | };
48 | }
49 |
50 | export function getDocumentSnapshotCloudEventWithAuthContext(
51 | cloudFunction: CloudFunction>,
52 | cloudEventPartial?: DeepPartial<
53 | firestore.FirestoreAuthEvent
54 | >
55 | ) {
56 | const eventWithoutAuthContext = getDocumentSnapshotCloudEvent(
57 | cloudFunction,
58 | cloudEventPartial
59 | );
60 | const authContext: { authId?: string; authType: firestore.AuthType } = {
61 | authType: 'unknown',
62 | };
63 | if (cloudEventPartial?.authId) {
64 | authContext.authId = cloudEventPartial.authId;
65 | }
66 | if (cloudEventPartial?.authType) {
67 | authContext.authType = cloudEventPartial.authType;
68 | }
69 | return {
70 | ...eventWithoutAuthContext,
71 | ...authContext,
72 | };
73 | }
74 |
75 | /** Creates a mock CloudEvent that contains a Change as its data. */
76 | export function getDocumentSnapshotChangeCloudEvent(
77 | cloudFunction: CloudFunction<
78 | firestore.FirestoreEvent>
79 | >,
80 | cloudEventPartial?: DeepPartial<
81 | firestore.FirestoreEvent | ChangeLike>
82 | >
83 | ) {
84 | const {
85 | location,
86 | project,
87 | database,
88 | namespace,
89 | document,
90 | params,
91 | } = getFirestoreEventFields(cloudFunction, cloudEventPartial);
92 | const data = getOrCreateDocumentSnapshotChange(
93 | cloudEventPartial?.data,
94 | document
95 | );
96 | return {
97 | ...getBaseCloudEvent(cloudFunction),
98 |
99 | location,
100 | project,
101 | database,
102 | namespace,
103 | document,
104 | params,
105 |
106 | data,
107 | };
108 | }
109 |
110 | export function getDocumentSnapshotChangeCloudEventWithAuthContext(
111 | cloudFunction: CloudFunction<
112 | firestore.FirestoreAuthEvent>
113 | >,
114 | cloudEventPartial?: DeepPartial<
115 | firestore.FirestoreAuthEvent | ChangeLike>
116 | >
117 | ) {
118 | const eventWithoutAuthContext = getDocumentSnapshotChangeCloudEvent(
119 | cloudFunction,
120 | cloudEventPartial
121 | );
122 | const authContext: { authId?: string; authType: firestore.AuthType } = {
123 | authType: 'unknown',
124 | };
125 | if (cloudEventPartial?.authId) {
126 | authContext.authId = cloudEventPartial.authId;
127 | }
128 | if (cloudEventPartial?.authType) {
129 | authContext.authType = cloudEventPartial.authType;
130 | }
131 |
132 | return {
133 | ...eventWithoutAuthContext,
134 | ...authContext,
135 | };
136 | }
137 |
138 | /** Finds or provides reasonable defaults for mock FirestoreEvent data. */
139 | function getFirestoreEventFields(
140 | cloudFunction: CloudFunction<
141 | firestore.FirestoreEvent>
142 | >,
143 | cloudEventPartial?: DeepPartial<
144 | firestore.FirestoreEvent>
145 | >
146 | ) {
147 | const location = cloudEventPartial?.location || 'us-central1';
148 |
149 | const project =
150 | cloudEventPartial?.project || process.env.GCLOUD_PROJECT || 'testproject';
151 |
152 | const databaseOrExpression =
153 | cloudEventPartial?.database ||
154 | cloudFunction?.__endpoint?.eventTrigger?.eventFilters?.database ||
155 | '(default)';
156 | const database = resolveStringExpression(databaseOrExpression);
157 |
158 | const namespaceOrExpression =
159 | cloudEventPartial?.namespace ||
160 | cloudFunction?.__endpoint?.eventTrigger?.eventFilters?.namespace ||
161 | '(default)';
162 | const namespace = resolveStringExpression(namespaceOrExpression);
163 |
164 | const params = cloudEventPartial?.params || {};
165 |
166 | const documentOrExpression =
167 | cloudEventPartial?.document ||
168 | cloudFunction?.__endpoint?.eventTrigger?.eventFilters?.document ||
169 | cloudFunction?.__endpoint?.eventTrigger?.eventFilterPathPatterns
170 | ?.document ||
171 | '/foo/bar';
172 | const documentRaw = resolveStringExpression(documentOrExpression);
173 | const document = extractRef(documentRaw, params);
174 |
175 | return {
176 | location,
177 | project,
178 | database,
179 | namespace,
180 | document,
181 | params,
182 | };
183 | }
184 |
185 | /** Make a DocumentSnapshot from the user-provided partial data. */
186 | function getOrCreateDocumentSnapshot(
187 | data: DocumentSnapshot | object,
188 | ref: string
189 | ) {
190 | if (data instanceof DocumentSnapshot) {
191 | return data;
192 | }
193 | if (data instanceof Object) {
194 | return makeDocumentSnapshot(data, ref);
195 | }
196 | return exampleDocumentSnapshot();
197 | }
198 |
199 | /** Make a DocumentSnapshotChange from the user-provided partial data. */
200 | function getOrCreateDocumentSnapshotChange(
201 | data: DeepPartial | ChangeLike>,
202 | ref: string
203 | ) {
204 | if (data instanceof Change) {
205 | return data;
206 | }
207 | // If only the "before" or "after" is specified (to simulate a
208 | // Created or Deleted event for the onWritten trigger), then we
209 | // include the user's before/after object in the mock event and
210 | // use an example snapshot for the other.
211 | if (data?.before || data?.after) {
212 | const beforeSnapshot = getOrCreateDocumentSnapshot(data.before, ref);
213 | const afterSnapshot = getOrCreateDocumentSnapshot(data.after, ref);
214 | return new Change(beforeSnapshot, afterSnapshot);
215 | }
216 | return exampleDocumentSnapshotChange();
217 | }
218 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/firestore/index.ts:
--------------------------------------------------------------------------------
1 | export { firestoreOnDocumentCreated } from './firestore-on-document-created';
2 | export { firestoreOnDocumentDeleted } from './firestore-on-document-deleted';
3 | export { firestoreOnDocumentUpdated } from './firestore-on-document-updated';
4 | export { firestoreOnDocumentWritten } from './firestore-on-document-written';
5 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/helpers.ts:
--------------------------------------------------------------------------------
1 | import * as v1 from 'firebase-functions/v1';
2 | import * as v2 from 'firebase-functions/v2';
3 | import { Expression } from 'firebase-functions/params';
4 |
5 | export const APP_ID = '__APP_ID__';
6 | export const PROJECT_ID = '42';
7 | export const FILENAME = 'file_name';
8 |
9 | type CloudFunction = v1.CloudFunction | v2.CloudFunction;
10 |
11 | export function getEventType(cloudFunction: CloudFunction): string {
12 | return cloudFunction?.__endpoint?.eventTrigger?.eventType || '';
13 | }
14 |
15 | export function getEventFilters(
16 | cloudFunction: CloudFunction
17 | ): Record> {
18 | return cloudFunction?.__endpoint?.eventTrigger?.eventFilters || {};
19 | }
20 |
21 | export function getBaseCloudEvent>(
22 | cloudFunction: v2.CloudFunction
23 | ): EventType {
24 | return {
25 | specversion: '1.0',
26 | id: makeEventId(),
27 | data: undefined,
28 | source: '', // Required field that will get overridden by Provider-specific MockCloudEventPartials
29 | type: getEventType(cloudFunction),
30 | time: new Date().toISOString(),
31 | } as EventType;
32 | }
33 |
34 | export function resolveStringExpression(
35 | stringOrExpression: string | Expression
36 | ) {
37 | if (typeof stringOrExpression === 'string') {
38 | return stringOrExpression;
39 | }
40 | return stringOrExpression?.value();
41 | }
42 |
43 | function makeEventId(): string {
44 | return (
45 | Math.random()
46 | .toString(36)
47 | .substring(2, 15) +
48 | Math.random()
49 | .toString(36)
50 | .substring(2, 15)
51 | );
52 | }
53 |
54 | /** Resolves param values and inserts them into the provided ref string. */
55 | export function extractRef(rawRef: string, params: Record) {
56 | const refSegments = rawRef.split('/');
57 |
58 | return refSegments
59 | .map((segment) => {
60 | if (segment.startsWith('{') && segment.endsWith('}')) {
61 | const param = segment
62 | .slice(1, -1)
63 | .replace('=**', '')
64 | .replace('=*', '');
65 | return params[param] || 'undefined';
66 | }
67 | return segment;
68 | })
69 | .join('/');
70 | }
71 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/partials.ts:
--------------------------------------------------------------------------------
1 | import { MockCloudEventAbstractFactory } from '../types';
2 | import { alertsOnAlertPublished } from './alerts/alerts-on-alert-published';
3 | import { alertsCrashlyticsOnNewAnrIssuePublished } from './alerts/crashlytics-on-new-anr-issue-published';
4 | import { alertsCrashlyticsOnNewFatalIssuePublished } from './alerts/crashlytics-on-new-fatal-issue-published';
5 | import { alertsCrashlyticsOnNewNonfatalIssuePublished } from './alerts/crashlytics-on-new-nonfatal-issue-published';
6 | import { alertsCrashlyticsOnRegressionAlertPublished } from './alerts/crashlytics-on-regression-alert-published';
7 | import { alertsCrashlyticsOnStabilityDigestPublished } from './alerts/crashlytics-on-stability-digest-published';
8 | import { alertsCrashlyticsOnVelocityAlertPublished } from './alerts/crashlytics-on-velocity-alert-published';
9 | import { alertsAppDistributionOnNewTesterIosDevicePublished } from './alerts/app-distribution-on-new-tester-ios-device-published';
10 | import { alertsBillingOnPlanAutomatedUpdatePublished } from './alerts/billing-on-plan-automated-update-published';
11 | import { alertsBillingOnPlanUpdatePublished } from './alerts/billing-on-plan-update-published';
12 | import { performanceThresholdOnThresholdAlertPublished } from './alerts/performance-on-threshold-alert-published';
13 | import { eventarcOnCustomEventPublished } from './eventarc/eventarc-on-custom-event-published';
14 | import { pubsubOnMessagePublished } from './pubsub/pubsub-on-message-published';
15 | import {
16 | databaseOnValueCreated,
17 | databaseOnValueDeleted,
18 | databaseOnValueUpdated,
19 | databaseOnValueWritten,
20 | } from './database';
21 | import {
22 | firestoreOnDocumentCreated,
23 | firestoreOnDocumentDeleted,
24 | firestoreOnDocumentUpdated,
25 | firestoreOnDocumentWritten,
26 | } from './firestore';
27 | import { storageV1 } from './storage';
28 | import { remoteConfigOnConfigUpdated } from './remoteconfig/remote-config-on-config-updated';
29 | import { testLabOnTestMatrixCompleted } from './testlab/test-lab-on-test-matrix-completed';
30 | import { firestoreOnDocumentCreatedWithAuthContext } from './firestore/firestore-on-document-created-with-auth-context';
31 | import { firestoreOnDocumentDeletedWithAuthContext } from './firestore/firestore-on-document-deleted-with-auth-context';
32 | import { firestoreOnDocumentUpdatedWithAuthContext } from './firestore/firestore-on-document-updated-with-auth-context';
33 | import { firestoreOnDocumentWrittenWithAuthContext } from './firestore/firestore-on-document-written-with-auth-context';
34 |
35 | /**
36 | * Note: Ordering matters. Some MockEventPartials will match more generally
37 | * (eg {@link alertsOnAlertPublished}). In addition,
38 | * {@link eventarcOnCustomEventPublished} acts as a catch-all.
39 | */
40 | export const LIST_OF_MOCK_CLOUD_EVENT_PARTIALS: Array> = [
43 | alertsCrashlyticsOnNewAnrIssuePublished,
44 | alertsCrashlyticsOnNewFatalIssuePublished,
45 | alertsCrashlyticsOnNewNonfatalIssuePublished,
46 | alertsCrashlyticsOnRegressionAlertPublished,
47 | alertsCrashlyticsOnStabilityDigestPublished,
48 | alertsCrashlyticsOnVelocityAlertPublished,
49 | alertsAppDistributionOnNewTesterIosDevicePublished,
50 | alertsBillingOnPlanAutomatedUpdatePublished,
51 | alertsBillingOnPlanUpdatePublished,
52 | performanceThresholdOnThresholdAlertPublished,
53 | alertsOnAlertPublished, // Note: alert.onAlertPublished matching is more generic
54 | remoteConfigOnConfigUpdated,
55 | testLabOnTestMatrixCompleted,
56 | storageV1,
57 | pubsubOnMessagePublished,
58 | databaseOnValueCreated,
59 | databaseOnValueDeleted,
60 | databaseOnValueUpdated,
61 | databaseOnValueWritten,
62 | firestoreOnDocumentCreated,
63 | firestoreOnDocumentDeleted,
64 | firestoreOnDocumentUpdated,
65 | firestoreOnDocumentWritten,
66 | firestoreOnDocumentCreatedWithAuthContext,
67 | firestoreOnDocumentDeletedWithAuthContext,
68 | firestoreOnDocumentUpdatedWithAuthContext,
69 | firestoreOnDocumentWrittenWithAuthContext,
70 |
71 | // CustomEventPublished must be called last
72 | eventarcOnCustomEventPublished,
73 | ];
74 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/pubsub/pubsub-on-message-published.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudEvent, CloudFunction, pubsub } from 'firebase-functions/v2';
3 | import {
4 | getBaseCloudEvent,
5 | getEventFilters,
6 | getEventType,
7 | PROJECT_ID,
8 | } from '../helpers';
9 |
10 | export const pubsubOnMessagePublished: MockCloudEventAbstractFactory> = {
13 | generateMock(
14 | cloudFunction: CloudFunction>,
15 | cloudEventPartial?: DeepPartial>
16 | ): CloudEvent {
17 | const topicId = getEventFilters(cloudFunction)?.topic || '';
18 | const source = `//pubsub.googleapis.com/projects/${PROJECT_ID}/topics/${topicId}`;
19 | const subscription = `projects/${PROJECT_ID}/subscriptions/pubsubexample-1`;
20 |
21 | // Used if no data.message.json is provided by the partial;
22 | const dataMessageJsonDefault = { hello: 'world' };
23 | const dataMessageAttributesDefault = {
24 | 'sample-attribute': 'I am an attribute',
25 | };
26 |
27 | const dataMessageJson =
28 | cloudEventPartial?.data?.message?.json || dataMessageJsonDefault;
29 |
30 | // We should respect if the user provides their own message.data.
31 | const dataMessageData =
32 | cloudEventPartial?.data?.message?.data ||
33 | Buffer.from(JSON.stringify(dataMessageJson)).toString('base64');
34 |
35 | // TODO - consider warning the user if their data does not match the json they provide
36 |
37 | const messageData = {
38 | data: dataMessageData,
39 | messageId: cloudEventPartial?.data?.message?.messageId || 'message_id',
40 | attributes:
41 | cloudEventPartial?.data?.message?.attributes ||
42 | dataMessageAttributesDefault,
43 | };
44 | const message = new pubsub.Message(messageData);
45 |
46 | return {
47 | // Spread common fields
48 | ...getBaseCloudEvent(cloudFunction),
49 | // Spread fields specific to this CloudEvent
50 | source,
51 | data: {
52 | /**
53 | * Note: Its very important we return the JSON representation of the message here. Without it,
54 | * ts-merge blows away `data.message`.
55 | */
56 | message: message.toJSON(),
57 | subscription,
58 | },
59 | };
60 | },
61 | match(cloudFunction: CloudFunction>): boolean {
62 | return (
63 | getEventType(cloudFunction) ===
64 | 'google.cloud.pubsub.topic.v1.messagePublished'
65 | );
66 | },
67 | };
68 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/remoteconfig/remote-config-on-config-updated.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction, CloudEvent } from 'firebase-functions/v2';
3 | import { ConfigUpdateData } from 'firebase-functions/v2/remoteConfig';
4 | import { getBaseCloudEvent, getEventType, PROJECT_ID } from '../helpers';
5 |
6 | export const remoteConfigOnConfigUpdated: MockCloudEventAbstractFactory> = {
9 | generateMock(
10 | cloudFunction: CloudFunction>
11 | ): CloudEvent {
12 | const source = `//firebaseremoteconfig.googleapis.com/projects/${PROJECT_ID}`;
13 | return {
14 | ...getBaseCloudEvent(cloudFunction),
15 | source,
16 | data: getConfigUpdateData(),
17 | };
18 | },
19 | match(cloudFunction: CloudFunction>): boolean {
20 | return (
21 | getEventType(cloudFunction) ===
22 | 'google.firebase.remoteconfig.remoteConfig.v1.updated'
23 | );
24 | },
25 | };
26 |
27 | function getConfigUpdateData(): ConfigUpdateData {
28 | const now = new Date().toISOString();
29 | return {
30 | versionNumber: 2,
31 | updateTime: now,
32 | updateUser: {
33 | name: 'testuser',
34 | email: 'test@example.com',
35 | imageUrl: 'test.com/img-url',
36 | },
37 | description: 'config update test',
38 | updateOrigin: 'REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED',
39 | updateType: 'REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED',
40 | rollbackSource: 0,
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/storage/index.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction, CloudEvent } from 'firebase-functions/v2';
3 | import { StorageEvent } from 'firebase-functions/v2/storage';
4 | import {
5 | FILENAME,
6 | resolveStringExpression,
7 | getBaseCloudEvent,
8 | getEventFilters,
9 | getEventType,
10 | } from '../helpers';
11 | import { getStorageObjectData } from './storage-data';
12 |
13 | export const storageV1: MockCloudEventAbstractFactory = {
14 | generateMock(
15 | cloudFunction: CloudFunction,
16 | cloudEventPartial?: DeepPartial
17 | ): StorageEvent {
18 | const bucketOrExpression =
19 | cloudEventPartial?.bucket ||
20 | getEventFilters(cloudFunction)?.bucket ||
21 | 'bucket_name';
22 | const bucket = resolveStringExpression(bucketOrExpression);
23 | const source =
24 | cloudEventPartial?.source ||
25 | `//storage.googleapis.com/projects/_/buckets/${bucket}`;
26 | const subject = cloudEventPartial?.subject || `objects/${FILENAME}`;
27 |
28 | return {
29 | // Spread common fields
30 | ...getBaseCloudEvent(cloudFunction),
31 | // Spread fields specific to this CloudEvent
32 | bucket,
33 | source,
34 | subject,
35 | data: getStorageObjectData(bucket, FILENAME, 1),
36 | };
37 | },
38 | match(cloudFunction: CloudFunction>): boolean {
39 | return getEventType(cloudFunction).startsWith(
40 | 'google.cloud.storage.object.v1'
41 | );
42 | },
43 | };
44 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/storage/storage-data.ts:
--------------------------------------------------------------------------------
1 | import { storage } from 'firebase-functions/v2';
2 | import { FILENAME } from '../helpers';
3 |
4 | /** Storage Data */
5 | export function getStorageObjectData(
6 | bucket: string,
7 | filename: string,
8 | generation: number
9 | ): storage.StorageObjectData {
10 | const now = new Date().toISOString();
11 | return {
12 | metageneration: 1,
13 | metadata: {
14 | firebaseStorageDownloadTokens: '00000000-0000-0000-0000-000000000000',
15 | },
16 | kind: 'storage#object',
17 | mediaLink: `https://www.googleapis.com/download/storage/v1/b/${bucket}/o/${FILENAME}?generation=${generation}&alt=media`,
18 | etag: 'xxxxxxxxx/yyyyy=',
19 | timeStorageClassUpdated: now,
20 | generation,
21 | md5Hash: 'E9LIfVl7pcVu3/moXc743w==',
22 | crc32c: 'qqqqqq==',
23 | selfLink: `https://www.googleapis.com/storage/v1/b/${bucket}/o/${FILENAME}`,
24 | name: FILENAME,
25 | storageClass: 'REGIONAL',
26 | size: 42,
27 | updated: now,
28 | contentDisposition: `inline; filename*=utf-8''${FILENAME}`,
29 | contentType: 'image/gif',
30 | timeCreated: now,
31 | id: `${bucket}/${FILENAME}/${generation}`,
32 | bucket,
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/src/cloudevent/mocks/testlab/test-lab-on-test-matrix-completed.ts:
--------------------------------------------------------------------------------
1 | import { DeepPartial, MockCloudEventAbstractFactory } from '../../types';
2 | import { CloudFunction, CloudEvent } from 'firebase-functions/v2';
3 | import { TestMatrixCompletedData } from 'firebase-functions/v2/testLab';
4 | import { getBaseCloudEvent, getEventType, PROJECT_ID } from '../helpers';
5 |
6 | export const testLabOnTestMatrixCompleted: MockCloudEventAbstractFactory> = {
9 | generateMock(
10 | cloudFunction: CloudFunction>
11 | ): CloudEvent {
12 | const source = `//firebasetestlab.googleapis.com/projects/${PROJECT_ID}`;
13 | return {
14 | ...getBaseCloudEvent(cloudFunction),
15 | source,
16 | data: getTestMatrixCompletedData(),
17 | };
18 | },
19 | match(cloudFunction: CloudFunction>) {
20 | return (
21 | getEventType(cloudFunction) ===
22 | 'google.firebase.testlab.testMatrix.v1.completed'
23 | );
24 | },
25 | };
26 |
27 | function getTestMatrixCompletedData(): TestMatrixCompletedData {
28 | const now = new Date().toISOString();
29 | return {
30 | createTime: now,
31 | state: 'TEST_STATE_UNSPECIFIED',
32 | invalidMatrixDetails: '',
33 | outcomeSummary: 'OUTCOME_SUMMARY_UNSPECIFIED',
34 | resultStorage: {
35 | toolResultsHistory: `projects/${PROJECT_ID}/histories/1234`,
36 | toolResultsExecution: `projects/${PROJECT_ID}/histories/1234/executions/5678`,
37 | resultsUri: 'console.firebase.google.com/test/results',
38 | gcsPath: 'gs://bucket/path/to/test',
39 | },
40 | clientInfo: {
41 | client: 'gcloud',
42 | details: {},
43 | },
44 | testMatrixId: '1234',
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/cloudevent/types.ts:
--------------------------------------------------------------------------------
1 | import { CloudEvent, CloudFunction } from 'firebase-functions/v2';
2 |
3 | export type DeepPartial = {
4 | [Key in keyof T]?: T[Key] extends object ? DeepPartial : T[Key];
5 | };
6 | type MockCloudEventFunction> = (
7 | cloudFunction: CloudFunction,
8 | cloudEventPartial?: DeepPartial
9 | ) => EventType;
10 | type MockCloudEventMatchFunction> = (
11 | cloudFunction: CloudFunction
12 | ) => boolean;
13 |
14 | export interface MockCloudEventAbstractFactory<
15 | EventType extends CloudEvent
16 | > {
17 | generateMock: MockCloudEventFunction;
18 | match: MockCloudEventMatchFunction;
19 | }
20 |
--------------------------------------------------------------------------------
/src/features.ts:
--------------------------------------------------------------------------------
1 | import { makeChange, wrap, mockConfig } from './main';
2 | import * as analytics from './providers/analytics';
3 | import * as auth from './providers/auth';
4 | import * as database from './providers/database';
5 | import * as firestore from './providers/firestore';
6 | import * as pubsub from './providers/pubsub';
7 | import * as storage from './providers/storage';
8 | import { FirebaseFunctionsTest } from './lifecycle';
9 |
10 | export interface LazyFeatures {
11 | mockConfig: typeof mockConfig;
12 | wrap: typeof wrap;
13 | makeChange: typeof makeChange;
14 | analytics: typeof analytics;
15 | auth: typeof auth;
16 | database: typeof database;
17 | firestore: typeof firestore;
18 | pubsub: typeof pubsub;
19 | storage: typeof storage;
20 | }
21 |
22 | export const features: LazyFeatures = {
23 | mockConfig,
24 | wrap,
25 | makeChange,
26 | analytics,
27 | auth,
28 | database,
29 | firestore,
30 | pubsub,
31 | storage,
32 | };
33 |
34 | export interface FeaturesList extends LazyFeatures {
35 | cleanup: InstanceType['cleanup'];
36 | }
37 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { AppOptions } from 'firebase-admin';
24 | import { merge } from 'lodash';
25 |
26 | import { FirebaseFunctionsTest } from './lifecycle';
27 | import { FeaturesList } from './features';
28 |
29 | export = (
30 | firebaseConfig?: AppOptions,
31 | pathToServiceAccountKey?: string
32 | ): FeaturesList => {
33 | const test = new FirebaseFunctionsTest();
34 | test.init(firebaseConfig, pathToServiceAccountKey);
35 | // Ensure other files get loaded after init function, since they load `firebase-functions`
36 | // which will issue warning if process.env.FIREBASE_CONFIG is not yet set.
37 | let features = require('./features').features;
38 | features = merge({}, features, {
39 | cleanup: () => test.cleanup(),
40 | });
41 | return features;
42 | };
43 |
--------------------------------------------------------------------------------
/src/lifecycle.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { isEmpty } from 'lodash';
24 | import { AppOptions } from 'firebase-admin';
25 | import { forEach } from 'lodash';
26 |
27 | import { testApp } from './app';
28 |
29 | export class FirebaseFunctionsTest {
30 | private _oldEnv: { [key: string]: string };
31 |
32 | constructor() {
33 | this._oldEnv = {};
34 | }
35 |
36 | /** Initialize the SDK. */
37 | init(
38 | /** Firebase config values for initializing a Firebase app for your test code to
39 | * interact with (e.g. making database writes). It is recommended that you use
40 | * a project that is specifically for testing. If omitted, mock config values will
41 | * be used and your tests will not interact with a real Firebase app, and all Firebase
42 | * methods need to be stubbed, otherwise they will fail.
43 | */
44 | firebaseConfig?: AppOptions,
45 | /** Path to a service account key file to be used when initializing the Firebase app. */
46 | pathToServiceAccountKey?: string
47 | ) {
48 | this._oldEnv = {
49 | FIREBASE_CONFIG: process.env.FIREBASE_CONFIG,
50 | GOOGLE_APPLICATION_CREDENTIALS:
51 | process.env.GOOGLE_APPLICATION_CREDENTIALS,
52 | GCLOUD_PROJECT: process.env.GCLOUD_PROJECT,
53 | CLOUD_RUNTIME_CONFIG: process.env.CLOUD_RUNTIME_CONFIG,
54 | };
55 |
56 | if (isEmpty(firebaseConfig)) {
57 | process.env.FIREBASE_CONFIG = JSON.stringify({
58 | databaseURL: 'https://not-a-project.firebaseio.com',
59 | storageBucket: 'not-a-project.appspot.com',
60 | projectId: 'not-a-project',
61 | });
62 | } else {
63 | process.env.FIREBASE_CONFIG = JSON.stringify(firebaseConfig);
64 | if (pathToServiceAccountKey) {
65 | process.env.GOOGLE_APPLICATION_CREDENTIALS = pathToServiceAccountKey;
66 | }
67 | }
68 | process.env.GCLOUD_PROJECT = JSON.parse(
69 | process.env.FIREBASE_CONFIG
70 | ).projectId;
71 | }
72 |
73 | /** Complete clean up tasks. */
74 | cleanup() {
75 | forEach(this._oldEnv, (val, varName) => {
76 | if (typeof val !== 'undefined') {
77 | process.env[varName] = val;
78 | } else {
79 | delete process.env[varName];
80 | }
81 | });
82 | testApp().deleteApp();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import {
24 | CloudFunction as CloudFunctionV1,
25 | HttpsFunction,
26 | Runnable,
27 | } from 'firebase-functions/v1';
28 |
29 | import {
30 | CloudFunction as CloudFunctionV2,
31 | CloudEvent,
32 | } from 'firebase-functions/v2';
33 |
34 | import {
35 | CallableFunction,
36 | HttpsFunction as HttpsFunctionV2,
37 | } from 'firebase-functions/v2/https';
38 |
39 | import { wrapV1, WrappedFunction, WrappedScheduledFunction } from './v1';
40 |
41 | import { wrapV2, WrappedV2Function, WrappedV2CallableFunction } from './v2';
42 |
43 | type HttpsFunctionOrCloudFunctionV1 = U extends HttpsFunction &
44 | Runnable
45 | ? HttpsFunction & Runnable
46 | : CloudFunctionV1;
47 |
48 | export { mockSecretManager } from './secretManager';
49 |
50 | // Re-exporting V1 (to reduce breakage)
51 | export {
52 | ContextOptions,
53 | EventContextOptions,
54 | WrappedFunction,
55 | WrappedScheduledFunction,
56 | CallableContextOptions,
57 | makeChange,
58 | mockConfig,
59 | } from './v1';
60 |
61 | // V2 Exports
62 | export { WrappedV2Function } from './v2';
63 |
64 | export function wrap(
65 | cloudFunction: HttpsFunction & Runnable
66 | ): WrappedFunction>;
67 | export function wrap(
68 | cloudFunction: CallableFunction
69 | ): WrappedV2CallableFunction;
70 | export function wrap(
71 | cloudFunction: CloudFunctionV1
72 | ): WrappedScheduledFunction | WrappedFunction;
73 | export function wrap>(
74 | cloudFunction: CloudFunctionV2
75 | ): WrappedV2Function;
76 | export function wrap>(
77 | cloudFunction: CloudFunctionV1 | CloudFunctionV2 | HttpsFunctionV2
78 | ):
79 | | WrappedScheduledFunction
80 | | WrappedFunction
81 | | WrappedV2Function
82 | | WrappedV2CallableFunction {
83 | if (isV2CloudFunction(cloudFunction)) {
84 | return wrapV2(cloudFunction as CloudFunctionV2);
85 | }
86 | return wrapV1(
87 | cloudFunction as HttpsFunctionOrCloudFunctionV1
88 | );
89 | }
90 |
91 | /**
92 | * The key differences between V1 and V2 CloudFunctions are:
93 | *
94 | * - V1 CloudFunction is sometimes a binary function
95 | *
- V2 CloudFunction is always a unary function
96 | *
- V1 CloudFunction.run is always a binary function
97 | *
- V2 CloudFunction.run is always a unary function
98 | * @return True iff the CloudFunction is a V2 function.
99 | */
100 | function isV2CloudFunction>(
101 | cloudFunction: any
102 | ): cloudFunction is CloudFunctionV2 {
103 | return cloudFunction.length === 1 && cloudFunction?.run?.length === 1;
104 | }
105 |
--------------------------------------------------------------------------------
/src/providers/analytics.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { analytics } from 'firebase-functions/v1';
24 |
25 | /** Create an AnalyticsEvent */
26 | export function makeAnalyticsEvent(
27 | /** Fields of AnalyticsEvent that you'd like to specify. */
28 | fields: { [key: string]: string }
29 | ): analytics.AnalyticsEvent {
30 | const template = {
31 | reportingDate: '',
32 | name: '',
33 | params: {},
34 | logTime: '',
35 | };
36 | return Object.assign(template, fields);
37 | }
38 |
39 | /** Fetch an example AnalyticsEvent already populated with data. */
40 | export function exampleAnalyticsEvent(): analytics.AnalyticsEvent {
41 | return {
42 | reportingDate: '20170202',
43 | name: 'Loaded_In_Background',
44 | params: {
45 | build: '1350',
46 | calls_remaining: 10,
47 | fraction_calls_dropped: 0.0123456,
48 | average_call_rating: 4.5,
49 | },
50 | logTime: '2017-02-02T23:06:26.124Z',
51 | previousLogTime: '2017-02-02T23:01:19.797Z',
52 | valueInUSD: 1234.5,
53 | user: {
54 | userId: 'abcdefghijklmnop!',
55 | appInfo: {
56 | appId: 'com.appName',
57 | appInstanceId: 'E3C9939401814B9B954725A740B8C7BC',
58 | appPlatform: 'IOS',
59 | appStore: 'iTunes',
60 | appVersion: '5.2.0',
61 | },
62 | bundleInfo: {
63 | bundleSequenceId: 6034,
64 | serverTimestampOffset: 371,
65 | },
66 | deviceInfo: {
67 | deviceCategory: 'mobile',
68 | deviceModel: 'iPhone7,2',
69 | deviceTimeZoneOffsetSeconds: -21600,
70 | mobileBrandName: 'Apple',
71 | mobileMarketingName: 'iPhone 6',
72 | mobileModelName: 'iPhone 6',
73 | platformVersion: '10.2.1',
74 | userDefaultLanguage: 'en-us',
75 | deviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370',
76 | resettableDeviceId: '599F9C00-92DC-4B5C-9464-7971F01F8370',
77 | limitedAdTracking: true,
78 | },
79 | firstOpenTime: '2016-04-28T15:00:35.819Z',
80 | geoInfo: {
81 | city: 'Plano',
82 | continent: '021',
83 | country: 'United States',
84 | region: 'Texas',
85 | },
86 | userProperties: {
87 | build: {
88 | setTime: '2017-02-02T23:06:26.090Z',
89 | value: '1350',
90 | },
91 | calls_remaining: {
92 | setTime: '2017-02-02T23:06:26.094Z',
93 | value: '10',
94 | },
95 | version: {
96 | setTime: '2017-02-02T23:06:26.085Z',
97 | value: '5.2.0',
98 | },
99 | },
100 | },
101 | };
102 | }
103 |
--------------------------------------------------------------------------------
/src/providers/auth.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { auth } from 'firebase-functions/v1';
24 |
25 | /** Create a UserRecord. */
26 | export function makeUserRecord(
27 | /** Fields of AuthRecord that you'd like to specify. */
28 | fields: { [key: string]: string | boolean }
29 | ): auth.UserRecord {
30 | return auth.userRecordConstructor(Object.assign({ uid: '' }, fields));
31 | }
32 |
33 | /** Fetch an example UserRecord already populated with data. */
34 | export function exampleUserRecord(): auth.UserRecord {
35 | return auth.userRecordConstructor({
36 | email: 'user@gmail.com',
37 | metadata: {
38 | creationTime: '2018-03-13T01:24:48Z',
39 | lastSignInTime: '2018-04-03T03:52:48Z',
40 | },
41 | uid: 'SQol8dFfyMapsQtRD4JgZdC5r1G2',
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/providers/database.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { database, Change } from 'firebase-functions/v1';
24 | import { app } from 'firebase-admin';
25 |
26 | import { testApp } from '../app';
27 |
28 | /** Create a DataSnapshot. */
29 | export function makeDataSnapshot(
30 | /** Value of data for the snapshot. */
31 | val: string | number | boolean | any[] | object | null,
32 | /** Full path of the reference (e.g. 'users/alovelace'). */
33 | refPath: string,
34 | /** The Firebase app that the database belongs to.
35 | * The databaseURL supplied when initializing the app will be used for creating this snapshot.
36 | * You do not need to supply this parameter if you supplied Firebase config values when initializing
37 | * firebase-functions-test.
38 | */
39 | firebaseApp?: app.App,
40 | /**
41 | * The RTDB instance to use when creating snapshot. This will override the `firebaseApp` parameter.
42 | * If omitted the default RTDB instance is used.
43 | */
44 | instance?: string
45 | ): database.DataSnapshot {
46 | return new database.DataSnapshot(
47 | val,
48 | refPath,
49 | firebaseApp || testApp().getApp(),
50 | instance
51 | );
52 | }
53 |
54 | /** Fetch an example data snapshot already populated with data. Can be passed into a wrapped
55 | * database onCreate or onDelete function.
56 | */
57 | export function exampleDataSnapshot(
58 | refPath = 'messages/1234'
59 | ): database.DataSnapshot {
60 | return makeDataSnapshot({ foo: 'bar ' }, refPath);
61 | }
62 |
63 | /** Fetch an example Change object of data snapshots already populated with data.
64 | * Can be passed into a wrapped database onUpdate or onWrite function.
65 | */
66 | export function exampleDataSnapshotChange(
67 | beforeRefPath = 'messages/1234',
68 | afterRefPath = 'messages/1234'
69 | ): Change {
70 | return Change.fromObjects(
71 | makeDataSnapshot({ foo: 'faz' }, beforeRefPath),
72 | makeDataSnapshot({ foo: 'bar' }, afterRefPath)
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/providers/firestore.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { Change } from 'firebase-functions/v1';
24 | import { firestore, app } from 'firebase-admin';
25 | import { has, get, isEmpty, isPlainObject, mapValues } from 'lodash';
26 | import { inspect } from 'util';
27 |
28 | import { testApp } from '../app';
29 |
30 | import * as http from 'http';
31 | import {
32 | DocumentSnapshot,
33 | QueryDocumentSnapshot,
34 | } from 'firebase-admin/firestore';
35 |
36 | function dateToTimestampProto(
37 | timeString?: string
38 | ): { seconds: number; nanos: number } | undefined {
39 | if (typeof timeString === 'undefined') {
40 | return;
41 | }
42 | const date = new Date(timeString);
43 | const seconds = Math.floor(date.getTime() / 1000);
44 | let nanos = 0;
45 | if (timeString.length > 20) {
46 | const nanoString = timeString.substring(20, timeString.length - 1);
47 | const trailingZeroes = 9 - nanoString.length;
48 | nanos = parseInt(nanoString, 10) * Math.pow(10, trailingZeroes);
49 | }
50 | return { seconds, nanos };
51 | }
52 |
53 | /** Optional parameters for creating a DocumentSnapshot. */
54 | export interface DocumentSnapshotOptions {
55 | /** ISO timestamp string for the snapshot was read, default is current time. */
56 | readTime?: string;
57 | /** ISO timestamp string for the snapshot was created, default is current time. */
58 | createTime?: string;
59 | /** ISO timestamp string for the snapshot was last updated, default is current time. */
60 | updateTime?: string;
61 | /** The Firebase app that the Firestore database belongs to. You do not need to supply
62 | * this parameter if you supplied Firebase config values when initializing firebase-functions-test.
63 | */
64 | firebaseApp?: app.App;
65 | }
66 |
67 | /** Create a DocumentSnapshot. */
68 | export function makeDocumentSnapshot(
69 | /** Key-value pairs representing data in the document, pass in `{}` to mock the snapshot of
70 | * a document that doesn't exist.
71 | */
72 | data: { [key: string]: any },
73 | /** Full path of the reference (e.g. 'users/alovelace') */
74 | refPath: string,
75 | options?: DocumentSnapshotOptions
76 | ) {
77 | let firestoreService;
78 | let project;
79 | if (has(options, 'app')) {
80 | firestoreService = firestore(options.firebaseApp);
81 | project = get(options, 'app.options.projectId');
82 | } else {
83 | firestoreService = firestore(testApp().getApp());
84 | project = process.env.GCLOUD_PROJECT;
85 | }
86 |
87 | const resource = `projects/${project}/databases/(default)/documents/${refPath}`;
88 | const proto = isEmpty(data)
89 | ? resource
90 | : {
91 | fields: objectToValueProto(data),
92 | createTime: dateToTimestampProto(
93 | get(options, 'createTime', new Date().toISOString())
94 | ),
95 | updateTime: dateToTimestampProto(
96 | get(options, 'updateTime', new Date().toISOString())
97 | ),
98 | name: resource,
99 | };
100 |
101 | const readTimeProto = dateToTimestampProto(
102 | get(options, 'readTime') || new Date().toISOString()
103 | );
104 | return firestoreService.snapshot_(proto, readTimeProto, 'json');
105 | }
106 |
107 | /** Fetch an example document snapshot already populated with data. Can be passed into a wrapped
108 | * Firestore onCreate or onDelete function.
109 | */
110 | export function exampleDocumentSnapshot(): firestore.DocumentSnapshot {
111 | return makeDocumentSnapshot(
112 | {
113 | aString: 'foo',
114 | anObject: {
115 | a: 'bar',
116 | b: 'faz',
117 | },
118 | aNumber: 7,
119 | },
120 | 'records/1234'
121 | );
122 | }
123 |
124 | /** Fetch an example Change object of document snapshots already populated with data.
125 | * Can be passed into a wrapped Firestore onUpdate or onWrite function.
126 | */
127 | export function exampleDocumentSnapshotChange(): Change<
128 | firestore.DocumentSnapshot
129 | > {
130 | return Change.fromObjects(
131 | makeDocumentSnapshot(
132 | {
133 | anObject: {
134 | a: 'bar',
135 | },
136 | aNumber: 7,
137 | },
138 | 'records/1234'
139 | ),
140 | makeDocumentSnapshot(
141 | {
142 | aString: 'foo',
143 | anObject: {
144 | a: 'qux',
145 | b: 'faz',
146 | },
147 | aNumber: 7,
148 | },
149 | 'records/1234'
150 | )
151 | );
152 | }
153 |
154 | /** @internal */
155 | export function objectToValueProto(data: object) {
156 | const encodeHelper = (val) => {
157 | if (typeof val === 'string') {
158 | return {
159 | stringValue: val,
160 | };
161 | }
162 | if (typeof val === 'boolean') {
163 | return {
164 | booleanValue: val,
165 | };
166 | }
167 | if (typeof val === 'number') {
168 | if (val % 1 === 0) {
169 | return {
170 | integerValue: val,
171 | };
172 | }
173 | return {
174 | doubleValue: val,
175 | };
176 | }
177 | if (val instanceof Date) {
178 | return {
179 | timestampValue: val.toISOString(),
180 | };
181 | }
182 | if (val instanceof Array) {
183 | let encodedElements = [];
184 | for (const elem of val) {
185 | const enc = encodeHelper(elem);
186 | if (enc) {
187 | encodedElements.push(enc);
188 | }
189 | }
190 | return {
191 | arrayValue: {
192 | values: encodedElements,
193 | },
194 | };
195 | }
196 | if (val === null) {
197 | // TODO: Look this up. This is a google.protobuf.NulLValue,
198 | // and everything in google.protobuf has a customized JSON encoder.
199 | // OTOH, Firestore's generated .d.ts files don't take this into
200 | // account and have the default proto layout.
201 | return {
202 | nullValue: 'NULL_VALUE',
203 | };
204 | }
205 | if (val instanceof Buffer || val instanceof Uint8Array) {
206 | return {
207 | bytesValue: val,
208 | };
209 | }
210 | if (val instanceof firestore.DocumentReference) {
211 | const projectId: string = get(val, '_referencePath.projectId');
212 | const database: string = get(val, '_referencePath.databaseId');
213 | const referenceValue: string = [
214 | 'projects',
215 | projectId,
216 | 'databases',
217 | database,
218 | val.path,
219 | ].join('/');
220 | return { referenceValue };
221 | }
222 | if (val instanceof firestore.Timestamp) {
223 | return {
224 | timestampValue: val.toDate().toISOString(),
225 | };
226 | }
227 | if (val instanceof firestore.GeoPoint) {
228 | return {
229 | geoPointValue: {
230 | latitude: val.latitude,
231 | longitude: val.longitude,
232 | },
233 | };
234 | }
235 | if (isPlainObject(val)) {
236 | return {
237 | mapValue: {
238 | fields: objectToValueProto(val),
239 | },
240 | };
241 | }
242 | throw new Error(
243 | 'Cannot encode ' +
244 | val +
245 | 'to a Firestore Value.' +
246 | ` Local testing does not yet support objects of type ${val?.constructor?.name}.`
247 | );
248 | };
249 |
250 | return mapValues(data, encodeHelper);
251 | }
252 |
253 | const FIRESTORE_ADDRESS_ENVS = [
254 | 'FIRESTORE_EMULATOR_HOST',
255 | 'FIREBASE_FIRESTORE_EMULATOR_ADDRESS',
256 | ];
257 |
258 | /** Clears all data in firestore. Works only in offline mode.
259 | */
260 | export function clearFirestoreData(options: { projectId: string } | string) {
261 | const FIRESTORE_ADDRESS = FIRESTORE_ADDRESS_ENVS.reduce(
262 | (addr, name) => process.env[name] || addr,
263 | 'localhost:8080'
264 | );
265 |
266 | return new Promise((resolve, reject) => {
267 | let projectId;
268 |
269 | if (typeof options === 'string') {
270 | projectId = options;
271 | } else if (typeof options === 'object' && has(options, 'projectId')) {
272 | projectId = options.projectId;
273 | } else {
274 | throw new Error('projectId not specified');
275 | }
276 |
277 | const config = {
278 | method: 'DELETE',
279 | hostname: FIRESTORE_ADDRESS.split(':')[0],
280 | port: FIRESTORE_ADDRESS.split(':')[1],
281 | path: `/emulator/v1/projects/${projectId}/databases/(default)/documents`,
282 | };
283 |
284 | const req = http.request(config, (res) => {
285 | if (res.statusCode !== 200) {
286 | reject(new Error(`statusCode: ${res.statusCode}`));
287 | }
288 | res.on('data', () => {});
289 | res.on('end', resolve);
290 | });
291 |
292 | req.on('error', (error) => {
293 | reject(error);
294 | });
295 |
296 | const postData = JSON.stringify({
297 | database: `projects/${projectId}/databases/(default)`,
298 | });
299 |
300 | req.setHeader('Content-Length', postData.length);
301 |
302 | req.write(postData);
303 | req.end();
304 | });
305 | }
306 |
--------------------------------------------------------------------------------
/src/providers/pubsub.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { pubsub } from 'firebase-functions/v1';
24 |
25 | /** Create a Message from a JSON object. */
26 | export function makeMessage(
27 | /** Content of message. */
28 | json: { [key: string]: any },
29 | /** Optional Pubsub message attributes. */
30 | attributes?: { [key: string]: string }
31 | ): pubsub.Message;
32 |
33 | /** Create a Message from a base-64 encoded string. */
34 | export function makeMessage(
35 | /** Base-64 encoded message string. */
36 | encodedString: string,
37 | /** Optional Pubsub message attributes. */
38 | attributes?: { [key: string]: string }
39 | ): pubsub.Message;
40 |
41 | export function makeMessage(
42 | jsonOrEncodedString: { [key: string]: any } | string,
43 | attributes?: { [key: string]: string }
44 | ): pubsub.Message {
45 | let data = jsonOrEncodedString;
46 | if (typeof data !== 'string') {
47 | try {
48 | data = new Buffer(JSON.stringify(data)).toString('base64');
49 | } catch (e) {
50 | throw new Error(
51 | 'Please provide either a JSON object or a base 64 encoded string.'
52 | );
53 | }
54 | }
55 | return new pubsub.Message({
56 | data,
57 | attributes: attributes || {},
58 | });
59 | }
60 |
61 | /** Fetch an example Message already populated with data. */
62 | export function exampleMessage(): pubsub.Message {
63 | return makeMessage({ message: 'Hello World!' });
64 | }
65 |
--------------------------------------------------------------------------------
/src/providers/storage.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { storage } from 'firebase-functions/v1';
24 |
25 | /** Create an ObjectMetadata */
26 | export function makeObjectMetadata(
27 | /** Fields of ObjectMetadata that you'd like to specify. */
28 | fields: { [key: string]: string }
29 | ): storage.ObjectMetadata {
30 | const configBucket = JSON.parse(process.env.FIREBASE_CONFIG || '{}')
31 | .storageBucket;
32 | const template = {
33 | kind: 'storage#object',
34 | id: '',
35 | bucket: configBucket || '',
36 | timeCreated: '',
37 | updated: '',
38 | storageClass: 'STANDARD',
39 | size: '',
40 | };
41 | return Object.assign(template, fields);
42 | }
43 |
44 | /** Fetch an example ObjectMetadata already populated with data. */
45 | export function exampleObjectMetadata(): storage.ObjectMetadata {
46 | return {
47 | bucket: 'bucket',
48 | contentDisposition: "inline; filename*=utf-8''my-file",
49 | contentType: 'application/octet-stream',
50 | crc32c: 'pbXl9g==',
51 | etag: 'CK/F2KHP79kCEAE=',
52 | generation: '1521161254347439',
53 | id: 'bucket/my-file/1521161254347439',
54 | kind: 'storage#object',
55 | md5Hash: 'nvpapVwyoKYUTPwuxMe3Sg==',
56 | mediaLink:
57 | 'https://www.googleapis.com/download/storage/v1/b/bucket/' +
58 | 'o/my-file?generation=1521161254347439&alt=media',
59 | metadata: {
60 | firebaseStorageDownloadTokens: 'fb577445-2f50-408b-80f2-c2f9991505b8',
61 | },
62 | metageneration: '1',
63 | name: 'my-file',
64 | selfLink: 'https://www.googleapis.com/storage/v1/b/bucket/o/my-file',
65 | size: '102101',
66 | storageClass: 'STANDARD',
67 | timeCreated: '2018-03-16T00:47:34.340Z',
68 | timeStorageClassUpdated: '2018-03-16T00:47:34.340Z',
69 | updated: '2018-03-16T00:47:34.340Z',
70 | };
71 | }
72 |
--------------------------------------------------------------------------------
/src/secretManager.ts:
--------------------------------------------------------------------------------
1 | /** Mock values returned by `functions.config()`. */
2 | export function mockSecretManager(conf: Record) {
3 | for (const [key, value] of Object.entries(conf)) {
4 | process.env[key] = value;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/v1.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { has, merge, random, get } from 'lodash';
24 |
25 | import {
26 | CloudFunction,
27 | EventContext,
28 | Change,
29 | https,
30 | config,
31 | database,
32 | firestore,
33 | HttpsFunction,
34 | Runnable,
35 | // @ts-ignore
36 | resetCache,
37 | } from 'firebase-functions/v1';
38 | import { Expression } from 'firebase-functions/params';
39 | import {
40 | getEventFilters,
41 | getEventType,
42 | resolveStringExpression,
43 | } from './cloudevent/mocks/helpers';
44 |
45 | /** Fields of the event context that can be overridden/customized. */
46 | export type EventContextOptions = {
47 | /** ID of the event. If omitted, a random ID will be generated. */
48 | eventId?: string;
49 | /** ISO time string of when the event occurred. If omitted, the current time is used. */
50 | timestamp?: string;
51 | /** The values for the wildcards in the reference path that a database or Firestore function is listening to.
52 | * If omitted, random values will be generated.
53 | */
54 | params?: { [option: string]: any };
55 | /** (Only for database functions and https.onCall.) Firebase auth variable representing the user that triggered
56 | * the function. Defaults to null.
57 | */
58 | auth?: any;
59 | /** (Only for database and https.onCall functions.) The authentication state of the user that triggered the function.
60 | * Default is 'UNAUTHENTICATED'.
61 | */
62 | authType?: 'ADMIN' | 'USER' | 'UNAUTHENTICATED';
63 |
64 | /** Resource is a standard format for defining a resource (google.rpc.context.AttributeContext.Resource).
65 | * In Cloud Functions, it is the resource that triggered the function - such as a storage bucket.
66 | */
67 | resource?: {
68 | service: string;
69 | name: string;
70 | type?: string;
71 | labels?: {
72 | [tag: string]: string;
73 | };
74 | };
75 | };
76 |
77 | /** Fields of the callable context that can be overridden/customized. */
78 | export type CallableContextOptions = {
79 | /**
80 | * The result of decoding and verifying a Firebase AppCheck token.
81 | */
82 | app?: any;
83 |
84 | /**
85 | * The result of decoding and verifying a Firebase Auth ID token.
86 | */
87 | auth?: any;
88 |
89 | /**
90 | * An unverified token for a Firebase Instance ID.
91 | */
92 | instanceIdToken?: string;
93 |
94 | /**
95 | * The raw HTTP request object.
96 | */
97 | rawRequest?: https.Request;
98 | };
99 |
100 | /* Fields for both Event and Callable contexts, checked at runtime */
101 | export type ContextOptions = T extends HttpsFunction & Runnable
102 | ? CallableContextOptions
103 | : EventContextOptions;
104 |
105 | /** A function that can be called with test data and optional override values for the event context.
106 | * It will subsequently invoke the cloud function it wraps with the provided test data and a generated event context.
107 | */
108 | export type WrappedFunction = (
109 | data: T,
110 | options?: ContextOptions
111 | ) => any | Promise;
112 |
113 | /** A scheduled function that can be called with optional override values for the event context.
114 | * It will subsequently invoke the cloud function it wraps with a generated event context.
115 | */
116 | export type WrappedScheduledFunction = (
117 | options?: ContextOptions
118 | ) => any | Promise;
119 |
120 | /** Takes a cloud function to be tested, and returns a WrappedFunction which can be called in test code. */
121 | export function wrapV1(
122 | cloudFunction: HttpsFunction & Runnable
123 | ): WrappedFunction>;
124 | export function wrapV1(
125 | cloudFunction: CloudFunction
126 | ): WrappedScheduledFunction | WrappedFunction> {
127 | if (!has(cloudFunction, '__endpoint')) {
128 | throw new Error(
129 | 'Wrap can only be called on functions written with the firebase-functions SDK.'
130 | );
131 | }
132 |
133 | if (has(cloudFunction, '__endpoint.scheduleTrigger')) {
134 | const scheduledWrapped: WrappedScheduledFunction = (
135 | options: ContextOptions
136 | ) => {
137 | // Although in Typescript we require `options` some of our JS samples do not pass it.
138 | options = options || {};
139 |
140 | _checkOptionValidity(['eventId', 'timestamp'], options);
141 | const defaultContext = _makeDefaultContext(cloudFunction, options);
142 | const context = merge({}, defaultContext, options);
143 |
144 | // @ts-ignore
145 | return cloudFunction.run(context);
146 | };
147 | return scheduledWrapped;
148 | }
149 |
150 | if (has(cloudFunction, '__endpoint.httpsTrigger')) {
151 | throw new Error(
152 | 'Wrap function is only available for `onCall` HTTP functions, not `onRequest`.'
153 | );
154 | }
155 |
156 | if (!has(cloudFunction, 'run')) {
157 | throw new Error(
158 | 'This library can only be used with functions written with firebase-functions v1.0.0 and above'
159 | );
160 | }
161 |
162 | const isCallableFunction = has(cloudFunction, '__endpoint.callableTrigger');
163 |
164 | let wrapped: WrappedFunction = (data, options) => {
165 | // Although in Typescript we require `options` some of our JS samples do not pass it.
166 | const _options = { ...options };
167 | let context;
168 |
169 | if (isCallableFunction) {
170 | _checkOptionValidity(
171 | ['app', 'auth', 'instanceIdToken', 'rawRequest'],
172 | _options
173 | );
174 | let callableContextOptions = _options as CallableContextOptions;
175 | context = {
176 | ...callableContextOptions,
177 | };
178 | } else {
179 | _checkOptionValidity(
180 | ['eventId', 'timestamp', 'params', 'auth', 'authType', 'resource'],
181 | _options
182 | );
183 | const defaultContext = _makeDefaultContext(cloudFunction, _options, data);
184 |
185 | if (
186 | has(defaultContext, 'eventType') &&
187 | defaultContext.eventType !== undefined &&
188 | defaultContext.eventType.match(/firebase.database/)
189 | ) {
190 | defaultContext.authType = 'UNAUTHENTICATED';
191 | defaultContext.auth = null;
192 | }
193 | context = merge({}, defaultContext, _options);
194 | }
195 |
196 | return cloudFunction.run(data, context);
197 | };
198 |
199 | return wrapped;
200 | }
201 |
202 | /** @internal */
203 | export function _makeResourceName(
204 | triggerResource: string | Expression,
205 | params = {}
206 | ): string {
207 | const resource = resolveStringExpression(triggerResource);
208 | const wildcardRegex = new RegExp('{[^/{}]*}', 'g');
209 | let resourceName = resource.replace(wildcardRegex, (wildcard) => {
210 | let wildcardNoBraces = wildcard.slice(1, -1); // .slice removes '{' and '}' from wildcard
211 | let sub = get(params, wildcardNoBraces);
212 | return sub || wildcardNoBraces + random(1, 9);
213 | });
214 | return resourceName;
215 | }
216 |
217 | function _makeEventId(): string {
218 | return (
219 | Math.random()
220 | .toString(36)
221 | .substring(2, 15) +
222 | Math.random()
223 | .toString(36)
224 | .substring(2, 15)
225 | );
226 | }
227 |
228 | function _checkOptionValidity(
229 | validFields: string[],
230 | options: { [s: string]: any }
231 | ) {
232 | Object.keys(options).forEach((key) => {
233 | if (validFields.indexOf(key) === -1) {
234 | throw new Error(
235 | `Options object ${JSON.stringify(options)} has invalid key "${key}"`
236 | );
237 | }
238 | });
239 | }
240 |
241 | function _makeDefaultContext(
242 | cloudFunction: HttpsFunction & Runnable,
243 | options: CallableContextOptions,
244 | triggerData?: T
245 | );
246 | function _makeDefaultContext(
247 | cloudFunction: CloudFunction,
248 | options: EventContextOptions,
249 | triggerData?: T
250 | ): EventContext {
251 | let eventContextOptions = options as EventContextOptions;
252 | const eventType = getEventType(cloudFunction);
253 | const eventResource = getEventFilters(cloudFunction).resource;
254 |
255 | const optionsParams = eventContextOptions.params ?? {};
256 | let triggerParams = {};
257 | if (eventResource && eventType && triggerData) {
258 | if (eventType.startsWith('google.firebase.database.ref.')) {
259 | let data: database.DataSnapshot;
260 | if (eventType.endsWith('.write')) {
261 | // Triggered with change
262 | if (!(triggerData instanceof Change)) {
263 | throw new Error('Must be triggered by database change');
264 | }
265 | data = triggerData.before;
266 | } else {
267 | data = triggerData as any;
268 | }
269 | triggerParams = _extractDatabaseParams(eventResource, data);
270 | } else if (eventType.startsWith('google.firestore.document.')) {
271 | let data: firestore.DocumentSnapshot;
272 | if (eventType.endsWith('.write')) {
273 | // Triggered with change
274 | if (!(triggerData instanceof Change)) {
275 | throw new Error('Must be triggered by firestore document change');
276 | }
277 | data = triggerData.before;
278 | } else {
279 | data = triggerData as any;
280 | }
281 | triggerParams = _extractFirestoreDocumentParams(eventResource, data);
282 | }
283 | }
284 | const params = { ...triggerParams, ...optionsParams };
285 |
286 | const defaultContext: EventContext = {
287 | eventId: _makeEventId(),
288 | resource: eventResource && {
289 | service: serviceFromEventType(eventType),
290 | name: _makeResourceName(eventResource, params),
291 | },
292 | eventType,
293 | timestamp: new Date().toISOString(),
294 | params,
295 | };
296 | return defaultContext;
297 | }
298 |
299 | function _extractDatabaseParams(
300 | triggerResource: string | Expression,
301 | data: database.DataSnapshot
302 | ): EventContext['params'] {
303 | const resource = resolveStringExpression(triggerResource);
304 | const path = data.ref.toString().replace(data.ref.root.toString(), '');
305 | return _extractParams(resource, path);
306 | }
307 |
308 | function _extractFirestoreDocumentParams(
309 | triggerResource: string | Expression,
310 | data: firestore.DocumentSnapshot
311 | ): EventContext['params'] {
312 | const resource = resolveStringExpression(triggerResource);
313 | // Resource format: databases/(default)/documents/
314 | return _extractParams(
315 | resource.replace(/^databases\/[^\/]+\/documents\//, ''),
316 | data.ref.path
317 | );
318 | }
319 |
320 | /**
321 | * Extracts the `{wildcard}` values from `dataPath`.
322 | * E.g. A wildcard path of `users/{userId}` with `users/FOO` would result in `{ userId: 'FOO' }`.
323 | * @internal
324 | */
325 | export function _extractParams(
326 | wildcardTriggerPath: string,
327 | dataPath: string
328 | ): EventContext['params'] {
329 | // Trim start and end / and split into path components
330 | const wildcardPaths = wildcardTriggerPath
331 | .replace(/^\/?(.*?)\/?$/, '$1')
332 | .split('/');
333 | const dataPaths = dataPath.replace(/^\/?(.*?)\/?$/, '$1').split('/');
334 | const params = {};
335 | if (wildcardPaths.length === dataPaths.length) {
336 | for (let idx = 0; idx < wildcardPaths.length; idx++) {
337 | const wildcardPath = wildcardPaths[idx];
338 | const name = wildcardPath.replace(/^{([^/{}]*)}$/, '$1');
339 | if (name !== wildcardPath) {
340 | // Wildcard parameter
341 | params[name] = dataPaths[idx];
342 | }
343 | }
344 | }
345 | return params;
346 | }
347 |
348 | function serviceFromEventType(eventType?: string): string {
349 | if (eventType) {
350 | const providerToService: Array<[string, string]> = [
351 | ['google.analytics', 'app-measurement.com'],
352 | ['google.firebase.auth', 'firebaseauth.googleapis.com'],
353 | ['google.firebase.database', 'firebaseio.com'],
354 | ['google.firestore', 'firestore.googleapis.com'],
355 | ['google.pubsub', 'pubsub.googleapis.com'],
356 | ['google.firebase.remoteconfig', 'firebaseremoteconfig.googleapis.com'],
357 | ['google.storage', 'storage.googleapis.com'],
358 | ['google.testing', 'testing.googleapis.com'],
359 | ];
360 |
361 | const match = providerToService.find(([provider]) => {
362 | eventType.includes(provider);
363 | });
364 | if (match) {
365 | return match[1];
366 | }
367 | }
368 | return 'unknown-service.googleapis.com';
369 | }
370 |
371 | /** Make a Change object to be used as test data for Firestore and real time database onWrite and onUpdate functions. */
372 | export function makeChange(before: T, after: T): Change {
373 | return Change.fromObjects(before, after);
374 | }
375 |
376 | /** Mock values returned by `functions.config()`. */
377 | export function mockConfig(conf: { [key: string]: { [key: string]: any } }) {
378 | if (resetCache) {
379 | resetCache();
380 | }
381 | process.env.CLOUD_RUNTIME_CONFIG = JSON.stringify(conf);
382 | }
383 |
--------------------------------------------------------------------------------
/src/v2.ts:
--------------------------------------------------------------------------------
1 | // The MIT License (MIT)
2 | //
3 | // Copyright (c) 2018 Firebase
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in all
13 | // copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | // SOFTWARE.
22 |
23 | import { CloudFunction, CloudEvent } from 'firebase-functions/v2';
24 | import { CallableFunction, CallableRequest } from 'firebase-functions/v2/https';
25 |
26 | import { generateCombinedCloudEvent } from './cloudevent/generate';
27 | import { DeepPartial } from './cloudevent/types';
28 | import * as express from 'express';
29 |
30 | /** A function that can be called with test data and optional override values for {@link CloudEvent}
31 | * It will subsequently invoke the cloud function it wraps with the provided {@link CloudEvent}
32 | */
33 | export type WrappedV2Function> = (
34 | cloudEventPartial?: DeepPartial
35 | ) => any | Promise;
36 |
37 | export type WrappedV2CallableFunction = (
38 | data: CallableRequest
39 | ) => T | Promise;
40 |
41 | function isCallableV2Function>(
42 | cf: CloudFunction | CallableFunction
43 | ): cf is CallableFunction {
44 | return !!cf?.__endpoint?.callableTrigger;
45 | }
46 |
47 | function assertIsCloudFunction>(
48 | cf: CloudFunction | CallableFunction
49 | ): asserts cf is CloudFunction {
50 | if (!('run' in cf) || !cf.run) {
51 | throw new Error(
52 | 'This library can only be used with functions written with firebase-functions v3.20.0 and above'
53 | );
54 | }
55 | }
56 |
57 | /**
58 | * Takes a v2 cloud function to be tested, and returns a {@link WrappedV2Function}
59 | * which can be called in test code.
60 | */
61 | export function wrapV2>(
62 | cloudFunction: CloudFunction
63 | ): WrappedV2Function;
64 |
65 | /**
66 | * Takes a v2 HTTP function to be tested, and returns a {@link WrappedV2HttpsFunction}
67 | * which can be called in test code.
68 | */
69 | export function wrapV2(
70 | cloudFunction: CallableFunction
71 | ): WrappedV2CallableFunction;
72 |
73 | export function wrapV2>(
74 | cloudFunction: CloudFunction | CallableFunction
75 | ): WrappedV2Function | WrappedV2CallableFunction {
76 | if (isCallableV2Function(cloudFunction)) {
77 | return (req: CallableRequest) => {
78 | return cloudFunction.run(req);
79 | };
80 | }
81 |
82 | assertIsCloudFunction(cloudFunction);
83 |
84 | if (cloudFunction?.__endpoint?.platform !== 'gcfv2') {
85 | throw new Error('This function can only wrap V2 CloudFunctions.');
86 | }
87 |
88 | return (cloudEventPartial?: DeepPartial) => {
89 | const cloudEvent = generateCombinedCloudEvent(
90 | cloudFunction,
91 | cloudEventPartial
92 | );
93 | return cloudFunction.run(cloudEvent);
94 | };
95 | }
96 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["es6"],
4 | "module": "commonjs",
5 | "noImplicitAny": false,
6 | "outDir": ".tmp",
7 | "sourceMap": true,
8 | "target": "es6",
9 | "typeRoots": ["node_modules/@types"]
10 | },
11 | "include": ["src/**/*.ts", "spec/**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.release.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "lib": ["es6"],
5 | "module": "commonjs",
6 | "noImplicitAny": false,
7 | "outDir": "lib",
8 | "stripInternal": true,
9 | "target": "es6",
10 | "typeRoots": ["node_modules/@types"]
11 | },
12 | "files": ["src/index.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tslint:recommended",
3 | "rules": {
4 | "quotemark": [true, "single", "avoid-escape"],
5 | "interface-name": [false],
6 | "interface-over-type-literal": false,
7 | "variable-name": [true, "check-format", "allow-leading-underscore"],
8 | "object-literal-sort-keys": false,
9 | "ordered-imports": false,
10 | "prefer-const": false,
11 | "trailing-comma": [
12 | true,
13 | {
14 | "functions": "never"
15 | }
16 | ],
17 | "whitespace": [true],
18 | "member-access": [false],
19 | "no-console": [false],
20 | "no-namespace": [false],
21 | "no-unused-expression": false,
22 | "no-empty": false,
23 | "unified-signatures": false
24 | }
25 | }
26 |
--------------------------------------------------------------------------------