diff --git a/.github/resources/integ-service-account.json.gpg b/.github/resources/integ-service-account.json.gpg index 7740dccd8..5a52805c9 100644 Binary files a/.github/resources/integ-service-account.json.gpg and b/.github/resources/integ-service-account.json.gpg differ diff --git a/.github/scripts/generate_changelog.sh b/.github/scripts/generate_changelog.sh index e393f40e4..a0afa6e41 100755 --- a/.github/scripts/generate_changelog.sh +++ b/.github/scripts/generate_changelog.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/scripts/publish_preflight_check.sh b/.github/scripts/publish_preflight_check.sh index 632960eb8..691192d53 100755 --- a/.github/scripts/publish_preflight_check.sh +++ b/.github/scripts/publish_preflight_check.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/scripts/run_all_tests.sh b/.github/scripts/run_all_tests.sh index c47961b32..96d02d5be 100755 --- a/.github/scripts/run_all_tests.sh +++ b/.github/scripts/run_all_tests.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2020 Google Inc. +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70970b34e..f4f1ac58b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,10 +12,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ matrix.go }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9a8197957..44b949504 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,4 +1,4 @@ -# Copyright 2021 Google Inc. +# Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -29,12 +29,12 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.event.client_payload.ref || github.ref }} - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.23' @@ -53,7 +53,7 @@ jobs: - name: Send email on failure if: failure() - uses: firebase/firebase-admin-node/.github/actions/send-email@master + uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} @@ -68,7 +68,7 @@ jobs: - name: Send email on cancelled if: cancelled() - uses: firebase/firebase-admin-node/.github/actions/send-email@master + uses: firebase/firebase-admin-node/.github/actions/send-email@2e2b36a84ba28679bcb7aecdacabfec0bded2d48 # Admin Node SDK v13.6.0 with: api-key: ${{ secrets.OSS_BOT_MAILGUN_KEY }} domain: ${{ secrets.OSS_BOT_MAILGUN_DOMAIN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfb0d04e6..533b4e0c9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -# Copyright 2020 Google Inc. +# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -36,16 +36,12 @@ jobs: runs-on: ubuntu-latest - # When manually triggering the build, the requester can specify a target branch or a tag - # via the 'ref' client parameter. steps: - name: Check out code - uses: actions/checkout@v4 - with: - ref: ${{ github.event.client_payload.ref || github.ref }} + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: '1.23' @@ -71,57 +67,34 @@ jobs: # 3. with the label 'release:publish', and # 4. the title prefix '[chore] Release '. if: github.event.pull_request.merged && - github.ref == 'refs/heads/dev' && + github.base_ref == 'dev' && contains(github.event.pull_request.labels.*.name, 'release:publish') && startsWith(github.event.pull_request.title, '[chore] Release ') runs-on: ubuntu-latest permissions: - contents: write + pull-requests: write steps: - name: Checkout source for publish - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: + ref: dev persist-credentials: false - name: Publish preflight check id: preflight run: ./.github/scripts/publish_preflight_check.sh - # We authorize this step with an access token that has write access to the master branch. - - name: Merge to master - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.FIREBASE_GITHUB_TOKEN }} - script: | - github.rest.repos.merge({ - owner: context.repo.owner, - repo: context.repo.repo, - base: 'master', - head: 'dev' - }) - - # See: https://cli.github.com/manual/gh_release_create - - name: Create release tag + # Create a PR to merge dev into master. + - name: Create Release PR env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release create ${{ steps.preflight.outputs.version }} - --title "Firebase Admin Go SDK ${{ steps.preflight.outputs.version }}" - --notes '${{ steps.preflight.outputs.changelog }}' - --target "master" - - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - - name: Post to Twitter - if: success() && - contains(github.event.pull_request.labels.*.name, 'release:tweet') - uses: firebase/firebase-admin-node/.github/actions/send-tweet@master - with: - status: > - ${{ steps.preflight.outputs.version }} of @Firebase Admin Go SDK is available. - https://github.com/firebase/firebase-admin-go/releases/tag/${{ steps.preflight.outputs.version }} - consumer-key: ${{ secrets.FIREBASE_TWITTER_CONSUMER_KEY }} - consumer-secret: ${{ secrets.FIREBASE_TWITTER_CONSUMER_SECRET }} - access-token: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN }} - access-token-secret: ${{ secrets.FIREBASE_TWITTER_ACCESS_TOKEN_SECRET }} - continue-on-error: true + RELEASE_BODY: ${{ steps.preflight.outputs.changelog }} + RELEASE_TITLE: "[chore] Release ${{ steps.preflight.outputs.version }}" + run: | + gh pr create \ + --base master \ + --head dev \ + --title "$RELEASE_TITLE" \ + --body "$RELEASE_BODY" diff --git a/.github/workflows/tag_release.yml b/.github/workflows/tag_release.yml new file mode 100644 index 000000000..a209e7042 --- /dev/null +++ b/.github/workflows/tag_release.yml @@ -0,0 +1,49 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Tag Release + +on: + push: + branches: + - master + paths: + - 'firebase.go' + +jobs: + tag_release: + if: startsWith(github.event.head_commit.message, '[chore] Release ') + + runs-on: ubuntu-latest + environment: Release + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Publish preflight check + id: preflight + run: ./.github/scripts/publish_preflight_check.sh + + - name: Create release tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_VER: ${{ steps.preflight.outputs.version }} + RELEASE_NOTES: ${{ steps.preflight.outputs.changelog }} + run: gh release create "$RELEASE_VER" \ + --title "Firebase Admin Go SDK $RELEASE_VER" \ + --notes "$RELEASE_NOTES" \ + --target "master" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..b2d6d89a7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,124 @@ +# Firebase Admin Go SDK - Agent Guide + +This document provides a comprehensive guide for AI agents to understand the conventions, design patterns, and architecture of the Firebase Admin Go SDK. Adhering to these guidelines is crucial for making idiomatic and consistent code contributions. + +## 1. High-Level Overview + +The Firebase Admin Go SDK enables server-side (backend) applications to interact with Firebase services. Its design emphasizes idiomatic Go, thread-safety, and a consistent, discoverable API surface. + +## 2. Directory Structure + +- `firebase.go`: The primary entry point for initializing a Firebase `App` instance. +- `internal/`: Contains private implementation details, such as HTTP clients and utility functions, that are not part of the public API. +- `auth/`, `db/`, `messaging/`, etc.: Each directory contains a specific Firebase service client. +- `*_test.go`: Unit tests are located alongside the code they test (e.g., `auth/auth_test.go`). +- `integration/`: Contains integration tests that make live network calls to Firebase services. +- `snippets/`: Contains code snippets used in documentation. +- `errorutils/`: Contains common error type checkers and other error handling utils. +- `testdata/`: Contains mock data used in some tests. + +## 3. Core Design Patterns + +- **Initialization:** The SDK is initialized by creating an `App` instance via `firebase.NewApp()`. This `App` object is the central point for accessing all service clients. +- **Service Clients:** Service clients (e.g., `auth.Client`, `db.Client`) are obtained from the `App` instance (e.g., `app.Auth(ctx)`). These clients are lightweight and are typically created as needed. +- **Error Handling:** Errors are handled using standard Go idioms. Firbase errors are defined in `internal/errors.go` however these errors can be further modified within each service. This modification is applied using that service's set `internal.HTTPClient.CreateErrFn` value. +- **HTTP Communication:** All outgoing HTTP requests are managed by a centralized client located in `internal/http_client.go`. This ensures consistent handling of authentication, retries, and error parsing. +- **Asynchronous Operations:** The SDK uses `context.Context` to manage deadlines, cancellations, and request-scoped values for all asynchronous operations. + +## 4. Coding Style and Naming Conventions + +- **Naming:** + - Public functions, types, and fields use `PascalCase`. + - Private functions and types use `camelCase`. + - Constants are written in `PascalCase`. + +## 5. Testing Philosophy + +- **Unit Tests:** Unit tests follow the `*_test.go` naming pattern and are placed in the same directory as the code under test. They use standard Go testing packages and mocks to isolate dependencies. +- **Integration Tests:** Integration tests are located in the `integration/` directory. They are designed to run against actual Firebase services and require a configured Firebase project. + +## 6. Dependency Management + +- **Manager:** The SDK uses Go Modules for dependency management. +- **Manifest:** Dependencies are declared in the `go.mod` file. +- **Command:** To add or update dependencies, use `go get` or `go mod tidy`. + +## 7. Critical Developer Journeys + +### Journey 1: How to Add a New API Method + +1. **Define Public Method:** Add the new method or change to the appropriate service client files (e.g., `auth/user_mgt.go`). +2. **Internal Logic:** Implement the core logic within the service package. +3. **HTTP Client:** Use the client in `internal/http_client.go` to make the API calls. +4. **Error Handling:** New or updated error codes implemented in the appropriate location. +5. **Testing:** + - Add unit tests in the corresponding `*_test.go` file (e.g., `auth/user_mgt_test.go`). + - Add integration tests in the `integration/` directory if applicable. +6. **Snippets:** (Optional) Add or update code snippets in the `snippets/` directory. + +### Journey 2: How to Deprecate a Field/Method in an Existing API + +1. **Add Deprecation Note:** Locate where the deprecated object is defined and add a deprecation warning with a note (e.g. `// Deprecated: Use X instead.`). + +## 8. Critical Do's and Don'ts + +- **DO:** Use the centralized HTTP client in `internal/http_client.go` for all network calls. +- **DO:** Pass `context.Context` as the first argument to all functions that perform I/O or other blocking operations. +- **DO:** Run `go fmt` after implementing a change and fix any linting errors. +- **DON'T:** Expose types or functions from the `internal/` directory in the public API. +- **DON'T:** Introduce new third-party dependencies without a strong, documented justification and team consensus. + +## 9. Branch Creation +- When creating a new barnch use the format `agentName-short-description`. + * Example: `jules-auth-token-parsing` + * Example: `gemini-add-storage-file-signer` + + +## 10. Commit and Pull Request Generation + +After implementing and testing a change, you may create a commit and pull request which must follow the following these rules: + +### Commit and Pull Request Title Format: +Use the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification: `type(scope): subject` +- `type` should be one of `feat`, `fix` or `chore`. +- `scope` should be the service package changed (e.g., `auth`, `rtdb`, `deps`). + - **Note**: Some services use specific abbreviations. Use the abbreviation if one exists. Common abbreviations include: + - `messaging` -> `fcm` + - `dataconnect` -> `fdc` + - `database` -> `rtdb` + - `appcheck` -> `fac` +- `subject` should be a brief summary of the change depending on the action: + - For pull requests this should focus on the larger goal the included commits achieve. + - Example: `fix(auth): Resolved issue with custom token verification` + - For commits this should focus on the specific changes made in that commit. + - Example: `fix(auth): Added a new token verification check` + +### Commit Body: + This should be a brief explanation of code changes. + +Example: +``` +feat(fcm): Added `SendEachForMulticast` support for multicast messages + +Added a new `SendEachForMulticast` method to the messaging client. This method wraps the `SendEach` method and sends the same message to each token. +``` + +### Pull Request Body: +- A brief explanation of the problem and the solution. +- A summary of the testing strategy (e.g., "Added a new unit test to verify the fix."). +- A **Context Sources** section that lists the `id` and repository path of every `AGENTS.md` file you used. + +Example: +``` +feat(fcm): Added support for multicast messages + +This change introduces a new `SendEachForMulticast` method to the messaging client, allowing developers to send a single message to multiple tokens efficiently. + +Testing: Added unit tests in `messaging_test.go` with a mock server and an integration test in `integration/messaging_test.go`. + +Context Sources Used: +- id: firebase-admin-go (`/AGENTS.md`) +``` + +## 11. Metadata +- id: firebase-admin-go \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eacfcda42..901fad267 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -148,8 +148,12 @@ Set up your Firebase project as follows: 2. Enable Firestore: 1. Go to the Firebase Console, and select **Firestore Database** from the **Build** menu. - 2. Click on the **Create database** button. You can choose to set up Firestore either in - the production mode or in the test mode. + 2. Click on the **Create database** button and create a default database. You can choose + to set up Firestore either in the production mode or in the test mode. + > **Note:** Integration tests are run against both the default database and an additional + database named "testing-database". + 3. After the default database is created, click the **Add database** button to create a + second database named "testing-database". 3. Enable Realtime Database: diff --git a/appcheck/appcheck.go b/appcheck/appcheck.go index 89868916e..c2fefaa88 100644 --- a/appcheck/appcheck.go +++ b/appcheck/appcheck.go @@ -1,4 +1,4 @@ -// Copyright 2022 Google Inc. All Rights Reserved. +// Copyright 2022 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/auth.go b/auth/auth.go index a70286985..6bd3b0f27 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/auth_appengine.go b/auth/auth_appengine.go index 06eee2a9f..1d2c2cc0a 100644 --- a/auth/auth_appengine.go +++ b/auth/auth_appengine.go @@ -1,7 +1,7 @@ //go:build appengine // +build appengine -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/auth_std.go b/auth/auth_std.go index e5a9cb87d..b8eda30b0 100644 --- a/auth/auth_std.go +++ b/auth/auth_std.go @@ -1,7 +1,7 @@ //go:build !appengine // +build !appengine -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/auth_test.go b/auth/auth_test.go index 9c8b15236..69127b22f 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/email_action_links.go b/auth/email_action_links.go index 282e79fec..bc08d401b 100644 --- a/auth/email_action_links.go +++ b/auth/email_action_links.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/email_action_links_test.go b/auth/email_action_links_test.go index 2575ff32b..8ed55e66a 100644 --- a/auth/email_action_links_test.go +++ b/auth/email_action_links_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/export_users.go b/auth/export_users.go index 7e5670a71..5b086b807 100644 --- a/auth/export_users.go +++ b/auth/export_users.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/hash/hash.go b/auth/hash/hash.go index dce1217db..b18f8bd6b 100644 --- a/auth/hash/hash.go +++ b/auth/hash/hash.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/hash/hash_test.go b/auth/hash/hash_test.go index 675e7d8f6..c1406ac60 100644 --- a/auth/hash/hash_test.go +++ b/auth/hash/hash_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/import_users.go b/auth/import_users.go index 6de0c37b7..6a1e1ff9f 100644 --- a/auth/import_users.go +++ b/auth/import_users.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/multi_factor_config_mgt.go b/auth/multi_factor_config_mgt.go index d01e2f46c..8e0bc1097 100644 --- a/auth/multi_factor_config_mgt.go +++ b/auth/multi_factor_config_mgt.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. All Rights Reserved. +// Copyright 2023 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/multi_factor_config_mgt_test.go b/auth/multi_factor_config_mgt_test.go index ebbef4ebd..be31774fb 100644 --- a/auth/multi_factor_config_mgt_test.go +++ b/auth/multi_factor_config_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. All Rights Reserved. +// Copyright 2023 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/project_config_mgt.go b/auth/project_config_mgt.go index 5ae739cda..510eb9156 100644 --- a/auth/project_config_mgt.go +++ b/auth/project_config_mgt.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. All Rights Reserved. +// Copyright 2023 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/project_config_mgt_test.go b/auth/project_config_mgt_test.go index ec0fa7564..ad19dde36 100644 --- a/auth/project_config_mgt_test.go +++ b/auth/project_config_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. All Rights Reserved. +// Copyright 2023 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/provider_config.go b/auth/provider_config.go index 312fa3491..c33eaf0dd 100644 --- a/auth/provider_config.go +++ b/auth/provider_config.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/provider_config_test.go b/auth/provider_config_test.go index 0defec125..4e2e25513 100644 --- a/auth/provider_config_test.go +++ b/auth/provider_config_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/tenant_mgt.go b/auth/tenant_mgt.go index 0e3ab2059..35b27de1b 100644 --- a/auth/tenant_mgt.go +++ b/auth/tenant_mgt.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/tenant_mgt_test.go b/auth/tenant_mgt_test.go index 77c268519..539d24e50 100644 --- a/auth/tenant_mgt_test.go +++ b/auth/tenant_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -90,6 +90,35 @@ func TestTenantGetUser(t *testing.T) { } } +func TestTenantQueryUsers(t *testing.T) { + resp := `{ + "usersInfo": [], + "recordsCount": "0" + }` + s := echoServer([]byte(resp), t) + defer s.Close() + + tenantClient, err := s.Client.TenantManager.AuthForTenant("test-tenant") + if err != nil { + t.Fatalf("Failed to create tenant client: %v", err) + } + + returnUserInfo := true + query := &QueryUsersRequest{ + ReturnUserInfo: &returnUserInfo, + } + + _, err = tenantClient.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() with tenant client = %v", err) + } + + wantPath := "/projects/mock-project-id/tenants/test-tenant/accounts:query" + if s.Req[0].RequestURI != wantPath { + t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath) + } +} + func TestTenantGetUserByEmail(t *testing.T) { s := echoServer(testGetUserResponse, t) defer s.Close() diff --git a/auth/token_generator.go b/auth/token_generator.go index 7aa2d5648..fed6facf5 100644 --- a/auth/token_generator.go +++ b/auth/token_generator.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/token_generator_test.go b/auth/token_generator_test.go index c79e3a9aa..6ed5144e4 100644 --- a/auth/token_generator_test.go +++ b/auth/token_generator_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/token_verifier.go b/auth/token_verifier.go index 25e7bdc0f..fe6cdf0c9 100644 --- a/auth/token_verifier.go +++ b/auth/token_verifier.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/token_verifier_test.go b/auth/token_verifier_test.go index e11c56d92..e24d7d1c8 100644 --- a/auth/token_verifier_test.go +++ b/auth/token_verifier_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/auth/user_mgt.go b/auth/user_mgt.go index 63a5c3814..05e123a18 100644 --- a/auth/user_mgt.go +++ b/auth/user_mgt.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -1048,6 +1048,178 @@ func (c *baseClient) GetUsers( return &GetUsersResult{userRecords, notFound}, nil } +// QueryUserInfoResponse is the response from the QueryUsers function. +type QueryUserInfoResponse struct { + Users []*UserRecord + Count int64 +} + +type queryUsersResponse struct { + Users []*userQueryResponse `json:"userInfo"` + Count int64 `json:"recordsCount,string,omitempty"` +} + +// Expression represents a query condition used to filter results. +// +// Specify only one of Email, PhoneNumber, or UID. If you specify more than one, +// only the first (in order of Email, PhoneNumber, then UID) is applied. +type Expression struct { + // Email is a case-insensitive string that the account's email must match. + Email string `json:"email,omitempty"` + // PhoneNumber is a string that the account's phone number must match. + PhoneNumber string `json:"phoneNumber,omitempty"` + // UID is a string that the account's local ID must match. + UID string `json:"userId,omitempty"` +} + +// QueryUsersRequest is the request structure for the QueryUsers function. +type QueryUsersRequest struct { + // ReturnUserInfo specifies whether to return user accounts that match the query. + // If set to false, only the count of matching accounts is returned. + // Defaults to true. + ReturnUserInfo *bool `json:"returnUserInfo,omitempty"` + // Limit is the maximum number of accounts to return with an upper limit of 500. + // Defaults to 500. This field is valid only when ReturnUserInfo is true. + Limit int64 `json:"limit,string,omitempty"` + // Offset is the number of accounts to skip from the beginning of matching records. + // This field is valid only when ReturnUserInfo is true. + Offset int64 `json:"offset,string,omitempty"` + // SortBy is the field to use for sorting user accounts. + SortBy SortBy `json:"-"` + // Order is the sort order for the query results. + Order Order `json:"-"` + // TenantID is the ID of the tenant to which the results are scoped. + TenantID string `json:"tenantId,omitempty"` + // Expression is a list of query conditions used to filter the results. + Expression []*Expression `json:"expression,omitempty"` +} + +// build builds the query request (for internal use only). +func (q *QueryUsersRequest) build() interface{} { + var sortBy string + if q.SortBy != sortByUnspecified { + sortBys := map[SortBy]string{ + UID: "USER_ID", + Name: "NAME", + CreatedAt: "CREATED_AT", + LastLoginAt: "LAST_LOGIN_AT", + UserEmail: "USER_EMAIL", + } + sortBy = sortBys[q.SortBy] + } + + var order string + if q.Order != orderUnspecified { + orders := map[Order]string{ + Asc: "ASC", + Desc: "DESC", + } + order = orders[q.Order] + } + + type queryUsersRequestInternal QueryUsersRequest + internal := (*queryUsersRequestInternal)(q) + if internal.ReturnUserInfo == nil { + t := true + internal.ReturnUserInfo = &t + } + + return &struct { + SortBy string `json:"sortBy,omitempty"` + Order string `json:"order,omitempty"` + *queryUsersRequestInternal + }{ + SortBy: sortBy, + Order: order, + queryUsersRequestInternal: internal, + } +} + +func (q *QueryUsersRequest) validate() error { + if q.Limit != 0 && (q.Limit < 1 || q.Limit > 500) { + return fmt.Errorf("limit must be between 1 and 500") + } + if q.Offset < 0 { + return fmt.Errorf("offset must be non-negative") + } + for _, exp := range q.Expression { + if exp.Email != "" { + if err := validateEmail(exp.Email); err != nil { + return err + } + } + if exp.PhoneNumber != "" { + if err := validatePhone(exp.PhoneNumber); err != nil { + return err + } + } + if exp.UID != "" { + if err := validateUID(exp.UID); err != nil { + return err + } + } + } + return nil +} + +// SortBy defines the fields available for sorting user accounts. +type SortBy int + +const ( + sortByUnspecified SortBy = iota + // UID sorts results by user ID. + UID + // Name sorts results by name. + Name + // CreatedAt sorts results by creation time. + CreatedAt + // LastLoginAt sorts results by the last login time. + LastLoginAt + // UserEmail sorts results by user email. + UserEmail +) + +// Order defines the sort order for query results. +type Order int + +const ( + orderUnspecified Order = iota + // Asc sorts results in ascending order. + Asc + // Desc sorts results in descending order. + Desc +) + +// QueryUsers queries for user accounts based on the provided query configuration. +func (c *baseClient) QueryUsers(ctx context.Context, query *QueryUsersRequest) (*QueryUserInfoResponse, error) { + if query == nil { + return nil, fmt.Errorf("query request must not be nil") + } + if err := query.validate(); err != nil { + return nil, err + } + + var parsed queryUsersResponse + _, err := c.post(ctx, "/accounts:query", query.build(), &parsed) + if err != nil { + return nil, err + } + + var userRecords []*UserRecord + for _, user := range parsed.Users { + userRecord, err := user.makeUserRecord() + if err != nil { + return nil, fmt.Errorf("error while parsing response: %w", err) + } + userRecords = append(userRecords, userRecord) + } + + return &QueryUserInfoResponse{ + Users: userRecords, + Count: parsed.Count, + }, nil +} + type userQueryResponse struct { UID string `json:"localId,omitempty"` DisplayName string `json:"displayName,omitempty"` @@ -1510,7 +1682,7 @@ func parseErrorResponse(resp *internal.Response) (string, string) { idx := strings.Index(code, ":") if idx != -1 { detail = strings.TrimSpace(code[idx+1:]) - code = code[:idx] + code = strings.TrimSpace(code[:idx]) } return code, detail diff --git a/auth/user_mgt_test.go b/auth/user_mgt_test.go index 53ccdc580..094716c2f 100644 --- a/auth/user_mgt_test.go +++ b/auth/user_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -1899,6 +1899,247 @@ func TestDeleteUsers(t *testing.T) { }) } +func TestQueryUsers(t *testing.T) { + resp := `{ + "userInfo": [{ + "localId": "testuser", + "email": "testuser@example.com", + "phoneNumber": "+1234567890", + "emailVerified": true, + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "validSince": "1494364393", + "disabled": false, + "createdAt": "1234567890000", + "lastLoginAt": "1233211232000", + "customAttributes": "{\"admin\": true, \"package\": \"gold\"}", + "tenantId": "testTenant", + "providerUserInfo": [{ + "providerId": "password", + "displayName": "Test User", + "photoUrl": "http://www.example.com/testuser/photo.png", + "email": "testuser@example.com", + "rawId": "testuid" + }, { + "providerId": "phone", + "phoneNumber": "+1234567890", + "rawId": "testuid" + }], + "mfaInfo": [{ + "phoneInfo": "+1234567890", + "mfaEnrollmentId": "enrolledPhoneFactor", + "displayName": "My MFA Phone", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + }, { + "totpInfo": {}, + "mfaEnrollmentId": "enrolledTOTPFactor", + "displayName": "My MFA TOTP", + "enrolledAt": "2021-03-03T13:06:20.542896Z" + }] + }], + "recordsCount": "1" + }` + s := echoServer([]byte(resp), t) + defer s.Close() + + returnUserInfo := true + query := &QueryUsersRequest{ + ReturnUserInfo: &returnUserInfo, + Limit: 1, + SortBy: UserEmail, + Order: Asc, + Expression: []*Expression{ + { + Email: "testuser@example.com", + }, + }, + } + + result, err := s.Client.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() = %v", err) + } + + if len(result.Users) != 1 { + t.Fatalf("QueryUsers() returned %d users; want 1", len(result.Users)) + } + + if result.Count != 1 { + t.Errorf("QueryUsers() returned count %d; want 1", result.Count) + } + + if !reflect.DeepEqual(result.Users[0], testUser) { + t.Errorf("QueryUsers() = %#v; want = %#v", result.Users[0], testUser) + } + + wantPath := "/projects/mock-project-id/accounts:query" + if s.Req[0].RequestURI != wantPath { + t.Errorf("QueryUsers() URL = %q; want = %q", s.Req[0].RequestURI, wantPath) + } +} + +func TestQueryUsersError(t *testing.T) { + resp := `{ + "error": { + "message": "INVALID_QUERY" + } + }` + s := echoServer([]byte(resp), t) + defer s.Close() + s.Status = http.StatusBadRequest + + returnUserInfo := true + query := &QueryUsersRequest{ + ReturnUserInfo: &returnUserInfo, + Limit: 1, + SortBy: UserEmail, + Order: Asc, + Expression: []*Expression{ + { + Email: "testuser@example.com", + }, + }, + } + + result, err := s.Client.QueryUsers(context.Background(), query) + if result != nil || err == nil { + t.Fatalf("QueryUsers() = (%v, %v); want = (nil, error)", result, err) + } +} + +func TestQueryUsersNilQuery(t *testing.T) { + s := echoServer([]byte("{}"), t) + defer s.Close() + result, err := s.Client.QueryUsers(context.Background(), nil) + if result != nil || err == nil { + t.Fatalf("QueryUsers(nil) = (%v, %v); want = (nil, error)", result, err) + } +} + +func TestQueryUsersMalformedCustomAttributes(t *testing.T) { + resp := `{ + "userInfo": [{ + "localId": "testuser", + "customAttributes": "invalid-json" + }] + }` + s := echoServer([]byte(resp), t) + defer s.Close() + query := &QueryUsersRequest{} + result, err := s.Client.QueryUsers(context.Background(), query) + if result != nil || err == nil { + t.Fatalf("QueryUsers() = (%v, %v); want = (nil, error)", result, err) + } +} + +func TestQueryUsersMalformedLastRefreshTimestamp(t *testing.T) { + resp := `{ + "userInfo": [{ + "localId": "testuser", + "lastRefreshAt": "invalid-timestamp" + }] + }` + s := echoServer([]byte(resp), t) + defer s.Close() + query := &QueryUsersRequest{} + result, err := s.Client.QueryUsers(context.Background(), query) + if result != nil || err == nil { + t.Fatalf("QueryUsers() = (%v, %v); want = (nil, error)", result, err) + } +} + +func TestQueryUsersDefaultReturnUserInfo(t *testing.T) { + resp := `{ + "userInfo": [{ + "localId": "testuser" + }], + "recordsCount": "1" + }` + s := echoServer([]byte(resp), t) + defer s.Close() + + // ReturnUserInfo is nil, should default to true in build() + query := &QueryUsersRequest{ + Limit: 1, + } + + _, err := s.Client.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() = %v", err) + } + + var got map[string]interface{} + if err := json.Unmarshal(s.Rbody, &got); err != nil { + t.Fatal(err) + } + + if got["returnUserInfo"] != true { + t.Errorf("QueryUsers() request[\"returnUserInfo\"] = %v; want true", got["returnUserInfo"]) + } +} + +func TestQueryUsersValidation(t *testing.T) { + s := echoServer([]byte("{}"), t) + defer s.Close() + + tests := []struct { + name string + query *QueryUsersRequest + }{ + { + name: "Invalid Limit Low", + query: &QueryUsersRequest{ + Limit: -1, + }, + }, + { + name: "Invalid Limit High", + query: &QueryUsersRequest{ + Limit: 501, + }, + }, + { + name: "Invalid Offset", + query: &QueryUsersRequest{ + Offset: -1, + }, + }, + { + name: "Invalid Email in Expression", + query: &QueryUsersRequest{ + Expression: []*Expression{ + {Email: "invalid-email"}, + }, + }, + }, + { + name: "Invalid Phone in Expression", + query: &QueryUsersRequest{ + Expression: []*Expression{ + {PhoneNumber: "invalid-phone"}, + }, + }, + }, + { + name: "Invalid UID in Expression", + query: &QueryUsersRequest{ + Expression: []*Expression{ + {UID: string(make([]byte, 129))}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := s.Client.QueryUsers(context.Background(), tt.query) + if err == nil { + t.Errorf("QueryUsers() with %s; want error, got nil", tt.name) + } + }) + } +} + func TestMakeExportedUser(t *testing.T) { queryResponse := &userQueryResponse{ UID: "testuser", @@ -2191,7 +2432,7 @@ func TestHTTPErrorWithCode(t *testing.T) { } func TestAuthErrorWithCodeAndDetails(t *testing.T) { - resp := []byte(`{"error":{"message":"USER_NOT_FOUND: extra details"}}`) + resp := []byte(`{"error":{"message":"USER_NOT_FOUND : extra details"}}`) s := echoServer(resp, t) defer s.Close() s.Client.baseClient.httpClient.RetryConfig = nil diff --git a/db/auth_override_test.go b/db/auth_override_test.go index 02f2671a3..c179bd74b 100644 --- a/db/auth_override_test.go +++ b/db/auth_override_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/db.go b/db/db.go index f53a197ca..f30c534c9 100644 --- a/db/db.go +++ b/db/db.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/db_test.go b/db/db_test.go index 5766c0452..685171891 100644 --- a/db/db_test.go +++ b/db/db_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/query.go b/db/query.go index 424e00a8e..ca826db9a 100644 --- a/db/query.go +++ b/db/query.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/query_test.go b/db/query_test.go index 9c72971e6..df05bf1c2 100644 --- a/db/query_test.go +++ b/db/query_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/ref.go b/db/ref.go index bb26a531c..e41af43fb 100644 --- a/db/ref.go +++ b/db/ref.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/db/ref_test.go b/db/ref_test.go index 969eedc5f..b34277001 100644 --- a/db/ref_test.go +++ b/db/ref_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/errorutils/errorutils.go b/errorutils/errorutils.go index fe81b756d..ebf8d1976 100644 --- a/errorutils/errorutils.go +++ b/errorutils/errorutils.go @@ -1,4 +1,4 @@ -// Copyright 2020 Google Inc. All Rights Reserved. +// Copyright 2020 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/firebase.go b/firebase.go index 6101a8d99..d9709f98f 100644 --- a/firebase.go +++ b/firebase.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ import ( var defaultAuthOverrides = make(map[string]interface{}) // Version of the Firebase Go Admin SDK. -const Version = "4.18.0" +const Version = "4.19.0" // firebaseEnvName is the name of the environment variable with the Config. const firebaseEnvName = "FIREBASE_CONFIG" @@ -105,10 +105,16 @@ func (a *App) Storage(ctx context.Context) (*storage.Client, error) { // Firestore returns a new firestore.Client instance from the https://godoc.org/cloud.google.com/go/firestore // package. func (a *App) Firestore(ctx context.Context) (*firestore.Client, error) { + return a.FirestoreWithDatabaseID(ctx, firestore.DefaultDatabaseID) +} + +// FirestoreWithDatabaseID returns a new firestore.Client instance with the specified named database from the +// https://godoc.org/cloud.google.com/go/firestore package. +func (a *App) FirestoreWithDatabaseID(ctx context.Context, databaseID string) (*firestore.Client, error) { if a.projectID == "" { return nil, errors.New("project id is required to access Firestore") } - return firestore.NewClient(ctx, a.projectID, a.opts...) + return firestore.NewClientWithDatabase(ctx, a.projectID, databaseID, a.opts...) } // InstanceID returns an instance of iid.Client. diff --git a/firebase_test.go b/firebase_test.go index 7830225e6..ddc130a10 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -287,6 +287,18 @@ func TestFirestore(t *testing.T) { } } +func TestFirestoreWithDatabaseID(t *testing.T) { + ctx := context.Background() + app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json")) + if err != nil { + t.Fatal(err) + } + + if c, err := app.FirestoreWithDatabaseID(ctx, "other-db"); c == nil || err != nil { + t.Errorf("FirestoreWithDatabaseID() = (%v, %v); want (client, nil)", c, err) + } +} + func TestFirestoreWithProjectID(t *testing.T) { verify := func(varName string) { current := os.Getenv(varName) @@ -336,6 +348,10 @@ func TestFirestoreWithNoProjectID(t *testing.T) { if c, err := app.Firestore(ctx); c != nil || err == nil { t.Errorf("Firestore() = (%v, %v); want (nil, error)", c, err) } + + if c, err := app.FirestoreWithDatabaseID(ctx, "other-db"); c != nil || err == nil { + t.Errorf("FirestoreWithDatabaseID() = (%v, %v); want (nil, error)", c, err) + } } func TestInstanceID(t *testing.T) { diff --git a/iid/iid.go b/iid/iid.go index 7a3e9b555..c97f6e0fe 100644 --- a/iid/iid.go +++ b/iid/iid.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/iid/iid_test.go b/iid/iid_test.go index 208da3575..5597cb629 100644 --- a/iid/iid_test.go +++ b/iid/iid_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/auth/auth_test.go b/integration/auth/auth_test.go index 668099720..3bf43461e 100644 --- a/integration/auth/auth_test.go +++ b/integration/auth/auth_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/auth/project_config_mgt_test.go b/integration/auth/project_config_mgt_test.go index 502840056..72df927bc 100644 --- a/integration/auth/project_config_mgt_test.go +++ b/integration/auth/project_config_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2023 Google Inc. All Rights Reserved. +// Copyright 2023 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/auth/provider_config_test.go b/integration/auth/provider_config_test.go index f0c9f2851..7d789d7aa 100644 --- a/integration/auth/provider_config_test.go +++ b/integration/auth/provider_config_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/auth/tenant_mgt_test.go b/integration/auth/tenant_mgt_test.go index 8682cb332..c5ab6de2a 100644 --- a/integration/auth/tenant_mgt_test.go +++ b/integration/auth/tenant_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -427,6 +427,23 @@ func testTenantAwareUserManagement(t *testing.T, id string) { } }) + t.Run("QueryUsers()", func(t *testing.T) { + query := &auth.QueryUsersRequest{ + Expression: []*auth.Expression{ + { + Email: want.Email, + }, + }, + } + result, err := tenantClient.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() = %v", err) + } + if len(result.Users) != 1 || result.Users[0].UID != user.UID { + t.Errorf("QueryUsers(email=%s) = %v; want user %s", want.Email, result.Users, user.UID) + } + }) + t.Run("DeleteUser()", func(t *testing.T) { if err := tenantClient.DeleteUser(context.Background(), user.UID); err != nil { t.Fatalf("DeleteUser() = %v", err) diff --git a/integration/auth/user_mgt_test.go b/integration/auth/user_mgt_test.go index 1c37cd0ab..399c3fe21 100644 --- a/integration/auth/user_mgt_test.go +++ b/integration/auth/user_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import ( const ( continueURL = "http://localhost/?a=1&b=2#c=3" + invalidContinueURL = "http://www.localhost/?a=1&b=2#c=3" continueURLKey = "continueUrl" oobCodeKey = "oobCode" modeKey = "mode" @@ -1297,6 +1298,19 @@ func TestEmailSignInLink(t *testing.T) { } } +func TestAuthErrorParse(t *testing.T) { + user := newUserWithParams(t) + defer deleteUser(user.UID) + _, err := client.EmailSignInLink(context.Background(), user.Email, &auth.ActionCodeSettings{ + URL: invalidContinueURL, + HandleCodeInApp: false, + }) + want := "domain of the continue url is not whitelisted: " + if err == nil || !auth.IsUnauthorizedContinueURI(err) || !strings.HasPrefix(err.Error(), want) { + t.Errorf("EmailSignInLink() expected error, got: %s, want: %s", err, want) + } +} + func resetPassword(email, oldPassword, newPassword, oobCode string) error { req := map[string]interface{}{ "email": email, @@ -1428,3 +1442,39 @@ func deletePhoneNumberUser(t *testing.T, phoneNumber string) { t.Fatal(err) } } +func TestQueryUsers(t *testing.T) { + u1 := newUserWithParams(t) + defer deleteUser(u1.UID) + u2 := newUserWithParams(t) + defer deleteUser(u2.UID) + + // Query by email + query := &auth.QueryUsersRequest{ + Expression: []*auth.Expression{ + { + Email: u1.Email, + }, + }, + } + result, err := client.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() = %v", err) + } + if len(result.Users) != 1 || result.Users[0].UID != u1.UID { + t.Errorf("QueryUsers(uid=%s) = %v; want user %s", u1.UID, result.Users, u1.UID) + } + + // Query with limit and sort + query = &auth.QueryUsersRequest{ + Limit: 2, + SortBy: auth.CreatedAt, + Order: auth.Desc, + } + result, err = client.QueryUsers(context.Background(), query) + if err != nil { + t.Fatalf("QueryUsers() = %v", err) + } + if len(result.Users) < 2 { + t.Errorf("QueryUsers(limit=2) = %d users; want >= 2", len(result.Users)) + } +} diff --git a/integration/db/db_test.go b/integration/db/db_test.go index 93be1eef9..9a5d715a8 100644 --- a/integration/db/db_test.go +++ b/integration/db/db_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/db/query_test.go b/integration/db/query_test.go index 6bb48cf02..392cb4981 100644 --- a/integration/db/query_test.go +++ b/integration/db/query_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/firestore/firestore_test.go b/integration/firestore/firestore_test.go index 8e3dd60fa..3300f882d 100644 --- a/integration/firestore/firestore_test.go +++ b/integration/firestore/firestore_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,18 +16,42 @@ package firestore import ( "context" + "flag" "log" + "os" "reflect" "testing" "firebase.google.com/go/v4/integration/internal" ) -func TestFirestore(t *testing.T) { +const testDatabaseID = "testing-database" + +var ( + cityData = map[string]interface{}{ + "name": "Mountain View", + "country": "USA", + "population": int64(77846), + "capital": false, + } + movieData = map[string]interface{}{ + "Name": "Interstellar", + "Year": int64(2014), + "Runtime": "2h 49m", + "Academy Award Winner": true, + } +) + +func TestMain(m *testing.M) { + flag.Parse() if testing.Short() { log.Println("skipping Firestore integration tests in short mode.") - return + os.Exit(0) } + os.Exit(m.Run()) +} + +func TestFirestore(t *testing.T) { ctx := context.Background() app, err := internal.NewTestApp(ctx, nil) if err != nil { @@ -40,23 +64,93 @@ func TestFirestore(t *testing.T) { } doc := client.Collection("cities").Doc("Mountain View") - data := map[string]interface{}{ - "name": "Mountain View", - "country": "USA", - "population": int64(77846), - "capital": false, + if _, err := doc.Set(ctx, cityData); err != nil { + t.Fatal(err) + } + defer doc.Delete(ctx) + + snap, err := doc.Get(ctx) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(snap.Data(), cityData) { + t.Errorf("Get() = %v; want %v", snap.Data(), cityData) + } +} + +func TestFirestoreWithDatabaseID(t *testing.T) { + ctx := context.Background() + app, err := internal.NewTestApp(ctx, nil) + if err != nil { + t.Fatal(err) + } + + // This test requires the target non-default database to exist in the project. + // If it doesn't exist, this test will fail. + client, err := app.FirestoreWithDatabaseID(ctx, testDatabaseID) + if err != nil { + t.Fatal(err) } - if _, err := doc.Set(ctx, data); err != nil { + + doc := client.Collection("cities").NewDoc() + if _, err := doc.Set(ctx, cityData); err != nil { t.Fatal(err) } + defer doc.Delete(ctx) + snap, err := doc.Get(ctx) if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(snap.Data(), data) { - t.Errorf("Get() = %v; want %v", snap.Data(), data) + if !reflect.DeepEqual(snap.Data(), cityData) { + t.Errorf("Get() = %v; want %v", snap.Data(), cityData) } - if _, err := doc.Delete(ctx); err != nil { +} + +func TestFirestoreMultiDB(t *testing.T) { + ctx := context.Background() + app, err := internal.NewTestApp(ctx, nil) + if err != nil { t.Fatal(err) } + + cityClient, err := app.Firestore(ctx) + if err != nil { + t.Fatal(err) + } + // This test requires the target non-default database to exist in the project. + // If it doesn't exist, this test will fail. + movieClient, err := app.FirestoreWithDatabaseID(ctx, testDatabaseID) + if err != nil { + t.Fatal(err) + } + + cityDoc := cityClient.Collection("cities").NewDoc() + movieDoc := movieClient.Collection("movies").NewDoc() + + if _, err := cityDoc.Set(ctx, cityData); err != nil { + t.Fatal(err) + } + defer cityDoc.Delete(ctx) + + if _, err := movieDoc.Set(ctx, movieData); err != nil { + t.Fatal(err) + } + defer movieDoc.Delete(ctx) + + citySnap, err := cityDoc.Get(ctx) + if err != nil { + t.Fatal(err) + } + movieSnap, err := movieDoc.Get(ctx) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(citySnap.Data(), cityData) { + t.Errorf("City Get() = %v; want %v", citySnap.Data(), cityData) + } + if !reflect.DeepEqual(movieSnap.Data(), movieData) { + t.Errorf("Movie Get() = %v; want %v", movieSnap.Data(), movieData) + } } diff --git a/integration/iid/iid_test.go b/integration/iid/iid_test.go index a14b2fdf4..ff9067dd8 100644 --- a/integration/iid/iid_test.go +++ b/integration/iid/iid_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/internal/internal.go b/integration/internal/internal.go index 3a7948cc5..d57b3f91f 100644 --- a/integration/internal/internal.go +++ b/integration/internal/internal.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index e32aac9d1..4552988a7 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/integration/storage/storage_test.go b/integration/storage/storage_test.go index 9912c6645..860288bce 100644 --- a/integration/storage/storage_test.go +++ b/integration/storage/storage_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/errors.go b/internal/errors.go index e209d158c..2c8b5d9c0 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -1,4 +1,4 @@ -// Copyright 2020 Google Inc. All Rights Reserved. +// Copyright 2020 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/errors_test.go b/internal/errors_test.go index 3733429ee..ccd38f204 100644 --- a/internal/errors_test.go +++ b/internal/errors_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Google Inc. All Rights Reserved. +// Copyright 2020 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/http_client.go b/internal/http_client.go index 9463f98e6..9d1257bc5 100644 --- a/internal/http_client.go +++ b/internal/http_client.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/http_client_test.go b/internal/http_client_test.go index 2d8d97c46..22b498cf3 100644 --- a/internal/http_client_test.go +++ b/internal/http_client_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/internal.go b/internal/internal.go index a6eb1294b..58450f45c 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/json_http_client_test.go b/internal/json_http_client_test.go index 15ebed732..be827b75a 100644 --- a/internal/json_http_client_test.go +++ b/internal/json_http_client_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/messaging.go b/messaging/messaging.go index 8f484684b..2eda22a90 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/messaging_batch.go b/messaging/messaging_batch.go index 365190a4b..031295876 100644 --- a/messaging/messaging_batch.go +++ b/messaging/messaging_batch.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/messaging_batch_test.go b/messaging/messaging_batch_test.go index e8603eaef..ed9a1a429 100644 --- a/messaging/messaging_batch_test.go +++ b/messaging/messaging_batch_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 659a0c962..0cb720659 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/messaging_utils.go b/messaging/messaging_utils.go index e69dd652f..5716b3286 100644 --- a/messaging/messaging_utils.go +++ b/messaging/messaging_utils.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/topic_mgt.go b/messaging/topic_mgt.go index 884925859..0e7e9d0c7 100644 --- a/messaging/topic_mgt.go +++ b/messaging/topic_mgt.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/messaging/topic_mgt_test.go b/messaging/topic_mgt_test.go index 6d58114d6..1b0adba9a 100644 --- a/messaging/topic_mgt_test.go +++ b/messaging/topic_mgt_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 Google Inc. All Rights Reserved. +// Copyright 2019 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/condition_evaluator.go b/remoteconfig/condition_evaluator.go index 02efd4517..accbb3593 100644 --- a/remoteconfig/condition_evaluator.go +++ b/remoteconfig/condition_evaluator.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/condition_evaluator_test.go b/remoteconfig/condition_evaluator_test.go index c7265cffa..07b816360 100644 --- a/remoteconfig/condition_evaluator_test.go +++ b/remoteconfig/condition_evaluator_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/remoteconfig.go b/remoteconfig/remoteconfig.go index 7c6228d11..55117cf54 100644 --- a/remoteconfig/remoteconfig.go +++ b/remoteconfig/remoteconfig.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/remoteconfig_test.go b/remoteconfig/remoteconfig_test.go index 14578ab5c..f9a39f8ae 100644 --- a/remoteconfig/remoteconfig_test.go +++ b/remoteconfig/remoteconfig_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/server_config.go b/remoteconfig/server_config.go index fc06a235e..4aa1668cb 100644 --- a/remoteconfig/server_config.go +++ b/remoteconfig/server_config.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/server_template.go b/remoteconfig/server_template.go index b6d6e9ecd..693d32b66 100644 --- a/remoteconfig/server_template.go +++ b/remoteconfig/server_template.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/server_template_test.go b/remoteconfig/server_template_test.go index d09e4e2a8..076e17191 100644 --- a/remoteconfig/server_template_test.go +++ b/remoteconfig/server_template_test.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/remoteconfig/server_template_types.go b/remoteconfig/server_template_types.go index 059e66fd3..4986e0dd7 100644 --- a/remoteconfig/server_template_types.go +++ b/remoteconfig/server_template_types.go @@ -1,4 +1,4 @@ -// Copyright 2025 Google Inc. All Rights Reserved. +// Copyright 2025 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/snippets/auth.go b/snippets/auth.go index df0033399..a3f8cd8aa 100644 --- a/snippets/auth.go +++ b/snippets/auth.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. +// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/snippets/db.go b/snippets/db.go index 9277ff04d..e37e182a3 100644 --- a/snippets/db.go +++ b/snippets/db.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. All Rights Reserved. +// Copyright 2018 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/snippets/init.go b/snippets/init.go index 3125a702a..d7db13130 100644 --- a/snippets/init.go +++ b/snippets/init.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. +// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/snippets/messaging.go b/snippets/messaging.go index 2b073dcbc..1b25cb1c8 100644 --- a/snippets/messaging.go +++ b/snippets/messaging.go @@ -1,4 +1,4 @@ -// Copyright 2018 Google Inc. +// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/snippets/storage.go b/snippets/storage.go index 5ef1b1f2a..22c094024 100644 --- a/snippets/storage.go +++ b/snippets/storage.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. +// Copyright 2017 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/storage/storage.go b/storage/storage.go index 3f1b9d1bd..cbe548c21 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/storage/storage_test.go b/storage/storage_test.go index f0eec7a53..4c66ae716 100644 --- a/storage/storage_test.go +++ b/storage/storage_test.go @@ -1,4 +1,4 @@ -// Copyright 2017 Google Inc. All Rights Reserved. +// Copyright 2017 Google LLC All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.