Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ Do **not** include app-specific business context, private data, secrets, or toke
## Platform notes
Most projects are multi-targeted (`net9.0`, `net9.0-android`, `net9.0-ios`).
Prefer validating `net9.0` in CI unless platform-specific changes require more.

## Integration tests
- Before editing `tests/Plugin.Firebase.IntegrationTests`, read `tests/Plugin.Firebase.IntegrationTests/README.md`.
- Use `tests/Plugin.Firebase.IntegrationTests/ACCEPTANCE_COVERAGE.md` as the package coverage checklist.
- Keep emulator-backed coverage automatic and real-backend, destructive, paid, or externally delivered checks opt-in.
- Run `scripts/check-integration-coverage.rb` after adding, renaming, moving, or splitting fixtures.
3 changes: 3 additions & 0 deletions .github/workflows/integration-emulators.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- name: Install Firebase CLI and XHarness
run: scripts/install-integration-test-tools.sh

- name: Check integration coverage metadata
run: scripts/check-integration-coverage.rb

- name: Build Cloud Functions
run: |
npm ci --prefix tests/cloud-functions/functions --legacy-peer-deps
Expand Down
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Do **not** include private app details, proprietary business context, or secrets
- Build (all TFMs): `dotnet build Plugin.Firebase.sln -c Release`
- Build only `net9.0` (no mobile workloads): `dotnet build src/Auth/Auth.csproj -c Release -f net9.0`
- Unit tests: `dotnet test tests/Plugin.Firebase.UnitTests/Plugin.Firebase.UnitTests.csproj`
- Integration coverage audit: `scripts/check-integration-coverage.rb`
- Integration tests are device-only. See `BUILDING.md`.

