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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,36 @@ npm install
2. Update the `.env` file in `nsc-events-nestjs` with your PostgreSQL credentials
3. The database tables will be automatically created when you start the backend

### Database Seeding

Once both the backend and the database are running, you can populate the database with sample data for development:

```bash
# Seed both users and events (recommended β€” runs users first, then events)
npm run seed:all

# Seed only sample users (admin, creator, and regular user accounts)
npm run seed:users

# Seed only sample events (requires the seed creator account to exist)
npm run seed
```

The seed scripts connect to the NestJS API (default: `http://localhost:3000/api`). Override the target with the `PLAYWRIGHT_API_URL` environment variable if your backend is on a different port.

**Seeded accounts**

| Email | Password | Role |
|-------|----------|------|
| `admin@nsc.dev` | `admin@admin123` | admin |
| `admin2@nsc.dev` | `Admin2@dev456` | admin |
| `seed-creator@nsc.dev` | `Seed@Creator123` | creator |
| `user@nsc.dev` | `User@Alex123` | user |
| `user2@nsc.dev` | `User@Sam456` | user |
| `user3@nsc.dev` | `User@Jordan789` | user |

> **Note:** These accounts are for local development only. Do not use these credentials in production environments.

### Running the Applications

**Backend:**
Expand Down
70 changes: 59 additions & 11 deletions e2e/fixtures/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
import { test as base, expect } from '@playwright/test';
import { ApiClient } from '../utils/api-client';

const ADMIN_API_URL =
process.env.PLAYWRIGHT_API_URL || "http://localhost:3000/api";
const ADMIN_CREDENTIALS = {
email: "admin@nsc.dev",
password: "admin@admin123",
};

/** Returns an ApiClient authenticated as the seed admin. Best-effort β€” returns null if login fails. */
async function getAdminClient(): Promise<ApiClient | null> {
try {
const adminClient = new ApiClient(ADMIN_API_URL);
const res = await adminClient.login(
ADMIN_CREDENTIALS.email,
ADMIN_CREDENTIALS.password,
);
const token = res.data?.token || res.data?.data?.token;
if (!token) return null;
adminClient.setToken(token);
return adminClient;
} catch {
return null;
}
}

