diff --git a/.gcloudignore b/.gcloudignore new file mode 100644 index 0000000..6a596c9 --- /dev/null +++ b/.gcloudignore @@ -0,0 +1,3 @@ +venv +build +!integration_test/.env \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c1d44da..be336d8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -7,6 +7,25 @@ on: - main jobs: + integration-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: 3.12 + - name: Setup Node 18 + uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install Node dependencies + run: | + cd integration_tests && npm install + - name: Run Integration Tests + run: | + cd integration_tests && npm start + test: runs-on: ubuntu-latest strategy: @@ -51,11 +70,11 @@ jobs: - name: Lint with pylint run: | source venv/bin/activate - python3.10 -m pylint $(git ls-files '*.py') + python3.10 -m pylint src tests - name: Lint with mypy run: | source venv/bin/activate - python3.10 -m mypy . + python3.10 -m mypy src tests docs: runs-on: ubuntu-latest diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..327b351 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,22 @@ +steps: + - name: "gcr.io/cloud-builders/git" + id: "Checkout tests" + dir: "integration_test" + entrypoint: "bash" + args: + - "./checkout.sh" + - name: "node:18" + entrypoint: "npm" + dir: "integration_test" + args: ["install"] + - name: "node:18" + entrypoint: "npx" + dir: "integration_test" + args: ["firebase", "use", "cf3-integration-tests-d7be6"] + - name: "gcr.io/cf3-integration-tests-d7be6/node_python:0.0.2" + entrypoint: "npm" + dir: "integration_test" + args: ["start"] + +options: + defaultLogsBucketBehavior: REGIONAL_USER_OWNED_BUCKET diff --git a/integration_test/.env.example b/integration_test/.env.example new file mode 100644 index 0000000..fd451d0 --- /dev/null +++ b/integration_test/.env.example @@ -0,0 +1,11 @@ +TEST_RUNTIME=python +REGION=us-central1 +PROJECT_ID= +DATABASE_URL= +STORAGE_BUCKET= +FIREBASE_ADMIN=6.5.0 +FIREBASE_APP_ID= +FIREBASE_MEASUREMENT_ID= +FIREBASE_AUTH_DOMAIN= +FIREBASE_API_KEY= +GOOGLE_ANALYTICS_API_SECRET= diff --git a/integration_test/builders/python-node/Dockerfile b/integration_test/builders/python-node/Dockerfile new file mode 100644 index 0000000..04e3f07 --- /dev/null +++ b/integration_test/builders/python-node/Dockerfile @@ -0,0 +1,10 @@ +# Use a combined base image with Python 3.11 and Node 18 +FROM python:3.11 + +RUN python -m pip install virtualenv + +# Install Node.js 18 using the Nodesource repository +RUN apt-get update && \ + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/integration_test/checkout.sh b/integration_test/checkout.sh new file mode 100755 index 0000000..1b4283e --- /dev/null +++ b/integration_test/checkout.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env sh + +set -ex + +cleanup() { + current_dir_name=$(basename "$PWD") + + if [ "$current_dir_name" = ".tmp" ]; then + cd .. + fi + + if [ -z "$DEBUG" ]; then + rm -rf .tmp + fi +} + +trap cleanup EXIT + +if [ -d ".tmp" ]; then + rm -rf .tmp +fi + +mkdir .tmp +cd .tmp + +git init +git remote add origin https://github.com/firebase/firebase-functions.git +git config core.sparseCheckout true +mkdir -p .git/info +echo 'integration_test/**/*' > .git/info/sparse-checkout +git fetch origin v2-integration-tests +git pull origin v2-integration-tests +git checkout v2-integration-tests + +rm -rf integration_test/functions +rm -f integration_test/.env.example + +mv integration_test/* ../ +for file in integration_test/.[!.]* integration_test/..?*; do + [ -e "$file" ] && mv "$file" ../ +done + +cd .. +rm -rf tests/v1 +rm -rf .tmp diff --git a/integration_test/functions/.gitignore b/integration_test/functions/.gitignore new file mode 100644 index 0000000..6fd881e --- /dev/null +++ b/integration_test/functions/.gitignore @@ -0,0 +1,2 @@ +*.local +venv \ No newline at end of file diff --git a/integration_test/functions/main.py b/integration_test/functions/main.py new file mode 100644 index 0000000..06541d1 --- /dev/null +++ b/integration_test/functions/main.py @@ -0,0 +1,14 @@ +from firebase_admin import initialize_app +from v2.database_tests import * +from v2.eventarc_tests import * +from v2.firestore_tests import * +from v2.https_tests import * +from v2.identity_tests import * +from v2.pubsub_tests import * +from v2.remote_config_tests import * +from v2.scheduler_tests import * +from v2.storage_tests import * +from v2.tasks_tests import * +from v2.test_lab_tests import * + +initialize_app() diff --git a/integration_test/functions/region.py b/integration_test/functions/region.py new file mode 100644 index 0000000..8db215e --- /dev/null +++ b/integration_test/functions/region.py @@ -0,0 +1 @@ +REGION = "us-central1" diff --git a/integration_test/functions/v2/database_tests.py b/integration_test/functions/v2/database_tests.py new file mode 100644 index 0000000..10ff5a2 --- /dev/null +++ b/integration_test/functions/v2/database_tests.py @@ -0,0 +1,76 @@ +import json + +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.db_fn import (on_value_created, Event, on_value_deleted, + on_value_updated, on_value_written, + Change) + +from region import REGION + + +@on_value_created(reference="databaseCreatedTests/{testId}/start", + region=REGION) +def databaseCreatedTests(event: Event[object]): + test_id = event.params['testId'] + + firestore.client().collection("databaseCreatedTests").document(test_id).set( + { + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + "url": event.reference, + }) + + +@on_value_deleted(reference="databaseDeletedTests/{testId}/start", + region=REGION) +def databaseDeletedTests(event: Event[object]): + test_id = event.params['testId'] + + firestore.client().collection("databaseDeletedTests").document(test_id).set( + { + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + "url": event.reference, + }) + + +@on_value_updated(reference="databaseUpdatedTests/{testId}/start", + region=REGION) +def databaseUpdatedTests(event: Event[Change[object]]): + test_id = event.params['testId'] + data = event.data.after + + firestore.client().collection("databaseUpdatedTests").document(test_id).set( + { + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + "url": event.reference, + "data": json.dumps(data if data is not None else {}) + }) + + +@on_value_written(reference="databaseWrittenTests/{testId}/start", + region=REGION) +def databaseWrittenTests(event: Event[Change[object]]): + test_id = event.params['testId'] + if event.data.after is None: + logger.info( + f"Event for {test_id} is None; presuming data cleanup, so skipping." + ) + return + + firestore.client().collection("databaseWrittenTests").document(test_id).set( + { + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + "url": event.reference, + }) diff --git a/integration_test/functions/v2/eventarc_tests.py b/integration_test/functions/v2/eventarc_tests.py new file mode 100644 index 0000000..f4574cc --- /dev/null +++ b/integration_test/functions/v2/eventarc_tests.py @@ -0,0 +1,19 @@ +import json + +from firebase_admin import firestore +from firebase_functions.eventarc_fn import (on_custom_event_published, + CloudEvent) + + +@on_custom_event_published(event_type="achieved-leaderboard") +def eventarcOnCustomEventPublishedTests(event: CloudEvent): + test_id = event.data["testId"] + + firestore.client().collection( + "eventarcOnCustomEventPublishedTests").document(test_id).set({ + "id": event.id, + "type": event.type, + "time": event.time, + "source": event.source, + "data": json.dumps(event.data), + }) diff --git a/integration_test/functions/v2/firestore_tests.py b/integration_test/functions/v2/firestore_tests.py new file mode 100644 index 0000000..41eab84 --- /dev/null +++ b/integration_test/functions/v2/firestore_tests.py @@ -0,0 +1,67 @@ +from firebase_admin import firestore +from firebase_functions.firestore_fn import (on_document_created, Event, + DocumentSnapshot, + on_document_deleted, + on_document_updated, + on_document_written) +from region import REGION + + +@on_document_created(document='tests/{documentId}', + region=REGION, + timeout_sec=540) +def firestoreOnDocumentCreatedTests(event: Event[DocumentSnapshot]): + document_id = event.params['documentId'] + + firestore.client().collection('firestoreOnDocumentCreatedTests').document( + document_id).set({ + 'time': event.time, + 'id': event.id, + 'type': event.type, + 'source': event.source, + }) + + +@on_document_deleted(document='tests/{documentId}', + region=REGION, + timeout_sec=540) +def firestoreOnDocumentDeletedTests(event: Event[DocumentSnapshot]): + document_id = event.params['documentId'] + + firestore.client().collection('firestoreOnDocumentDeletedTests').document( + document_id).set({ + 'time': event.time, + 'id': event.id, + 'type': event.type, + 'source': event.source, + }) + + +@on_document_updated(document='tests/{documentId}', + region=REGION, + timeout_sec=540) +def firestoreOnDocumentUpdatedTests(event: Event[DocumentSnapshot]): + document_id = event.params['documentId'] + + firestore.client().collection('firestoreOnDocumentUpdatedTests').document( + document_id).set({ + 'time': event.time, + 'id': event.id, + 'type': event.type, + 'source': event.source, + }) + + +@on_document_written(document='tests/{documentId}', + region=REGION, + timeout_sec=540) +def firestoreOnDocumentWrittenTests(event: Event[DocumentSnapshot]): + document_id = event.params['documentId'] + + firestore.client().collection('firestoreOnDocumentWrittenTests').document( + document_id).set({ + 'time': event.time, + 'id': event.id, + 'type': event.type, + 'source': event.source, + }) diff --git a/integration_test/functions/v2/https_tests.py b/integration_test/functions/v2/https_tests.py new file mode 100644 index 0000000..2b3355d --- /dev/null +++ b/integration_test/functions/v2/https_tests.py @@ -0,0 +1,19 @@ +from firebase_admin import firestore +from firebase_functions.https_fn import (on_call, CallableRequest, on_request, + Request) + +from region import REGION + + +@on_call(invoker="private", region=REGION) +def httpsOnCallV2Tests(request: CallableRequest): + data = request.data + firestore.client().collection("httpsOnCallV2Tests").document( + data["testId"]).set(data) + + +@on_request(invoker="private", region=REGION) +def httpsOnRequestV2Tests(request: Request): + data = request.json + firestore.client().collection("httpsOnRequestV2Tests").document( + data["testId"]).set(data) diff --git a/integration_test/functions/v2/identity_tests.py b/integration_test/functions/v2/identity_tests.py new file mode 100644 index 0000000..f2c37c9 --- /dev/null +++ b/integration_test/functions/v2/identity_tests.py @@ -0,0 +1,32 @@ +from firebase_admin import firestore +from firebase_functions.identity_fn import (before_user_created, + BeforeCreateResponse, + before_user_signed_in, + BeforeSignInResponse, + AuthBlockingEvent) + + +@before_user_created() +def identityBeforeUserCreatedTests( + event: AuthBlockingEvent) -> BeforeCreateResponse: + uid = event.data.uid + + firestore.client().collection("identityBeforeUserCreatedTests").document( + uid).set({ + "eventId": event.event_id, + "timestamp": event.timestamp, + }) + + return BeforeCreateResponse(**event.data.__dict__) + + +@before_user_signed_in() +def identityBeforeUserSignedInTests( + event: AuthBlockingEvent) -> BeforeSignInResponse: + uid = event.data.uid + + firestore.client().collection("identityBeforeUserSignedInTests").document( + uid).set({ + "eventId": event.event_id, + "timestamp": event.timestamp, + }) diff --git a/integration_test/functions/v2/pubsub_tests.py b/integration_test/functions/v2/pubsub_tests.py new file mode 100644 index 0000000..b12ae6d --- /dev/null +++ b/integration_test/functions/v2/pubsub_tests.py @@ -0,0 +1,29 @@ +import json + +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.pubsub_fn import (on_message_published, CloudEvent, + MessagePublishedData) + +from region import REGION + + +@on_message_published(topic="custom_message_tests", region=REGION) +def pubsubOnMessagePublishedTests( + event: CloudEvent[MessagePublishedData[dict]]) -> None: + json_data = event.data.message.json + if json_data is None: + logger.error("Message is not JSON") + return + + test_id = json_data["testId"] + + firestore.client().collection("pubsubOnMessagePublishedTests").document( + test_id).set({ + "id": event.id, + "source": event.source, + "subject": event.subject, + "time": event.time, + "type": event.type, + "message": json.dumps(event.data.message), + }) diff --git a/integration_test/functions/v2/remote_config_tests.py b/integration_test/functions/v2/remote_config_tests.py new file mode 100644 index 0000000..f3b6bf5 --- /dev/null +++ b/integration_test/functions/v2/remote_config_tests.py @@ -0,0 +1,19 @@ +from firebase_admin import firestore +from firebase_functions.remote_config_fn import (on_config_updated, CloudEvent, + ConfigUpdateData) + +from region import REGION + + +@on_config_updated(region=REGION) +def remoteConfigOnConfigUpdatedTests( + event: CloudEvent[ConfigUpdateData]) -> None: + test_id = event.data.description + + firestore.client().collection("remoteConfigOnConfigUpdatedTests").document( + test_id).set({ + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + }) diff --git a/integration_test/functions/v2/scheduler_tests.py b/integration_test/functions/v2/scheduler_tests.py new file mode 100644 index 0000000..c4f8dd6 --- /dev/null +++ b/integration_test/functions/v2/scheduler_tests.py @@ -0,0 +1,16 @@ +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.scheduler_fn import (on_schedule, ScheduledEvent) + +from region import REGION + + +@on_schedule(schedule="every 10 hours", region=REGION) +def schedule(event: ScheduledEvent): + test_id = event.job_name + if test_id is None: + logger.error("TestId not found for scheduled function execution") + return + + firestore.client().collection("schedulerOnScheduleV2Tests").document( + test_id).set({"success": True}) diff --git a/integration_test/functions/v2/storage_tests.py b/integration_test/functions/v2/storage_tests.py new file mode 100644 index 0000000..4065ce9 --- /dev/null +++ b/integration_test/functions/v2/storage_tests.py @@ -0,0 +1,57 @@ +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.storage_fn import (on_object_deleted, + on_object_finalized, + on_object_metadata_updated, + CloudEvent, StorageObjectData) + +from region import REGION + + +# TODO: (b/372315689) Re-enable function once bug is fixed +# @on_object_deleted(region=REGION) +# def storageOnDeleteTests(event: CloudEvent[StorageObjectData]) -> None: +# test_id = event.data.name.split(".")[0] +# if test_id is None: +# logger.error("TestId not found for storage onObjectDeleted") +# return +# +# firestore.client().collection("storageOnObjectDeletedTests").document( +# test_id).set({ +# "id": event.id, +# "time": event.time, +# "type": event.type, +# "source": event.source, +# }) + + +@on_object_finalized(region=REGION) +def storageOnFinalizeTests(event: CloudEvent[StorageObjectData]) -> None: + test_id = event.data.name.split(".")[0] + if test_id is None: + logger.error("TestId not found for storage onObjectFinalized") + return + + firestore.client().collection("storageOnObjectFinalizedTests").document( + test_id).set({ + "id": event.id, + "time": event.time, + "type": event.type, + "source": event.source, + }) + + +@on_object_metadata_updated(region=REGION) +def storageOnMetadataUpdateTests(event: CloudEvent[StorageObjectData]) -> None: + test_id = event.data.name.split(".")[0] + if test_id is None: + logger.error("TestId not found for storage onObjectMetadataUpdated") + return + + firestore.client().collection( + "storageOnObjectMetadataUpdatedTests").document(test_id).set({ + "id": event.id, + "time": event.time, + "type": event.type, + "source": event.source, + }) diff --git a/integration_test/functions/v2/tasks_tests.py b/integration_test/functions/v2/tasks_tests.py new file mode 100644 index 0000000..6afb745 --- /dev/null +++ b/integration_test/functions/v2/tasks_tests.py @@ -0,0 +1,13 @@ +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.tasks_fn import (on_task_dispatched, CallableRequest) + +from region import REGION + + +@on_task_dispatched(region=REGION) +def tasksOnTaskDispatchedTests(request: CallableRequest): + test_id = request.data["testId"] + + firestore.client().collection("tasksOnTaskDispatchedTests").document( + test_id).set({"testId": test_id}) diff --git a/integration_test/functions/v2/test_lab_tests.py b/integration_test/functions/v2/test_lab_tests.py new file mode 100644 index 0000000..2ad5cdd --- /dev/null +++ b/integration_test/functions/v2/test_lab_tests.py @@ -0,0 +1,21 @@ +from firebase_admin import firestore +from firebase_functions import logger +from firebase_functions.test_lab_fn import (on_test_matrix_completed, + CloudEvent, TestMatrixCompletedData) + +from region import REGION + + +@on_test_matrix_completed(region=REGION) +def testLabOnTestMatrixCompletedTests( + event: CloudEvent[TestMatrixCompletedData]) -> None: + test_id = event.data.client_info.details["testId"] + + firestore.client().collection("testLabOnTestMatrixCompletedTests").document( + test_id).set({ + "testId": test_id, + "type": event.type, + "id": event.id, + "time": event.time, + "state": event.data.state + }) diff --git a/integration_test/requirements.txt.template b/integration_test/requirements.txt.template new file mode 100644 index 0000000..6603fc5 --- /dev/null +++ b/integration_test/requirements.txt.template @@ -0,0 +1,2 @@ +firebase_admin==__FIREBASE_ADMIN__ +__LOCAL_FIREBASE_FUNCTIONS__ \ No newline at end of file