## AI workflow
Expand All @@ -31,6 +32,13 @@ Do **not** include private app details, proprietary business context, or secrets
3) **Propose change set**: Design a small, verifiable change before editing code.
4) **Keep it documented**: Ensure public API changes are explicit and documented per [Documentation](CONTRIBUTING.md#documentation).

## Integration test workflow
- Before changing `tests/Plugin.Firebase.IntegrationTests`, read `tests/Plugin.Firebase.IntegrationTests/README.md` and `tests/Plugin.Firebase.IntegrationTests/ACCEPTANCE_COVERAGE.md`.
- Keep tests close to native Firebase behavior; avoid app-specific policy, private project details, or hidden real-backend assumptions.
- Use the harness fact attributes, fixture metadata, resource scopes, and file layout documented in the integration test README.
- Update acceptance coverage and real-backend setup docs when public API coverage or backend requirements change.
- Run `scripts/check-integration-coverage.rb` after adding, renaming, moving, or splitting fixtures.

## Packaging & CI
- Packaging guidelines: `docs/packaging-github-packages.md`
- CI guidance: `docs/ci-cd.md`
Expand Down
2 changes: 1 addition & 1 deletion docs/BUILDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ Required functions:
| `returnNumberPayload` | `https.onCall` | Verifies callable number response deserialization |
| `returnBooleanPayload` | `https.onCall` | Verifies callable boolean response deserialization |
| `returnNullPayload` | `https.onCall` | Verifies callable null response deserialization |
| `createCustomToken` | `https.onCall` | Mints custom tokens for Auth acceptance tests |
| `createCustomToken` | `https.onCall` | Emulator-only helper that mints custom tokens for Auth acceptance tests; it rejects non-emulator calls and must not be deployed as a real token-minting endpoint |
| `echoAuthContext` | `https.onCall` | Verifies callable auth context propagation |
| `throwStructuredError` | `https.onCall` | Verifies callable error propagation |
| `regionalPing` | `https.onCall`, `southamerica-east1` | Verifies configured Functions regions |
Expand Down
121 changes: 121 additions & 0 deletions scripts/check-integration-coverage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env ruby

require "pathname"

repo_root = Pathname.new(__dir__).join("..").realpath
tests_root = repo_root.join("tests", "Plugin.Firebase.IntegrationTests")
coverage_file = tests_root.join("ACCEPTANCE_COVERAGE.md")

PACKAGE_LABELS = {
"Analytics" => "Analytics",
"AppCheck" => "App Check",
"Auth" => "Auth",
"Bundled" => "Bundled initializer",
"CloudMessaging" => "Cloud Messaging",
"Crashlytics" => "Crashlytics",
"Firestore" => "Firestore",
"Functions" => "Functions",
"Installations" => "Installations",
"PerformanceMonitoring" => "Performance Monitoring",
"RemoteConfig" => "Remote Config",
"Storage" => "Storage"
}.freeze

def coverage_packages(coverage_file)
packages = {}
File.readlines(coverage_file).each do |line|
next unless line.start_with?("|")

cells = line.split("|").map(&:strip)
package = cells[1]
next if package.nil? || package.empty? || package == "Package" || package.start_with?("---")

packages[package] = true
end
packages
end

def class_declarations(content)
declarations = []
class_pattern = /^\s*(?:public|internal|private)?\s*(?:sealed\s+)?(?:partial\s+)?class\s+(\w+Fixture)\b/
content.to_enum(:scan, class_pattern).each do
match = Regexp.last_match
prefix = content[0...match.begin(0)].lines.last(10).join
declarations << {
name: match[1],
package: prefix[/IntegrationTestFixture\(IntegrationTestPackage\.(\w+)\)/, 1],
ignored: prefix.include?("IntegrationTestCoverageIgnore(")
}
end
declarations
end

def test_case_count(content)
content.scan(/^\s*\[(?:\w*(?:Fact|Theory)|Fact|Theory)(?:\(|\])/).size
end

coverage = coverage_packages(coverage_file)
errors = []
fixtures = {}
test_cases = 0

Dir.glob(tests_root.join("**", "*.cs")).sort.each do |file|
next if file.include?("/bin/") || file.include?("/obj/")

content = File.read(file)
test_cases += test_case_count(content)
class_declarations(content).each do |declaration|
fixture = fixtures[declaration[:name]] ||= {
files: [],
packages: [],
ignored: false
}
fixture[:files] << Pathname.new(file).relative_path_from(repo_root).to_s
fixture[:packages] << declaration[:package] if declaration[:package]
fixture[:ignored] ||= declaration[:ignored]
end
end

fixtures.each do |name, fixture|
next if fixture[:ignored]

packages = fixture[:packages].uniq
if packages.empty?
errors << "#{name} is missing IntegrationTestFixture metadata."
next
end

packages.each do |package|
label = PACKAGE_LABELS[package]
if label.nil?
errors << "#{name} references unknown IntegrationTestPackage.#{package}."
elsif !coverage.key?(label)
errors << "#{name} maps to #{label}, but #{coverage_file.relative_path_from(repo_root)} does not list that package."
end
end
end

if coverage.key?("Dynamic Links")
errors << "Dynamic Links is intentionally excluded and must not be listed in #{coverage_file.relative_path_from(repo_root)}."
end

if errors.any?
warn "Integration coverage metadata audit failed:"
errors.each { |error| warn " - #{error}" }
exit 1
end

tracked_packages = fixtures
.values
.flat_map { |fixture| fixture[:packages] }
.compact
.uniq
.sort
.map { |package| PACKAGE_LABELS.fetch(package, package) }

ignored_fixtures = fixtures.count { |_, fixture| fixture[:ignored] }

puts "Integration coverage metadata audit passed."
puts "Fixtures: #{fixtures.length} (#{ignored_fixtures} ignored harness fixture)"
puts "Packages: #{tracked_packages.join(", ")}"
puts "Test cases discovered: #{test_cases}"
8 changes: 7 additions & 1 deletion scripts/validate-cloudmessaging-trimming.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ manifest="$obj_dir/AndroidManifest.xml"
managed_type="Plugin.Firebase.CloudMessaging.Platforms.Android.MyFirebaseMessagingService"
messaging_action="com.google.firebase.MESSAGING_EVENT"

# This probe validates the generated Android service metadata after full trimming.
# The integration app includes broad test fixtures and runner packages that are not
# linker-warning-clean, so do not let unrelated trim warnings skip the manifest check.
dotnet build "$project" \
-c Release \
-f net9.0-android \
/p:TrimMode=full \
/p:AndroidPackageFormat=apk
/p:AndroidPackageFormat=apk \
/p:TreatWarningsAsErrors=false \
/p:WarningsAsErrors= \
/p:ILLinkTreatWarningsAsErrors=false

python3 - "$acw_map" "$manifest" "$managed_type" "$messaging_action" <<'PY'
from pathlib import Path
Expand Down
2 changes: 1 addition & 1 deletion src/Analytics/Shared/IFirebaseAnalytics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,4 @@ public interface IFirebaseAnalytics : IDisposable
/// By default it is enabled.
/// </summary>
bool IsAnalyticsCollectionEnabled { set; }
}
}
47 changes: 47 additions & 0 deletions tests/Plugin.Firebase.IntegrationTests/ACCEPTANCE_COVERAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Integration Acceptance Coverage

`Plugin.Firebase.IntegrationTests` is the acceptance suite for active packages. Dynamic Links is intentionally excluded.

Use this file as the checklist when adding or changing public mobile APIs: every active public surface should be covered automatically, explicitly gated behind a real-backend or opt-in attribute, or documented as not locally assertable.

Run `scripts/check-integration-coverage.rb` after adding or renaming fixtures. The script verifies that every fixture is annotated with `IntegrationTestFixture` metadata and maps to a package listed in the table below. Harness-only fixtures must opt out with `IntegrationTestCoverageIgnore` and a reason.