type AuthFixtures = {
authenticatedPage: {
page: any;
Expand Down Expand Up @@ -36,31 +60,37 @@ export const test = base.extend<AuthFixtures>({
);

// Login to get token
const loginResponse = await apiClient.login(testUser.email, testUser.password);
const loginResponse = await apiClient.login(
testUser.email,
testUser.password,
);
const token = loginResponse.data.access_token;

apiClient.setToken(token);

// Set authentication context in page
await page.context().addCookies([
{
name: 'next-auth.session-token',
name: "next-auth.session-token",
value: token,
domain: 'localhost',
path: '/',
domain: "localhost",
path: "/",
httpOnly: true,
secure: false,
sameSite: 'Lax',
sameSite: "Lax",
},
]);

// Store token in localStorage
await page.goto('/');
await page.evaluate(({ tokenValue }) => {
if (tokenValue) {
localStorage.setItem('authToken', tokenValue);
}
}, { tokenValue: token });
await page.goto("/");
await page.evaluate(
({ tokenValue }) => {
if (tokenValue) {
localStorage.setItem("authToken", tokenValue);
}
},
{ tokenValue: token },
);

const user = signupResponse.data.user || loginResponse.data.user;

Expand All @@ -69,6 +99,24 @@ export const test = base.extend<AuthFixtures>({
apiClient,
user,
});

// ── Cleanup: delete the test user via admin account ──────────────────
try {
const adminClient = await getAdminClient();
if (adminClient) {
// Resolve user ID (may come from signup or login response)
let userId: string | undefined = user?.id;
if (!userId) {
const found = await adminClient.getUserByEmail(testUser.email);
userId = found.data?.id || found.data?.data?.id;
}
if (userId) {
await adminClient.deleteUser(userId);
}
}
} catch {
// Best-effort β€” global teardown will sweep any stragglers
}
} catch (error) {
console.error('Failed to set up authenticated user:', error);
throw error;
Expand Down
19 changes: 19 additions & 0 deletions e2e/tests/event-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ test.describe("Event Management", () => {
createdEventIds = [];
});

test.afterAll(async () => {
// Delete the test creator user via admin account
if (!userId) return;
try {
const adminClient = new ApiClient();
const loginRes = await adminClient.login(
"admin@nsc.dev",
"admin@admin123",
);
const adminToken = loginRes.data?.token || loginRes.data?.data?.token;
if (adminToken) {
adminClient.setToken(adminToken);
await adminClient.deleteUser(userId);
}
} catch {
// Best-effort β€” global teardown will handle any remaining test users
}
});

test("should create a new event via UI and redirect to event detail", async ({ page, browserName }) => {
// Skip on Mobile Chrome due to MUI DatePicker mobile dialog interaction complexity
test.skip(browserName === "chromium" && test.info().project.name === "Mobile Chrome",
Expand Down
42 changes: 42 additions & 0 deletions e2e/tests/event-registration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ async function signupWithRetry(page, data, attempts = 3) {
test.describe('Event Registration', () => {
let userToken: string;
let eventId: string;
let userId: string;

test.beforeAll(async ({ browser }) => {
// Setup: Create user and event for registration tests
Expand All @@ -38,6 +39,7 @@ test.describe('Event Registration', () => {
if (signupResponse.ok()) {
const data = await signupResponse.json();
userToken = data.token;
userId = data.user?.id || data.data?.user?.id;

// Create an event
const createEventResponse = await page.request.post('http://localhost/api/event-registration', {
Expand All @@ -63,6 +65,46 @@ test.describe('Event Registration', () => {
await page.close();
});

test.afterAll(async ({ browser }) => {
const page = await browser.newPage();
try {
// Delete the test event
if (eventId && userToken) {
await page.request.delete(
`http://localhost:3000/api/events/remove/${eventId}`,
{
headers: { Authorization: `Bearer ${userToken}` },
},
);
}
// Delete the test user via admin
if (userId) {
const adminRes = await page.request.post(
"http://localhost:3000/api/auth/login",
{
data: { email: "admin@nsc.dev", password: "admin@admin123" },
},
);
if (adminRes.ok()) {
const adminData = await adminRes.json();
const adminToken = adminData.token || adminData.data?.token;
if (adminToken) {
await page.request.delete(
`http://localhost:3000/api/users/remove/${userId}`,
{
headers: { Authorization: `Bearer ${adminToken}` },
},
);
}
}
}
} catch {
// Best-effort β€” global teardown will handle stragglers
} finally {
await page.close();
}
});

test('should register for an event', async ({ page }) => {
if (!userToken || !eventId) {
test.skip(true, 'Test setup failed to create user or event');
Expand Down
39 changes: 39 additions & 0 deletions e2e/tests/user-management.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function setAuthToken(page: Page, token: string): Promise<void> {
test.describe('User Management & Admin Functions', () => {
let adminToken: string;
let testUserId: string;
let testAdminUserId: string;

test.beforeAll(async ({ browser }) => {
// Setup: Create admin user
Expand All @@ -38,6 +39,7 @@ test.describe('User Management & Admin Functions', () => {
if (signupResponse.ok()) {
const data = await signupResponse.json();
adminToken = data.token;
testAdminUserId = data.user?.id || data.data?.user?.id;

// Create a regular user for role management tests
const regularUser = generateTestUser();
Expand All @@ -54,6 +56,43 @@ test.describe('User Management & Admin Functions', () => {
await page.close();
});

test.afterAll(async ({ browser }) => {
const page = await browser.newPage();
try {
const adminRes = await page.request.post(
"http://localhost:3000/api/auth/login",
{
data: { email: "admin@nsc.dev", password: "admin@admin123" },
},
);
if (adminRes.ok()) {
const adminData = await adminRes.json();
const seedAdminToken = adminData.token || adminData.data?.token;
if (seedAdminToken) {
const headers = { Authorization: `Bearer ${seedAdminToken}` };
// Delete regular test user
if (testUserId) {
await page.request.delete(
`http://localhost:3000/api/users/remove/${testUserId}`,
{ headers },
);
}
// Delete the test admin user created by this suite
if (testAdminUserId) {
await page.request.delete(
`http://localhost:3000/api/users/remove/${testAdminUserId}`,
{ headers },
);
}
}
}
} catch {
// Best-effort β€” global teardown will handle stragglers
} finally {
await page.close();
}
});

test('should view user profile', async ({ page, browserName }) => {
// Skip on Firefox due to CORS issues with API calls in authenticated context
test.skip(browserName === "firefox",
Expand Down
24 changes: 22 additions & 2 deletions e2e/utils/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ export class ApiClient {
private client: AxiosInstance;
private token: string | null = null;

constructor(baseURL: string = process.env.PLAYWRIGHT_API_URL || "http://localhost/api") {
constructor(
baseURL: string = process.env.PLAYWRIGHT_API_URL || "http://localhost/api",
) {
this.client = axios.create({
baseURL,
headers: {
Expand All @@ -28,7 +30,7 @@ export class ApiClient {
firstName: string,
lastName: string,
pronouns: string = "they/them",
role: string = "user"
role: string = "user",
) {
return this.client.post("/auth/signup", {
email,
Expand Down Expand Up @@ -91,6 +93,24 @@ export class ApiClient {
this.token = null;
delete this.client.defaults.headers.common["Authorization"];
}

// ─── Admin helpers ───────────────────────────────────────────────────────────

async getAllUsers() {
return this.client.get("/users");
}

async getUserByEmail(email: string) {
return this.client.get(`/users/email/${encodeURIComponent(email)}`);
}

async deleteUser(id: string) {
return this.client.delete(`/users/remove/${id}`);
}

async getAllEvents(limit = 500) {
return this.client.get(`/events?numberOfEventsToGet=${limit}`);
}
}

export const createApiClient = () => new ApiClient();
Loading
Loading