diff --git a/.github/workflows/e2e_tests_fdc.yaml b/.github/workflows/e2e_tests_fdc.yaml new file mode 100644 index 000000000000..a756517226fb --- /dev/null +++ b/.github/workflows/e2e_tests_fdc.yaml @@ -0,0 +1,230 @@ +name: e2e-fdc + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '**/flutterfire_ui/**' + - '**.md' + push: + branches: + - master + paths-ignore: + - 'docs/**' + - 'website/**' + - '**/example/**' + - '**.md' + +jobs: + android: + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Firebase Emulator Cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: | + sudo npm i -g firebase-tools + - name: Start Firebase Emulator + run: | + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Gradle cache + uses: gradle/actions/setup-gradle@v3 + - name: AVD cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + id: avd-cache + with: + path: | + ~/.android/avd/* + ~/.android/adb* + key: avd-${{ runner.os }} + - name: Start AVD then run E2E tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: google_apis + arch: x86_64 + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + script: | + flutter test integration_test/e2e_test.dart --dart-define=CI=true -d emulator-5554 + + ios: + runs-on: macos-14 + timeout-minutes: 45 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: hendrikmuhs/ccache-action@c92f40bee50034e84c763e33b317c77adaa81c92 + name: Xcode Compile Cache + with: + key: xcode-cache-${{ runner.os }} + max-size: 700M + - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + name: Pods Cache + id: pods-cache + with: + path: tests/ios/Pods + key: ${{ runner.os }}-fdc-pods-v3-${{ hashFiles('tests/ios/Podfile.lock') }} + restore-keys: ${{ runner.os }}-ios-pods-v2 + - name: Firebase Emulator Cache + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: | + sudo npm i -g firebase-tools + - name: 'Build Application' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + run: | + export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" + export CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros + export CCACHE_FILECLONE=true + export CCACHE_DEPEND=true + export CCACHE_INODECACHE=true + ccache -s + flutter build ios --no-codesign --simulator --debug --target=./integration_test/e2e_test.dart --dart-define=CI=true + ccache -s + - name: Start Firebase Emulator + run: | + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: 'E2E Tests' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + run: | + # Boot simulator and wait for System app to be ready. + # List of available simulators: https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md#installed-simulators + SIMULATOR="iPhone 15" + xcrun simctl bootstatus "$SIMULATOR" -b + xcrun simctl logverbose "$SIMULATOR" enable + # Sleep to allow simulator to settle. + sleep 15 + # Uncomment following line to have simulator logs printed out for debugging purposes. + # xcrun simctl spawn booted log stream --predicate 'eventMessage contains "flutter"' & + flutter test integration_test/e2e_test.dart -d "$SIMULATOR" --dart-define=CI=true + + web: + runs-on: macos-latest + timeout-minutes: 15 + strategy: + fail-fast: false + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 + name: Install Node.js 20 + with: + node-version: '20' + - uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 + with: + distribution: 'temurin' + java-version: '17' + - name: Setup PostgreSQL for Linux/macOS/Windows + uses: ikalnytskyi/action-setup-postgres@v6 + - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 + with: + channel: 'stable' + cache: true + - uses: bluefireteam/melos-action@6085791af7036f6366c9a4b9d55105c0ef9c6388 + with: + run-bootstrap: false + melos-version: '5.3.0' + - name: 'Bootstrap package' + run: melos bootstrap --scope "firebase_data_connect*" + - name: 'Install Tools' + run: sudo npm i -g firebase-tools + - name: Cache Firebase Emulator + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + with: + path: ~/.cache/firebase/emulators + key: firebase-emulators-v3-fdc-${{ runner.os }} + restore-keys: firebase-emulators-v3 + - name: Start Firebase Emulator + run: | + sudo chown -R 501:20 "/Users/runner/.npm" + cd ./packages/firebase_data_connect/firebase_data_connect/example + unset PGSERVICEFILE + firebase experiments:enable dataconnect + ./start-firebase-emulator.sh + - name: 'E2E Tests' + working-directory: 'packages/firebase_data_connect/firebase_data_connect/example' + # Web devices are not supported for the `flutter test` command yet. As a + # workaround we can use the `flutter drive` command. Tracking issue: + # https://github.com/flutter/flutter/issues/66264 + run: | + chromedriver --port=4444 --trace-buffer-size=100000 & + flutter drive --target=./integration_test/e2e_test.dart --driver=./test_driver/integration_test.dart -d chrome --dart-define=CI=true | tee output.log + # We have to check the output for failed tests matching the string "[E]" + output=$( + android:icon="@mipmap/ic_launcher" + android:usesCleartextTraffic="true"> + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + - - + + - + \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle index 7fb86d70412c..536165d35a42 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle +++ b/packages/firebase_data_connect/firebase_data_connect/example/android/settings.gradle @@ -19,9 +19,6 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false - // START: FlutterFire Configuration - id "com.google.gms.google-services" version "4.3.15" apply false - // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.7.10" apply false } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql index e9da28933899..5fa1104b564e 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/input.gql @@ -25,6 +25,8 @@ input DirectedBy_ListFilter { input DirectedBy_Order { movieId: OrderDirection directedbyId: OrderDirection + directedby: Person_Order + movie: Movie_Order } input Movie_Data { id: UUID diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql index 0feaae08b761..4daa8a6f0c64 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/mutation.gql @@ -12,6 +12,18 @@ extend type Mutation { """ person_insert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: INSERT_SINGLE) """ + Insert DirectedBy entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + directedBy_insertMany(data: [DirectedBy_Data!]!): [DirectedBy_Key!]! @fdc_generated(from: "DirectedBy", purpose: INSERT_MULTIPLE) + """ + Insert Movie entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + movie_insertMany(data: [Movie_Data!]!): [Movie_Key!]! @fdc_generated(from: "Movie", purpose: INSERT_MULTIPLE) + """ + Insert Person entries into the table. Columns not specified in `data` will receive defaults (e.g. `null`). + """ + person_insertMany(data: [Person_Data!]!): [Person_Key!]! @fdc_generated(from: "Person", purpose: INSERT_MULTIPLE) + """ Insert or update a single DirectedBy into the table, based on the primary key. Returns the key of the newly inserted DirectedBy. """ directedBy_upsert(data: DirectedBy_Data!): DirectedBy_Key! @fdc_generated(from: "DirectedBy", purpose: UPSERT_SINGLE) @@ -24,6 +36,18 @@ extend type Mutation { """ person_upsert(data: Person_Data!): Person_Key! @fdc_generated(from: "Person", purpose: UPSERT_SINGLE) """ + Insert or update DirectedBy entries into the table, based on the primary key. Returns the key of the newly inserted DirectedBy. + """ + directedBy_upsertMany(data: [DirectedBy_Data!]): [DirectedBy_Key!]! @fdc_generated(from: "DirectedBy", purpose: UPSERT_MULTIPLE) + """ + Insert or update Movie entries into the table, based on the primary key. Returns the key of the newly inserted Movie. + """ + movie_upsertMany(data: [Movie_Data!]): [Movie_Key!]! @fdc_generated(from: "Movie", purpose: UPSERT_MULTIPLE) + """ + Insert or update Person entries into the table, based on the primary key. Returns the key of the newly inserted Person. + """ + person_upsertMany(data: [Person_Data!]): [Person_Key!]! @fdc_generated(from: "Person", purpose: UPSERT_MULTIPLE) + """ Update a single DirectedBy based on `id` or `key`, setting columns specified in `data`. Returns `null` if not found. """ directedBy_update(key: DirectedBy_Key, data: DirectedBy_Data!): DirectedBy_Key @fdc_generated(from: "DirectedBy", purpose: UPDATE_SINGLE) diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql index a1d2505e4111..db0f2136ff69 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/query.gql @@ -14,13 +14,13 @@ extend type Query { """ List DirectedBy entries in the table, optionally filtered by `where` conditions. """ - directedBies(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE) + directedBies(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE) """ List Movie entries in the table, optionally filtered by `where` conditions. """ - movies(where: Movie_Filter, orderBy: [Movie_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) + movies(where: Movie_Filter, orderBy: [Movie_Order!], offset: Int, limit: Int = 100): [Movie!]! @fdc_generated(from: "Movie", purpose: QUERY_MULTIPLE) """ List Person entries in the table, optionally filtered by `where` conditions. """ - people(where: Person_Filter, orderBy: [Person_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "Person", purpose: QUERY_MULTIPLE) + people(where: Person_Filter, orderBy: [Person_Order!], offset: Int, limit: Int = 100): [Person!]! @fdc_generated(from: "Person", purpose: QUERY_MULTIPLE) } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql index 0691f598203e..0f8049ef0be2 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/main/relation.gql @@ -2,19 +2,19 @@ extend type Movie { """ ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.movie` equals this object). """ - directedBies_on_movie(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + directedBies_on_movie(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.movie", purpose: QUERY_MULTIPLE_ONE_TO_MANY) """ ✨ List related Person entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `movie` == this and `directedby` == that). """ - people_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Person!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + people_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [Person!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) } extend type Person { """ ✨ List DirectedBy entries in a one-to-many relationship with this object (i.e. where `DirectedBy.directedby` equals this object). """ - directedBies_on_directedby(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.directedby", purpose: QUERY_MULTIPLE_ONE_TO_MANY) + directedBies_on_directedby(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [DirectedBy!]! @fdc_generated(from: "DirectedBy.directedby", purpose: QUERY_MULTIPLE_ONE_TO_MANY) """ ✨ List related Movie entries using DirectedBy as a join table (i.e. where an entry of DirectedBy exists whose `directedby` == this and `movie` == that). """ - movies_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], limit: Int = 100): [Movie!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) + movies_via_DirectedBy(where: DirectedBy_Filter, orderBy: [DirectedBy_Order!], offset: Int, limit: Int = 100): [Movie!]! @fdc_generated(from: "DirectedBy", purpose: QUERY_MULTIPLE_MANY_TO_MANY) } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql index a313edf0cca7..c75ac521add7 100755 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/.dataconnect/schema/prelude.gql @@ -46,22 +46,55 @@ directive @auth( """ expr: Boolean_Expr @fdc_oneOf(required: true) ) on QUERY | MUTATION -"Conditions on a string value" +"Query filter criteria for `String` scalar fields." input String_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: String @fdc_oneOf(group: "eq") + """ + Match if field is exactly equal to the result of the provided server value + expression. Currently only `auth.uid` is supported as an expression. + """ eq_expr: String_Expr @fdc_oneOf(group: "eq") + "Match if field is not equal to provided value." ne: String @fdc_oneOf(group: "ne") + """ + Match if field is not equal to the result of the provided server value + expression. Currently only `auth.uid` is supported as an expression. + """ ne_expr: String_Expr @fdc_oneOf(group: "ne") + "Match if field value is among the provided list of values." in: [String!] + "Match if field value is not among the provided list of values." nin: [String!] + "Match if field value is greater than the provided value." gt: String + "Match if field value is greater than or equal to the provided value." ge: String + "Match if field value is less than the provided value." lt: String + "Match if field value is less than or equal to the provided value." le: String + """ + Match if field value contains the provided value as a substring. Equivalent + to `LIKE '%value%'` + """ contains: String + """ + Match if field value starts with the provided value. Equivalent to + `LIKE 'value%'` + """ startsWith: String + """ + Match if field value ends with the provided value. Equivalent to + `LIKE '%value'` + """ endsWith: String + """ + Match if field value matches the provided pattern. See `String_Pattern` for + more details. + """ pattern: String_Pattern } @@ -70,135 +103,205 @@ The pattern match condition on a string. Specify either like or regex. https://www.postgresql.org/docs/current/functions-matching.html """ input String_Pattern { - "the LIKE expression to use" + "Match using the provided `LIKE` expression." like: String - "the POSIX regular expression" + "Match using the provided POSIX regular expression." regex: String - "when true, it's case-insensitive. In Postgres: ILIKE, ~*" + "When true, ignore case when matching." ignoreCase: Boolean - "when true, invert the condition. In Postgres: NOT LIKE, !~" + "When true, invert the match result. Equivalent to `NOT LIKE` or `!~`." invert: Boolean } -"Conditions on a string list" +"Query filter criteris for `[String!]` scalar fields." input String_ListFilter { + "Match if list field contains the provided value as a member." includes: String + "Match if list field does not contain the provided value as a member." excludes: String + "Match if list field contains all of the provided values as members." includesAll: [String!] + "Match if list field does not contain any of the provided values as members." excludesAll: [String!] } -"Conditions on a UUID value" +"Query filter criteria for `UUID` scalar fields." input UUID_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: UUID + "Match if field is not equal to provided value." ne: UUID + "Match if field value is among the provided list of values." in: [UUID!] + "Match if field value is not among the provided list of values." nin: [UUID!] } -"Conditions on a UUID list" +"Query filter criteris for `[UUID!]` scalar fields." input UUID_ListFilter { + "Match if list field contains the provided value as a member." includes: UUID + "Match if list field does not contain the provided value as a member." excludes: UUID + "Match if list field contains all of the provided values as members." includesAll: [UUID!] + "Match if list field does not contain any of the provided values as members." excludesAll: [UUID!] } -"Conditions on an Int value" +"Query filter criteria for `Int` scalar fields." input Int_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Int + "Match if field is not equal to provided value." ne: Int + "Match if field value is among the provided list of values." in: [Int!] + "Match if field value is not among the provided list of values." nin: [Int!] + "Match if field value is greater than the provided value." gt: Int + "Match if field value is greater than or equal to the provided value." ge: Int + "Match if field value is less than the provided value." lt: Int + "Match if field value is less than or equal to the provided value." le: Int } -"Conditions on an Int list" +"Query filter criteris for `[Int!]` scalar fields." input Int_ListFilter { + "Match if list field contains the provided value as a member." includes: Int + "Match if list field does not contain the provided value as a member." excludes: Int + "Match if list field contains all of the provided values as members." includesAll: [Int!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Int!] } -"Conditions on an Int64 value" +"Query filter criteria for `Int64` scalar fields." input Int64_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Int64 + "Match if field is not equal to provided value." ne: Int64 + "Match if field value is among the provided list of values." in: [Int64!] + "Match if field value is not among the provided list of values." nin: [Int64!] + "Match if field value is greater than the provided value." gt: Int64 + "Match if field value is greater than or equal to the provided value." ge: Int64 + "Match if field value is less than the provided value." lt: Int64 + "Match if field value is less than or equal to the provided value." le: Int64 } -"Conditions on an Int64 list" +"Query filter criteria for `[Int64!]` scalar fields." input Int64_ListFilter { + "Match if list field contains the provided value as a member." includes: Int64 + "Match if list field does not contain the provided value as a member." excludes: Int64 + "Match if list field contains all of the provided values as members." includesAll: [Int64!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Int64!] } -"Conditions on a Float value" +"Query filter criteria for `Float` scalar fields." input Float_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Float + "Match if field is not equal to provided value." ne: Float + "Match if field value is among the provided list of values." in: [Float!] + "Match if field value is not among the provided list of values." nin: [Float!] + "Match if field value is greater than the provided value." gt: Float + "Match if field value is greater than or equal to the provided value." ge: Float + "Match if field value is less than the provided value." lt: Float + "Match if field value is less than or equal to the provided value." le: Float } -"Conditions on a Float list" +"Query filter criteria for `[Float!]` scalar fields." input Float_ListFilter { + "Match if list field contains the provided value as a member." includes: Float + "Match if list field does not contain the provided value as a member." excludes: Float + "Match if list field contains all of the provided values as members." includesAll: [Float!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Float!] } -"Conditions on a Boolean value" +"Query filter criteria for `Boolean` scalar fields." input Boolean_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Boolean + "Match if field is not equal to provided value." ne: Boolean + "Match if field value is among the provided list of values." in: [Boolean!] + "Match if field value is not among the provided list of values." nin: [Boolean!] } -"Conditions on a Boolean list" +"Query filter criteria for `[Boolean!]` scalar fields." input Boolean_ListFilter { + "Match if list field contains the provided value as a member." includes: Boolean + "Match if list field does not contain the provided value as a member." excludes: Boolean + "Match if list field contains all of the provided values as members." includesAll: [Boolean!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Boolean!] } -"Conditions on an Any value" +"Query filter criteria for `Any` scalar fields." input Any_Filter { + "When true, match if field `IS NULL`. When false, match if field is `NOT NULL`." isNull: Boolean + "Match if field is exactly equal to provided value." eq: Any + "Match if field is not equal to provided value." ne: Any + "Match if field value is among the provided list of values." in: [Any!] + "Match if field value is not among the provided list of values." nin: [Any!] } -"Conditions on a Any list" +"Query filter criteria for `[Any!]` scalar fields." input Any_ListFilter { + "Match if list field contains the provided value as a member." includes: Any + "Match if list field does not contain the provided value as a member." excludes: Any + "Match if list field contains all of the provided values as members." includesAll: [Any!] + "Match if list field does not contain any of the provided values as members." excludesAll: [Any!] } """ @@ -278,13 +381,30 @@ directive @index( """ fields: [String!] """ - Only allowed when used on OBJECT. + Only allowed when used on OBJECT and BTREE index. Index order of each column. Default to all ASC. """ order: [IndexFieldOrder!] + """ + For array field, default to `GIN`. + For Vector field, default to `HNSW`. + """ + type: IndexType + """ + Only allowed when used on vector field. + The vector similarity method. Default to `INNER_PRODUCT`. + """ + vector_method: VectorSimilarityMethod ) repeatable on FIELD_DEFINITION | OBJECT enum IndexFieldOrder { ASC DESC } + +enum IndexType { + BTREE + GIN + HNSW + IVFFLAT +} type Query { _service: _Service! } @@ -860,6 +980,25 @@ enum Date_Interval @fdc_forbiddenAsFieldType { MONTH YEAR } +""" +Defines a unique constraint. + +Given `type TableName @table @unique(fields: [“fieldName”, “secondFieldName”])`, +`table_name_field_name_second_field_name_uidx` is the SQL unique index id. +Given `type TableName @table { fieldName: Int @unique } ` +`table_name_field_name_uidx` is the SQL unique index id. + +Override with `@unique(indexName)` in case of index name conflicts. +""" +directive @unique( + "The SQL database unique index name. Defaults to __uidx." + indexName: String + """ + Only allowed and required when used on OBJECT. + The fields to create a unique constraint on. + """ + fields: [String!] +) repeatable on FIELD_DEFINITION | OBJECT "Update input of a String value" input String_Update { set: String @fdc_oneOf(group: "set") diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql index a1d3e11e2137..a869f6fea236 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/mutations.gql @@ -1,14 +1,10 @@ # Create a movie based on user input mutation addPerson($name: String) @auth(level: PUBLIC) { - person_insert(data: { - name: $name - }) + person_insert(data: { name: $name }) } -mutation addDirectorToMovie($personId: Person_Key, $movieId: UUID) { - directedBy_insert(data: { - directedby: $personId, - movieId: $movieId - }) +mutation addDirectorToMovie($personId: Person_Key, $movieId: UUID) +@auth(level: PUBLIC) { + directedBy_insert(data: { directedby: $personId, movieId: $movieId }) } mutation createMovie( $title: String! @@ -24,11 +20,15 @@ mutation createMovie( genre: $genre rating: $rating description: $description - } ) } +# Delete a movie by its ID +mutation deleteMovie($id: UUID!) @auth(level: PUBLIC) { + movie_delete(id: $id) +} + # # Update movie information based on the provided ID # mutation updateMovie( # $id: UUID! @@ -54,11 +54,6 @@ mutation createMovie( # ) # } -# # Delete a movie by its ID -# mutation deleteMovie($id: UUID!) { -# movie_delete(id: $id) -# } - # # Delete movies with a rating lower than the specified minimum rating # mutation deleteUnpopularMovies($minRating: Float!) { # movie_deleteMany(where: { rating: { le: $minRating } }) @@ -118,4 +113,4 @@ mutation createMovie( # id_expr: "auth.uid", # username: $username # }) -# } \ No newline at end of file +# } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql index d3f265b1a2d9..25787ca9aa33 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/connector/queries.gql @@ -1,15 +1,30 @@ - # List subset of fields for movies query ListMovies @auth(level: USER) { movies { id - title, + title directed_by: people_via_DirectedBy { - name, + name } - }, + } } +# List movies by partial title match +query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { + movies(where: { title: { contains: $input } }) { + id + title + genre + rating + } +} + +query ListPersons @auth(level: USER) { + people { + id + name + } +} # List subset of fields for users # query ListUsers @auth(level: PUBLIC) { @@ -215,17 +230,6 @@ query ListMovies @auth(level: USER) { # } # } -# # List movies by partial title match -# query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) { -# movies(where: { title: { contains: $input } }) { -# id -# title -# genre -# rating -# imageUrl -# } -# } - # # Fetch a single movie using key scalars (same as get movie by id) # query MovieByKey($key: Movie_Key!) @auth(level: PUBLIC) { # movie(key: $key) { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql index 605bb1a624d7..8fa70b4ec39d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql +++ b/packages/firebase_data_connect/firebase_data_connect/example/dataconnect/schema/schema.gql @@ -48,7 +48,7 @@ type Movie @table { # # Movie - Actors (or vice versa) is a many to many relationship # type Actor @table { # id: UUID! -# imageUrl: String! +# imageUrl: String! # name: String! @col(name: "name", dataType: "varchar(30)") # biography: String # } @@ -77,7 +77,7 @@ type Movie @table { # id: String! @col(name: "user_auth") # username: String! @col(name: "username", dataType: "varchar(50)") # # The following are generated from the @ref in the Review table -# # reviews_on_user +# # reviews_on_user # # movies_via_Review # } @@ -111,4 +111,4 @@ type Movie @table { # rating: Int # reviewText: String # reviewDate: Date! @default(expr: "request.time") -# } \ No newline at end of file +# } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/firebase.json b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json index 73f599717a9c..2d00ab15a830 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/firebase.json +++ b/packages/firebase_data_connect/firebase_data_connect/example/firebase.json @@ -1,5 +1,10 @@ { "dataconnect": { "source": "dataconnect" + }, + "emulators": { + "auth": { + "port": 9099 + } } -} +} \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart new file mode 100644 index 000000000000..1ea81e9e0aa3 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/e2e_test.dart @@ -0,0 +1,42 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/firebase_options.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'generation_e2e.dart'; +import 'instance_e2e.dart'; +import 'listen_e2e.dart'; +import 'query_e2e.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('firebase_data_connect', () { + setUpAll(() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + + final connector = MoviesConnector.connectorConfig; + + FirebaseDataConnect.instanceFor(connectorConfig: connector) + .useDataConnectEmulator('localhost', 9399); + await FirebaseAuth.instance.useAuthEmulator('localhost', 9099); + + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: 'test@mail.com', password: 'password'); + }); + + runInstanceTests(); + runQueryTests(); + runGenerationTest(); + runListenTests(); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart new file mode 100644 index 000000000000..242acecf666d --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/generation_e2e.dart @@ -0,0 +1,70 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runGenerationTest() { + group( + '$FirebaseDataConnect generation', + () { + late FirebaseDataConnect fdc; + + setUpAll(() async { + fdc = FirebaseDataConnect.instanceFor( + connectorConfig: MoviesConnector.connectorConfig, + ); + }); + + testWidgets('should have generated correct MoviesConnector', + (WidgetTester tester) async { + final connector = MoviesConnector(dataConnect: fdc); + expect(connector, isNotNull); + expect(connector.addPerson, isNotNull); + expect(connector.createMovie, isNotNull); + expect(connector.listMovies, isNotNull); + expect(connector.addDirectorToMovie, isNotNull); + }); + + testWidgets('should have generated correct MutationRef', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct QueryRef', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.listMovies.ref(); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct MutationRef using name', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.addPerson.ref( + name: 'Keanu Reeves', + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + + testWidgets('should have generated correct MutationRef using nested id', + (WidgetTester tester) async { + final ref = MoviesConnector.instance.addDirectorToMovie.ref( + movieId: 'movieId', + personId: AddDirectorToMovieVariablesPersonId(id: 'personId'), + ); + expect(ref, isNotNull); + expect(ref.execute, isNotNull); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart new file mode 100644 index 000000000000..475af5d79a5a --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/instance_e2e.dart @@ -0,0 +1,34 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void runInstanceTests() { + group( + '$FirebaseDataConnect.instance', + () { + late FirebaseDataConnect fdc; + late FirebaseApp app; + + setUpAll(() async { + app = Firebase.app(); + fdc = FirebaseDataConnect.instanceFor( + app: app, + connectorConfig: MoviesConnector.connectorConfig, + ); + }); + + testWidgets('can instantiate', (WidgetTester tester) async { + expect(fdc, isNotNull); + }); + + testWidgets('can access app', (WidgetTester tester) async { + expect(fdc.app == app, isTrue); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart new file mode 100644 index 000000000000..a41c3668d525 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/listen_e2e.dart @@ -0,0 +1,78 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'query_e2e.dart'; + +void runListenTests() { + group( + '$FirebaseDataConnect.instance listen', + () { + setUp(() async { + await deleteAllMovies(); + }); + + testWidgets('should be able to listen to the list of movies', + (WidgetTester tester) async { + final initialValue = + await MoviesConnector.instance.listMovies.ref().execute(); + expect(initialValue.data.movies.length, 0, + reason: 'Initial movie list should be empty'); + + final Completer isReady = Completer(); + final Completer hasBeenListened = Completer(); + int count = 0; + + final listener = MoviesConnector.instance.listMovies + .ref() + .subscribe() + .listen((value) { + final movies = value.data.movies; + + if (count == 0) { + expect(movies.length, 0, + reason: 'First emission should contain an empty list'); + isReady.complete(); + } else { + expect(movies.length, 1, + reason: 'Second emission should contain one movie'); + expect(movies[0].title, 'The Matrix', + reason: 'The movie should be The Matrix'); + hasBeenListened.complete(true); + } + count++; + }); + + // Wait for the listener to be ready + await isReady.future; + + // Create the movie + await MoviesConnector.instance.createMovie + .ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ) + .execute(); + + await MoviesConnector.instance.listMovies.ref().execute(); + + // Wait for the listener to receive the movie update + final bool hasListenerReceived = await hasBeenListened.future; + + // Cancel the listener and wait for it to finish + await listener.cancel(); + + expect(hasListenerReceived, isTrue, + reason: 'The stream should have emitted new data'); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart new file mode 100644 index 000000000000..55fd6e78de11 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/integration_test/query_e2e.dart @@ -0,0 +1,123 @@ +// Copyright 2020, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect_example/generated/movies.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future deleteAllMovies() async { + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + for (var movie in result.movies) { + await MoviesConnector.instance.deleteMovie.ref(id: movie.id).execute(); + } +} + +void runQueryTests() { + group( + '$FirebaseDataConnect.instance query', + () { + setUp(() async { + await deleteAllMovies(); + }); + + testWidgets('can query', (WidgetTester tester) async { + final value = await MoviesConnector.instance.listMovies.ref().execute(); + + final result = value.data; + expect(result.movies.length, 0); + }); + + testWidgets('can add a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 1); + expect(result.movies[0].title, 'The Matrix'); + }); + + testWidgets('can add a director to a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.addPerson.ref( + name: 'Keanu Reeves', + ); + + await ref.execute(); + + final personId = + (await MoviesConnector.instance.listPersons.ref().execute()) + .data + .people[0] + .id; + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 0); + + ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value2 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result2 = value2.data; + expect(result2.movies.length, 1); + + final movieId = result2.movies[0].id; + + ref = MoviesConnector.instance.addDirectorToMovie.ref( + movieId: movieId, + personId: AddDirectorToMovieVariablesPersonId(id: personId), + ); + + await ref.execute(); + + final value3 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result3 = value3.data; + expect(result3.movies.length, 1); + expect(result3.movies[0].directed_by.length, 1); + expect(result3.movies[0].directed_by[0].name, 'Keanu Reeves'); + }); + + testWidgets('can delete a movie', (WidgetTester tester) async { + MutationRef ref = MoviesConnector.instance.createMovie.ref( + genre: 'Action', + title: 'The Matrix', + releaseYear: 1999, + rating: 4.5, + ); + + await ref.execute(); + + final value = await MoviesConnector.instance.listMovies.ref().execute(); + final result = value.data; + expect(result.movies.length, 1); + + final movieId = result.movies[0].id; + + ref = MoviesConnector.instance.deleteMovie.ref(id: movieId); + + await ref.execute(); + + final value2 = + await MoviesConnector.instance.listMovies.ref().execute(); + final result2 = value2.data; + expect(result2.movies.length, 0); + }); + }, + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj index fa521e1c50ab..26f10ca2a748 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_data_connect/firebase_data_connect/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -50,7 +49,6 @@ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 467C8E0E18669B1A171B55D8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 5363A146E4B954495368B56D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -115,7 +113,6 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 4B076066C0A79965E90ABC43 /* GoogleService-Info.plist */, EE2E4CC9AAC39360CE2CCED5 /* Pods */, BDBC9B622C75229E8F7832D1 /* Frameworks */, ); @@ -267,7 +264,6 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - 44748ABD01BE3A9752CF3BFC /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart new file mode 100644 index 000000000000..daecca3d327c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/firebase_options.dart @@ -0,0 +1,98 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return android; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:87e25e51afe982cd3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-JN95N1JV2E', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:175ea7a64b2faf5e3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.firestore.example', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:0670bc5fe8574a9c3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-17qn06u8a0dc717u8ul7s49ampk13lul.apps.googleusercontent.com', + iosClientId: + '406099696497-l9gojfp6b3h1cgie1se28a9ol9fmsvvk.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.firestore.example', + ); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart index 31f9e0f435ee..0929d0a602b9 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_director_to_movie.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class AddDirectorToMovie { @@ -128,8 +124,8 @@ class AddDirectorToMovieVariables { } AddDirectorToMovieVariables({ - this.personId, - this.movieId, + AddDirectorToMovieVariablesPersonId? this.personId, + String? this.movieId, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart index f71192de4435..9d418952fcd1 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/add_person.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class AddPerson { @@ -86,7 +82,7 @@ class AddPersonVariables { } AddPersonVariables({ - this.name, + String? this.name, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart index 481a519b686f..6d1e020cce3b 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/create_movie.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class CreateMovie { @@ -121,8 +117,8 @@ class CreateMovieVariables { required this.title, required this.releaseYear, required this.genre, - this.rating, - this.description, + double? this.rating, + String? this.description, }) { // TODO(mtewani): Only show this if there are optional fields. } diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart new file mode 100644 index 000000000000..5aeb6cdd1938 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/delete_movie.dart @@ -0,0 +1,89 @@ +part of movies; + +class DeleteMovie { + String name = "deleteMovie"; + DeleteMovie({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + DeleteMovieResponse.fromJson(jsonDecode(json) as Map); + Serializer varsSerializer = + (DeleteMovieVariables vars) => jsonEncode(vars.toJson()); + MutationRef ref( + {required String id, DeleteMovieVariables? deleteMovieVariables}) { + DeleteMovieVariables vars1 = DeleteMovieVariables( + id: id, + ); + DeleteMovieVariables vars = deleteMovieVariables ?? vars1; + return dataConnect.mutation( + this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class DeleteMovieMovieDelete { + late String id; + + DeleteMovieMovieDelete.fromJson(Map json) + : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + DeleteMovieMovieDelete({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class DeleteMovieResponse { + late DeleteMovieMovieDelete? movie_delete; + + DeleteMovieResponse.fromJson(Map json) + : movie_delete = DeleteMovieMovieDelete.fromJson(json['movie_delete']) {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + if (movie_delete != null) { + json['movie_delete'] = movie_delete!.toJson(); + } + + return json; + } + + DeleteMovieResponse({ + DeleteMovieMovieDelete? movie_delete, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class DeleteMovieVariables { + late String id; + + DeleteMovieVariables.fromJson(Map json) : id = json['id'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + return json; + } + + DeleteMovieVariables({ + required this.id, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart index e3b2086b8478..58e13493a23e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - part of movies; class ListMovies { @@ -12,8 +8,7 @@ class ListMovies { ListMoviesResponse.fromJson(jsonDecode(json) as Map); QueryRef ref() { - return dataConnect.query( - this.name, dataDeserializer, emptySerializer, null); + return dataConnect.query(this.name, dataDeserializer, null, null); } FirebaseDataConnect dataConnect; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart new file mode 100644 index 000000000000..f06a7f68d304 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_movies_by_partial_title.dart @@ -0,0 +1,114 @@ +part of movies; + +class ListMoviesByPartialTitle { + String name = "ListMoviesByPartialTitle"; + ListMoviesByPartialTitle({required this.dataConnect}); + + Deserializer dataDeserializer = + (String json) => ListMoviesByPartialTitleResponse.fromJson( + jsonDecode(json) as Map); + Serializer varsSerializer = + (ListMoviesByPartialTitleVariables vars) => jsonEncode(vars.toJson()); + QueryRef + ref( + {required String input, + ListMoviesByPartialTitleVariables? + listMoviesByPartialTitleVariables}) { + ListMoviesByPartialTitleVariables vars1 = ListMoviesByPartialTitleVariables( + input: input, + ); + ListMoviesByPartialTitleVariables vars = + listMoviesByPartialTitleVariables ?? vars1; + return dataConnect.query(this.name, dataDeserializer, varsSerializer, vars); + } + + FirebaseDataConnect dataConnect; +} + +class ListMoviesByPartialTitleMovies { + late String id; + + late String title; + + late String genre; + + late double? rating; + + ListMoviesByPartialTitleMovies.fromJson(Map json) + : id = json['id'], + title = json['title'], + genre = json['genre'], + rating = json['rating'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + json['title'] = title; + + json['genre'] = genre; + + if (rating != null) { + json['rating'] = rating; + } + + return json; + } + + ListMoviesByPartialTitleMovies({ + required this.id, + required this.title, + required this.genre, + double? rating, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesByPartialTitleResponse { + late List movies; + + ListMoviesByPartialTitleResponse.fromJson(Map json) + : movies = (json['movies'] as List) + .map((e) => ListMoviesByPartialTitleMovies.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['movies'] = movies.map((e) => e.toJson()).toList(); + + return json; + } + + ListMoviesByPartialTitleResponse({ + required this.movies, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListMoviesByPartialTitleVariables { + late String input; + + ListMoviesByPartialTitleVariables.fromJson(Map json) + : input = json['input'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['input'] = input; + + return json; + } + + ListMoviesByPartialTitleVariables({ + required this.input, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart new file mode 100644 index 000000000000..8a4d7e4cb0a6 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/list_persons.dart @@ -0,0 +1,67 @@ +part of movies; + +class ListPersons { + String name = "ListPersons"; + ListPersons({required this.dataConnect}); + + Deserializer dataDeserializer = (String json) => + ListPersonsResponse.fromJson(jsonDecode(json) as Map); + + QueryRef ref() { + return dataConnect.query(this.name, dataDeserializer, null, null); + } + + FirebaseDataConnect dataConnect; +} + +class ListPersonsPeople { + late String id; + + late String name; + + ListPersonsPeople.fromJson(Map json) + : id = json['id'], + name = json['name'] {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['id'] = id; + + json['name'] = name; + + return json; + } + + ListPersonsPeople({ + required this.id, + required this.name, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} + +class ListPersonsResponse { + late List people; + + ListPersonsResponse.fromJson(Map json) + : people = (json['people'] as List) + .map((e) => ListPersonsPeople.fromJson(e)) + .toList() {} + + // TODO(mtewani): Fix up to create a map on the fly + Map toJson() { + Map json = {}; + + json['people'] = people.map((e) => e.toJson()).toList(); + + return json; + } + + ListPersonsResponse({ + required this.people, + }) { + // TODO(mtewani): Only show this if there are optional fields. + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart index bc93d11d5910..9c0f15b0ac39 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/generated/movies.dart @@ -1,7 +1,3 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - library movies; import 'package:firebase_data_connect/firebase_data_connect.dart'; @@ -13,8 +9,14 @@ part 'add_director_to_movie.dart'; part 'create_movie.dart'; +part 'delete_movie.dart'; + part 'list_movies.dart'; +part 'list_movies_by_partial_title.dart'; + +part 'list_persons.dart'; + class MoviesConnector { AddPerson get addPerson { return AddPerson(dataConnect: dataConnect); @@ -28,10 +30,22 @@ class MoviesConnector { return CreateMovie(dataConnect: dataConnect); } + DeleteMovie get deleteMovie { + return DeleteMovie(dataConnect: dataConnect); + } + ListMovies get listMovies { return ListMovies(dataConnect: dataConnect); } + ListMoviesByPartialTitle get listMoviesByPartialTitle { + return ListMoviesByPartialTitle(dataConnect: dataConnect); + } + + ListPersons get listPersons { + return ListPersons(dataConnect: dataConnect); + } + static ConnectorConfig connectorConfig = ConnectorConfig( 'us-west2', 'movies', diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart index f88ae430deaf..5b5676e4f120 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/login.dart @@ -2,11 +2,11 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:math'; + import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_data_connect_example/main.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; class Login extends StatefulWidget { const Login({super.key}); @@ -16,32 +16,9 @@ class Login extends StatefulWidget { } class _LoginState extends State { - Future signInWithGoogle() async { - // Trigger the authentication flow - if (kIsWeb) { - GoogleAuthProvider googleProvider = GoogleAuthProvider(); - - googleProvider - .addScope('https://www.googleapis.com/auth/contacts.readonly'); - - // Once signed in, return the UserCredential - return await FirebaseAuth.instance.signInWithPopup(googleProvider); - } else { - final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn(); - - // Obtain the auth details from the request - final GoogleSignInAuthentication? googleAuth = - await googleUser?.authentication; - - // Create a new credential - final credential = GoogleAuthProvider.credential( - accessToken: googleAuth?.accessToken, - idToken: googleAuth?.idToken, - ); - - // Once signed in, return the UserCredential - return await FirebaseAuth.instance.signInWithCredential(credential); - } + Future signInWithGoogle() async { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: '${Random().nextInt(100000)}@mail.com', password: 'password'); } void logIn() async { diff --git a/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart index e44256df786c..10da1ce05f60 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart +++ b/packages/firebase_data_connect/firebase_data_connect/example/lib/main.dart @@ -3,26 +3,24 @@ // BSD-style license that can be found in the LICENSE file. import 'package:firebase_app_check/firebase_app_check.dart'; -import 'package:firebase_data_connect_example/login.dart'; - -import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; // Uncomment this line after running flutterfire configure // import 'firebase_options.dart'; import 'package:firebase_data_connect/firebase_data_connect.dart'; -import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect_example/firebase_options.dart'; +import 'package:firebase_data_connect_example/login.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; import 'generated/movies.dart'; const appCheckEnabled = false; -const configureEmulator = false; - -// Required for web. Set equal to `DefaultFirebaseOptions.currentPlatform` -FirebaseOptions? options; +const configureEmulator = true; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: options); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); if (appCheckEnabled) { await FirebaseAppCheck.instance.activate( // You can also use a `ReCaptchaEnterpriseProvider` provider instance as an @@ -43,6 +41,15 @@ void main() async { appleProvider: AppleProvider.appAttest, ); } + if (configureEmulator) { + MoviesConnector.instance.dataConnect + .useDataConnectEmulator('127.0.0.1', 9399); + FirebaseAuth.instance.useAuthEmulator( + 'localhost', + 9099, + ); + } + runApp(const MyApp()); } @@ -62,12 +69,22 @@ class MyApp extends StatelessWidget { } } -class MyHomePage extends StatefulWidget { +class MyHomePage extends StatelessWidget { const MyHomePage({super.key, required this.title}); final String title; @override - State createState() => _MyHomePageState(); + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(title), + ), + body: const Center( + child: DataConnectWidget(), + ), + ); + } } class DataConnectWidget extends StatefulWidget { @@ -91,14 +108,10 @@ class _DataConnectWidgetState extends State { @override void initState() { super.initState(); - if (configureEmulator) { - int port = 9399; - MoviesConnector.instance.dataConnect - .useDataConnectEmulator('127.0.0.1', port); - } QueryRef ref = MoviesConnector.instance.listMovies.ref(); + ref.subscribe().listen((event) { setState(() { _movies = event.data.movies; @@ -173,10 +186,11 @@ class _DataConnectWidgetState extends State { } MutationRef ref = MoviesConnector.instance.createMovie.ref( - title: title, - releaseYear: _releaseYearDate.year, - genre: genre, - rating: _rating); + title: title, + releaseYear: _releaseYearDate.year, + genre: genre, + rating: _rating, + ); try { await ref.execute(); triggerReload(); @@ -243,18 +257,3 @@ class _DataConnectWidgetState extends State { ); } } - -class _MyHomePageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text(widget.title), - ), - body: const Center( - child: DataConnectWidget(), - ), - ); - } -} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj index 57bda15d2206..ac658b11a81e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/firebase_data_connect/firebase_data_connect/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,7 +27,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -78,7 +77,6 @@ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ @@ -127,7 +125,6 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 93CDFB6B9272C6F1BE9AB929 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -288,7 +285,6 @@ files = ( 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 6A09C092ECC09506AD892B52 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json b/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json new file mode 100644 index 000000000000..6e4aa92b1a0e --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "example", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml index 7f5c88cde257..fa7fcc9c1bc3 100644 --- a/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/example/pubspec.yaml @@ -1,44 +1,22 @@ name: firebase_data_connect_example -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +description: 'Firebase Data Connect example app' + +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=3.2.0 <4.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter firebase_core: ^3.2.0 google_sign_in: ^6.1.0 firebase_auth: ^5.1.2 - firebase_data_connect: + firebase_data_connect: path: ../ - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.6 flutter_rating_bar: ^4.0.1 protobuf: ^3.1.0 @@ -50,51 +28,9 @@ dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^3.0.0 + integration_test: + sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh new file mode 100755 index 000000000000..c40ced9decb4 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/start-firebase-emulator.sh @@ -0,0 +1,3 @@ +#!/bin/bash +firebase emulators:start --project flutterfire-e2e-tests & +sleep 30 \ No newline at end of file diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart deleted file mode 100644 index 5ef64fc01675..000000000000 --- a/packages/firebase_data_connect/firebase_data_connect/example/test/widget_test.dart +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2024, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:firebase_data_connect_example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..f1ac26f27b88 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart index 63819ce1dfdf..3befd9588c65 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/firebase_data_connect.dart @@ -10,6 +10,7 @@ import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'src/common/common_library.dart'; import 'src/network/transport_library.dart' @@ -18,8 +19,8 @@ import 'src/network/transport_library.dart' export 'src/common/common_library.dart'; +part 'src/core/empty_serializer.dart'; part 'src/core/ref.dart'; part 'src/firebase_data_connect.dart'; part 'src/optional.dart'; part 'src/timestamp.dart'; -part 'src/core/empty_serializer.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart index 3d23ef63432c..4f55b88cc784 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/common/dataconnect_options.dart @@ -21,9 +21,9 @@ class ConnectorConfig { /// String representation of connectorConfig String toJson() { return jsonEncode({ - location: location, - connector: connector, - serviceId: serviceId, + 'location': location, + 'connector': connector, + 'serviceId': serviceId, }); } } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart index 228e65f2df90..f9891bd43eb7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/core/ref.dart @@ -35,8 +35,9 @@ abstract class OperationRef { } /// Tracks currently active queries, and emits events when a new query is executed. -class _QueryManager { - _QueryManager(this.dataConnect); +@visibleForTesting +class QueryManager { + QueryManager(this.dataConnect); /// FirebaseDataConnect instance; FirebaseDataConnect dataConnect; @@ -97,7 +98,7 @@ class QueryRef extends OperationRef { : super(dataConnect, operationName, transport, deserializer, serializer, variables); - _QueryManager _queryManager; + QueryManager _queryManager; @override Future> execute() async { try { diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart index 47979a6ae22a..b68d2069298e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/firebase_data_connect.dart @@ -7,22 +7,23 @@ part of firebase_data_connect; /// DataConnect class class FirebaseDataConnect extends FirebasePluginPlatform { /// Constructor for initializing Data Connect - FirebaseDataConnect._({ + @visibleForTesting + FirebaseDataConnect({ required this.app, required this.connectorConfig, this.auth, this.appCheck, - }) : _options = DataConnectOptions( + }) : options = DataConnectOptions( app.options.projectId, connectorConfig.location, connectorConfig.connector, connectorConfig.serviceId), super(app.name, 'plugins.flutter.io/firebase_data_connect') { - _queryManager = _QueryManager(this); + _queryManager = QueryManager(this); } /// QueryManager manages ongoing queries, and their subscriptions. - late _QueryManager _queryManager; + late QueryManager _queryManager; /// FirebaseApp FirebaseApp app; @@ -38,30 +39,39 @@ class FirebaseDataConnect extends FirebasePluginPlatform { FirebaseAuth? auth; /// ConnectorConfig + projectId - DataConnectOptions _options; + @visibleForTesting + DataConnectOptions options; /// Data Connect specific config information ConnectorConfig connectorConfig; /// Custom transport options for connecting to the Data Connect service. - TransportOptions? _transportOptions; + @visibleForTesting + TransportOptions? transportOptions; /// Checks whether the transport has been properly initialized. - void _checkTransport() { - _transportOptions ??= + @visibleForTesting + void checkTransport() { + transportOptions ??= TransportOptions('firebasedataconnect.googleapis.com', null, true); - transport = getTransport(_transportOptions!, _options, auth, appCheck); + transport = getTransport(transportOptions!, options, auth, appCheck); } /// Returns a [QueryRef] object. QueryRef query( String operationName, Deserializer dataDeserializer, - Serializer varsSerializer, + Serializer? varsSerializer, Variables? vars) { - _checkTransport(); - return QueryRef(this, operationName, transport, - dataDeserializer, _queryManager, varsSerializer, vars); + checkTransport(); + return QueryRef( + this, + operationName, + transport, + dataDeserializer, + _queryManager, + varsSerializer ?? emptySerializer, + vars); } /// Returns a [MutationRef] object. @@ -70,7 +80,7 @@ class FirebaseDataConnect extends FirebasePluginPlatform { Deserializer dataDeserializer, Serializer varsSerializer, Variables? vars) { - _checkTransport(); + checkTransport(); return MutationRef( this, operationName, transport, dataDeserializer, varsSerializer, vars); } @@ -79,11 +89,12 @@ class FirebaseDataConnect extends FirebasePluginPlatform { void useDataConnectEmulator(String host, int port, {bool automaticHostMapping = true, bool isSecure = false}) { String mappedHost = automaticHostMapping ? getMappedHost(host) : host; - _transportOptions = TransportOptions(mappedHost, port, isSecure); + transportOptions = TransportOptions(mappedHost, port, isSecure); } /// Currently cached DataConnect instances. Maps from app name to . - static final Map> _cachedInstances = + @visibleForTesting + static final Map> cachedInstances = {}; /// Returns an instance using a specified [FirebaseApp]. @@ -100,20 +111,20 @@ class FirebaseDataConnect extends FirebasePluginPlatform { auth ??= FirebaseAuth.instanceFor(app: app); appCheck ??= FirebaseAppCheck.instanceFor(app: app); - if (_cachedInstances[app.name] != null && - _cachedInstances[app.name]![connectorConfig.toJson()] != null) { - return _cachedInstances[app.name]![connectorConfig.toJson()]!; + if (cachedInstances[app.name] != null && + cachedInstances[app.name]![connectorConfig.toJson()] != null) { + return cachedInstances[app.name]![connectorConfig.toJson()]!; } - FirebaseDataConnect newInstance = FirebaseDataConnect._( + FirebaseDataConnect newInstance = FirebaseDataConnect( app: app, auth: auth, appCheck: appCheck, connectorConfig: connectorConfig); - if (_cachedInstances[app.name] == null) { - _cachedInstances[app.name] = {}; + if (cachedInstances[app.name] == null) { + cachedInstances[app.name] = {}; } - _cachedInstances[app.name]![connectorConfig.toJson()] = newInstance; + cachedInstances[app.name]![connectorConfig.toJson()] = newInstance; return newInstance; } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart index 917da7c3a0ae..80b0923ed7b7 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_library.dart @@ -9,6 +9,7 @@ import 'dart:developer'; import 'package:firebase_app_check/firebase_app_check.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../common/common_library.dart'; diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart index 0a9599487cb1..1d1bffcf7446 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/network/rest_transport.dart @@ -19,7 +19,7 @@ class RestTransport implements DataConnectTransport { String location = options.location; String service = options.serviceId; String connector = options.connector; - _url = + url = '$protocol://$host:$port/v1alpha/projects/$project/locations/$location/services/$service/connectors/$connector'; } @@ -30,7 +30,15 @@ class RestTransport implements DataConnectTransport { FirebaseAppCheck? appCheck; /// Current endpoint URL. - late String _url; + @visibleForTesting + late String url; + + @visibleForTesting + setHttp(http.Client client) { + _client = client; + } + + http.Client _client = http.Client(); /// Current host configuration. @override @@ -84,7 +92,7 @@ class RestTransport implements DataConnectTransport { body['variables'] = json.decode(serializer(vars)); } try { - http.Response r = await http.post(Uri.parse('$_url:$endpoint'), + http.Response r = await _client.post(Uri.parse('$url:$endpoint'), body: json.encode(body), headers: headers); if (r.statusCode != 200) { Map bodyJson = diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart index a1298dd36d01..ee054b97aa0e 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/optional.dart @@ -66,35 +66,21 @@ class Optional { } String nativeToJson(T type) { - switch (T.runtimeType) { - case bool: - case int: - case double: - case num: - return type.toString(); - case String: - return type as String; - default: - throw UnimplementedError( - 'This type is unimplemented: ${type.runtimeType}'); + if (type is bool || type is int || type is double || type is num) { + return type.toString(); + } else if (type is String) { + return type; + } else { + throw UnimplementedError('This type is unimplemented: ${type.runtimeType}'); } } T nativeFromJson(String json) { - switch (T.runtimeType) { - case bool: - return bool.parse(json) as T; - case int: - return int.parse(json) as T; - case double: - double.parse(json); - case num: - return num.parse(json) as T; - case String: - return json as T; - default: - throw UnimplementedError( - 'This type is unimplemented: ${json.runtimeType}'); - } - throw Exception('Null!'); + if (T == bool) return (json.toLowerCase() == 'true') as T; + if (T == int) return int.parse(json) as T; + if (T == double) return double.parse(json) as T; + if (T == num) return num.parse(json) as T; + if (T == String) return json as T; + + throw UnimplementedError('This type is unimplemented: ${T.runtimeType}'); } diff --git a/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart index fd3e954c83a4..d4d5343f2a67 100644 --- a/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart +++ b/packages/firebase_data_connect/firebase_data_connect/lib/src/timestamp.dart @@ -6,18 +6,20 @@ part of firebase_data_connect; /// Timestamp class is a custom class that allows for storing of nanoseconds. class Timestamp { + // ignore: use_raw_strings + final regex = RegExp( + r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{0,9})?(Z|[+-]\d{2}:\d{2})$'); + /// Constructor Timestamp(this.nanoseconds, this.seconds); + // TODO(mtewani): Fix this so that it keeps track of positional arguments so you don't have to repeatedly search the string multiple times. Timestamp.fromJson(String date) { - // ignore: use_raw_strings - var regex = RegExp( - r'^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{0,9})?(Z|[+-]\\d{2}:\\d{2})\$'); if (!regex.hasMatch(date)) { throw Exception('Invalid Date provided!'); } DateTime dateTime = DateTime.parse(date); - seconds = dateTime.second; + seconds = dateTime.millisecondsSinceEpoch ~/ 1000; String nanoStr = ''; int dotIdx = date.indexOf('.'); if (dotIdx > -1) { @@ -32,28 +34,23 @@ class Timestamp { if (nanoStr.isNotEmpty) { nanoseconds = int.parse(nanoStr.padRight(9, '0')); } - // TODO(mtewani): Add offset values. - if (date.contains('Z')) { - return; - } - int addIdx = date.indexOf('+'); - bool isAdd = addIdx > -1; - int signIdx = isAdd ? addIdx : date.indexOf('-'); - int timeHour = int.parse(date.substring(signIdx + 1, signIdx + 3)); - int timeMin = int.parse(date.substring(signIdx + 4, signIdx + 6)); - int timeZoneDiffer = timeHour * 3600 + timeMin * 60; - seconds = seconds + (isAdd ? -timeZoneDiffer : timeZoneDiffer); } + String toJson() { String secondsStr = DateTime.fromMillisecondsSinceEpoch(seconds * 1000, isUtc: true) .toIso8601String(); + if (nanoseconds == 0) { + return secondsStr; + } String nanoStr = nanoseconds.toString().padRight(9, '0'); - return '${secondsStr.substring(0, nanoStr.length - 1)}.${nanoStr}Z'; + return '${secondsStr.substring(0, 19)}.${nanoStr}Z'; } DateTime toDateTime() { - return DateTime.utc((seconds * 1000 + (nanoseconds / 1000000)).floor()); + final string = toJson(); + final date = DateTime.parse(string); + return date; } /// Current nanoseconds diff --git a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml index 165c44c9bc6f..c6807c19df0d 100644 --- a/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml +++ b/packages/firebase_data_connect/firebase_data_connect/pubspec.yaml @@ -1,5 +1,5 @@ name: firebase_data_connect -description: "Firebase Data Connect" +description: 'Firebase Data Connect' version: 0.1.0 homepage: https://firebase.google.com/docs/data-connect/quickstart?platform=flutter false_secrets: @@ -8,7 +8,7 @@ false_secrets: environment: sdk: '>=3.2.0 <4.0.0' - flutter: ">=3.3.0" + flutter: '>=3.3.0' dependencies: firebase_app_check: ^0.3.0+3 @@ -22,8 +22,11 @@ dependencies: protobuf: ^3.1.0 dev_dependencies: + build_runner: ^2.4.12 flutter_lints: ^4.0.0 flutter_test: sdk: flutter mockito: ^5.0.0 plugin_platform_interface: ^2.1.3 + firebase_app_check_platform_interface: ^0.1.0+35 + firebase_auth_platform_interface: ^7.4.4 diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart new file mode 100644 index 000000000000..785d438a9b5f --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/common_library_test.dart @@ -0,0 +1,130 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Mock classes for Firebase dependencies +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +void main() { + group('TransportOptions', () { + test('should properly initialize with given parameters', () { + final transportOptions = TransportOptions('localhost', 8080, true); + + expect(transportOptions.host, 'localhost'); + expect(transportOptions.port, 8080); + expect(transportOptions.isSecure, true); + }); + + test('should allow null values for optional parameters', () { + final transportOptions = TransportOptions('localhost', null, null); + + expect(transportOptions.host, 'localhost'); + expect(transportOptions.port, null); + expect(transportOptions.isSecure, null); + }); + + test('should update properties correctly', () { + final transportOptions = TransportOptions('localhost', 8080, true); + + transportOptions.host = 'newhost'; + transportOptions.port = 9090; + transportOptions.isSecure = false; + + expect(transportOptions.host, 'newhost'); + expect(transportOptions.port, 9090); + expect(transportOptions.isSecure, false); + }); + }); + + group('DataConnectTransport', () { + late DataConnectTransport transport; + late TransportOptions transportOptions; + late DataConnectOptions dataConnectOptions; + late MockFirebaseAuth mockFirebaseAuth; + late MockFirebaseAppCheck mockFirebaseAppCheck; + + setUp(() { + transportOptions = TransportOptions('localhost', 8080, true); + dataConnectOptions = DataConnectOptions( + 'projectId', + 'location', + 'connector', + 'serviceId', + ); + mockFirebaseAuth = MockFirebaseAuth(); + mockFirebaseAppCheck = MockFirebaseAppCheck(); + + transport = TestDataConnectTransport( + transportOptions, + dataConnectOptions, + auth: mockFirebaseAuth, + appCheck: mockFirebaseAppCheck, + ); + }); + + test('should properly initialize with given parameters', () { + expect(transport.transportOptions.host, 'localhost'); + expect(transport.transportOptions.port, 8080); + expect(transport.transportOptions.isSecure, true); + }); + + test('should handle invokeQuery with proper deserializer', () async { + final queryName = 'testQuery'; + final deserializer = (json) => json; + final result = + await transport.invokeQuery(queryName, deserializer, null, null); + + expect(result, isNotNull); + }); + + test('should handle invokeMutation with proper deserializer', () async { + final queryName = 'testMutation'; + final deserializer = (json) => json; + final result = + await transport.invokeMutation(queryName, deserializer, null, null); + + expect(result, isNotNull); + }); + }); +} + +// Test class extending DataConnectTransport for testing purposes +class TestDataConnectTransport extends DataConnectTransport { + TestDataConnectTransport( + TransportOptions transportOptions, DataConnectOptions options, + {FirebaseAuth? auth, FirebaseAppCheck? appCheck}) + : super(transportOptions, options) { + this.auth = auth; + this.appCheck = appCheck; + } + + @override + Future invokeQuery( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + // Simulate query invocation logic here + return deserializer('{}'); + } + + @override + Future invokeMutation( + String queryName, + Deserializer deserializer, + Serializer? serializer, + Variables? vars, + ) async { + // Simulate mutation invocation logic here + return deserializer('{}'); + } +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart new file mode 100644 index 000000000000..7892f72dbbfe --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_error_test.dart @@ -0,0 +1,77 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DataConnectErrorCode', () { + test('should have the correct enum values', () { + expect(DataConnectErrorCode.unavailable.toString(), + 'DataConnectErrorCode.unavailable'); + expect(DataConnectErrorCode.unauthorized.toString(), + 'DataConnectErrorCode.unauthorized'); + expect( + DataConnectErrorCode.other.toString(), 'DataConnectErrorCode.other'); + }); + }); + + group('DataConnectError', () { + test('should initialize with correct error code and message', () { + final error = DataConnectError( + DataConnectErrorCode.unavailable, 'Service is unavailable'); + + expect(error.dataConnectErrorCode, DataConnectErrorCode.unavailable); + expect(error.plugin, 'Data Connect'); + expect(error.code, 'DataConnectErrorCode.unavailable'); + expect(error.message, 'Service is unavailable'); + }); + + test('should handle different error codes properly', () { + final unauthorizedError = DataConnectError( + DataConnectErrorCode.unauthorized, 'Unauthorized access'); + final otherError = DataConnectError( + DataConnectErrorCode.other, 'Unknown error occurred'); + + expect(unauthorizedError.dataConnectErrorCode, + DataConnectErrorCode.unauthorized); + expect(unauthorizedError.plugin, 'Data Connect'); + expect(unauthorizedError.code, 'DataConnectErrorCode.unauthorized'); + expect(unauthorizedError.message, 'Unauthorized access'); + + expect(otherError.dataConnectErrorCode, DataConnectErrorCode.other); + expect(otherError.plugin, 'Data Connect'); + expect(otherError.code, 'DataConnectErrorCode.other'); + expect(otherError.message, 'Unknown error occurred'); + }); + + test('should allow null message', () { + final error = DataConnectError(DataConnectErrorCode.unavailable, null); + + expect(error.message, null); + }); + }); + + group('Serializer and Deserializer', () { + test('should serialize variables into string format', () { + Serializer> serializer = + (Map vars) => vars.toString(); + + final inputVars = {'key1': 'value1', 'key2': 123}; + final serializedString = serializer(inputVars); + + expect(serializedString, "{key1: value1, key2: 123}"); + }); + + test('should deserialize string data into expected format', () { + Deserializer> deserializer = + (String data) => {'data': data}; + + final inputData = '{"message": "Hello World"}'; + final deserializedData = deserializer(inputData); + + expect(deserializedData, {'data': '{"message": "Hello World"}'}); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart new file mode 100644 index 000000000000..4c2932a00774 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/common/dataconnect_options_test.dart @@ -0,0 +1,80 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ConnectorConfig', () { + test('should initialize with correct parameters', () { + final config = ConnectorConfig('us-central1', 'cloud-sql', 'service-123'); + + expect(config.location, 'us-central1'); + expect(config.connector, 'cloud-sql'); + expect(config.serviceId, 'service-123'); + }); + + test('should return correct JSON representation', () { + final config = ConnectorConfig('us-central1', 'cloud-sql', 'service-123'); + + final jsonResult = config.toJson(); + final expectedJson = jsonEncode({ + 'location': 'us-central1', + 'connector': 'cloud-sql', + 'serviceId': 'service-123', + }); + + expect(jsonResult, expectedJson); + }); + + test('should handle empty string parameters in JSON', () { + final config = ConnectorConfig('', '', ''); + + final jsonResult = config.toJson(); + final expectedJson = jsonEncode({ + 'location': '', + 'connector': '', + 'serviceId': '', + }); + + expect(jsonResult, expectedJson); + }); + }); + + group('DataConnectOptions', () { + test( + 'should initialize with correct parameters and inherit from ConnectorConfig', + () { + final options = DataConnectOptions( + 'project-abc', 'us-central1', 'cloud-sql', 'service-123'); + + // Test inherited fields from ConnectorConfig + expect(options.location, 'us-central1'); + expect(options.connector, 'cloud-sql'); + expect(options.serviceId, 'service-123'); + + // Test new field specific to DataConnectOptions + expect(options.projectId, 'project-abc'); + }); + + test( + 'should return correct JSON representation for DataConnectOptions via ConnectorConfig toJson', + () { + final options = DataConnectOptions( + 'project-abc', 'us-central1', 'cloud-sql', 'service-123'); + + final jsonResult = options.toJson(); + final expectedJson = jsonEncode({ + 'location': 'us-central1', + 'connector': 'cloud-sql', + 'serviceId': 'service-123', + }); + + // Even though DataConnectOptions has a new field, toJson only reflects fields in ConnectorConfig + expect(jsonResult, expectedJson); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart new file mode 100644 index 000000000000..476a35f0768c --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/core/empty_serializer_test.dart @@ -0,0 +1,27 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('emptySerializer', () { + test('should return an empty string when null is passed', () { + final result = emptySerializer(null); + expect(result, ''); + }); + + test('should return an empty string when any value is passed', () { + final resultWithVoid = emptySerializer(null); // void type simulation + final resultWithInt = emptySerializer(42); + final resultWithString = emptySerializer('Some String'); + final resultWithList = emptySerializer([1, 2, 3]); + + expect(resultWithVoid, ''); + expect(resultWithInt, ''); + expect(resultWithString, ''); + expect(resultWithList, ''); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart new file mode 100644 index 000000000000..a8d320881a55 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/core/ref_test.dart @@ -0,0 +1,87 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'dart:async'; + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Mock classes +class MockDataConnectTransport extends Mock implements DataConnectTransport {} + +class MockFirebaseDataConnect extends Mock implements FirebaseDataConnect {} + +class MockQueryManager extends Mock implements QueryManager {} + +class MockOperationRef extends Mock implements OperationRef {} + +void main() { + group('OperationResult', () { + test('should initialize correctly with provided data and ref', () { + final mockData = 'sampleData'; + final mockRef = MockOperationRef(); + final mockFirebaseDataConnect = MockFirebaseDataConnect(); + + final result = + OperationResult(mockFirebaseDataConnect, mockData, mockRef); + + expect(result.data, mockData); + expect(result.ref, mockRef); + expect(result.dataConnect, mockFirebaseDataConnect); + }); + }); + + group('QueryResult', () { + test('should initialize correctly and inherit from OperationResult', () { + final mockData = 'sampleData'; + final mockRef = MockOperationRef(); + final mockFirebaseDataConnect = MockFirebaseDataConnect(); + + final queryResult = + QueryResult(mockFirebaseDataConnect, mockData, mockRef); + + expect(queryResult.data, mockData); + expect(queryResult.ref, mockRef); + expect(queryResult.dataConnect, mockFirebaseDataConnect); + }); + }); + + group('_QueryManager', () { + late MockFirebaseDataConnect mockDataConnect; + late QueryManager queryManager; + + setUp(() { + mockDataConnect = MockFirebaseDataConnect(); + queryManager = QueryManager(mockDataConnect); + }); + + test( + 'addQuery should create a new StreamController if query does not exist', + () { + final stream = + queryManager.addQuery('testQuery', 'variables', 'varsAsStr'); + + expect(queryManager.trackedQueries['testQuery'], isNotNull); + expect(queryManager.trackedQueries['testQuery']!['varsAsStr'], isNotNull); + expect(stream, isA()); + }); + }); + + group('MutationRef', () { + late MockDataConnectTransport mockTransport; + late MockFirebaseDataConnect mockDataConnect; + late Serializer serializer; + late Deserializer deserializer; + + setUp(() { + mockTransport = MockDataConnectTransport(); + mockDataConnect = MockFirebaseDataConnect(); + serializer = (data) => 'serializedData'; + deserializer = (data) => 'deserializedData'; + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart new file mode 100644 index 000000000000..c597b0471568 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.dart @@ -0,0 +1,175 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +import 'firebase_data_connect_test.mocks.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +class MockTransportOptions extends Mock implements TransportOptions {} + +class MockDataConnectTransport extends Mock implements DataConnectTransport {} + +class MockQueryManager extends Mock implements QueryManager {} + +void main() { + group('FirebaseDataConnect', () { + late MockFirebaseApp mockApp; + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockConnectorConfig mockConnectorConfig; + + setUp(() { + mockApp = MockFirebaseApp(); + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockConnectorConfig = MockConnectorConfig(); + + when(mockApp.options).thenReturn(FirebaseOptions( + apiKey: 'fake_api_key', + appId: 'fake_app_id', + messagingSenderId: 'fake_messaging_sender_id', + projectId: 'fake_project_id', + )); + when(mockConnectorConfig.location).thenReturn('us-central1'); + when(mockConnectorConfig.connector).thenReturn('connector'); + when(mockConnectorConfig.serviceId).thenReturn('serviceId'); + }); + + test('constructor initializes with correct parameters', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(dataConnect.app, equals(mockApp)); + expect(dataConnect.auth, equals(mockAuth)); + expect(dataConnect.appCheck, equals(mockAppCheck)); + expect(dataConnect.connectorConfig, equals(mockConnectorConfig)); + expect(dataConnect.options.projectId, 'fake_project_id'); + }); + + test('checkTransport initializes transport correctly', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + dataConnect.checkTransport(); + + expect(dataConnect.transport, isNotNull); + }); + + test('query method returns QueryRef', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + final queryRef = dataConnect.query( + 'operationName', + (json) => json, + (variables) => variables.toString(), + null, + ); + + expect(queryRef, isA()); + }); + + test('mutation method returns MutationRef', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + final mutationRef = dataConnect.mutation( + 'operationName', + (json) => json, + (variables) => variables.toString(), + null, + ); + + expect(mutationRef, isA()); + }); + + test('useDataConnectEmulator sets correct transport options', () { + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + dataConnect.useDataConnectEmulator('localhost', 8080); + + expect(dataConnect.transportOptions, isNotNull); + expect(dataConnect.transportOptions!.host, '10.0.2.2'); + expect(dataConnect.transportOptions!.port, 8080); + }); + + test('instanceFor returns cached instance if available', () { + FirebaseDataConnect.cachedInstances.clear(); // Clear cache first + + when(mockApp.name).thenReturn('appName'); + when(mockConnectorConfig.toJson()).thenReturn('connectorConfigStr'); + + final dataConnect = FirebaseDataConnect( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + FirebaseDataConnect.cachedInstances['appName'] = { + 'connectorConfigStr': dataConnect + }; + + final instance = FirebaseDataConnect.instanceFor( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(instance, equals(dataConnect)); + }); + + test('instanceFor creates new instance if not cached', () { + FirebaseDataConnect.cachedInstances.clear(); // Clear cache first + + when(mockApp.name).thenReturn('appName'); + when(mockConnectorConfig.toJson()).thenReturn('connectorConfigStr'); + + final instance = FirebaseDataConnect.instanceFor( + app: mockApp, + connectorConfig: mockConnectorConfig, + auth: mockAuth, + appCheck: mockAppCheck, + ); + + expect(instance, isA()); + expect( + FirebaseDataConnect.cachedInstances['appName']!['connectorConfigStr'], + equals(instance)); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart new file mode 100644 index 000000000000..3365f28aadab --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/firebase_data_connect_test.mocks.dart @@ -0,0 +1,204 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Mocks generated by Mockito 5.4.4 from annotations +// in firebase_data_connect/test/src/firebase_data_connect_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:firebase_core/firebase_core.dart' as _i3; +import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart' + as _i2; +import 'package:firebase_data_connect/src/common/common_library.dart' as _i6; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeFirebaseOptions_0 extends _i1.SmartFake + implements _i2.FirebaseOptions { + _FakeFirebaseOptions_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [FirebaseApp]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseApp extends _i1.Mock implements _i3.FirebaseApp { + @override + String get name => (super.noSuchMethod( + Invocation.getter(#name), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#name), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#name), + ), + ) as String); + + @override + _i2.FirebaseOptions get options => (super.noSuchMethod( + Invocation.getter(#options), + returnValue: _FakeFirebaseOptions_0( + this, + Invocation.getter(#options), + ), + returnValueForMissingStub: _FakeFirebaseOptions_0( + this, + Invocation.getter(#options), + ), + ) as _i2.FirebaseOptions); + + @override + bool get isAutomaticDataCollectionEnabled => (super.noSuchMethod( + Invocation.getter(#isAutomaticDataCollectionEnabled), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i5.Future delete() => (super.noSuchMethod( + Invocation.method( + #delete, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setAutomaticDataCollectionEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setAutomaticDataCollectionEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + _i5.Future setAutomaticResourceManagementEnabled(bool? enabled) => + (super.noSuchMethod( + Invocation.method( + #setAutomaticResourceManagementEnabled, + [enabled], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); +} + +/// A class which mocks [ConnectorConfig]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockConnectorConfig extends _i1.Mock implements _i6.ConnectorConfig { + @override + String get location => (super.noSuchMethod( + Invocation.getter(#location), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#location), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#location), + ), + ) as String); + + @override + set location(String? _location) => super.noSuchMethod( + Invocation.setter( + #location, + _location, + ), + returnValueForMissingStub: null, + ); + + @override + String get connector => (super.noSuchMethod( + Invocation.getter(#connector), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#connector), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#connector), + ), + ) as String); + + @override + set connector(String? _connector) => super.noSuchMethod( + Invocation.setter( + #connector, + _connector, + ), + returnValueForMissingStub: null, + ); + + @override + String get serviceId => (super.noSuchMethod( + Invocation.getter(#serviceId), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#serviceId), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.getter(#serviceId), + ), + ) as String); + + @override + set serviceId(String? _serviceId) => super.noSuchMethod( + Invocation.setter( + #serviceId, + _serviceId, + ), + returnValueForMissingStub: null, + ); + + @override + String toJson() => (super.noSuchMethod( + Invocation.method( + #toJson, + [], + ), + returnValue: _i4.dummyValue( + this, + Invocation.method( + #toJson, + [], + ), + ), + returnValueForMissingStub: _i4.dummyValue( + this, + Invocation.method( + #toJson, + [], + ), + ), + ) as String); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart new file mode 100644 index 000000000000..76aaf7aba059 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.dart @@ -0,0 +1,223 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect/src/network/rest_library.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'rest_transport_test.mocks.dart'; + +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +@GenerateMocks([http.Client, User, FirebaseAppCheck]) +void main() { + late RestTransport transport; + late MockClient mockHttpClient; + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockUser mockUser; + + setUp(() { + mockHttpClient = MockClient(); + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockUser = MockUser(); + when(mockAuth.currentUser).thenReturn(mockUser); + + transport = RestTransport( + TransportOptions('testhost', 443, true), + DataConnectOptions( + 'testProject', + 'testLocation', + 'testConnector', + 'testService', + ), + mockAuth, + mockAppCheck, + ); + + transport.setHttp(mockHttpClient); + }); + + group('RestTransport', () { + test('should correctly initialize URL with secure protocol', () { + expect( + transport.url, + 'https://testhost:443/v1alpha/projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + ); + }); + + test('should correctly initialize URL with insecure protocol', () { + final insecureTransport = RestTransport( + TransportOptions('testhost', 443, false), + DataConnectOptions( + 'testProject', + 'testLocation', + 'testConnector', + 'testService', + ), + mockAuth, + mockAppCheck, + ); + + expect( + insecureTransport.url, + 'http://testhost:443/v1alpha/projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + ); + }); + + test('invokeOperation should return deserialized data', () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + final result = await transport.invokeOperation( + 'testQuery', + deserializer, + null, + null, + 'executeQuery', + ); + + expect(result, 'Deserialized Data'); + }); + + test('invokeOperation should throw unauthorized error on 401 response', + () async { + final mockResponse = http.Response('Unauthorized', 401); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + expect( + () => transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'), + throwsA(isA()), + ); + }); + + test('invokeOperation should throw other errors on non-200 responses', + () async { + final mockResponse = http.Response('{"message": "Some error"}', 500); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + expect( + () => transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'), + throwsA(isA()), + ); + }); + + test('invokeQuery should call invokeOperation with correct endpoint', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeQuery('testQuery', deserializer, null, null); + + verify(mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: json.encode({ + 'name': + 'projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + 'operationName': 'testQuery' + }), + )).called(1); + }); + + test('invokeMutation should call invokeOperation with correct endpoint', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + final deserializer = (String data) => 'Deserialized Mutation Data'; + + await transport.invokeMutation('testMutation', deserializer, null, null); + + verify(mockHttpClient.post( + any, + headers: anyNamed('headers'), + body: json.encode({ + 'name': + 'projects/testProject/locations/testLocation/services/testService/connectors/testConnector', + 'operationName': 'testMutation' + }), + )).called(1); + }); + + test('invokeOperation should include auth and appCheck tokens in headers', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + when(mockUser.getIdToken()).thenAnswer((_) async => 'authToken123'); + when(mockAppCheck.getToken()).thenAnswer((_) async => 'appCheckToken123'); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'); + + verify(mockHttpClient.post( + any, + headers: argThat( + containsPair('X-Firebase-Auth-Token', 'authToken123'), + named: 'headers', + ), + body: anyNamed('body'), + )).called(1); + }); + + test( + 'invokeOperation should handle missing auth and appCheck tokens gracefully', + () async { + final mockResponse = http.Response('{"data": {"key": "value"}}', 200); + when(mockHttpClient.post(any, + headers: anyNamed('headers'), body: anyNamed('body'))) + .thenAnswer((_) async => mockResponse); + + when(mockUser.getIdToken()).thenThrow(Exception('Auth error')); + when(mockAppCheck.getToken()).thenThrow(Exception('AppCheck error')); + + final deserializer = (String data) => 'Deserialized Data'; + + await transport.invokeOperation( + 'testQuery', deserializer, null, null, 'executeQuery'); + + verify(mockHttpClient.post( + any, + headers: argThat( + isNot(contains('X-Firebase-Auth-Token')), + named: 'headers', + ), + body: anyNamed('body'), + )).called(1); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart new file mode 100644 index 000000000000..02270373a449 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/rest_transport_test.mocks.dart @@ -0,0 +1,824 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// Mocks generated by Mockito 5.4.4 from annotations +// in firebase_data_connect/test/src/network/rest_transport_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i6; +import 'dart:convert' as _i7; +import 'dart:typed_data' as _i9; + +import 'package:firebase_app_check/firebase_app_check.dart' as _i10; +import 'package:firebase_app_check_platform_interface/firebase_app_check_platform_interface.dart' + as _i11; +import 'package:firebase_auth/firebase_auth.dart' as _i4; +import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart' + as _i3; +import 'package:firebase_core/firebase_core.dart' as _i5; +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i8; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserMetadata_2 extends _i1.SmartFake implements _i3.UserMetadata { + _FakeUserMetadata_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeMultiFactor_3 extends _i1.SmartFake implements _i4.MultiFactor { + _FakeMultiFactor_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeIdTokenResult_4 extends _i1.SmartFake implements _i3.IdTokenResult { + _FakeIdTokenResult_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUserCredential_5 extends _i1.SmartFake + implements _i4.UserCredential { + _FakeUserCredential_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeConfirmationResult_6 extends _i1.SmartFake + implements _i4.ConfirmationResult { + _FakeConfirmationResult_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeUser_7 extends _i1.SmartFake implements _i4.User { + _FakeUser_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFirebaseApp_8 extends _i1.SmartFake implements _i5.FirebaseApp { + _FakeFirebaseApp_8( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + MockClient() { + _i1.throwOnMissingStub(this); + } + + @override + _i6.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i7.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i6.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i6.Future<_i2.Response>); + + @override + _i6.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future.value(_i8.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i6.Future); + + @override + _i6.Future<_i9.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i6.Future<_i9.Uint8List>.value(_i9.Uint8List(0)), + ) as _i6.Future<_i9.Uint8List>); + + @override + _i6.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i6.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i6.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [User]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockUser extends _i1.Mock implements _i4.User { + MockUser() { + _i1.throwOnMissingStub(this); + } + + @override + bool get emailVerified => (super.noSuchMethod( + Invocation.getter(#emailVerified), + returnValue: false, + ) as bool); + + @override + bool get isAnonymous => (super.noSuchMethod( + Invocation.getter(#isAnonymous), + returnValue: false, + ) as bool); + + @override + _i3.UserMetadata get metadata => (super.noSuchMethod( + Invocation.getter(#metadata), + returnValue: _FakeUserMetadata_2( + this, + Invocation.getter(#metadata), + ), + ) as _i3.UserMetadata); + + @override + List<_i3.UserInfo> get providerData => (super.noSuchMethod( + Invocation.getter(#providerData), + returnValue: <_i3.UserInfo>[], + ) as List<_i3.UserInfo>); + + @override + String get uid => (super.noSuchMethod( + Invocation.getter(#uid), + returnValue: _i8.dummyValue( + this, + Invocation.getter(#uid), + ), + ) as String); + + @override + _i4.MultiFactor get multiFactor => (super.noSuchMethod( + Invocation.getter(#multiFactor), + returnValue: _FakeMultiFactor_3( + this, + Invocation.getter(#multiFactor), + ), + ) as _i4.MultiFactor); + + @override + _i6.Future delete() => (super.noSuchMethod( + Invocation.method( + #delete, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getIdToken([bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdToken, + [forceRefresh], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i3.IdTokenResult> getIdTokenResult( + [bool? forceRefresh = false]) => + (super.noSuchMethod( + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + returnValue: _i6.Future<_i3.IdTokenResult>.value(_FakeIdTokenResult_4( + this, + Invocation.method( + #getIdTokenResult, + [forceRefresh], + ), + )), + ) as _i6.Future<_i3.IdTokenResult>); + + @override + _i6.Future<_i4.UserCredential> linkWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #linkWithCredential, + [credential], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithCredential, + [credential], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> linkWithProvider(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithProvider, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithProvider, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithProvider( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithProvider, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithPopup( + _i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithPopup, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future reauthenticateWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithRedirect, + [provider], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.UserCredential> linkWithPopup(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithPopup, + [provider], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #linkWithPopup, + [provider], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future linkWithRedirect(_i3.AuthProvider? provider) => + (super.noSuchMethod( + Invocation.method( + #linkWithRedirect, + [provider], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.ConfirmationResult> linkWithPhoneNumber( + String? phoneNumber, [ + _i4.RecaptchaVerifier? verifier, + ]) => + (super.noSuchMethod( + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + returnValue: + _i6.Future<_i4.ConfirmationResult>.value(_FakeConfirmationResult_6( + this, + Invocation.method( + #linkWithPhoneNumber, + [ + phoneNumber, + verifier, + ], + ), + )), + ) as _i6.Future<_i4.ConfirmationResult>); + + @override + _i6.Future<_i4.UserCredential> reauthenticateWithCredential( + _i3.AuthCredential? credential) => + (super.noSuchMethod( + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + returnValue: _i6.Future<_i4.UserCredential>.value(_FakeUserCredential_5( + this, + Invocation.method( + #reauthenticateWithCredential, + [credential], + ), + )), + ) as _i6.Future<_i4.UserCredential>); + + @override + _i6.Future reload() => (super.noSuchMethod( + Invocation.method( + #reload, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future sendEmailVerification( + [_i3.ActionCodeSettings? actionCodeSettings]) => + (super.noSuchMethod( + Invocation.method( + #sendEmailVerification, + [actionCodeSettings], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future<_i4.User> unlink(String? providerId) => (super.noSuchMethod( + Invocation.method( + #unlink, + [providerId], + ), + returnValue: _i6.Future<_i4.User>.value(_FakeUser_7( + this, + Invocation.method( + #unlink, + [providerId], + ), + )), + ) as _i6.Future<_i4.User>); + + @override + _i6.Future updateEmail(String? newEmail) => (super.noSuchMethod( + Invocation.method( + #updateEmail, + [newEmail], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePassword(String? newPassword) => (super.noSuchMethod( + Invocation.method( + #updatePassword, + [newPassword], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePhoneNumber( + _i3.PhoneAuthCredential? phoneCredential) => + (super.noSuchMethod( + Invocation.method( + #updatePhoneNumber, + [phoneCredential], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updateDisplayName(String? displayName) => + (super.noSuchMethod( + Invocation.method( + #updateDisplayName, + [displayName], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updatePhotoURL(String? photoURL) => (super.noSuchMethod( + Invocation.method( + #updatePhotoURL, + [photoURL], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future updateProfile({ + String? displayName, + String? photoURL, + }) => + (super.noSuchMethod( + Invocation.method( + #updateProfile, + [], + { + #displayName: displayName, + #photoURL: photoURL, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future verifyBeforeUpdateEmail( + String? newEmail, [ + _i3.ActionCodeSettings? actionCodeSettings, + ]) => + (super.noSuchMethod( + Invocation.method( + #verifyBeforeUpdateEmail, + [ + newEmail, + actionCodeSettings, + ], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); +} + +/// A class which mocks [FirebaseAppCheck]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFirebaseAppCheck extends _i1.Mock implements _i10.FirebaseAppCheck { + MockFirebaseAppCheck() { + _i1.throwOnMissingStub(this); + } + + @override + _i5.FirebaseApp get app => (super.noSuchMethod( + Invocation.getter(#app), + returnValue: _FakeFirebaseApp_8( + this, + Invocation.getter(#app), + ), + ) as _i5.FirebaseApp); + + @override + set app(_i5.FirebaseApp? _app) => super.noSuchMethod( + Invocation.setter( + #app, + _app, + ), + returnValueForMissingStub: null, + ); + + @override + _i6.Stream get onTokenChange => (super.noSuchMethod( + Invocation.getter(#onTokenChange), + returnValue: _i6.Stream.empty(), + ) as _i6.Stream); + + @override + Map get pluginConstants => (super.noSuchMethod( + Invocation.getter(#pluginConstants), + returnValue: {}, + ) as Map); + + @override + _i6.Future activate({ + _i11.WebProvider? webProvider, + _i11.AndroidProvider? androidProvider = _i11.AndroidProvider.playIntegrity, + _i11.AppleProvider? appleProvider = _i11.AppleProvider.deviceCheck, + }) => + (super.noSuchMethod( + Invocation.method( + #activate, + [], + { + #webProvider: webProvider, + #androidProvider: androidProvider, + #appleProvider: appleProvider, + }, + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getToken([bool? forceRefresh]) => (super.noSuchMethod( + Invocation.method( + #getToken, + [forceRefresh], + ), + returnValue: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future setTokenAutoRefreshEnabled( + bool? isTokenAutoRefreshEnabled) => + (super.noSuchMethod( + Invocation.method( + #setTokenAutoRefreshEnabled, + [isTokenAutoRefreshEnabled], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + + @override + _i6.Future getLimitedUseToken() => (super.noSuchMethod( + Invocation.method( + #getLimitedUseToken, + [], + ), + returnValue: _i6.Future.value(_i8.dummyValue( + this, + Invocation.method( + #getLimitedUseToken, + [], + ), + )), + ) as _i6.Future); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart new file mode 100644 index 000000000000..eb2484625cac --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/network/transport_stub_test.dart @@ -0,0 +1,87 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_app_check/firebase_app_check.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:firebase_data_connect/src/network/transport_library.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +// Create mock classes for FirebaseAuth, FirebaseAppCheck, and other dependencies. +class MockFirebaseAuth extends Mock implements FirebaseAuth {} + +class MockFirebaseAppCheck extends Mock implements FirebaseAppCheck {} + +class MockTransportOptions extends Mock implements TransportOptions {} + +class MockDataConnectOptions extends Mock implements DataConnectOptions {} + +void main() { + group('TransportStub', () { + late MockFirebaseAuth mockAuth; + late MockFirebaseAppCheck mockAppCheck; + late MockTransportOptions mockTransportOptions; + late MockDataConnectOptions mockDataConnectOptions; + + setUp(() { + mockAuth = MockFirebaseAuth(); + mockAppCheck = MockFirebaseAppCheck(); + mockTransportOptions = MockTransportOptions(); + mockDataConnectOptions = MockDataConnectOptions(); + }); + + test('constructor initializes with correct parameters', () { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect(transportStub.auth, equals(mockAuth)); + expect(transportStub.appCheck, equals(mockAppCheck)); + expect(transportStub.transportOptions, equals(mockTransportOptions)); + expect(transportStub.options, equals(mockDataConnectOptions)); + }); + + test('invokeMutation throws UnimplementedError', () async { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect( + () async => await transportStub.invokeMutation( + 'queryName', + (json) => json, + null, + null, + ), + throwsA(isA()), + ); + }); + + test('invokeQuery throws UnimplementedError', () async { + final transportStub = TransportStub( + mockTransportOptions, + mockDataConnectOptions, + mockAuth, + mockAppCheck, + ); + + expect( + () async => await transportStub.invokeQuery( + 'queryName', + (json) => json, + null, + null, + ), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart new file mode 100644 index 000000000000..47cdd1778ed8 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/optional_test.dart @@ -0,0 +1,91 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// ignore_for_file: unused_local_variable + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +typedef Serializer = String Function(T value); +typedef Deserializer = T Function(String json); + +void main() { + group('Optional', () { + late Deserializer stringDeserializer; + late Serializer stringSerializer; + late Deserializer intDeserializer; + late Serializer intSerializer; + + setUp(() { + stringDeserializer = (json) => json; + stringSerializer = (value) => value.toString(); + intDeserializer = (json) => int.parse(json); + intSerializer = (value) => value.toString(); + }); + + test('constructor initializes with deserializer', () { + final optional = Optional(stringDeserializer); + expect(optional.deserializer, equals(stringDeserializer)); + expect(optional.state, equals(OptionalState.unset)); + expect(optional.value, isNull); + }); + + test('constructor initializes with deserializer and serializer', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + expect(optional.deserializer, equals(stringDeserializer)); + expect(optional.serializer, equals(stringSerializer)); + }); + + test('value setter updates value and sets state', () { + final optional = Optional(stringDeserializer); + + optional.value = 'Test'; + expect(optional.value, equals('Test')); + expect(optional.state, equals(OptionalState.set)); + }); + + test('fromJson correctly deserializes and sets value', () { + final optional = Optional(stringDeserializer); + + optional.fromJson('Test'); + expect(optional.value, equals('Test')); + expect(optional.state, equals(OptionalState.set)); + }); + + test('toJson correctly serializes the value', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + + optional.value = 'Test'; + expect(optional.toJson(), equals('Test')); + }); + + test('toJson returns empty string when value is null', () { + final optional = Optional.optional(stringDeserializer, stringSerializer); + + optional.value = null; + expect(optional.toJson(), equals('')); + }); + + test('nativeToJson correctly serializes primitive types', () { + expect(nativeToJson(42), equals('42')); + expect(nativeToJson(true), equals('true')); + expect(nativeToJson('Test'), equals('Test')); + }); + + test('nativeFromJson correctly deserializes primitive types', () { + expect(nativeFromJson('42'), equals(42)); + expect(nativeFromJson('true'), equals(true)); + expect(nativeFromJson('Test'), equals('Test')); + }); + + test('nativeToJson throws UnimplementedError for unsupported types', () { + expect(() => nativeToJson(DateTime.now()), throwsUnimplementedError); + }); + + test('nativeFromJson throws UnimplementedError for unsupported types', () { + expect(() => nativeFromJson('2024-01-01'), + throwsUnimplementedError); + }); + }); +} diff --git a/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart b/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart new file mode 100644 index 000000000000..a54d2c1a69f2 --- /dev/null +++ b/packages/firebase_data_connect/firebase_data_connect/test/src/timestamp_test.dart @@ -0,0 +1,64 @@ +// Copyright 2024, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Timestamp', () { + test('constructor initializes with correct nanoseconds and seconds', () { + final timestamp = Timestamp(500, 864000); // Example timestamp values + expect(timestamp.nanoseconds, 500); + expect(timestamp.seconds, 864000); + }); + + test('fromJson throws exception for invalid date format', () { + expect(() => Timestamp.fromJson('invalid-date'), throwsException); + }); + + test('fromJson correctly parses date with nanoseconds and UTC (Z) format', + () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00.123456789Z'); + expect(timestamp.seconds, 864000); + expect(timestamp.nanoseconds, 123456789); + }); + + test('fromJson correctly parses date without nanoseconds', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00Z'); + expect(timestamp.seconds, 864000); + expect(timestamp.nanoseconds, 0); + }); + + test('fromJson correctly handles timezones with positive offset', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00+02:00'); + expect(timestamp.seconds, + 864000 - (2 * 3600)); // Adjusts by the positive timezone offset + }); + + test('fromJson correctly handles timezones with negative offset', () { + final timestamp = Timestamp.fromJson('1970-01-11T00:00:00-05:00'); + expect(timestamp.seconds, + 864000 + (5 * 3600)); // Adjusts by the negative timezone offset + }); + + test('toJson correctly serializes to ISO8601 string with nanoseconds', () { + final timestamp = Timestamp(123456789, 864000); // Example timestamp + final json = timestamp.toJson(); + expect(json, '1970-01-11T00:00:00.123456789Z'); + }); + + test('toJson correctly serializes to ISO8601 string without nanoseconds', + () { + final timestamp = Timestamp(0, 864000); // No nanoseconds + final json = timestamp.toJson(); + expect(json, '1970-01-11T00:00:00.000Z'); + }); + + test('toDateTime correctly converts to DateTime object', () { + final timestamp = Timestamp(0, 864000); // Example timestamp + final dateTime = timestamp.toDateTime(); + expect(dateTime, DateTime.utc(1970, 1, 11, 0, 0, 0, 0)); + }); + }); +}