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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { INestApplication } from "@nestjs/common";
import request from "supertest";
import { createTestApp, withTenant } from "../utils/app.factory";
import { loginAndGetToken, authHeaderFromToken } from "../utils/auth.helper";

describe("RBAC Privileges (e2e)", () => {
let app: INestApplication;
let token: string | undefined;
let createdPrivilegeId: string | undefined;

beforeAll(async () => {
app = await createTestApp();
token = (await loginAndGetToken(app))?.access_token;
});

afterAll(async () => {
await app.close();
});

it("GET /rbac/privileges returns 200 with required query params", async () => {
const tenantId = process.env.E2E_TENANT_ID || "00000000-0000-0000-0000-000000000000";
const roleId = "00000000-0000-0000-0000-000000000001"; // dummy UUID; may return 404 but should be 200/204 if exists
const res = await request(app.getHttpServer())
.get(`/rbac/privileges?tenantId=${tenantId}&roleId=${roleId}`)
.set(withTenant(Object.assign({}, authHeaderFromToken(token))));

expect([200, 204]).toContain(res.status);
});

it("POST /rbac/privileges/create should create privilege (201) and capture id", async () => {
const code = `e2e_code_${Date.now()}`;
const payload = { privileges: [{ title: "E2E Privilege", code }] };

const res = await request(app.getHttpServer())
.post("/rbac/privileges/create")
.set(withTenant(Object.assign({}, authHeaderFromToken(token))))
.send(payload);

expect([200, 201]).toContain(res.status);
const created = res.body?.result?.privileges?.[0];
expect(!!created && !!created.privilegeId).toBe(true);
createdPrivilegeId = created?.privilegeId;
});

it("GET /rbac/privileges/:privilegeId with valid id returns 200", async () => {
expect(!!createdPrivilegeId).toBe(true);
const res = await request(app.getHttpServer())
.get(`/rbac/privileges/${createdPrivilegeId}`)
.set(withTenant(authHeaderFromToken(token)));

expect(res.status).toBe(200);
expect(res.body?.result?.privilegeId).toBe(createdPrivilegeId);
});

it("DELETE /rbac/privileges/:privilegeId with valid id returns 200/204", async () => {
expect(!!createdPrivilegeId).toBe(true);
const res = await request(app.getHttpServer())
.delete(`/rbac/privileges/${createdPrivilegeId}`)
.set(withTenant(authHeaderFromToken(token)));

expect([200, 204]).toContain(res.status);
});

it("DELETE /rbac/privileges/:privilegeId (after deletion) returns 404", async () => {
expect(!!createdPrivilegeId).toBe(true);
const res = await request(app.getHttpServer())
.delete(`/rbac/privileges/${createdPrivilegeId}`)
.set(withTenant(authHeaderFromToken(token)));

expect(res.status).toBe(404);
});
});
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,34 @@ lerna-debug.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
package-lock.json

# Additional ignores
# Env files
.env.*
!.env.example

# Lockfiles (team policy)
yarn.lock
pnpm-lock.yaml

# Build info
*.tsbuildinfo

# Caches and temp
.eslintcache
.cache/
tmp/
.tmp/

# Reports
junit.xml
test-results.xml

# Local logs and artifacts
combined.log
error.log

# Project-specific artifacts
shiksha-backend-v2@*
NODE_ENV*
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,92 @@ There are two types of fields: core/primary and custom. Core fields are directly
For instance, in a Learning Management System (LMS), tenants can be defined as different programs. Cohorts would represent student classes or groups within a particular state. Roles could include Admin, Teacher, and Student. Privileges might encompass actions like creating or deleting users, as well as viewing or updating profiles. Core fields would consist of fundamental information such as username, email, and contact details. Custom fields could include attributes like gender, with a radio button type offering options like male or female.

Refer to the Documentation link for more details - https://tekdi.github.io/docs/user-service/about

## Testing (Jest)

## Testing (Jest)
...
<!-- npm ci
npm test
npm run test:watch
npm run test:cov
npm run test:e2e -->
...

The project is preconfigured with Jest for unit and e2e testing.

### Install

```bash
npm ci
```

### Run unit tests

```bash
npm test
```

### Watch mode

```bash
npm run test:watch
```

### Coverage

```bash
npm run test:cov
```

### Run e2e tests

```bash
npm run test:e2e
```

Notes:
- e2e tests run with `test/jest-e2e.json`.
- If an endpoint is guarded, tests should override or mock guards and external dependencies (like database or Kafka).
- Prefer supertest-based e2e tests for API validation:

```ts
import { Test } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './src/app.module';

describe('Health (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});

afterAll(async () => {
await app.close();
});

it('/health (GET)', async () => {
await request(app.getHttpServer()).get('/health').expect(200);
});
});
```

### e2e auth login for protected APIs

Provide credentials via environment variables before running e2e tests:

```bash
export E2E_USERNAME="your-username"
export E2E_PASSWORD="your-password"
# optional, if your APIs expect tenant header
export E2E_TENANT_ID="tenant-uuid"
npm run test:e2e
```

The helper at `test/e2e/utils/auth.helper.ts` logs in with `/auth/login` and uses the returned `access_token` in the `Authorization` header for subsequent requests (as required by `AuthController`). If no credentials are set, auth e2e tests are skipped automatically.
133 changes: 133 additions & 0 deletions docs/e2e-auth-login-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
## E2E Auth Login Flow — How it works and how to run it

