# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: auth/magic-link.spec.ts >> Magic Link Authentication >> request magic link, receive email, verify token, get session
- Location: src/tests/auth/magic-link.spec.ts:12:7

# Error details

```
Error: Mailpit API error: 404 Not Found
```

# Test source

```ts
  1   | interface MailpitMessage {
  2   |   ID: string;
  3   |   From: { Address: string; Name: string };
  4   |   To: Array<{ Address: string; Name: string }>;
  5   |   Subject: string;
  6   |   Created: string;
  7   |   Snippet: string;
  8   | }
  9   | 
  10  | interface MailpitSearchResult {
  11  |   total: number;
  12  |   messages: MailpitMessage[];
  13  | }
  14  | 
  15  | interface MailpitMessageDetail {
  16  |   ID: string;
  17  |   HTML: string;
  18  |   Text: string;
  19  |   Subject: string;
  20  | }
  21  | 
  22  | /**
  23  |  * Client for the Mailpit API to intercept emails during E2E tests.
  24  |  * Used primarily for magic-link token extraction.
  25  |  */
  26  | export class MailpitClient {
  27  |   private baseUrl: string;
  28  |   private authHeader: string | undefined;
  29  | 
  30  |   constructor() {
  31  |     this.baseUrl =
  32  |       process.env.E2E_MAILPIT_URL ?? 'http://localhost:8025';
  33  |     const user = process.env.E2E_MAILPIT_USER;
  34  |     const pass = process.env.E2E_MAILPIT_PASSWORD;
  35  |     if (user && pass) {
  36  |       this.authHeader = `Basic ${btoa(`${user}:${pass}`)}`;
  37  |     }
  38  |   }
  39  | 
  40  |   private async request<T>(
  41  |     path: string,
  42  |     options?: RequestInit
  43  |   ): Promise<T> {
  44  |     const headers: Record<string, string> = {
  45  |       ...(options?.headers as Record<string, string>),
  46  |     };
  47  |     if (this.authHeader) {
  48  |       headers['Authorization'] = this.authHeader;
  49  |     }
  50  | 
  51  |     const response = await fetch(`${this.baseUrl}${path}`, {
  52  |       ...options,
  53  |       headers,
  54  |     });
  55  | 
  56  |     if (!response.ok) {
> 57  |       throw new Error(
      |             ^ Error: Mailpit API error: 404 Not Found
  58  |         `Mailpit API error: ${response.status} ${response.statusText}`
  59  |       );
  60  |     }
  61  | 
  62  |     if (response.headers.get('content-type')?.includes('application/json')) {
  63  |       return response.json() as T;
  64  |     }
  65  |     return undefined as T;
  66  |   }
  67  | 
  68  |   /**
  69  |    * Wait for an email to arrive for a specific recipient.
  70  |    * Polls the Mailpit API until a matching email is found or timeout is reached.
  71  |    */
  72  |   async waitForEmail(
  73  |     to: string,
  74  |     timeout = 15_000,
  75  |     pollInterval = 500
  76  |   ): Promise<MailpitMessage> {
  77  |     const start = Date.now();
  78  | 
  79  |     while (Date.now() - start < timeout) {
  80  |       const result = await this.request<MailpitSearchResult>(
  81  |         `/api/v1/search?query=${encodeURIComponent(`to:${to}`)}`
  82  |       );
  83  | 
  84  |       const firstMessage = result.messages?.[0];
  85  |       if (firstMessage) {
  86  |         return firstMessage;
  87  |       }
  88  | 
  89  |       await new Promise((resolve) => setTimeout(resolve, pollInterval));
  90  |     }
  91  | 
  92  |     throw new Error(
  93  |       `No email found for ${to} within ${timeout}ms`
  94  |     );
  95  |   }
  96  | 
  97  |   /**
  98  |    * Get the full email body (HTML) by message ID.
  99  |    */
  100 |   async getEmailBody(id: string): Promise<MailpitMessageDetail> {
  101 |     return this.request<MailpitMessageDetail>(`/api/v1/message/${id}`);
  102 |   }
  103 | 
  104 |   /**
  105 |    * Delete all emails in Mailpit. Use in beforeEach for clean state.
  106 |    */
  107 |   async deleteAllEmails(): Promise<void> {
  108 |     await this.request('/api/v1/messages', { method: 'DELETE' });
  109 |   }
  110 | 
  111 |   /**
  112 |    * Extract the magic link token from an HTML email body.
  113 |    * Looks for the verify URL pattern used by the magic-link endpoint.
  114 |    */
  115 |   extractMagicLinkUrl(htmlBody: string): string {
  116 |     // Match the verify URL: /students/verify?token=...&customer=...
  117 |     const match = /href="([^"]*\/students\/verify\?[^"]*)"/.exec(htmlBody);
  118 |     if (!match?.[1]) {
  119 |       throw new Error(
  120 |         'Could not find magic link URL in email body'
  121 |       );
  122 |     }
  123 |     return match[1];
  124 |   }
  125 | 
  126 | }
  127 | 
```