# 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: tests/ui/auth.spec.ts >> sign-in and premium sections >> /blocks/ is open to an entitled visitor and closed to others
- Location: tests/ui/auth.spec.ts:80:9

# Error details

```
Error: /blocks/ sends this visitor to sign-in or the premium notice

expect(received).toBe(expected) // Object.is equality

Expected: true
Received: false
```

# Test source

```ts
  1   | import { test, expect } from '@playwright/test';
  2   | import { premiumGated } from './helpers';
  3   | 
  4   | // Sign-in and premium sections (electinfo/sites#270). /auth/whoami reports
  5   | // whether Authentik is on for the host under test:
  6   | //   - CI test pods: enabled "false" (no Traefik in front), so nothing is gated
  7   | //     and the homepage keeps its premium cards greyed out;
  8   | //   - preview.elect.info front door: enabled "true", signed in as the
  9   | //     preview-e2e service account (preview-e2e@e2e.elect.news, ops#703): a plain
  10  | //     registered visitor, not elevated, so the premium sections are closed to it;
  11  | //   - us.elect.info front door, anonymous (the prod e2e run): /auth/ itself is
  12  | //     forward-auth gated, so whoami answers the outpost's redirect to sign-in,
  13  | //     which identity-bar.js reads as Authentik on and signed out.
  14  | const PREMIUM = [
  15  |   { path: '/voters/', label: 'Voters' },
  16  |   { path: '/individuals/', label: 'Individual Donors' },
  17  |   { path: '/blocks/', label: 'Census Blocks' },
  18  | ];
  19  | 
  20  | interface WhoAmI {
  21  |   enabled: string;
  22  |   email: string;
  23  |   elevated: boolean;
  24  |   entitlements: { voters: boolean; individuals: boolean; blocks: boolean; curator: boolean; preview: boolean };
  25  | }
  26  | 
  27  | const SIGNED_OUT: WhoAmI = {
  28  |   enabled: 'true',
  29  |   email: '',
  30  |   elevated: false,
  31  |   entitlements: { voters: false, individuals: false, blocks: false, curator: false, preview: false },
  32  | };
  33  | 
  34  | async function whoami(request: import('@playwright/test').APIRequestContext, headers: Record<string, string> = {}): Promise<WhoAmI> {
  35  |   const res = await request.get('/auth/whoami', { headers, maxRedirects: 0 });
  36  |   if (res.status() === 302) {
  37  |     expect(res.headers()['location'], 'a redirected /auth/whoami goes to sign-in').toMatch(/\/application\/o\/authorize\/|outpost\.goauthentik\.io/);
  38  |     return SIGNED_OUT;
  39  |   }
  40  |   expect(res.status(), '/auth/whoami answers 200').toBe(200);
  41  |   expect(res.headers()['cache-control']).toContain('no-store');
  42  |   return (await res.json()) as WhoAmI;
  43  | }
  44  | 
  45  | test.describe('sign-in and premium sections', () => {
  46  |   test('/auth/whoami returns well-formed JSON', async ({ request }) => {
  47  |     const me = await whoami(request);
  48  |     expect(['true', 'false']).toContain(me.enabled);
  49  |     expect(typeof me.email).toBe('string');
  50  |     expect(typeof me.elevated).toBe('boolean');
  51  |     // Per-section entitlements (sites#305), always all four booleans.
  52  |     expect(Object.keys(me.entitlements).sort()).toEqual(['blocks', 'curator', 'individuals', 'preview', 'voters']);
  53  |     for (const v of Object.values(me.entitlements)) expect(typeof v).toBe('boolean');
  54  |     expect(me.elevated).toBe(me.entitlements.voters || me.entitlements.individuals || me.entitlements.blocks);
  55  |     if (me.enabled === 'false') {
  56  |       expect(me.email).toBe('');
  57  |       expect(me.elevated).toBe(false);
  58  |       expect(Object.values(me.entitlements).some(Boolean)).toBe(false);
  59  |     }
  60  |   });
  61  | 
  62  |   test('a forged identity header grants nothing where Authentik is off', async ({ request }) => {
  63  |     const me = await whoami(request, { 'X-authentik-email': 'someone@elect.info' });
  64  |     test.skip(me.enabled === 'true', 'host has Authentik: Traefik sets the header');
  65  |     expect(me.email).toBe('');
  66  |     expect(me.elevated).toBe(false);
  67  |   });
  68  | 
  69  |   for (const group of ['elect-info-premium', 'ei-voter-viewers', 'ei-donor-viewers', 'ei-block-viewers', 'elect-news-curators', 'ei-preview-viewers']) {
  70  |     test(`a forged ${group} group header grants nothing where Authentik is off`, async ({ request }) => {
  71  |       const me = await whoami(request, { 'X-authentik-email': 'someone@example.com', 'X-authentik-groups': group });
  72  |       test.skip(me.enabled === 'true', 'host has Authentik: Traefik sets the headers');
  73  |       expect(me.email).toBe('');
  74  |       expect(me.elevated).toBe(false);
  75  |       expect(Object.values(me.entitlements).some(Boolean)).toBe(false);
  76  |     });
  77  |   }
  78  | 
  79  |   for (const { path } of PREMIUM) {
  80  |     test(`${path} is open to an entitled visitor and closed to others`, async ({ page, request }) => {
  81  |       const me = await whoami(request);
  82  |       const area = path.replace(/\//g, '') as 'voters' | 'individuals' | 'blocks';
  83  |       if (me.enabled === 'true' && !me.entitlements[area]) {
> 84  |         expect(await premiumGated(request, path), `${path} sends this visitor to sign-in or the premium notice`).toBe(true);
      |                                                                                                                  ^ Error: /blocks/ sends this visitor to sign-in or the premium notice
  85  |         return;
  86  |       }
  87  |       const response = await page.goto(path);
  88  |       expect(response?.status()).toBe(200);
  89  |     });
  90  |   }
  91  | 
  92  |   // The placeholder became the voter search page (sites#277, tests/voters.spec.ts).
  93  |   test('/voters/ is the voter search page', async ({ page, request }) => {
  94  |     const me = await whoami(request);
  95  |     // A signed-in visitor without the voters entitlement (the front door as preview-e2e, ops#703) gets the premium notice.
  96  |     test.skip(me.enabled === 'true' && !me.entitlements.voters, 'no voters access (signed out, or signed in without it)');
  97  |     await page.goto('/voters/');
  98  |     await expect(page.locator('h1')).toHaveText('Voters');
  99  |     await expect(page.locator('script[src="/js/voter-index.js"]')).toHaveCount(1);
  100 |     await expect(page.locator('.coming-soon')).toHaveCount(0);
  101 |   });
  102 | 
  103 |   test('the premium notice page is not directly reachable', async ({ page }) => {
  104 |     const response = await page.goto('/premium-required.html');
  105 |     expect(response?.status()).toBe(404);
  106 |   });
  107 | 
  108 |   test('homepage premium cards follow the account', async ({ page, request }) => {
  109 |     const me = await whoami(request);
  110 |     await page.goto('/');
  111 |     for (const { path, label } of PREMIUM) {
  112 |       const card = page.locator(`[data-premium-href="${path}"]`);
  113 |       await expect(card).toHaveCount(1);
  114 |       await expect(card).toContainText(label);
  115 |       const area = path.replace(/\//g, '') as 'voters' | 'individuals' | 'blocks';
  116 |       if (me.enabled === 'true' && me.entitlements[area]) {
  117 |         await expect(page.locator(`a[data-premium-href="${path}"]`)).toHaveAttribute('href', path);
  118 |         await expect(card).not.toHaveClass(/entity-card--disabled/);
  119 |       } else {
  120 |         await expect(card).toHaveClass(/entity-card--disabled/);
  121 |         await expect(page.locator(`a[data-premium-href="${path}"]`)).toHaveCount(0);
  122 |       }
  123 |     }
  124 |   });
  125 | 
  126 |   test('account slot shows the signed-in email where Authentik is on', async ({ page, request }) => {
  127 |     const me = await whoami(request);
  128 |     await page.goto('/');
  129 |     const slot = page.locator('#auth-slot');
  130 |     if (me.enabled !== 'true') {
  131 |       await expect(slot).toBeEmpty();
  132 |       return;
  133 |     }
  134 |     if (me.email) {
  135 |       await expect(slot.locator('.auth-email')).toHaveText(me.email);
  136 |       await expect(slot.locator('a.auth-link', { hasText: 'Log out' })).toHaveAttribute('href', '/outpost.goauthentik.io/sign_out');
  137 |       await expect(slot.locator('.ei-chip').first()).toHaveText('Registered');
  138 |     } else {
  139 |       await expect(slot.locator('a.auth-link')).toHaveText('Log in');
  140 |     }
  141 |   });
  142 | });
  143 | 
  144 | // Sign-in panel (electinfo/sites#303): Log In opens the site's own dialog;
  145 | // "Continue with Google" is /signin/google (njs/signin.js), "Continue with
  146 | // email" the outpost's /start.
  147 | test.describe('sign-in panel', () => {
  148 |   test('/signin/google redirects to the Google flow, or falls back to the sign-in page', async ({ request }) => {
  149 |     const res = await request.get('/signin/google?rd=%2Fvoters%2F', { maxRedirects: 0 });
  150 |     expect(res.status()).toBe(302);
  151 |     expect(res.headers()['cache-control']).toContain('no-store');
  152 |     const location = res.headers()['location'] || '';
  153 |     // With an outpost for this host: Authentik's Google-only flow, the
  154 |     // outpost's authorize URL as a relative next. Anywhere else (CI pods):
  155 |     // the outpost's own /start, i.e. the normal sign-in page.
  156 |     if (location.startsWith('https://auth.elect.info/')) {
  157 |       const url = new URL(location);
  158 |       expect(url.pathname).toBe('/if/flow/elect-info-google/');
  159 |       expect(url.searchParams.get('next') || '').toMatch(/^\/application\/o\/authorize\/\?/);
  160 |     } else {
  161 |       expect(location).toMatch(/^https:\/\/[^/]+\/outpost\.goauthentik\.io\/start\?rd=https%3A%2F%2F[^%]+%2Fvoters%2F$/);
  162 |     }
  163 |   });
  164 | 
  165 |   test('/signin/register goes to the sign-up flow, or falls back to the sign-in page', async ({ request }) => {
  166 |     const res = await request.get('/signin/register?rd=%2F', { maxRedirects: 0 });
  167 |     expect(res.status()).toBe(302);
  168 |     const location = res.headers()['location'] || '';
  169 |     if (location.startsWith('https://auth.elect.info/')) {
  170 |       expect(new URL(location).pathname).toBe('/if/flow/elect-info-enrollment/');
  171 |     } else {
  172 |       expect(location).toMatch(/^https:\/\/[^/]+\/outpost\.goauthentik\.io\/start\?rd=/);
  173 |     }
  174 |   });
  175 | 
  176 |   test('the shared identity bar is served and revalidated', async ({ request }) => {
  177 |     for (const asset of ['/identity/identity-bar.js', '/identity/identity-bar.css']) {
  178 |       const res = await request.get(asset);
  179 |       expect(res.status(), asset).toBe(200);
  180 |       expect(res.headers()['cache-control'] || '').toContain('no-cache');
  181 |     }
  182 |   });
  183 | 
  184 |   test('/signin/google only answers GET, and the outpost hop is internal', async ({ request }) => {
```