# Scenario: Connected App Security E2E (NexusCRM port)

**Source:** [`MetaDrivenPlatform4AI/tests/scenarios/connected-app-security-e2e.md`](https://github.com/pingcap-inc/MetaDrivenPlatform4AI/blob/main/tests/scenarios/connected-app-security-e2e.md) (mdp4ai era, 30 TCs)
**Target:** NexusCRM staging via `pingcap-inc/nexuscrm-pingcap-pack`
**Date ported:** 2026-05-21
**Skill alignment:** `story-ac-regression` v6 — emits per-TC JSON via `emit_verdict`, runs from `qa/regression/`

---

## NexusCRM-specific deltas from the mdp4ai source

| Aspect | mdp4ai | **NexusCRM port** |
|---|---|---|
| Object name | `ConnectedApplication` (SF parity) | **`_System_ConnectedApp`** (system-managed table prefix) |
| Setup UI | `Setup → Connected Apps` | `Setup → Connected Apps` (same path; on the Setup home page the tile sits visually under a "Security" section header, but the click-target is still just `Connected Apps`) |
| ConsumerKey / ConsumerSecret generation | Server-assigned (shown once at create) | **Caller-supplied** at POST time (random hex preferred) |
| `client_credentials` grant precondition | RunAsUser optional in some flows | **RunAsUserId mandatory** — server rejects with `Connected App must have a Run As User configured for the client_credentials flow` |
| Hardcoded test seed | `mdp4ai-ui` test client | `nexuscrm-ui` (`ConsumerSecret=nexuscrm-ui-internal`) — see [#734](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/734) (removal tracker) and [#834](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/834) (externalize) |
| Scope values | `api.read` / `api` (string field, comma-separated) | Same shape; field is plain `string`, validation TBD per [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) |

---

## Pre-conditions

Use `qa/regression/common/setup_fixture.sh` to provision the sales fixture user before running. Then verify:

| ID | Check | How to verify |
|----|-------|---------------|
| PRE-01 | Admin token via lib.sh | `source qa/regression/common/lib.sh; get_admin_token` returns ≥500-char JWT |
| PRE-02 | At least 2 real CRM Sales users with Opportunity visibility | `SELECT Id,Username FROM User WHERE Username LIKE 'qa.regression.r6%' AND IsActive=true` → ≥2 rows. If <2, rerun `setup_fixture.sh` or create a second via `1331_AC2_user_happy_path.sh` pattern. |
| PRE-03 | Selected RunAsUsers can read Account/Opportunity | Sales-token-based `GET /sobjects/Account/{id}` returns 200 (use `lib.sh::get_sales_token`) |
| PRE-04 | Opportunity records exist | `GET /services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1` returns ≥1 |
| PRE-05 | Staging backend healthy | `POST /services/oauth2/token` with `nexuscrm-ui` creds returns JSON (not 503/HTML) — see [`Avoid these failure modes`](../common/lib.sh) for INVALID_SESSION_ID hint |

**Required env / fixture state captured into `/tmp/ca_e2e_fixture.json` (caller-owned):**

```bash
RUN_TS=$(date +%s)
HOST="https://d2ivgwliouoia2.cloudfront.net"
RO_KEY="r14sec_${RUN_TS}_ro_key"
RO_SECRET="r14sec_${RUN_TS}_ro_$(openssl rand -hex 12)"
FA_KEY="r14sec_${RUN_TS}_fa_key"
FA_SECRET="r14sec_${RUN_TS}_fa_$(openssl rand -hex 12)"
RUNAS_RO=$(curl -sS -G "$HOST/services/data/v63.0/query" \
  --data-urlencode "q=SELECT Id FROM User WHERE Username LIKE 'qa.regression.r6%' AND IsActive=true LIMIT 1" \
  -H "Authorization: Bearer $ADMIN_TOK" | python3 -c "import json,sys; print(json.load(sys.stdin)['records'][0]['Id'])")
# RUNAS_FA: pick a different sales user for FullAccess app — see PRE-02
```

---

## Phase 1: Connected App Creation

### SEC-CA-01: Create Read-Only Connected App

**API path** (preferred for automation):

```bash
curl -sS -X POST "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d "{
    \"Name\":\"SEC-E2E-ReadOnly\",
    \"ConsumerKey\":\"$RO_KEY\",
    \"ConsumerSecret\":\"$RO_SECRET\",
    \"Scopes\":\"api.read\",
    \"IsActive\":true
  }"
```

**UI path** (alternative; required for human exploration): Setup → **Connected Apps** → + New Connected App → fill Name + select `api.read` scope.

**Pass criteria:**
- API: returns `{"success":true,"id":"<uuid>"}`
- UI: app appears in list with scope `api.read`, status Active
- `Id`, `ConsumerKey`, `ConsumerSecret` captured into fixture file

### SEC-CA-02: Create Full-Access Connected App

Same as SEC-CA-01 but:
- `Name`: `SEC-E2E-FullAccess`
- `ConsumerKey`: `$FA_KEY`
- `ConsumerSecret`: `$FA_SECRET`
- `Scopes`: `api`

---

## Phase 2: OAuth Policies Configuration

### SEC-CA-03: Set RunAsUser and verify persistence

```bash
# Set RunAsUserId on RO app
curl -sS -X PATCH "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp/$RO_APP_ID" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d "{\"RunAsUserId\":\"$RUNAS_RO\"}"

# Verify persistence via re-read
curl -sS "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp/$RO_APP_ID" \
  -H "Authorization: Bearer $ADMIN_TOK" | python3 -c "import json,sys; print(json.load(sys.stdin).get('RunAsUserId'))"
```

**Pass criteria:**
- PATCH returns 204
- Re-read returns the same RunAsUserId
- UI alternative: Setup → Connected Apps → gear icon → set "Run As User" → Save Policies → nav away → return → RunAsUser still shown

### SEC-CA-04: Set different RunAsUser on FullAccess app

Same as SEC-CA-03 but for `$FA_APP_ID` with `$RUNAS_FA`.

---

## Phase 3: API — Read-Only App

### SEC-CA-05: Token acquisition (ReadOnly)

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=$RO_KEY" \
  -d "client_secret=$RO_SECRET"
```

**Pass criteria:**
- HTTP 200 with `access_token` field
- Response header has NO `X-Deprecation-Warning` (RunAsUserId was correctly used)
- JWT payload (decode middle segment) shows `user.id` == `$RUNAS_RO`, `scope` contains `api.read`

**Failure shape to watch:** `{"error":"invalid_client","error_description":"Connected App must have a Run As User configured for the client_credentials flow"}` → SEC-CA-03 didn't persist; recheck.

### SEC-CA-06: Read succeeds (ReadOnly)

```bash
curl -sS -H "Authorization: Bearer $RO_TOKEN" \
  "$HOST/services/data/v63.0/query?q=SELECT+Id,Name,StageName+FROM+Opportunity+LIMIT+5"
```

**Pass criteria:** HTTP 200 with `records` array (length 0–5 depending on RunAsUser's RLS scope)

### SEC-CA-07: POST blocked by scope (ReadOnly)

```bash
curl -sS -w "\nHTTP: %{http_code}\nContent-Type: %{content_type}\n" -X POST \
  -H "Authorization: Bearer $RO_TOKEN" -H "Content-Type: application/json" \
  "$HOST/services/data/v63.0/sobjects/Account" \
  -d '{"Name":"SEC-CA07-WriteBlock"}'
```

**Pass criteria:**
- HTTP 403
- Body: `{"errorCode":"INSUFFICIENT_SCOPE",...}` (per [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) target)
- Content-Type: `application/json` (NOT `text/html` — HTML means request hit CDN not backend)

**Current-build caveat:** today the response may be `{"error":"cross-origin request blocked"}` from CloudFront, masking the real 403 INSUFFICIENT_SCOPE. If observed, file under [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) as "scope enforcement not yet wired through CDN — error shape regression".

### SEC-CA-08: PATCH blocked by scope (ReadOnly)

```bash
ANY_ID=$(curl -sS -G "$HOST/services/data/v63.0/query" --data-urlencode "q=SELECT Id FROM Opportunity LIMIT 1" -H "Authorization: Bearer $RO_TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['records'][0]['Id'])")

curl -sS -w "\nHTTP: %{http_code}\n" -X PATCH \
  -H "Authorization: Bearer $RO_TOKEN" -H "Content-Type: application/json" \
  "$HOST/services/data/v63.0/sobjects/Opportunity/$ANY_ID" \
  -d '{"Name":"SEC-CA08-PatchBlock"}'
```

**Pass criteria:** HTTP 403 `INSUFFICIENT_SCOPE`

### SEC-CA-09: DELETE blocked by scope (ReadOnly)

```bash
curl -sS -w "\nHTTP: %{http_code}\n" -X DELETE \
  -H "Authorization: Bearer $RO_TOKEN" \
  "$HOST/services/data/v63.0/sobjects/Opportunity/$ANY_ID"
```

**Pass criteria:** HTTP 403 `INSUFFICIENT_SCOPE`

---

## Phase 4: API — Full-Access App

### SEC-CA-10: Token acquisition (FullAccess)

Same as SEC-CA-05 but with `$FA_KEY` / `$FA_SECRET`. JWT `scope` should be `api`.

### SEC-CA-11: Read succeeds (FullAccess)

Same as SEC-CA-06. **Additional check:** if `$RUNAS_RO != $RUNAS_FA`, verify the two tokens return **different** Opportunity result sets (RLS is effective per RunAs).

### SEC-CA-12: Write succeeds (FullAccess)

```bash
CREATED=$(curl -sS -X POST -H "Authorization: Bearer $FA_TOKEN" \
  -H "Content-Type: application/json" \
  "$HOST/services/data/v63.0/sobjects/Account" \
  -d '{"Name":"SEC-E2E-WriteTest"}')
echo "$CREATED"
CREATED_ID=$(echo "$CREATED" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))")
```

**Pass criteria:** HTTP 201, `success:true`, record `id` returned

### SEC-CA-13: Clean up test record

```bash
curl -sS -X DELETE -H "Authorization: Bearer $FA_TOKEN" \
  "$HOST/services/data/v63.0/sobjects/Account/$CREATED_ID"
```

**Pass criteria:** HTTP 204 if RunAsUser has Account delete permission. If 403, delete as admin and record that delete authorization was not in scope of this RunAs's perms (not a security failure).

---

## Phase 5: User Deactivation Lifecycle

### SEC-CA-14: Deactivate RunAsUser

```bash
curl -sS -w "HTTP %{http_code}\n" -X PATCH "$HOST/services/data/v63.0/sobjects/User/$RUNAS_RO" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d '{"IsActive":false}'
```

**Pass criteria:**
- HTTP 204
- Status changes to `IsActive=false` (verify via re-read)
- **Zero `validation error on field 'Password': is required` errors** — this was a historical bug in mdp4ai (PR #797 fixed). Watch for regression.

### SEC-CA-15: New token rejected after user deactivation

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** HTTP 400 or 401 (RunAsUser inactive → `rejectInactiveUser()` blocks)

### SEC-CA-16: Old token rejected after user deactivation

```bash
# $RO_TOKEN was captured in SEC-CA-05 BEFORE deactivation
curl -sS -i -H "Authorization: Bearer $RO_TOKEN" \
  "$HOST/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` within 60s grace period (sessions revoked on user deactivation per PR #751 equivalent)

**If still 200:** session revocation broken — file as critical, parallel of mdp4ai #500.

### SEC-CA-17: Reactivate user and verify recovery

```bash
curl -sS -X PATCH "$HOST/services/data/v63.0/sobjects/User/$RUNAS_RO" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d '{"IsActive":true}'

# Get new token
curl -sS -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** Token issued (HTTP 200, access_token present), subsequent read returns data.

---

## Phase 6: App Deactivation Lifecycle

### SEC-CA-18: Deactivate ReadOnly app

```bash
curl -sS -w "HTTP %{http_code}\n" -X PATCH "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp/$RO_APP_ID" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d '{"IsActive":false}'
```

**Pass criteria:** HTTP 204, re-read shows `IsActive=false`

### SEC-CA-19: New token rejected after app deactivation

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** HTTP 400 `invalid_client` (app disabled)

### SEC-CA-20: Old token rejected after app deactivation

Use a token captured between SEC-CA-17 (reactivation) and SEC-CA-18 (app deactivation) — i.e. obtained after user-recovery, before app-deactivation. Then:

```bash
curl -sS -i -H "Authorization: Bearer $RO_TOKEN_FRESH" \
  "$HOST/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` (sessions revoked on app deactivation)

### SEC-CA-21: Reactivate and verify recovery

```bash
curl -sS -X PATCH "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp/$RO_APP_ID" \
  -H "Authorization: Bearer $ADMIN_TOK" -H "Content-Type: application/json" \
  -d '{"IsActive":true}'

# New token + read
curl -sS -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET"
```

**Pass criteria:** Token issued, read succeeds.

---

## Phase 7: App Deletion Lifecycle

### SEC-CA-22: Delete FullAccess app

```bash
# Capture a fresh FA token first for SEC-CA-24
FA_TOKEN_FRESH=$(curl -sS -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$FA_KEY&client_secret=$FA_SECRET" \
  | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token',''))")

curl -sS -w "HTTP %{http_code}\n" -X DELETE \
  "$HOST/services/data/v63.0/sobjects/_System_ConnectedApp/$FA_APP_ID" \
  -H "Authorization: Bearer $ADMIN_TOK"
```

**Pass criteria:** HTTP 204. App no longer visible via list/SOQL.

**Current-build watch:** [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444) "Connected App delete broken — generic CRUD assumes all tables have IsDeleted" — if DELETE returns 500 / 400, this is exactly that regression. File evidence under #444.

### SEC-CA-23: New token rejected after deletion

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$FA_KEY&client_secret=$FA_SECRET"
```

**Pass criteria:** HTTP 400 with `invalid_client` AND error description like `unknown client_id` (NOT `app disabled` — that would indicate soft-delete; we want hard delete)

### SEC-CA-24: Old token rejected after deletion

```bash
curl -sS -i -H "Authorization: Bearer $FA_TOKEN_FRESH" \
  "$HOST/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401 `INVALID_SESSION_ID` (sessions revoked in same transaction as delete; PR #751 equivalent)

### SEC-CA-25: Verify hard delete (Anti-Security-Theater check)

Re-run SEC-CA-23. The error description must be `unknown client_id` — confirms the row is physically gone, not just soft-deleted with IsActive=false.

**Additional probe** (NexusCRM-specific):

```bash
curl -sS -G "$HOST/services/data/v63.0/query" \
  --data-urlencode "q=SELECT Id FROM _System_ConnectedApp WHERE ConsumerKey='$FA_KEY'" \
  -H "Authorization: Bearer $ADMIN_TOK"
```

**Pass criteria:** `totalSize:0, records:[]`. If row still present with `IsActive=false`, soft-delete regression — file under [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444).

---

## Phase 8: Edge Cases

### SEC-CA-26: Wrong ConsumerSecret

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=WRONG_SECRET_$(openssl rand -hex 8)"
```

**Pass criteria:** HTTP 400 `invalid_client`

### SEC-CA-27: Non-existent ConsumerKey

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=nonexistent-key-$(date +%s)&client_secret=anything"
```

**Pass criteria:** HTTP 400 `invalid_client`

### SEC-CA-28: Missing parameters

```bash
curl -sS -i -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials"
```

**Pass criteria:** HTTP 400 `invalid_request` (client_id and client_secret required)

### SEC-CA-29: Scope escalation attempt

```bash
# ReadOnly app (Scopes="api.read") but request "api" scope
curl -sS -X POST "$HOST/services/oauth2/token" \
  -d "grant_type=client_credentials&client_id=$RO_KEY&client_secret=$RO_SECRET&scope=api"
```

**Pass criteria:** Token issued (200) but JWT `scope` claim is `api.read` (downgraded, not escalated). Decode JWT middle segment and assert.

**Watch:** if response is HTTP 200 but JWT shows `scope=api`, that's an escalation bug — file as P0 under [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443).

### SEC-CA-30: Invalid bearer token

```bash
curl -sS -i -H "Authorization: Bearer INVALID_TOKEN_$(openssl rand -hex 16)" \
  "$HOST/services/data/v63.0/query?q=SELECT+Id+FROM+Opportunity+LIMIT+1"
```

**Pass criteria:** HTTP 401

---

## Phase 9: Cleanup

| Action | Verify |
|--------|--------|
| `DELETE /sobjects/_System_ConnectedApp/$RO_APP_ID` (if not already deleted in Phase 7) | 204 |
| `DELETE /sobjects/_System_ConnectedApp/$FA_APP_ID` (if not already deleted in SEC-CA-22) | 204 |
| `PATCH User/$RUNAS_RO` → `IsActive=true` (if Phase 5 left it deactivated) | 204 |
| `PATCH User/$RUNAS_FA` → `IsActive=true` (if applicable) | 204 |
| Any test Accounts/Opportunities created via FA app cleaned up | SOQL `WHERE Name LIKE 'SEC-E2E-%'` returns 0 |
| Fixture file `/tmp/ca_e2e_fixture.json` removed (contains plaintext ConsumerSecret) | `[ ! -f /tmp/ca_e2e_fixture.json ]` |

---

## Summary Checklist

| # | Test | Security Level | Expected | Mapped tracker (if FAIL) |
|---|------|---------------|----------|--------|
| 03 | RunAsUser persistence | P0 | PATCH+reread match | new |
| 05 | RO token (no deprecation warning) | P0 | 200, correct user/scope | — |
| 06 | RO read | P0 | 200 + data | — |
| 07 | RO POST blocked | P0-Critical | 403 INSUFFICIENT_SCOPE | [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) if not 403 |
| 08 | RO PATCH blocked | P0-Critical | 403 INSUFFICIENT_SCOPE | [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) |
| 09 | RO DELETE blocked | P0-Critical | 403 INSUFFICIENT_SCOPE | [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) |
| 12 | FA write | P1 | 201 Created | — |
| 14 | User deactivate (no Password error) | P1 | IsActive=false, 0 validation errors | watch for [#1491](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/1491) regression |
| 15 | Deactivated user → new token rejected | P0 | 400/401 | new tracker if 200 |
| 16 | Deactivated user → old token rejected | P0 | 401 within 60s | new tracker if 200 |
| 19 | Deactivated app → new token rejected | P0 | 400 | new tracker |
| 20 | Deactivated app → old token revoked | P0 | 401 | new tracker |
| 22 | Delete app | P0 | 204 | [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444) if 5xx |
| 23 | Deleted app → new token rejected | P0 | 400 `unknown client_id` | [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444) |
| 24 | Deleted app → old token revoked | P0 | 401 | new tracker |
| 25 | Hard delete verified | P0 | SOQL totalSize=0 | [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444) if row remains |
| 26 | Wrong secret | P1 | 400 | — |
| 29 | Scope escalation blocked | P1 | JWT scope=api.read (downgraded) | [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) if escalated |
| 30 | Invalid token | P1 | 401 | — |

**All 30 tests should PASS for release approval; deviations file against the listed tracker.**

---

## How to run

Manual exploration (one-at-a-time, copy-paste each curl):

```bash
cd <pack-repo>
source qa/regression/common/lib.sh
ADMIN_TOK=$(get_admin_token)
HOST="https://d2ivgwliouoia2.cloudfront.net"
# ... follow Phase 1 → 9 in order
```

Automated batch (recommended; emits one JSON-line per TC compatible with `story-ac-regression` skill):

```bash
# Future work — wire each TC into qa/regression/r{n}/SEC_CA_{NN}_*.sh per skill convention
# Each script: AC contract verbatim header, fixture setup, probe, emit_verdict
# Then run via run_all.sh or selectively per TC
```

Per-TC shell scripts are not yet generated; this scenario doc is the canonical source until they are. When generated, each script will follow the pattern of `qa/regression/r6/732_AC1_*.sh` etc.

---

## Known substrate gaps to file before running

Before posting any FAIL verdicts from this scenario, cross-check the following open trackers — they may already explain a verdict before you re-investigate:

- [#443](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/443) — design: API Access Control / Connected App scope enforcement / per-app integration user
- [#444](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/444) — Connected App delete broken (generic CRUD assumes IsDeleted)
- [#530](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/530) — earlier design iteration (CLOSED)
- [#734](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/734) — Hardcoded test Connected App seeded in production startup
- [#834](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/834) — Externalize hardcoded test Connected App credentials from source code

If a new failure is found that doesn't map to any above, file a new `[Bug][P?]` issue per skill `story-ac-regression` discipline (see `Filing new issues` section).