| Package | Default emulator gate | Real backend | Opt-in/manual | Notes |
|---|---:|---:|---:|---|
| Analytics | No | Yes | No | Verifies SDK acceptance and observable local state; Firebase Console ingestion is not asserted. |
| App Check | Partial | Optional | `PLUGIN_FIREBASE_RUN_APPCHECK_TOKEN_TESTS` | Disabled/debug/provider behavior is automatic; token enforcement requires a real project. |
| Auth | Yes | Partial | `PLUGIN_FIREBASE_RUN_PHONE_AUTH_TESTS` | Email/password, anonymous, custom tokens, email links, metadata, and claims are covered. Phone auth requires external credentials. |
| Bundled initializer | Yes | Yes | No | Verifies singleton access and dispose/reacquire behavior without reconfiguring initialized native SDKs. |
| Cloud Messaging | Partial | Optional | `PLUGIN_FIREBASE_RUN_FCM_TOKEN_TESTS`, `PLUGIN_FIREBASE_RUN_FCM_DELIVERY_TESTS` | Synthetic events are automatic. Token and push delivery require a real project; iOS delivery requires a physical device with APNs. |
| Crashlytics | Partial | Partial | `PLUGIN_FIREBASE_FORCE_CRASHLYTICS_CRASH`, `PLUGIN_FIREBASE_EXPECT_PREVIOUS_CRASH` | Non-crash APIs are automatic. Previous-crash detection needs a destructive two-run flow. |
| Firestore | Yes | Yes | No | Emulator-backed PR gate covers document, query, conversion, nullability, listener, lifecycle, and offline behavior. |
| Functions | Yes | Yes | No | Real backend requires deploying `tests/cloud-functions/functions`. |
| Installations | Partial | Yes | `PLUGIN_FIREBASE_RUN_INSTALLATIONS_DELETE_TESTS` | Delete is opt-in because it resets the shared installation identity. |
| Performance Monitoring | Partial | Yes | No | Verifies SDK acceptance and local state; Firebase Console ingestion is not asserted. |
| Remote Config | No | Yes | No | Requires published real-project parameters. |
| Storage | Yes | Yes | No | Real backend requires bucket seed files and permissive test rules. |

## Expected Fixture Metadata

| Metadata package | Acceptance coverage row |
|---|---|
| `IntegrationTestPackage.Analytics` | Analytics |
| `IntegrationTestPackage.AppCheck` | App Check |
| `IntegrationTestPackage.Auth` | Auth |
| `IntegrationTestPackage.Bundled` | Bundled initializer |
| `IntegrationTestPackage.CloudMessaging` | Cloud Messaging |
| `IntegrationTestPackage.Crashlytics` | Crashlytics |
| `IntegrationTestPackage.Firestore` | Firestore |
| `IntegrationTestPackage.Functions` | Functions |
| `IntegrationTestPackage.Installations` | Installations |
| `IntegrationTestPackage.PerformanceMonitoring` | Performance Monitoring |
| `IntegrationTestPackage.RemoteConfig` | Remote Config |
| `IntegrationTestPackage.Storage` | Storage |

## Harness Rules

- Prefer `Fact`, `EmulatorBackendFact`, `RealFirebaseFact`, `OptInFact`, or `RealFirebaseOptInFact` over runtime skips. Runtime skips can be reported as failures by device runners.
- Put `IntegrationTestFixture(IntegrationTestPackage.X)` on each package fixture root. Use the backend/platform/opt-in fact attributes for method-level metadata, and add `IntegrationTestCase` only when a plain xUnit attribute needs explicit metadata.
- Use `IntegrationTestData` for unique resource names and opt-in configuration values.
- Use `WaitForTestAsync` or `EventuallyAsync` for asynchronous device callbacks so timeout failures include the operation being awaited.
- Keep destructive, paid, external-delivery, or console-ingestion checks behind opt-in attributes.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
namespace Plugin.Firebase.IntegrationTests.Analytics
{
#if ANDROID || IOS
[IntegrationTestFixture(IntegrationTestPackage.Analytics)]
[Preserve(AllMembers = true)]
public sealed class AnalyticsConsentMappingFixture
{
Expand Down Expand Up @@ -232,10 +233,10 @@ public void mixed_valid_and_invalid_consent_status_settings_throw_argument_out_o
[Fact]
public void null_consent_settings_throw_argument_null_exception()
{
IDictionary<ConsentType, ConsentStatus> settings = null;
IDictionary<ConsentType, ConsentStatus>? settings = null;

var exception = Assert.Throws<ArgumentNullException>(
() => settings.ToNativeConsentSettings()
() => settings!.ToNativeConsentSettings()
);

Assert.Equal("consentSettings", exception.ParamName);
Expand Down Expand Up @@ -315,4 +316,4 @@ private static NativeConsentStatus ExpectedNativeStatus(ConsentStatus consentSta
}
}
#endif
}
}
Loading
Loading