This document explains the end-to-end login test flow, execution order, the internal calls that happen, and how you can run just the login test or the full flow.

### What this test verifies
- The API accepts credentials at `POST /auth/login` and returns an auth envelope with `access_token` and `refresh_token`.
- The returned `access_token` can be used to call a protected route (here, `GET /auth`), with the token placed directly in the `Authorization` header (no `Bearer ` prefix).

---

### Execution order inside the e2e test

1) `beforeAll` — Boot the Nest application for tests
- Creates a `TestingModule` importing the real `AppModule`
- Overrides `KeycloakService` to avoid external Keycloak calls (returns fake tokens)
- Overrides `JwtAuthGuard` so protected routes are accessible in tests
- Initializes the Nest app instance

2) Test: “should login and return access + refresh tokens”
- Uses `loginAndGetToken(app)` helper which internally:
- `supertest` POSTs `{"username","password"}` to `/auth/login`
- Extracts `result` from the API response envelope
- Returns `{ access_token, refresh_token, ... }`

3) Test: “should use token to call /auth protected route”
- Calls `loginAndGetToken(app)` again to obtain a token
- Sends `GET /auth` with:
- `Authorization: <access_token>` (raw token, no `Bearer`)
- Optionally `tenantid: <uuid>` if `E2E_TENANT_ID` is set
- Expects `200` and a successful response envelope

4) `afterAll` — Close the Nest app

---

### Internal call flow (data path)

1) `supertest` → `POST /auth/login` on the in-memory app server
2) Nest routes request → `AuthController.login`
3) `AuthController.login` → `AuthService.login`
4) `AuthService.login` → `KeycloakService.login(username,password)`
- In e2e, `KeycloakService` is mocked to return a fake JWT and tokens
5) `AuthService.login` wraps tokens with `APIResponse.success` and returns
6) The test gets `res.body.result` → tokens
7) `supertest` → `GET /auth` with `Authorization: <token>` (and optional `tenantid`)
8) `JwtAuthGuard` is overridden in e2e to allow access, so the route responds `200`

---

### Where the pieces live (key files)
- E2E spec:
- `test/e2e/auth-login.e2e-spec.ts`
- Helper used by spec:
- `test/e2e/utils/auth.helper.ts`
- Login endpoint:
- `src/auth/auth.controller.ts` (`POST /auth/login`)
- `src/auth/auth.service.ts` (calls `KeycloakService.login`, wraps response)
- Guard mocked for e2e:
- `src/common/guards/keycloak.guard.ts` (overridden in the e2e spec)
- Keycloak client (mocked in e2e):
- `src/common/utils/keycloak.service.ts`

---

### Environment variables used by the e2e
These allow the helper and mocked token to derive values. In the current setup they are required by the helper even though Keycloak is mocked:

- Required by helper:
- `E2E_USERNAME`, `E2E_PASSWORD`
- `KEYCLOAK`, `KEYCLOAK_REALM`, `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET`
- Optional for protected route:
- `E2E_TENANT_ID` (added as `tenantid` header if present)

Example:
```bash
export E2E_USERNAME="test-user" E2E_PASSWORD="secret" \
KEYCLOAK="1" KEYCLOAK_REALM="1" KEYCLOAK_CLIENT_ID="1" KEYCLOAK_CLIENT_SECRET="1"
```

If you have a `.env.test`, you can source it:
```bash
export $(grep -v '^#' .env.test | xargs)
```

---

### Run only the login e2e
```bash
npm run test:e2e -- --runTestsByPath test/e2e/user/login.e2e-spec.ts
# Or a single test by name:
npm run test:e2e -- -t "should login and return access + refresh tokens"
```

### Run the full e2e suite
```bash
npm run test:e2e
```

### Manual curl (when the app is running)
- Start the server (e.g. `npm run start:dev`)
- Login:
```bash
BASE_URL="http://localhost:3000"
curl -X POST "$BASE_URL/auth/login" \
-H 'Content-Type: application/json' \
-d '{"username":"<your-username>","password":"<your-password>"}'
```
- Call protected route (note: raw token in `Authorization`, no `Bearer `):
```bash
ACCESS="<paste-access-token>"
TENANTID="<optional-tenant-uuid>"
curl -H "Authorization: $ACCESS" -H "tenantid: $TENANTID" "$BASE_URL/auth"
```

---

### Why mocking is used in e2e
External dependencies (Keycloak, RSA verification) can make e2e tests slow and flaky. The e2e spec overrides:
- `KeycloakService.login` to return synthetic tokens
- `JwtAuthGuard` to bypass signature validation

This keeps the test focused on our API contract/flow while remaining fast and deterministic.

---

### Negative cases covered
- Login with invalid username/password:
- In a separate e2e block we override `KeycloakService.login` to throw with `response.status = 401`. The API responds with a 404 and a failed response envelope (as per `AuthService` + `AllExceptionsFilter`).
- RBAC token endpoint header validation:
- `GET /auth/rbac/token` without `tenantid` → 400
- `GET /auth/rbac/token` with non-UUID `tenantid` → 400


Loading