|
| 1 | +# SDK Test Coverage Plan (BMAD-style) |
| 2 | + |
| 3 | +Repo: `@frontegg/client` (nodejs-sdk), branch `next`. |
| 4 | +Baseline: 85 source files, 12 existing spec files (~14% file coverage). Jest + ts-jest, thresholds currently set very low (stmts 17 / br 24 / fn 20 / ln 18). |
| 5 | +Goal: comprehensive **unit** coverage for all behavior-bearing modules + a **real-sandbox e2e** suite covering the primary public SDK flows. |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Phases (BMAD) |
| 10 | + |
| 11 | +| Phase | Owner | Deliverable | |
| 12 | +|---|---|---| |
| 13 | +| **B – Brief** | this doc | Scope, risks, test matrix (below) | |
| 14 | +| **M – Model/Design** | this doc | Per-module spec outline, e2e flow list, fixtures & mocks strategy | |
| 15 | +| **A – Act** | follow-up PRs | Spec files, e2e harness, CI wiring | |
| 16 | +| **D – Done** | follow-up | Coverage thresholds raised, CI green on unit + e2e-on-demand | |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 2. Existing coverage (keep / extend) |
| 21 | + |
| 22 | +Already specced — I will only **extend** these where gaps are obvious: |
| 23 | + |
| 24 | +- `src/cache/local-cache.manager.spec.ts` |
| 25 | +- `src/cache/ioredis-cache.manager.spec.ts` |
| 26 | +- `src/clients/http/http-client.spec.ts` |
| 27 | +- `src/clients/identity/identity-client.spec.ts` |
| 28 | +- `src/clients/identity/step-up/step-up.validator.spec.ts` |
| 29 | +- `src/clients/hosted-login/hosted-login.client.spec.ts` |
| 30 | +- `src/clients/entitlements/entitlements-client.spec.ts` |
| 31 | +- `src/clients/entitlements/entitlements.user-scoped.spec.ts` |
| 32 | +- `src/clients/entitlements/storage/in-memory/in-memory.cache.spec.ts` |
| 33 | +- `src/clients/entitlements/storage/in-memory/mappers/plan-tuple.mapper.spec.ts` |
| 34 | +- `src/clients/entitlements/storage/in-memory/mappers/feature-flag-tuple.mapper.spec.ts` |
| 35 | +- `src/middlewares/with-authentication.spec.ts` |
| 36 | + |
| 37 | +Gap check for each of the above: run with `--coverage`, note < 90% branch lines, file patch-spec tasks. |
| 38 | + |
| 39 | +--- |
| 40 | + |
| 41 | +## 3. Unit test matrix — files to add |
| 42 | + |
| 43 | +Grouped by module. Each row = one new `*.spec.ts` next to the source file. |
| 44 | + |
| 45 | +### 3.1 cache |
| 46 | +| File | Focus | |
| 47 | +|---|---| |
| 48 | +| `src/cache/redis-cache.manager.spec.ts` | `set/get/del/ttl`, JSON (de)serialization, connection error paths, key-prefixing, `ioredis` mocked via `ioredis-mock` or `jest-mock-extended` | |
| 49 | + |
| 50 | +### 3.2 http client |
| 51 | +Extend `http-client.spec.ts` — retries, auth header injection, base URL resolution, error mapping, timeout. |
| 52 | + |
| 53 | +### 3.3 identity — token resolvers |
| 54 | +| File | Focus | |
| 55 | +|---|---| |
| 56 | +| `token-resolvers/token-resolver.spec.ts` | Dispatch to access vs authorization resolver, unknown token → `InvalidTokenTypeException` | |
| 57 | +| `token-resolvers/access-token-resolver.spec.ts` | Tenant vs user access-token routing, cache hit/miss, error propagation | |
| 58 | +| `token-resolvers/authorization-token-resolver.spec.ts` | JWT verification happy path, `jsonwebtoken.verify` mocked, expired / bad signature / wrong issuer | |
| 59 | +| `access-token-services/services/access-token.service.spec.ts` | Base service contract (fetch, parse, error) | |
| 60 | +| `access-token-services/services/tenant-access-token.service.spec.ts` | Tenant-specific endpoint + payload shape | |
| 61 | +| `access-token-services/services/user-access-token.service.spec.ts` | User-specific endpoint + payload shape | |
| 62 | +| `access-token-services/cache-services/cache-access-token.service.spec.ts` | Cache key strategy, TTL, bypass on miss | |
| 63 | +| `access-token-services/cache-services/cache-tenant-access-token.service.spec.ts` | Tenant cache key uniqueness + eviction | |
| 64 | +| `access-token-services/cache-services/cache-user-access-token.service.spec.ts` | User cache key uniqueness + eviction | |
| 65 | + |
| 66 | +### 3.4 identity — exceptions |
| 67 | +One combined spec `src/clients/identity/exceptions/exceptions.spec.ts` that exercises: |
| 68 | +- `FailedToAuthenticateException` |
| 69 | +- `InsufficientPermissionException` |
| 70 | +- `InsufficientRoleException` |
| 71 | +- `InvalidTokenTypeException` |
| 72 | +- `MaxAgeExceededException` |
| 73 | +- `MissingAcrException` |
| 74 | +- `MissingAmrException` |
| 75 | +- `StatusCodeErrorException` |
| 76 | + |
| 77 | +Assert: message, `name`, `statusCode` (where applicable), `instanceof Error`, serialization. |
| 78 | + |
| 79 | +### 3.5 entitlements |
| 80 | +| File | Focus | |
| 81 | +|---|---| |
| 82 | +| `entitlements-client.events.spec.ts` | Event emitter wiring: subscribe/unsubscribe, replay, error isolation per listener | |
| 83 | +| `helpers/frontegg-entity.helper.spec.ts` | Entity-key building, normalization, edge cases (empty tenant/user) | |
| 84 | +| `storage/exp-time.utils.spec.ts` | `isExpired`, `computeTtl`, off-by-one & clock skew | |
| 85 | +| `storage/in-memory/in-memory.cache-key.utils.spec.ts` | Key shape per entity type, collision guards | |
| 86 | +| `storage/in-memory/mappers/helper.spec.ts` | Shared mapper helpers | |
| 87 | +| `storage/in-memory/mappers/sources.mapper.spec.ts` | Source record → tuple mapping, unknown source fallback | |
| 88 | +| `api-types/.../feature-set.spec.ts` | If it contains runtime logic (otherwise skip — pure types) | |
| 89 | +| `api-types/.../feature.spec.ts` | Same | |
| 90 | + |
| 91 | +### 3.6 events client |
| 92 | +| File | Focus | |
| 93 | +|---|---| |
| 94 | +| `src/clients/events/events.spec.ts` | `trigger` happy path, channel config validation, required-field errors (`types/errors.ts`), retries on 5xx, idempotency key | |
| 95 | + |
| 96 | +### 3.7 utils |
| 97 | +| File | Focus | |
| 98 | +|---|---| |
| 99 | +| `src/utils/package-loader.spec.ts` | Optional-peer loading for `ioredis` / `redis`: returns module when present, returns `null` on `MODULE_NOT_FOUND`, rethrows unexpected errors | |
| 100 | + |
| 101 | +### 3.8 authenticator |
| 102 | +`src/authenticator/index.ts` — spec for vendor-token acquisition, expiry refresh, concurrent-caller dedupe, failure propagation. |
| 103 | + |
| 104 | +### 3.9 middlewares |
| 105 | +Extend `with-authentication.spec.ts` — cover: missing header, malformed header, expired token, role / permission guards, next() on success, error formatting. |
| 106 | + |
| 107 | +### 3.10 components |
| 108 | +`src/components/frontegg-context/*` — if non-type runtime exists, spec context resolution from `req`. |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## 4. Shared unit-test infrastructure (new) |
| 113 | + |
| 114 | +Create `src/__test-utils__/` (excluded from publish via `files` in package.json): |
| 115 | + |
| 116 | +- `jwt.ts` — `signTestJwt(payload, { kid, exp, iss, aud })` using `jsonwebtoken` + a stable RSA keypair fixture. |
| 117 | +- `jwks.ts` — in-memory JWKS server fixture for `jsonwebtoken` verification paths that go through JWKS. |
| 118 | +- `http.ts` — `axios-mock-adapter` helpers: `mockAuthEndpoint`, `mockVendorToken`, `mockEntitlements`. |
| 119 | +- `cache.ts` — `makeInMemoryCache()` factory. |
| 120 | +- `fixtures/` — canonical user, tenant, entitlements, feature-flag payloads. |
| 121 | + |
| 122 | +Naming policy: all helpers live under `__test-utils__`, imported only from `*.spec.ts`. Jest `testPathIgnorePatterns` already excludes non-`.spec.ts`. |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +## 5. E2E suite — real Frontegg sandbox |
| 127 | + |
| 128 | +### 5.1 Location & config |
| 129 | +- New dir: `src/__e2e__/` |
| 130 | +- New file: `jest.e2e.config.js` (separate from unit) with: |
| 131 | + - `testRegex: 'src/__e2e__/.*\\.e2e\\.ts$'` |
| 132 | + - `setupFilesAfterEach`: tenant cleanup |
| 133 | + - `testTimeout: 60_000` |
| 134 | +- `npm run test:e2e` script. |
| 135 | +- Guarded by required env: `FRONTEGG_BASE_URL`, `FRONTEGG_CLIENT_ID`, `FRONTEGG_API_KEY`, `FRONTEGG_TEST_TENANT_ID`, `FRONTEGG_TEST_USER_ID`, `FRONTEGG_TEST_USER_JWT` (or the ability to mint one). If any are missing → `describe.skip` with a clear message. |
| 136 | + |
| 137 | +### 5.2 Credential hygiene |
| 138 | +- Credentials loaded via `process.env` only. Never committed. A `.env.e2e.example` documents the required keys. `.env.e2e` in `.gitignore`. |
| 139 | +- All E2E operations target a **dedicated sandbox tenant** — no writes against prod. |
| 140 | +- Each test cleans its own side effects (created users, triggered events) in `afterAll`. |
| 141 | + |
| 142 | +### 5.3 Flows to cover (one `*.e2e.ts` per flow) |
| 143 | + |
| 144 | +| Flow | Asserts | |
| 145 | +|---|---| |
| 146 | +| `identity-client.e2e.ts` | Real vendor-token fetch → `validateIdentityOnToken` against a freshly minted user JWT → permissions/roles echo back | |
| 147 | +| `http-client.e2e.ts` | Authenticated request to a known public Frontegg read endpoint returns 200 + expected shape | |
| 148 | +| `entitlements-client.e2e.ts` | `isEntitledTo` happy path for a known feature flag in the sandbox; unknown feature → not entitled | |
| 149 | +| `hosted-login.client.e2e.ts` | `requestAuthorize` URL construction against sandbox vendor; code-exchange stub if sandbox supports it | |
| 150 | +| `events.e2e.ts` | Trigger a channel-configured event into the sandbox event bus, assert 2xx | |
| 151 | +| `middlewares.e2e.ts` | Boot a minimal `express` app, hit it with a real sandbox user JWT, assert `req.frontegg.user` is populated | |
| 152 | +| `step-up.e2e.ts` | Validate a real step-up token from sandbox (or assert failure path if minting not available) | |
| 153 | + |
| 154 | +### 5.4 CI wiring |
| 155 | +- Unit tests: run on every PR (existing). |
| 156 | +- E2E: separate GH Actions workflow, `workflow_dispatch` + nightly cron, credentials from repo secrets, does **not** block PR merges. |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## 6. Coverage targets (raise after Act phase) |
| 161 | + |
| 162 | +Move `jest.config.js` thresholds from the current floor to: |
| 163 | + |
| 164 | +``` |
| 165 | +statements: 85, |
| 166 | +branches: 75, |
| 167 | +functions: 85, |
| 168 | +lines: 85, |
| 169 | +``` |
| 170 | + |
| 171 | +Any file that intentionally stays below (e.g. `src/types/express/index.d.ts`, pure type files) goes into `collectCoverageFrom` exclusions. |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## 7. Risks & open questions |
| 176 | + |
| 177 | +1. **Sandbox availability** — do you already have a Frontegg sandbox tenant dedicated to SDK e2e, or should I scaffold the e2e with `describe.skip` placeholders and let you fill in credentials? |
| 178 | +2. **User JWT minting** — does the sandbox expose a way to mint a user JWT for a test user via API, or will you provide a long-lived test JWT via env? This determines whether e2e is fully self-contained. |
| 179 | +3. **Rate limits** — nightly + on-demand should be fine; confirm no per-minute ceiling that batched e2e would hit. |
| 180 | +4. **Secrets storage** — confirm the GH Actions secret names to use (`FRONTEGG_E2E_*` prefix suggested). |
| 181 | +5. **Type-only files** — I'll skip files that are 100% `type`/`interface` exports; flag if you want stub specs for them anyway. |
| 182 | +6. **Test-utils publishing** — confirm `src/__test-utils__/` should be excluded from the published package (it will be, via `.npmignore` / tsconfig `exclude`). |
| 183 | + |
| 184 | +--- |
| 185 | + |
| 186 | +## 8. Proposed execution order (Act phase) |
| 187 | + |
| 188 | +1. Land `__test-utils__/` + fixtures (no behavior change). |
| 189 | +2. Unit specs for **identity token-resolvers** + **exceptions** (highest security value). |
| 190 | +3. Unit specs for **authenticator**, **events**, **package-loader**, **redis-cache.manager**. |
| 191 | +4. Unit specs for remaining **entitlements helpers/mappers/storage utils**. |
| 192 | +5. Extend existing specs to close branch-coverage gaps. |
| 193 | +6. Land `jest.e2e.config.js` + `__e2e__/` skeleton with one real flow (`identity-client.e2e.ts`) as proof. |
| 194 | +7. Fill remaining e2e flows. |
| 195 | +8. Raise coverage thresholds, wire e2e workflow, update README with `test` / `test:e2e` instructions. |
| 196 | + |
| 197 | +Each step ships as its own commit/PR to keep review tractable. |
0 commit comments