# Testing — Said Yaqub Sadat Personal Portfolio

## 1. Strategy

| Layer | Tool | Scope |
|---|---|---|
| Backend feature tests | PHPUnit 12 + Laravel TestCase (`RefreshDatabase`) | HTTP behaviour of every public and admin endpoint: status codes, response shapes, validation, authorization, publication rules |
| Backend unit tests | PHPUnit | none required at the current size — all logic is exercised through the HTTP layer |
| Frontend build/type check | `tsc -b` + `vite build` | TypeScript correctness and a production bundle |
| Frontend lint | `oxlint` | Static lint rules |
| Manual browser pass | Headless/real browser | Navigation, responsive layout, keyboard access, meta tags |

Automated browser (Playwright/Cypress) end-to-end tests are **not** part of this
delivery — see [Limitations](#7-limitations).

## 2. Running the suite

The suite runs against a **dedicated MySQL database** (`portfolio_test`) so it exercises
the same engine as production. Create it once:

```sql
CREATE DATABASE portfolio_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```

`backend/phpunit.xml` sets `DB_CONNECTION=mysql` and `DB_DATABASE=portfolio_test`; the
remaining credentials come from `backend/.env`.

```bash
cd backend
php artisan test                       # everything
php artisan test --compact             # machine readable summary
php artisan test tests/Feature/AdminCrudTest.php
php artisan test --filter=test_the_contact_endpoint_is_rate_limited
vendor/bin/phpunit                     # direct PHPUnit call
```

The suite never touches the development database (`portfolio_db`).

## 3. Test files

| File | Tests | What it covers |
|---|---|---|
| `tests/Feature/PublicApiTest.php` | 10 | Profile payload, skills listing, published-only project list, search and category filters, pagination metadata, featured flag, `per_page` validation, categories, experiences, education, certificates |
| `tests/Feature/ProjectPublicationTest.php` | 6 | Lookup by slug, screenshots in the payload, draft and archived projects invisible (404), unknown slug, draft exclusion from list and categories |
| `tests/Feature/ContactMessageTest.php` | 7 | Valid submission stored, missing body rejected, invalid e-mail rejected, over-long message rejected, honeypot rejection, rate limit returns 429 after five requests, default message status |
| `tests/Feature/AdminAuthenticationTest.php` | 9 | Successful sign-in, wrong password, non-admin sign-in refused, anonymous requests rejected (401), non-admin token rejected (403), dashboard access, `/admin/me`, sign-out invalidates the token, login rate limit |
| `tests/Feature/AdminCrudTest.php` | 12 | Project create, unique slugs for duplicate titles, title validation, publish/draft workflow, update and delete, certificate CRUD and validation, skill CRUD, settings whitelist, message status workflow, invalid status rejected, activity log written |
| `tests/Feature/AdminUserManagementTest.php` | 10 | User list, anonymous access rejected, create with validation and unique e-mail, update and delete, own account protected, last administrator protected, own profile updated, password change requires the current password |
| `tests/Feature/AdminSessionTest.php` | 1 | Signing in again keeps the sessions of other browsers and tabs alive |
| `tests/Feature/FileUploadTest.php` | 13 | Valid image upload stored under `projects/`, non-image rejected, oversized file rejected, upload requires a token, delete removes the file, path traversal rejected, profile photo stored under `profile/`, uploaded photo becomes the active one, profile photo requires a token, non-image profile photo rejected, logo upload stored under `logo/`, uploaded logo becomes the active one, logo upload requires a token |
| `tests/Feature/CvDownloadTest.php` | 5 | CV served as a PDF download with the correct headers, settings path honoured, `/api/cv` alias, missing file returns 404, sitemap lists published projects and omits drafts |
| `tests/Feature/ExampleTest.php` | 1 | Root route redirects to the frontend |
| `tests/Unit/ExampleTest.php` | 1 | Placeholder unit test |
| **Total** | **82** | **327 assertions** |

Model factories back the tests: `UserFactory` (with an `admin()` state),
`ProjectFactory` (with `draft()`, `archived()` and `featured()` states),
`SkillFactory`, `ExperienceFactory`, `EducationFactory`, `CertificateFactory`,
`MessageFactory`, `ProjectImageFactory`, `ActivityLogFactory`.

## 4. Last recorded run

```
$ php artisan test --compact
{"tool":"phpunit","result":"passed","tests":82,"passed":82,"assertions":327,"duration_ms":5163}
```

Frontend:

```
$ cd frontend && npm run build
✓ 131 modules transformed.
dist/index.html                   1.21 kB │ gzip:   0.64 kB
dist/assets/index-B84iH974.css   34.00 kB │ gzip:   6.60 kB
dist/assets/index-BWXdiBf3.js   466.25 kB │ gzip: 132.15 kB
✓ built in 1.95s
```

## 5. Coverage against the specification's minimum test list (§18.1)

| Required test area | Covered by |
|---|---|
| Public page status and project visibility | `PublicApiTest`, `ProjectPublicationTest` |
| Project detail lookup by slug and missing-project handling | `ProjectPublicationTest` |
| Admin login, logout and unauthorized access | `AdminAuthenticationTest` |
| CRUD validation and permission checks | `AdminCrudTest`, `AdminAuthenticationTest` |
| Contact form valid/invalid submissions and rate-limit behaviour | `ContactMessageTest` |
| Upload restrictions | `FileUploadTest` |
| CV download behaviour | `CvDownloadTest` |
| Database relationship and publication-state behaviour | `ProjectPublicationTest`, `AdminCrudTest`, `PublicApiTest` |

## 6. Manual and browser checks

Besides the automated suite, the following were exercised against the running
development servers (`php artisan serve --port=8000` plus `npm run dev`):

### 6.1 API checks

* `GET /api/profile`, `GET /api/projects?per_page=2`, `GET /cv` and `GET /sitemap.xml`
  answer with HTTP 200 and the documented payload shapes.
* `POST /api/admin/login` returns a token; the dashboard, project list and activity
  log are reachable with it.
* `PUT /api/admin/projects/{id}` with `{"status": "draft"}` flips the state, and the
  project disappears from the public API; switching it back to `published` restores it.

### 6.2 Browser checks (headless Chromium)

A scripted browser pass visited every public route and recorded the title, the `h1`,
the meta description, the canonical URL and the robots tag of each page:

| Route | Result |
|---|---|
| `/`, `/about`, `/skills`, `/projects`, `/projects/moraa-mis`, `/experience`, `/education`, `/certificates`, `/cv`, `/contact`, `/privacy` | HTTP 200, exactly one `h1`, one meta description, one canonical URL, no console errors, no failed requests |
| `/nope` (unknown route) | 404 page rendered, `noindex, nofollow` set, no canonical URL |
| `/admin/login` → sign-in with the local administrator | Token stored, redirect to `/admin`, dashboard shows 9 counters, recent messages and the activity log |
| `/admin`, `/admin/projects`, `/admin/skills`, `/admin/experiences`, `/admin/education`, `/admin/certificates`, `/admin/messages`, `/admin/settings` | Every screen renders its data (lists, forms, counters) with no console errors |
| Contact form | Submitted with valid input → message stored, success notice shown; the message appears in the dashboard and in the inbox |
| Home slideshow | One carousel region with three slides and three dot indicators; "Next slide" advances to the following slide; the caption and title are exposed to assistive technology |
| Project quick view | Clicking a card on `/` or `/projects` opens a dialog with the project title, category, role, technologies, problem and results, a "View full case study" link; `Escape` closes it and returns focus |
| Profile photo | Uploading a JPEG/PNG/WEBP in **Settings → Profile photo** stores it under `profile/`, makes it the active photo and shows it in the hero, the About page, the navigation and the footer |
| CV download buttons | No "Download CV" control remains in the navigation, the hero or the footer; the `/cv` page still offers the file |

Metadata audit result: 12 routes, 12 distinct meta descriptions, no duplicated tags.

These checks are run ad hoc from a script (not committed as a test suite) and were
used to find and fix three defects: a duplicated meta description, a duplicated page
title on the home page and an HTTP 422 on the featured-project filter.

## 7. Limitations

* No committed browser automation. The browser pass described above is scripted ad
  hoc, not part of the repository, so it is not repeated on every change.
* No load or performance testing; the LCP/INP/CLS targets in the specification are
  still to be measured on a deployed domain.
* No dedicated unit tests for isolated helpers (slug generation is covered indirectly
  through the CRUD tests).
* The e-mail notification path of the contact form is not asserted end-to-end, because
  it depends on the configured mail transport; the test asserts the stored message and
  the `mail_sent` flag instead.
