Skip to content
Merged
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
71 changes: 71 additions & 0 deletions .claude/skills/open-pr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: open-pr
description: Generate a PR title and description following the WCC repo template, infer change types from the diff, and print the GitHub URL to open the PR in the browser. Use when asked to open a PR or create a pull request.
disable-model-invocation: true
allowed-tools: Bash, Read, Glob
---

# Open PR Skill

> **Canonical runbook**: `.ai/skills/open-pr.md`
> This file is the Claude Code adapter. The workflow logic is defined in the canonical skill
> so it can be shared with other agents (Codex, Copilot, Cursor). Any changes to the workflow
> should be made in `.ai/skills/open-pr.md` first.

## Workflow

1. Gather context in parallel:
- `git log main..HEAD --oneline` — commits in this PR
- `git diff main...HEAD --stat` — files changed
- `git branch --show-current` — current branch name
- Read `.github/PULL_REQUEST_TEMPLATE.md`

2. Determine change types by mapping conventional-commit prefixes and the nature of the diff:

| Conventional prefix | PR Change Type |
| ------------------------ | -------------- |
| `feat` | New Feature |
| `fix` | Bug Fix |
| `refactor` | Code Refactor |
| `docs` / `doc` | Documentation |
| `test` | Test |
| `chore` / `ci` / `build` | Other |

Only keep the change types present in this PR — remove the rest from the template.

3. Suggest a PR title using Conventional Commits format: `<type>: <short imperative description>` (50–72 chars). When multiple types apply, use the most significant: `feat` > `fix` > `refactor` > `test` > `docs` > `chore`.

4. Generate the filled-in PR description. Pre-check only the applicable change type boxes; remove the inapplicable ones entirely. Write all prose sections (Description, Related Issue) as flowing paragraphs — do NOT wrap lines at any character limit. Each sentence or logical thought should continue on the same line so that GitHub renders the text correctly without unwanted line breaks.

5. **Screenshots section** — decide based on changed files:
- Frontend changes (`admin-wcc-app/**`, `*.tsx`, `*.css`, components, pages): include the section and list the specific screenshots needed (before/after UI, error states, label/title changes, GIF for interactions). Emphasise that screenshots are required for the reviewer to assess visual changes.
- Backend API changes (new/changed controllers or endpoints): include the section asking for Swagger UI screenshots (`/swagger-ui/index.html`).
- Only `test`, `docs`, `chore`, `ci`, or `refactor` with no endpoint changes: **omit the Screenshots section entirely**.

6. **Pull request checklist** — always include the contributor guide checkbox. Include "I have tested my changes locally" only for `feat`, `fix`, `refactor`, or frontend changes — omit it for pure `docs`, `chore`, or `ci` changes.

7. Derive the contributor's GitHub username from the `origin` remote:

```bash
git remote get-url origin
# https://github.com/<username>/wcc-backend.git or git@github.com:<username>/wcc-backend.git
```

Extract `<username>` from the URL (path segment before `/wcc-backend`).

Print:
- **Suggested PR title** (plain text)
- **PR description** (markdown code block)
- **GitHub compare URL**: `https://github.com/Women-Coding-Community/wcc-backend/compare/main...<username>:<branch-name>`
- Remind the user to add screenshots before submitting if required

## Rules

- **Never hard-wrap prose** — description text must not have forced line breaks mid-sentence or mid-paragraph; let GitHub reflow the text naturally

- Always target `main` on `Women-Coding-Community/wcc-backend` (upstream)
- Never use `gh pr create` — fork permissions require the user to open the PR through the browser
- If the issue number cannot be found in branch name or commits, use `#?` and ask the user
- Never include change type checkboxes for types not present in the diff
- Never include the Screenshots section for pure test/docs/chore/ci changes
- Never include "I have tested my changes locally" for pure docs/chore/ci changes
101 changes: 32 additions & 69 deletions src/__tests__/api/mentee-registration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { NextApiRequest, NextApiResponse } from 'next';

import * as api from '../../lib/api';
import handler from '../../pages/api/mentee-registration';

jest.mock('../../lib/api', () => ({
__esModule: true,
...jest.requireActual('../../lib/api'),
proxyRequest: jest.fn(),
}));

const makeReq = (overrides: Partial<NextApiRequest> = {}): NextApiRequest =>
({
method: 'POST',
Expand All @@ -20,18 +27,7 @@ const makeRes = (): NextApiResponse => {
};

describe('mentee-registration API handler', () => {
const originalEnv = process.env;

beforeEach(() => {
process.env = {
...originalEnv,
API_BASE_URL: 'http://localhost:8080/api/cms/v1',
API_KEY: 'test-key',
};
});

afterEach(() => {
process.env = originalEnv;
jest.resetAllMocks();
});

Expand All @@ -46,105 +42,72 @@ describe('mentee-registration API handler', () => {
expect(res.json).toHaveBeenCalledWith({ error: 'Method GET Not Allowed' });
});

it('returns 500 when API_BASE_URL is missing', async () => {
delete process.env.API_BASE_URL;
const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: 'Server configuration error',
});
});

it('returns 500 when API_KEY is missing', async () => {
delete process.env.API_KEY;
const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: 'Server configuration error',
});
});

it('proxies POST to the platform endpoint and returns 201 on success', async () => {
const responseBody = { id: 42 };
globalThis.fetch = jest.fn().mockResolvedValue({
ok: true,
status: 201,
json: jest.fn().mockResolvedValue(responseBody),
});
(api.proxyRequest as jest.Mock).mockResolvedValue(responseBody);

const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(globalThis.fetch).toHaveBeenCalledWith(
'http://localhost:8080/api/platform/v1/mentees',
expect(api.proxyRequest).toHaveBeenCalledWith(
'mentees',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'X-API-KEY': 'test-key',
'Content-Type': 'application/json',
}),
data: req.body,
}),
true,
);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith(responseBody);
});

it('forwards backend error status and message on failure', async () => {
const errorBody = { message: 'Email already registered' };
globalThis.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 409,
json: jest.fn().mockResolvedValue(errorBody),
});
const errorResponse = {
response: {
status: 409,
data: { message: 'Email already registered' },
},
};
(api.proxyRequest as jest.Mock).mockRejectedValue(errorResponse);

const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(409);
expect(res.json).toHaveBeenCalledWith(errorBody);
expect(res.json).toHaveBeenCalledWith(errorResponse.response.data);
});

it('falls back to generic error when backend returns no body', async () => {
globalThis.fetch = jest.fn().mockResolvedValue({
ok: false,
status: 500,
json: jest.fn().mockRejectedValue(new Error('no body')),
});
it('returns 500 on unexpected error', async () => {
(api.proxyRequest as jest.Mock).mockRejectedValue(
new Error('Network failure'),
);

const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
error: 'Registration failed. Please try again.',
});
expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' });
});

it('returns 500 on network error', async () => {
globalThis.fetch = jest
.fn()
.mockRejectedValue(new Error('Network failure'));
it('returns 500 when server configuration error occurs', async () => {
(api.proxyRequest as jest.Mock).mockRejectedValue(
new Error('Server configuration error'),
);

const req = makeReq();
const res = makeRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({ error: 'Internal server error' });
expect(res.json).toHaveBeenCalledWith({
error: 'Server configuration error',
});
});
});
Loading
Loading