Skip to content

Commit 14d70ee

Browse files
Merge pull request #200 from frontegg/feat/comprehensive-test-coverage
test: add comprehensive unit and e2e test coverage
2 parents d152022 + b0904e7 commit 14d70ee

34 files changed

Lines changed: 2538 additions & 34 deletions

.env.e2e.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# E2E test credentials — copy to .env.e2e and fill in
2+
FRONTEGG_BASE_URL=https://app-x4gr8g28fxr5.frontegg.com
3+
FRONTEGG_CLIENT_ID=your-client-id
4+
FRONTEGG_API_KEY=your-api-key

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ typings/
6767
# dotenv environment variables file
6868
.env
6969
.env.test
70+
.env.e2e
7071

7172
# parcel-bundler cache (https://parceljs.org/)
7273
.cache

docs/TEST_PLAN.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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.

jest.config.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@ module.exports = {
44
testRegex: 'src/.*\\.(test|spec)?\\.(ts|tsx)$',
55
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
66
rootDir: '.',
7-
collectCoverageFrom: ['src/**/*.{js,ts}', '!**/node_modules/**', '!**/dist/**', '!**/vendor/**'],
7+
collectCoverageFrom: [
8+
'src/**/*.{js,ts}',
9+
'!**/node_modules/**',
10+
'!**/dist/**',
11+
'!**/vendor/**',
12+
'!src/__test-utils__/**',
13+
'!src/__e2e__/**',
14+
'!src/types/**',
15+
],
816
coverageThreshold: {
917
global: {
10-
statements: 17,
11-
branches: 24,
12-
functions: 20,
13-
lines: 18,
18+
statements: 70,
19+
branches: 70,
20+
functions: 65,
21+
lines: 70,
1422
},
1523
},
1624
reporters: [

jest.e2e.config.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-env node */
2+
module.exports = {
3+
transform: { '^.+\\.ts?$': 'ts-jest' },
4+
testEnvironment: 'node',
5+
testRegex: 'src/__e2e__/.*\\.e2e\\.ts$',
6+
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
7+
rootDir: '.',
8+
testTimeout: 60000,
9+
forceExit: true,
10+
reporters: [
11+
'default',
12+
[
13+
'jest-junit',
14+
{
15+
outputDirectory: 'test-results',
16+
outputName: 'jest-e2e-junit.xml',
17+
},
18+
],
19+
],
20+
};

package-lock.json

Lines changed: 36 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)