diff --git a/.github/actions/latdx-test/action.yml b/.github/actions/latdx-test/action.yml new file mode 100644 index 000000000..67240175d --- /dev/null +++ b/.github/actions/latdx-test/action.yml @@ -0,0 +1,211 @@ +name: 'LATdx Apex Tests' +description: >- + Install the LATdx CLI, resolve a license (explicit key via the + LATDX_LICENSE_KEY env var, or a short-lived OSS license minted from the + GitHub Actions OIDC token on public repos), and run the full local Apex + test suite against the job's default Salesforce org. + +inputs: + cli-version: + # Must be >= 0.43.0: the OSS-license-uncaps-in-CI logic (#2190) landed + # 2026-06-10, one day after the last STABLE release (0.41.4), so 'latest' + # (= latest stable) still resolves to a binary that hard-blocks CI runs + # without a TEAM/CI token regardless of the OSS license. Pin a release + # that carries #2190 until a stable >= 0.43.0 ships; bump deliberately. + description: "LATdx CLI version to install (semver like '0.46.0') or 'latest'." + required: false + default: '0.46.0' + license: + # Pass an optional TEAM/CI license here as an INPUT, not via an + # `env: LATDX_LICENSE_KEY`. An unset secret passed as env sets an EMPTY + # LATDX_LICENSE_KEY at the action level, which overrides the OSS license + # this action resolves into $GITHUB_ENV and silently free-tier-caps the + # run. An empty input is inert. + description: 'Optional TEAM/CI LATdx license key. Leave empty to use the OSS auto-license on public repos.' + required: false + default: '' + +runs: + using: composite + steps: + # LATdx's cache Phase 4 runs an apex-ls bridge on the JVM. Hosted images + # vary (ubuntu-latest ships a recent Temurin, ubuntu-22.04 a default JDK + # too old for the bridge jar -> "JVM exited (code=1)"), so pin a known-good + # JDK on PATH rather than relying on the image default. + - name: 'Set up Java for LATdx apex-ls' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + + - name: 'Install LATdx CLI' + shell: bash + env: + CLI_VERSION: ${{ inputs.cli-version }} + run: | + set -euo pipefail + if [[ ! "$CLI_VERSION" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + echo "::error::Invalid 'cli-version' input. Must be 'latest' or a semver like '0.31.1'." + exit 1 + fi + # latdx.com serves the maintained install script, but Cloudflare + # returns 403 to some hosted-runner egress IP ranges; fall back to + # the GitHub raw mirror so the install succeeds from any runner. + script="" + for url in "https://latdx.com/install.sh" "https://raw.githubusercontent.com/nebulity/latdx-cli/main/install.sh"; do + if script="$(curl -fsSL "$url")"; then + break + fi + script="" + done + if [ -z "$script" ]; then + echo "::error::Could not download the LATdx install script from any source." + exit 1 + fi + if [ "$CLI_VERSION" = "latest" ]; then + printf '%s' "$script" | bash + else + printf '%s' "$script" | bash -s -- "$CLI_VERSION" + fi + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: 'Verify LATdx CLI' + shell: bash + run: latdx --version + + - name: 'Resolve LATdx license' + shell: bash + env: + INPUT_LICENSE: ${{ inputs.license }} + run: | + # The runner injects -e -o pipefail; this step must never fail the + # job: every problem degrades to the free-tier cap with a warning. + set +e +o pipefail + + # Precedence: + # 1. `license` input (TEAM/CI token) -> use it. + # 2. GH OIDC token available + repo public -> exchange for an OSS + # auto-license via https://latdx.com/api/oss/license. + # 3. Nothing -> daemon runs free-tier (capped at 100 tests, exit 2). + + if [ -n "${INPUT_LICENSE:-}" ]; then + echo "::add-mask::$INPUT_LICENSE" + echo "LATDX_LICENSE_KEY=$INPUT_LICENSE" >> "$GITHUB_ENV" + # A non-OSS token is live-resolved by the daemon against + # /api/license/resolve; route it around the latdx.com edge too. + echo "LATDX_LICENSE_BASE_URL=https://latdx-site.asolokh.workers.dev" >> "$GITHUB_ENV" + echo "Using provided LATdx license." + exit 0 + fi + + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "::warning title=LATdx OSS license::OIDC token unavailable (missing 'permissions: id-token: write' on the job). Free-tier cap (100 tests/run) applies." + exit 0 + fi + + OIDC_RESPONSE="$(curl -sS -f -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https%3A%2F%2Flatdx.com" 2>/dev/null)" + CURL_RC=$? + if [ "$CURL_RC" -ne 0 ]; then + echo "::warning title=LATdx OSS license::OIDC token mint failed (curl rc=${CURL_RC}); falling back to free-tier cap." + exit 0 + fi + OIDC_TOKEN="$(printf '%s' "$OIDC_RESPONSE" | jq -r '.value // empty' 2>/dev/null)" + if [ -z "$OIDC_TOKEN" ]; then + echo "::warning title=LATdx OSS license::OIDC response not parseable (length ${#OIDC_RESPONSE}); falling back to free-tier cap." + exit 0 + fi + echo "Minted GitHub OIDC token (length ${#OIDC_TOKEN})." + + EXCHANGE_BODY="$(mktemp)" + EXCHANGE_HEADERS="$(mktemp)" + trap 'rm -f "$EXCHANGE_BODY" "$EXCHANGE_HEADERS"' EXIT + + # Try the branded origin first (it self-heals once its edge allows + # GitHub Actions egress), then the Worker's *.workers.dev origin, + # which is not behind the latdx.com zone's WAF/security rules that + # currently 403 hosted-runner IPs. The OSS route authenticates on the + # OIDC token's audience claim, not the request Host, so the same token + # validates on either origin; the route's own OIDC + monthly-cap + + # kill-switch protections apply regardless of which origin serves it. + LATDX_JWT="" + for OSS_URL in \ + "https://latdx.com/api/oss/license" \ + "https://latdx-site.asolokh.workers.dev/api/oss/license"; do + HTTP_CODE="$(curl -sS -o "$EXCHANGE_BODY" -D "$EXCHANGE_HEADERS" -w "%{http_code}" \ + -X POST "$OSS_URL" \ + -H "Authorization: Bearer $OIDC_TOKEN" \ + -H "Content-Type: application/json" \ + --max-time 15)" + [ $? -ne 0 ] && HTTP_CODE="000" + echo "OSS license exchange via ${OSS_URL} -> HTTP ${HTTP_CODE}." + + if [ "$HTTP_CODE" = "200" ]; then + LATDX_JWT="$(jq -r '.license // empty' < "$EXCHANGE_BODY" 2>/dev/null)" + if [ -n "$LATDX_JWT" ]; then + echo "::add-mask::$LATDX_JWT" + echo "LATDX_LICENSE_KEY=$LATDX_JWT" >> "$GITHUB_ENV" + # The daemon live-resolves the license per run against + # LATDX_LICENSE_BASE_URL + /api/license/resolve (default + # latdx.com). Point it at the same origin that minted the + # license so the resolve isn't blocked by the latdx.com edge + # either; it self-heals to latdx.com once that edge is fixed. + LICENSE_ORIGIN="${OSS_URL%/api/oss/license}" + echo "LATDX_LICENSE_BASE_URL=$LICENSE_ORIGIN" >> "$GITHUB_ENV" + USED="$(jq -r '.monthly_used // "?"' < "$EXCHANGE_BODY" 2>/dev/null)" + CAP="$(jq -r '.monthly_cap // "?"' < "$EXCHANGE_BODY" 2>/dev/null)" + echo "OSS auto-license active (monthly_used=$USED, monthly_cap=$CAP) via ${LICENSE_ORIGIN}; full suite enabled." + break + fi + echo "::warning title=LATdx OSS license::200 without a license from ${OSS_URL}; trying next origin." + elif [ "$HTTP_CODE" = "429" ]; then + USED="$(jq -r '.monthly_used // "?"' < "$EXCHANGE_BODY" 2>/dev/null)" + CAP="$(jq -r '.monthly_cap // "?"' < "$EXCHANGE_BODY" 2>/dev/null)" + RESET="$(jq -r '.reset_at // "next UTC month"' < "$EXCHANGE_BODY" 2>/dev/null)" + echo "::warning title=LATdx OSS license::Monthly cap reached (${USED}/${CAP}); resets ${RESET}." + break + else + # 403/503/000/etc: log edge diagnostics (no secrets) and fall + # through to the next origin. + echo "--- server: $(grep -i '^server:' "$EXCHANGE_HEADERS" | tr -d '\r')" + echo "--- cf-ray: $(grep -i '^cf-ray:' "$EXCHANGE_HEADERS" | tr -d '\r')" + echo "--- body (first 200 chars): $(head -c 200 "$EXCHANGE_BODY" | tr '\n' ' ')" + fi + done + + if [ -z "$LATDX_JWT" ]; then + echo "::warning title=LATdx OSS license::No OSS license obtained from any origin; the run will halt on the CI license gate." + fi + + - name: 'Run Apex tests with LATdx' + shell: bash + run: | + # Retry ONLY the transient "Daemon failed to start" spawn timeout + # (cold-runner timing: the bun daemon + apex-ls JVM can miss the + # client's 5s connect budget). Real test failures (exit 1) and the + # license-gate halt (exit 2) are never retried. + rc=1 + for attempt in 1 2 3; do + set +e + latdx test run 2>&1 | tee /tmp/latdx-out.txt + rc=${PIPESTATUS[0]} + set -e + if [ "$rc" -ne 0 ] && grep -q "Daemon failed to start" /tmp/latdx-out.txt && [ "$attempt" -lt 3 ]; then + echo "Daemon spawn failed (transient, attempt ${attempt}/3); stopping daemon and retrying in 5s..." + latdx daemon stop >/dev/null 2>&1 || true + sleep 5 + continue + fi + break + done + # Exit 2 is EXIT_LICENSE_REQUIRED: the CI license gate halted the run. + # With no resolved license the gate blocks before any test executes + # (it does not fall back to a 100-test free tier); a resolved free/pro + # license would instead cap at 100. Either way no full suite ran. Keep + # the pipeline green but say so honestly: lift it with the OSS + # auto-license (OIDC, public repos) or LATDX_LICENSE_KEY. + if [ "$rc" -eq 2 ]; then + echo "::warning title=LATdx license::Run halted by the CI license gate (exit 2); no full suite ran. Provide an OSS auto-license or LATDX_LICENSE_KEY to run NebulaLogger's tests." + exit 0 + fi + exit "$rc" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58e705047..7a2b662c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,6 +22,25 @@ on: - config/scratch-orgs/** - nebula-logger/** - sfdx-project.json + # Mirrored upstream PRs are pushed with the workflow GITHUB_TOKEN, whose + # events never start new workflow runs; the mirror workflow dispatches + # builds explicitly instead. + workflow_dispatch: + +# Mirrored branches execute upstream-authored code, so the default token is +# read-only; id-token lets jobs mint a GitHub OIDC token to exchange for a +# LATdx OSS license. +permissions: + contents: read + pull-requests: read + id-token: write + +# The fork deploys every build to one shared reserved pool org, so all fork +# builds must serialize (queue, never cancel). Upstream creates a fresh org +# per run, so it keeps native cross-run parallelism via a per-run group. +concurrency: + group: ${{ github.repository == 'jongpie/NebulaLogger' && format('upstream-build-{0}', github.run_id) || 'nebula-fork-shared-org' }} + cancel-in-progress: false jobs: code-quality-tests: @@ -46,6 +65,9 @@ jobs: run: npm ci - name: 'Check for changes in core directory' + # paths-filter needs a PR or push payload to diff against; mirrored + # builds arrive via workflow_dispatch where it has no base. + if: ${{ github.event_name != 'workflow_dispatch' }} uses: dorny/paths-filter@v3 id: changes with: @@ -53,8 +75,10 @@ jobs: core: - './nebula-logger/core/**' + # Package version verification queries packages that exist only in the + # upstream maintainer's Dev Hub, so both steps stay upstream-only. - name: 'Authorize Dev Hub' - if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.draft == false) && (steps.changes.outputs.core == 'true') }} + if: ${{ (github.repository == 'jongpie/NebulaLogger') && (github.event_name == 'pull_request') && (github.event.pull_request.draft == false) && (steps.changes.outputs.core == 'true') }} shell: bash run: | npx sf version @@ -67,15 +91,14 @@ jobs: DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} - name: 'Verify package version number is updated' - if: ${{ (github.event_name == 'pull_request') && (github.event.pull_request.draft == false) && (steps.changes.outputs.core == 'true') }} + if: ${{ (github.repository == 'jongpie/NebulaLogger') && (github.event_name == 'pull_request') && (github.event.pull_request.draft == false) && (steps.changes.outputs.core == 'true') }} run: npm run package:version:number:verify - - name: 'Verify LWC with ESLint' + - name: 'Verify LWC with Code Analyzer' run: npm run scan:lwc - - name: 'Verify Apex with SFDX Scanner' + - name: 'Verify Apex with Code Analyzer' run: | - npm run sf:plugins:link:scanner npm run scan:apex # TODO - uncomment - temporarily commented-out due to an issue with apexdocs in the pipeline @@ -111,6 +134,8 @@ jobs: run: npm run test:lwc - name: 'Upload LWC code coverage to Codecov.io' + # Codecov is upstream's reporting concern; the fork has no token. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} uses: codecov/codecov-action@v4 with: fail_ci_if_error: true @@ -118,6 +143,10 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} advanced-scratch-org-tests: + # Feature-permutation orgs stay upstream-only: the fork's Dev Hub daily + # scratch allowance (6 orgs/day, 3 active) cannot fund the 6-org matrix + # per PR; LATdx coverage on the base org carries the runner-swap signal. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} environment: 'Advanced Scratch Org' name: 'Run Advanced Scratch Org Tests' needs: [code-quality-tests] @@ -140,56 +169,72 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/advanced-scratch-def.json --wait 20 --set-default --json # https://help.salesforce.com/s/articleView?id=000394906&type=1 - - name: 'Install OmniStudio managed package v250.7.1 (Summer ‘24 release)' - run: npx sf package install --package 04t4W000002Z6oC --security-type AdminsOnly --wait 30 --no-prompt - - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + - name: "Install OmniStudio managed package v258.6 (Winter '26 release)" + run: npx sf package install --package 04tKb000000tAtyIAE --security-type AdminsOnly --wait 30 --no-prompt + + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts - name: 'Deploy Test Metadata' run: npx sf project deploy start --source-dir ./config/scratch-orgs/ + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} - name: 'Delete Scratch Org' run: npx sf org delete scratch --no-prompt if: ${{ always() }} base-scratch-org-tests: + # In the fork, mirrored PRs are the signal; pushes to fork main skip the + # org job to preserve the small daily scratch allowance. + if: ${{ github.repository == 'jongpie/NebulaLogger' || github.event_name != 'push' }} environment: 'Base Scratch Org' name: 'Run Base Scratch Org Tests' needs: [code-quality-tests] @@ -212,7 +257,33 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # Upstream creates a throwaway scratch org per run. The fork instead + # reuses one long-lived org reserved from the LATdx scratch pool + # (the pool Dev Hub blocks fresh creates), authorized from its SFDX + # auth URL. The org is redeployed every build, which is idempotent. + - name: 'Authorize reserved pool org' + if: ${{ github.repository != 'jongpie/NebulaLogger' }} + shell: bash + run: | + npx sf version + echo "${TARGET_ORG_SFDX_AUTH_URL}" > ./target-auth-url.txt + # The org's OAuth token endpoint occasionally times out from a + # runner (transient RefreshTokenAuthError); retry before giving up. + for attempt in 1 2 3; do + if npx sf org login sfdx-url --sfdx-url-file ./target-auth-url.txt --alias target-org --set-default; then + break + fi + echo "Org auth attempt ${attempt} failed; retrying in 10s..." + sleep 10 + done + rm -f ./target-auth-url.txt + # Fail loudly if every attempt failed rather than proceeding unauthed. + npx sf org display --target-org target-org >/dev/null + env: + TARGET_ORG_SFDX_AUTH_URL: ${{ secrets.TARGET_ORG_SFDX_AUTH_URL }} + - name: 'Authorize Dev Hub' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} shell: bash run: | npx sf version @@ -225,30 +296,41 @@ jobs: DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} - name: 'Create Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/base-scratch-def.json --wait 20 --set-default --json - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} # This is the only place in Nebula Logger's pipeline where the `LoggerCore` test suite runs & the results are sent to Codecov.io. # This is specifically done in the base scratch org, using only the `LoggerCore test` suite, in order to help validate that the core metadata @@ -266,21 +348,31 @@ jobs: # So now only the core test suite's results, from a base scratch org, are used for reporting code coverage, even though the project's overall code coverage # is much higher when using the `extra-tests` directory. - name: 'Get Core Test Suite Code Coverage' + # Coverage run exists to feed Codecov, which is upstream's reporting + # concern; the fork has no Codecov token. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npm run test:apex:suite:core # This is the only scratch org that's used for uploading code coverage - name: 'Upload Apex test code coverage to Codecov.io' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} uses: codecov/codecov-action@v4 with: fail_ci_if_error: true flags: Apex token: ${{ secrets.CODECOV_TOKEN }} + # Only upstream's throwaway org is deleted; the fork's reserved pool + # org is long-lived and reused across builds. - name: 'Delete Scratch Org' run: npx sf org delete scratch --no-prompt - if: ${{ always() }} + if: ${{ always() && github.repository == 'jongpie/NebulaLogger' }} event-monitoring-scratch-org-tests: + # Feature-permutation orgs stay upstream-only: the fork's Dev Hub daily + # scratch allowance (6 orgs/day, 3 active) cannot fund the 6-org matrix + # per PR; LATdx coverage on the base org carries the runner-swap signal. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} environment: 'Event Monitoring Scratch Org' name: 'Run Event Monitoring Scratch Org Tests' needs: [code-quality-tests] @@ -303,49 +395,66 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/event-monitoring-scratch-def.json --wait 20 --set-default --json - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} - name: 'Delete Scratch Org' run: npx sf org delete scratch --no-prompt if: ${{ always() }} experience-cloud-scratch-org-tests: + # Feature-permutation orgs stay upstream-only: the fork's Dev Hub daily + # scratch allowance (6 orgs/day, 3 active) cannot fund the 6-org matrix + # per PR; LATdx coverage on the base org carries the runner-swap signal. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} environment: 'Experience Cloud Scratch Org' name: 'Run Experience Cloud Scratch Org Tests' needs: [code-quality-tests, advanced-scratch-org-tests] @@ -368,52 +477,69 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/experience-cloud-scratch-def.json --wait 20 --set-default --json - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts - name: 'Deploy Test Experience Sites Metadata' run: npx sf project deploy start --source-dir ./config/scratch-orgs/experience-cloud/ + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} - name: 'Delete Scratch Org' run: npx sf org delete scratch --no-prompt if: ${{ always() }} omnistudio-scratch-org-tests: + # Feature-permutation orgs stay upstream-only: the fork's Dev Hub daily + # scratch allowance (6 orgs/day, 3 active) cannot fund the 6-org matrix + # per PR; LATdx coverage on the base org carries the runner-swap signal. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} environment: 'OmniStudio Scratch Org' name: 'Run OmniStudio Scratch Org Tests' needs: [code-quality-tests, base-scratch-org-tests] @@ -436,56 +562,73 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/omnistudio-scratch-def.json --wait 20 --set-default --json # https://help.salesforce.com/s/articleView?id=000394906&type=1 - - name: 'Install OmniStudio managed package v250.7.1 (Summer ‘24 release)' - run: npx sf package install --package 04t4W000002Z6oC --security-type AdminsOnly --wait 30 --no-prompt - - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + - name: "Install OmniStudio managed package v258.6 (Winter '26 release)" + run: npx sf package install --package 04tKb000000tAtyIAE --security-type AdminsOnly --wait 30 --no-prompt + + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts - name: 'Deploy Test OmniStudio Metadata' run: npx sf project deploy start --source-dir ./config/scratch-orgs/omnistudio/ + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} - name: 'Delete Base Scratch Org' run: npx sf org delete scratch --no-prompt if: ${{ always() }} platform-cache-scratch-org-tests: + # Feature-permutation orgs stay upstream-only: the fork's Dev Hub daily + # scratch allowance (6 orgs/day, 3 active) cannot fund the 6-org matrix + # per PR; LATdx coverage on the base org carries the runner-swap signal. + if: ${{ github.repository == 'jongpie/NebulaLogger' }} environment: 'Platform Cache Scratch Org' name: 'Run Platform Cache Scratch Org Tests' needs: [code-quality-tests, event-monitoring-scratch-org-tests] @@ -508,50 +651,64 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --duration-days 1 --definition-file ./config/scratch-orgs/platform-cache-scratch-def.json --wait 20 --set-default --json - # To ensure that all of the Apex classes in the core directory have 75+ code coverage, - # deploy only the core directory & run all of its tests as part of the deployment, using `--test-level RunLocalTests` + # Upstream's core-coverage gate (deploy-validate core with RunLocalTests + # on a clean org) is upstream-only. The fork reuses one org that already + # has nebula-logger's example triggers (intentionally 0% coverage) + # deployed, which trips this org-wide coverage gate; the fork's concern + # is running the suite via LATdx, not re-validating core coverage. - name: 'Validate Core Source in Scratch Org' + if: ${{ github.repository == 'jongpie/NebulaLogger' }} run: npx sf project deploy validate --concise --source-dir ./nebula-logger/core/ --test-level RunLocalTests - # Now that the core directory has been deployed & tests have passed, deploy all of the metadata + # Deploy all of the metadata. `--ignore-conflicts` overrides the org's + # server-side SourceMember tracking, which on a reused org remembers a + # prior deploy and reports every component as a conflict; it is a no-op + # on a fresh org, where there is nothing to conflict with. - name: 'Deploy All Source to Scratch Org' - run: npx sf project deploy start --source-dir ./nebula-logger/ + run: npx sf project deploy start --source-dir ./nebula-logger/ --ignore-conflicts + # The fork reuses a long-lived org where this permset is already + # assigned from prior builds (a re-assign exits non-zero); tolerate it. + # No-op upstream, whose throwaway orgs start unassigned. - name: 'Assign Logger Admin Permission Set' run: npm run permset:assign:admin - - # Nebula Logger has functionality that queries the AuthSession object when the current user has an active session. - # The code should work with or without an active session, so the pipeline runs the tests twice - asynchronously and synchronously. - # This is done because, based on how you execute Apex tests, the running user may have an active session (synchrously) or not (asynchronously). - # This data is also mocked during tests, but running the Apex tests sync & async serves within the pipeline acts as an extra level of - # integration testing to ensure that everything works with or without an active session. - - name: 'Run Apex Tests Asynchronously' - run: npm run test:apex:nocoverage - - - name: 'Run Apex Tests Synchronously' - run: npm run test:apex:nocoverage -- --synchronous + continue-on-error: ${{ github.repository != 'jongpie/NebulaLogger' }} + + # Upstream runs the suite twice (async + sync, an AuthSession integration + # nuance) with `sf apex run test`. The fork runs the same local-test + # selection once through the LATdx CLI; the sync/async permutation stays + # covered by upstream's own CI, and the deploy validate step above still + # executes RunLocalTests server-side as part of the deploy lifecycle. + - name: 'Run Apex Tests with LATdx' + uses: ./.github/actions/latdx-test + with: + license: ${{ secrets.LATDX_CI_LICENSE_KEY }} - name: 'Delete Scratch Org' run: npx sf org delete scratch --no-prompt if: ${{ always() }} create-managed-package-beta: - if: ${{ github.ref != 'refs/heads/main' }} + # Package versions belong to the upstream maintainer's Dev Hub. + if: ${{ github.repository == 'jongpie/NebulaLogger' && github.ref != 'refs/heads/main' }} environment: 'Base Scratch Org' name: 'Create Managed Package Beta' @@ -603,7 +760,8 @@ jobs: run: npm run package:version:create:managed create-unlocked-package-release-candidate: - if: ${{ github.ref != 'refs/heads/main' }} + # Package versions belong to the upstream maintainer's Dev Hub. + if: ${{ github.repository == 'jongpie/NebulaLogger' && github.ref != 'refs/heads/main' }} environment: 'Base Scratch Org' name: 'Create Core Package Release Candidate' @@ -629,17 +787,20 @@ jobs: if: steps.cache-npm.outputs.cache-hit != 'true' run: npm ci + # The fork's Dev Hub has no JWT connected app; an SFDX auth URL secret + # authorizes the same default Dev Hub for scratch org creation. - name: 'Authorize Dev Hub' shell: bash run: | npx sf version - echo "${{ env.DEV_HUB_JWT_SERVER_KEY }}" > ./jwt-server.key - npx sf org login jwt --instance-url ${{ env.DEV_HUB_AUTH_URL }} --client-id ${{ env.DEV_HUB_CONSUMER_KEY }} --username ${{ env.DEV_HUB_BOT_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub + # --sfdx-url-stdin takes a value and consumes the next CLI token, + # so the file variant is used instead (same pattern as upstream's + # jwt-server.key). + echo "${DEV_HUB_SFDX_AUTH_URL}" > ./sfdx-auth-url.txt + npx sf org login sfdx-url --sfdx-url-file ./sfdx-auth-url.txt --alias devhub --set-default-dev-hub + rm -f ./sfdx-auth-url.txt env: - DEV_HUB_AUTH_URL: ${{ secrets.DEV_HUB_AUTH_URL }} - DEV_HUB_BOT_USERNAME: ${{ secrets.DEV_HUB_BOT_USERNAME }} - DEV_HUB_CONSUMER_KEY: ${{ secrets.DEV_HUB_CONSUMER_KEY }} - DEV_HUB_JWT_SERVER_KEY: ${{ secrets.DEV_HUB_JWT_SERVER_KEY }} + DEV_HUB_SFDX_AUTH_URL: ${{ secrets.DEV_HUB_SFDX_AUTH_URL }} - name: 'Create Scratch Org' run: npx sf org create scratch --no-namespace --no-track-source --alias base_package_subscriber_scratch_org --duration-days 1 --definition-file ./config/scratch-orgs/base-scratch-def.json --wait 20 --set-default --json @@ -738,7 +899,8 @@ jobs: run: git push promote-package-versions: - if: ${{ github.ref == 'refs/heads/main' }} + # Package versions belong to the upstream maintainer's Dev Hub. + if: ${{ github.repository == 'jongpie/NebulaLogger' && github.ref == 'refs/heads/main' }} name: 'Promote Package Versions' needs: diff --git a/.github/workflows/mirror-upstream-prs.yml b/.github/workflows/mirror-upstream-prs.yml new file mode 100644 index 000000000..e21c0d56e --- /dev/null +++ b/.github/workflows/mirror-upstream-prs.yml @@ -0,0 +1,183 @@ +# Mirrors open pull requests from the upstream repo (jongpie/NebulaLogger) +# into this fork so the fork's LATdx-based Build pipeline runs against them. +# +# For each open upstream PR the workflow: +# 1. Fetches the PR head and recreates it as branch `upstream-pr-`. +# 2. Overlays `.github/` from the fork's main so mirrored code can never +# alter the CI that runs against it (workflow-injection guard). +# 3. Pushes the branch and opens/updates a fork PR labeled `upstream-mirror`. +# 4. Dispatches the Build workflow on the branch (pushes made with +# GITHUB_TOKEN never trigger workflow runs by themselves). +# +# PRs that touch package.json / package-lock.json are mirrored but NOT +# auto-built: steps holding the reserved org's credentials execute `npx sf` +# resolved from node_modules, so a tampered lockfile could exfiltrate them. +# Such PRs get the `held-sensitive` label and need a manual dispatch after +# review. +# +# Fork PRs whose upstream PR closed are closed and their branches deleted. +name: Mirror Upstream PRs + +on: + schedule: + - cron: '*/30 * * * *' + workflow_dispatch: + inputs: + max-builds: + description: 'Max mirrored PRs to (re)build this cycle' + required: false + default: '1' + +permissions: + contents: write + pull-requests: write + issues: write + actions: write + +concurrency: + group: mirror-upstream + cancel-in-progress: false + +env: + UPSTREAM_REPO: jongpie/NebulaLogger + MIRROR_LABEL: upstream-mirror + HELD_LABEL: held-sensitive + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - name: 'Checkout fork' + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: 'Mirror open upstream PRs' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # All builds serialize on one shared reserved org (build.yml + # concurrency group), so dispatching many at once just queues them; + # default to one build per cycle to keep the queue short. + MAX_BUILDS: ${{ github.event.inputs.max-builds || '1' }} + run: | + set -euo pipefail + + git config user.name 'latdx-mirror-bot' + git config user.email 'ci@latdx.com' + git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null \ + || git remote set-url upstream "https://github.com/${UPSTREAM_REPO}.git" + git fetch --quiet upstream main + + gh label create "$MIRROR_LABEL" --repo "$GITHUB_REPOSITORY" \ + --color 0DF293 --description 'Auto-mirrored from an upstream PR' 2>/dev/null || true + gh label create "$HELD_LABEL" --repo "$GITHUB_REPOSITORY" \ + --color F2920C --description 'Touches npm manifests; build needs manual dispatch after review' 2>/dev/null || true + + builds=0 + gh pr list --repo "$UPSTREAM_REPO" --state open \ + --json number,title,headRefOid,updatedAt,url --limit 100 \ + | jq -c 'sort_by(.updatedAt) | reverse | .[]' > /tmp/upstream-prs.jsonl + + while IFS= read -r pr; do + n="$(jq -r .number <<<"$pr")" + sha="$(jq -r .headRefOid <<<"$pr")" + title="$(jq -r .title <<<"$pr")" + url="$(jq -r .url <<<"$pr")" + branch="upstream-pr-${n}" + + ci_tree="$(git rev-parse origin/main:.github)" + mirrored_sha='' + mirrored_tree='' + if git fetch --quiet origin "refs/heads/${branch}" 2>/dev/null; then + mirrored_sha="$(git log -1 --format=%B FETCH_HEAD | sed -n 's/^Upstream-SHA: //p' | head -1)" + mirrored_tree="$(git log -1 --format=%B FETCH_HEAD | sed -n 's/^Fork-CI-Tree: //p' | head -1)" + fi + if [ "$mirrored_sha" = "$sha" ] && [ "$mirrored_tree" = "$ci_tree" ]; then + echo "PR #${n}: up to date (${sha})" + continue + fi + if [ "$builds" -ge "$MAX_BUILDS" ]; then + echo "PR #${n}: build cap (${MAX_BUILDS}) reached, deferring to a later cycle" + continue + fi + + echo "PR #${n}: mirroring ${sha}" + git fetch --quiet upstream "pull/${n}/head" + head_sha="$(git rev-parse FETCH_HEAD)" + if [ "$head_sha" != "$sha" ]; then + echo "PR #${n}: head moved while mirroring (${sha} -> ${head_sha}), using fetched head" + sha="$head_sha" + fi + + merge_base="$(git merge-base upstream/main "$head_sha")" + sensitive="$(git diff --name-only "$merge_base" "$head_sha" -- package.json package-lock.json || true)" + dropped_ci="$(git diff --name-only "$merge_base" "$head_sha" -- .github || true)" + + git checkout --quiet -B "$branch" "$head_sha" + git checkout --quiet origin/main -- .github + git add .github + git commit --quiet -m "ci: overlay fork CI for upstream PR #${n}" \ + -m "Upstream-SHA: ${sha}" -m "Fork-CI-Tree: ${ci_tree}" + git push --quiet --force origin "$branch" + + body_file="$(mktemp)" + { + echo "Mirrors upstream PR ${url} so the fork's LATdx Build pipeline runs against it." + echo + echo "- Managed by the \`Mirror Upstream PRs\` workflow; the branch is force-pushed on upstream updates. Do not edit by hand." + echo "- \`.github/\` is overlaid with the fork main's version on every mirror, so upstream workflow changes never execute here." + if [ -n "$dropped_ci" ]; then + echo "- Note: this upstream PR changes \`.github/\` files; those changes are dropped by the overlay and not exercised." + fi + if [ -n "$sensitive" ]; then + echo "- **Held**: touches npm manifests (\`package.json\` / \`package-lock.json\`). Review them, then dispatch the Build workflow on \`${branch}\` manually." + fi + } > "$body_file" + + existing="$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$branch" --state open --json number --jq '.[0].number // empty')" + if [ -z "$existing" ]; then + gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$branch" \ + --title "[upstream #${n}] ${title}" --body-file "$body_file" + existing="$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$branch" --state open --json number --jq '.[0].number // empty')" + fi + # gh pr edit --add-label is unreliable on some repos; the issues + # labels endpoint is the stable path. + if [ -n "$existing" ]; then + gh api "repos/${GITHUB_REPOSITORY}/issues/${existing}/labels" -f "labels[]=${MIRROR_LABEL}" >/dev/null || true + if [ -n "$sensitive" ]; then + gh api "repos/${GITHUB_REPOSITORY}/issues/${existing}/labels" -f "labels[]=${HELD_LABEL}" >/dev/null || true + fi + fi + rm -f "$body_file" + + if [ -n "$sensitive" ]; then + echo "PR #${n}: held (npm manifest changes), not dispatching Build" + continue + fi + + gh workflow run build.yml --repo "$GITHUB_REPOSITORY" --ref "refs/heads/${branch}" + builds=$((builds + 1)) + echo "PR #${n}: Build dispatched on ${branch}" + done < /tmp/upstream-prs.jsonl + + - name: 'Close fork PRs whose upstream PR closed' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh pr list --repo "$GITHUB_REPOSITORY" --state open --label "$MIRROR_LABEL" \ + --json number,headRefName --limit 200 \ + | jq -c '.[]' | while IFS= read -r pr; do + fork_n="$(jq -r .number <<<"$pr")" + branch="$(jq -r .headRefName <<<"$pr")" + upstream_n="${branch#upstream-pr-}" + case "$upstream_n" in + ''|*[!0-9]*) continue ;; + esac + state="$(gh pr view "$upstream_n" --repo "$UPSTREAM_REPO" --json state --jq .state 2>/dev/null || echo UNKNOWN)" + if [ "$state" = "CLOSED" ] || [ "$state" = "MERGED" ]; then + echo "Fork PR #${fork_n}: upstream #${upstream_n} is ${state}, closing" + gh pr close "$fork_n" --repo "$GITHUB_REPOSITORY" \ + --comment "Upstream PR is ${state}; closing the mirror." --delete-branch || true + fi + done diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 000000000..13636145d --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,64 @@ +# Keeps the fork's main aligned with upstream: merges jongpie/NebulaLogger +# main into fork main daily. The fork's only divergence is its CI overlay +# (.github changes), so merges are conflict-free unless upstream edits the +# same workflow lines; on conflict the workflow opens an issue instead of +# guessing. +name: Sync Upstream Main + +on: + schedule: + - cron: '17 5 * * *' + workflow_dispatch: + +permissions: + contents: write + issues: write + +concurrency: + group: sync-upstream + cancel-in-progress: false + +env: + UPSTREAM_REPO: jongpie/NebulaLogger + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: 'Checkout fork main' + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + + - name: 'Merge upstream main' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + git config user.name 'latdx-mirror-bot' + git config user.email 'ci@latdx.com' + git remote add upstream "https://github.com/${UPSTREAM_REPO}.git" 2>/dev/null \ + || git remote set-url upstream "https://github.com/${UPSTREAM_REPO}.git" + git fetch --quiet upstream main + + if git merge-base --is-ancestor upstream/main HEAD; then + echo 'Fork main already contains upstream main; nothing to sync.' + exit 0 + fi + + if git merge --no-edit upstream/main; then + git push origin main + echo 'Merged upstream main and pushed.' + else + git merge --abort + existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \ + --search 'Upstream sync conflict in:title' --json number --jq '.[0].number // empty')" + if [ -z "$existing" ]; then + gh issue create --repo "$GITHUB_REPOSITORY" \ + --title 'Upstream sync conflict' \ + --body "Merging \`upstream/main\` into \`main\` hit conflicts (likely upstream edited \`.github/workflows/build.yml\` lines the fork also changes). Resolve manually: fetch upstream, merge, keep the fork's CI overlay, push." + fi + echo '::error::Upstream merge conflict; opened/kept an issue for manual resolution.' + exit 1 + fi diff --git a/nebula-logger/core/main/logger-engine/classes/CallableLogger.cls b/nebula-logger/core/main/logger-engine/classes/CallableLogger.cls index 3378d2d6c..9d6975a08 100644 --- a/nebula-logger/core/main/logger-engine/classes/CallableLogger.cls +++ b/nebula-logger/core/main/logger-engine/classes/CallableLogger.cls @@ -25,6 +25,9 @@ global without sharing class CallableLogger implements System.Callable { private static final String ARGUMENT_RECORD = 'record'; private static final String ARGUMENT_RECORD_LIST = 'recordList'; private static final String ARGUMENT_RECORD_MAP = 'recordMap'; + private static final String ARGUMENT_RESULT = 'result'; + private static final String ARGUMENT_RESULT_LIST = 'resultList'; + private static final String ARGUMENT_RESULT_TYPE = 'resultType'; private static final String ARGUMENT_REQUEST_ID = 'requestId'; private static final String ARGUMENT_SAVE_LOG = 'saveLog'; private static final String ARGUMENT_SAVE_METHOD_NAME = 'saveMethodName'; @@ -158,6 +161,11 @@ global without sharing class CallableLogger implements System.Callable { if (input.containsKey(ARGUMENT_RECORD_MAP)) { logEntryEventBuilder.setRecord((Map) input.get(ARGUMENT_RECORD_MAP)); } + if (input.containsKey(ARGUMENT_RESULT)) { + logEntryEventBuilder.setDatabaseResult((Object) input.get(ARGUMENT_RESULT), (System.Type) input.get(ARGUMENT_RESULT_TYPE)); + } else if (input.containsKey(ARGUMENT_RESULT_LIST)) { + logEntryEventBuilder.setDatabaseResult((List) input.get(ARGUMENT_RESULT_LIST), (System.Type) input.get(ARGUMENT_RESULT_TYPE)); + } if (input.containsKey(ARGUMENT_TAGS)) { List tags = (List) input.get(ARGUMENT_TAGS); for (Object tag : tags) { diff --git a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls index e6df9d2d5..5e1348712 100644 --- a/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls +++ b/nebula-logger/core/main/logger-engine/classes/LogEntryEventBuilder.cls @@ -254,6 +254,16 @@ global with sharing class LogEntryEventBuilder { // DML-result builder methods. All* of the Result classes behave the same (*except Upsert) // but Salesforce didn't bother to use an interface for the classes, so instead - overloads! + /** + * @description Sets the log entry event's database operation result fields + * @param databaseResult The instance of `Object` (generic Database Result) to log + * @param type The instance of `System.Type` that indicates the type of Database Result class to log + * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods + */ + global LogEntryEventBuilder setDatabaseResult(Object databaseResult, System.Type type) { + return this.setDatabaseDetails(new List{ databaseResult }, type, null); + } + /** * @description Sets the log entry event's database operation result fields * @param deleteResult The instance of `Database.DeleteResult` to log @@ -326,6 +336,16 @@ global with sharing class LogEntryEventBuilder { return this.setDatabaseDetails(new List{ undeleteResult }, Database.UndeleteResult.class, undeleteResult?.getId()); } + /** + * @description Sets the log entry event's database operation result fields + * @param databaseResults The list of `Object` (generic Database Result) instances to log + * @param type The type of Database Result instances to log + * @return The same instance of `LogEntryEventBuilder`, useful for chaining methods + */ + global LogEntryEventBuilder setDatabaseResult(List databaseResults, System.Type type) { + return this.setDatabaseDetails(databaseResults, type, null); + } + /** * @description Sets the log entry event's database operation result fields * @param deleteResults The list of `Database.DeleteResult` instances to log @@ -965,7 +985,7 @@ global with sharing class LogEntryEventBuilder { this.logEntryEvent.DatabaseResultCollectionType__c = this.logEntryEvent.DatabaseResultCollectionSize__c == 1 ? 'Single' : 'List'; String serializedResult = System.JSON.serializePretty(this.logEntryEvent.DatabaseResultCollectionSize__c == 1 ? databaseResults.get(0) : databaseResults); this.logEntryEvent.DatabaseResultJson__c = LoggerDataStore.truncateFieldValue(Schema.LogEntryEvent__e.DatabaseResultJson__c, serializedResult); - this.logEntryEvent.DatabaseResultType__c = resultType.getName(); + this.logEntryEvent.DatabaseResultType__c = resultType?.getName(); return this.setRecordId(possibleRecordId); } diff --git a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls index 8886c03a4..83d301f37 100644 --- a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls +++ b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls @@ -157,6 +157,21 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.DeleteResult` - mocks are used instead of actual instances + * to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults The number of `Database.DeleteResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.DeleteResult` + */ + public static List createDatabaseDeleteResultList(Integer numOfResults, Boolean isSuccess) { + List deleteResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + deleteResults.add(LoggerMockDataCreator.createDatabaseDeleteResult(isSuccess)); + } + return deleteResults; + } + /** * @description Creates a mock instance of `Database.EmptyRecycleBinResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake @@ -188,6 +203,21 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.EmptyRecycleBinResult` - mocks are used instead of actual + * instances to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults The number of `Database.EmptyRecycleBinResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.EmptyRecycleBinResult` + */ + public static List createDatabaseEmptyRecycleBinResultList(Integer numOfResults, Boolean isSuccess) { + List emptyRecycleBinResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + emptyRecycleBinResults.add(LoggerMockDataCreator.createDatabaseEmptyRecycleBinResult(isSuccess)); + } + return emptyRecycleBinResults; + } + /** * @description Creates a mock instance of `Database.LeadConvertResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake @@ -217,12 +247,27 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.LeadConvertResult` - mocks are used instead of actual instances + * to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults The number of `Database.LeadConvertResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.LeadConvertResult` + */ + public static List createDatabaseLeadConvertResultList(Integer numOfResults, Boolean isSuccess) { + List leadConvertResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + leadConvertResults.add(LoggerMockDataCreator.createDatabaseLeadConvertResult(isSuccess)); + } + return leadConvertResults; + } + /** * @description Creates a mock instance of `Database.MergeResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake * record ID is automatically included. * @param isSuccess Indicates if the generated mock should have `isSuccess` - * @return The mock instance of `Database.MergeResult` + * @return The list of mock instances of `Database.MergeResult` */ public static Database.MergeResult createDatabaseMergeResult(Boolean isSuccess) { return createDatabaseMergeResult(isSuccess, createId(Schema.Account.SObjectType)); @@ -246,6 +291,21 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.MergeResult` - a mock is used instead of an actual + * instance to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults Indicates how many `Database.MergeResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.MergeResult` + */ + public static List createDatabaseMergeResultList(Integer numOfResults, Boolean isSuccess) { + List mergeResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + mergeResults.add(LoggerMockDataCreator.createDatabaseMergeResult(isSuccess)); + } + return mergeResults; + } + /** * @description Creates a mock instance of `Database.SaveResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake @@ -275,6 +335,21 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.SaveResult` - mocks are used instead of actual instances + * to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults The number of `Database.SaveResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.SaveResult` + */ + public static List createDatabaseSaveResultList(Integer numOfResults, Boolean isSuccess) { + List saveResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + saveResults.add(LoggerMockDataCreator.createDatabaseSaveResult(isSuccess)); + } + return saveResults; + } + /** * @description Creates a mock instance of `Database.UndeleteResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake @@ -304,6 +379,21 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.UndeleteResult` - mocks are used instead of actual instances + * to help speed up tests, and to support writing unit tests (instead of integration tests) + * @param numOfResults The number of `Database.UndeleteResult` instances to create + * @param isSuccess Indicates if the generated mocks should have `isSuccess` + * @return The list of mock instances of `Database.UndeleteResult` + */ + public static List createDatabaseUndeleteResultList(Integer numOfResults, Boolean isSuccess) { + List undeleteResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + undeleteResults.add(LoggerMockDataCreator.createDatabaseUndeleteResult(isSuccess)); + } + return undeleteResults; + } + /** * @description Creates a mock instance of `Database.UpsertResult` - a mock is used instead of an actual instance * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake @@ -340,6 +430,23 @@ public class LoggerMockDataCreator { } } + /** + * @description Creates a list of mock instances of `Database.UpsertResult` - mocks are used instead of actual instances + * to help speed up tests, and to support writing unit tests (instead of integration tests). A fake + * record ID is automatically included. + * @param numOfResults The number of `Database.UpsertResult` instances to create + * @param isSuccess Indicates if the generated mock should have `isSuccess` + * @param isCreated Indicates if the generated mock should have `isCreated` + * @return The list of mock instances of `Database.UpsertResult` + */ + public static List createDatabaseUpsertResultList(Integer numOfResults, Boolean isSuccess, Boolean isCreated) { + List upsertResults = new List(); + for (Integer i = 0; i < numOfResults; i++) { + upsertResults.add(LoggerMockDataCreator.createDatabaseUpsertResult(isSuccess, isCreated)); + } + return upsertResults; + } + /** * @description Generates an instance of the class `MockHttpCallout` that implements the interface `System.HttpCalloutMock`. * This can be used when testing batch jobs. diff --git a/nebula-logger/core/tests/logger-engine/classes/CallableLogger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/CallableLogger_Tests.cls index 12cb57854..612b9f0d0 100644 --- a/nebula-logger/core/tests/logger-engine/classes/CallableLogger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/CallableLogger_Tests.cls @@ -3,7 +3,9 @@ // See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. // //------------------------------------------------------------------------------------------------// -@SuppressWarnings('PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.AvoidHardcodingId, PMD.MethodNamingConventions') +@SuppressWarnings( + 'PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.AvoidHardcodingId, PMD.CyclomaticComplexity, PMD.MethodNamingConventions, PMD.NcssTypeCount' +) @IsTest(IsParallel=true) private class CallableLogger_Tests { // Not all orgs have OmniStudio, so not all orgs will have the OmniProcess SObject. @@ -557,6 +559,540 @@ private class CallableLogger_Tests { System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); } + @IsTest + static void it_adds_new_entry_with_delete_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.DeleteResult result = LoggerMockDataCreator.createDatabaseDeleteResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.DeleteResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.DeleteResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_delete_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_delete_result_and_null_type_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.DeleteResult result = LoggerMockDataCreator.createDatabaseDeleteResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call('newEntry', new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result }); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.isNull(logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_delete_result_and_null_type_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_delete_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseDeleteResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'resultList' => resultList, 'resultType' => Database.DeleteResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.DeleteResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_delete_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_delete_result_list_and_null_type_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseDeleteResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call('newEntry', new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'resultList' => resultList }); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.isNull(logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_delete_result_list_and_null_type_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_empty_recycle_bin_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.EmptyRecycleBinResult result = LoggerMockDataCreator.createDatabaseEmptyRecycleBinResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ + 'loggingLevel' => loggingLevel, + 'message' => message, + 'result' => result, + 'resultType' => Database.EmptyRecycleBinResult.class + } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.EmptyRecycleBinResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_empty_recycle_bin_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_empty_recycle_bin_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseEmptyRecycleBinResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ + 'loggingLevel' => loggingLevel, + 'message' => message, + 'resultList' => resultList, + 'resultType' => Database.EmptyRecycleBinResult.class + } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.EmptyRecycleBinResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_empty_recycle_bin_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_lead_convert_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.LeadConvertResult result = LoggerMockDataCreator.createDatabaseLeadConvertResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.LeadConvertResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.LeadConvertResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_lead_convert_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_lead_convert_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseLeadConvertResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ + 'loggingLevel' => loggingLevel, + 'message' => message, + 'resultList' => resultList, + 'resultType' => Database.LeadConvertResult.class + } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.LeadConvertResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_lead_convert_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_merge_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.MergeResult result = LoggerMockDataCreator.createDatabaseMergeResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.MergeResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.MergeResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_merge_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_merge_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseMergeResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'resultList' => resultList, 'resultType' => Database.MergeResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.MergeResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_merge_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_save_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.SaveResult result = LoggerMockDataCreator.createDatabaseSaveResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.SaveResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.SaveResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_save_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_save_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseSaveResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'resultList' => resultList, 'resultType' => Database.SaveResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.SaveResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_save_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_undelete_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.UndeleteResult result = LoggerMockDataCreator.createDatabaseUndeleteResult(true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.UndeleteResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.UndeleteResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_undelete_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_undelete_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseUndeleteResultList(15, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ + 'loggingLevel' => loggingLevel, + 'message' => message, + 'resultList' => resultList, + 'resultType' => Database.UndeleteResult.class + } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.UndeleteResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_undelete_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_upsert_result_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + Database.UpsertResult result = LoggerMockDataCreator.createDatabaseUpsertResult(true, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'result' => result, 'resultType' => Database.UpsertResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.areEqual(System.JSON.serializePretty(result), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.UpsertResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_upsert_result_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + + @IsTest + static void it_adds_new_entry_with_upsert_result_list_when_using_standard_approach() { + System.LoggingLevel loggingLevel = System.LoggingLevel.ERROR; + String message = 'some log entry message'; + List resultList = LoggerMockDataCreator.createDatabaseUpsertResultList(15, true, true); + + System.Callable callableLoggerInstance = (System.Callable) System.Type.forName('CallableLogger').newInstance(); + Map returnedOutput = (Map) callableLoggerInstance + .call( + 'newEntry', + new Map{ 'loggingLevel' => loggingLevel, 'message' => message, 'resultList' => resultList, 'resultType' => Database.UpsertResult.class } + ); + + System.Assert.areEqual(5, returnedOutput.size(), System.JSON.serializePretty(returnedOutput.keySet())); + System.Assert.areEqual(true, returnedOutput.get('isSuccess'), 'Expected isSuccess == true. Output received: ' + returnedOutput); + System.Assert.areEqual(Logger.getTransactionId(), returnedOutput.get('transactionId')); + System.Assert.areEqual(Logger.getParentLogTransactionId(), returnedOutput.get('parentLogTransactionId')); + System.Assert.areEqual(Logger.getRequestId(), returnedOutput.get('requestId')); + + LogEntryEvent__e logEntryEvent = ((LogEntryEventBuilder) returnedOutput.get('logEntryEventBuilder')).getLogEntryEvent(); + System.Assert.areEqual(loggingLevel.name(), logEntryEvent.LoggingLevel__c); + System.Assert.areEqual(message, logEntryEvent.Message__c); + System.Assert.isNull(logEntryEvent.RecordId__c); + System.Assert.areEqual(System.JSON.serializePretty(resultList), logEntryEvent.DatabaseResultJson__c); + System.Assert.areEqual(Database.UpsertResult.class.getName(), logEntryEvent.DatabaseResultType__c); + System.Assert.isNull(logEntryEvent.Tags__c); + System.Assert.areEqual( + CallableLogger_Tests.class.getName() + '.it_adds_new_entry_with_upsert_result_list_when_using_standard_approach', + logEntryEvent.OriginLocation__c + ); + System.Assert.areEqual('Apex', logEntryEvent.OriginType__c); + } + @IsTest static void it_adds_new_entry_with_tags_when_using_standard_approach() { System.LoggingLevel loggingLevel = System.LoggingLevel.FINE; diff --git a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls index cfdd033b8..d60d93151 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -11501,51 +11501,27 @@ private class Logger_Tests { } static List getDeleteResultList() { - List deleteResults = new List(); - for (Integer i = 0; i < 3; i++) { - deleteResults.add(LoggerMockDataCreator.createDatabaseDeleteResult(true)); - } - return deleteResults; + return LoggerMockDataCreator.createDatabaseDeleteResultList(3, true); } static List getEmptyRecycleBinResultList() { - List emptyRecycleBinResults = new List(); - for (Integer i = 0; i < 3; i++) { - emptyRecycleBinResults.add(LoggerMockDataCreator.createDatabaseEmptyRecycleBinResult(true)); - } - return emptyRecycleBinResults; + return LoggerMockDataCreator.createDatabaseEmptyRecycleBinResultList(3, true); } static List getLeadConvertResultList() { - List leadConvertResults = new List(); - for (Integer i = 0; i < 3; i++) { - leadConvertResults.add(LoggerMockDataCreator.createDatabaseLeadConvertResult(true)); - } - return leadConvertResults; + return LoggerMockDataCreator.createDatabaseLeadConvertResultList(3, true); } static List getSaveResultList() { - List saveResults = new List(); - for (Integer i = 0; i < 3; i++) { - saveResults.add(LoggerMockDataCreator.createDatabaseSaveResult(true)); - } - return saveResults; + return LoggerMockDataCreator.createDatabaseSaveResultList(3, true); } - static List getUpsertResultList() { - List upsertResults = new List(); - for (Integer i = 0; i < 3; i++) { - upsertResults.add(LoggerMockDataCreator.createDatabaseUpsertResult(true, true)); - } - return upsertResults; + static List getUndeleteResultList() { + return LoggerMockDataCreator.createDatabaseUndeleteResultList(3, true); } - static List getUndeleteResultList() { - List undeleteResults = new List(); - for (Integer i = 0; i < 3; i++) { - undeleteResults.add(LoggerMockDataCreator.createDatabaseUndeleteResult(true)); - } - return undeleteResults; + static List getUpsertResultList() { + return LoggerMockDataCreator.createDatabaseUpsertResultList(3, true, true); } static String getOriginLocation() {