|
| 1 | +import { test, expect } from '@playwright/test'; |
| 2 | +import { postForm } from '@open-mercato/core/modules/core/__integration__/helpers/api'; |
| 3 | +import { DEFAULT_CREDENTIALS } from '@open-mercato/core/modules/core/__integration__/helpers/auth'; |
| 4 | + |
| 5 | +const BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; |
| 6 | + |
| 7 | +/** |
| 8 | + * TC-AUTH-017: API Token Refresh Flow |
| 9 | + * |
| 10 | + * Tests the header-based token refresh for mobile/API clients: |
| 11 | + * - Login with remember=true returns refreshToken in response |
| 12 | + * - POST /api/session/refresh accepts refreshToken in JSON body |
| 13 | + * - Returns new accessToken on success |
| 14 | + * - Returns proper errors for invalid/missing tokens |
| 15 | + */ |
| 16 | +test.describe('TC-AUTH-017: API Token Refresh Flow', () => { |
| 17 | + test('login with remember=true returns refreshToken in response', async ({ request }) => { |
| 18 | + const { email, password } = DEFAULT_CREDENTIALS.admin; |
| 19 | + |
| 20 | + const response = await postForm(request, '/api/auth/login', { |
| 21 | + email, |
| 22 | + password, |
| 23 | + remember: '1', |
| 24 | + }); |
| 25 | + |
| 26 | + expect(response.status()).toBe(200); |
| 27 | + const body = await response.json(); |
| 28 | + expect(body.ok).toBe(true); |
| 29 | + expect(body.token).toBeTruthy(); |
| 30 | + expect(body.refreshToken).toBeTruthy(); |
| 31 | + expect(typeof body.refreshToken).toBe('string'); |
| 32 | + expect(body.refreshToken.length).toBeGreaterThan(0); |
| 33 | + }); |
| 34 | + |
| 35 | + test('login without remember does not return refreshToken', async ({ request }) => { |
| 36 | + const { email, password } = DEFAULT_CREDENTIALS.admin; |
| 37 | + |
| 38 | + const response = await postForm(request, '/api/auth/login', { |
| 39 | + email, |
| 40 | + password, |
| 41 | + }); |
| 42 | + |
| 43 | + expect(response.status()).toBe(200); |
| 44 | + const body = await response.json(); |
| 45 | + expect(body.ok).toBe(true); |
| 46 | + expect(body.token).toBeTruthy(); |
| 47 | + expect(body.refreshToken).toBeUndefined(); |
| 48 | + }); |
| 49 | + |
| 50 | + test('POST /api/session/refresh returns new accessToken for valid refreshToken', async ({ request }) => { |
| 51 | + const { email, password } = DEFAULT_CREDENTIALS.admin; |
| 52 | + |
| 53 | + // First, login with remember=true to get a refresh token |
| 54 | + const loginResponse = await postForm(request, '/api/auth/login', { |
| 55 | + email, |
| 56 | + password, |
| 57 | + remember: '1', |
| 58 | + }); |
| 59 | + expect(loginResponse.status()).toBe(200); |
| 60 | + const loginBody = await loginResponse.json(); |
| 61 | + const refreshToken = loginBody.refreshToken; |
| 62 | + expect(refreshToken).toBeTruthy(); |
| 63 | + |
| 64 | + // Now use the refresh token to get a new access token |
| 65 | + const refreshResponse = await request.post(`${BASE_URL}/api/session/refresh`, { |
| 66 | + headers: { 'Content-Type': 'application/json' }, |
| 67 | + data: JSON.stringify({ refreshToken }), |
| 68 | + }); |
| 69 | + |
| 70 | + expect(refreshResponse.status()).toBe(200); |
| 71 | + const refreshBody = await refreshResponse.json(); |
| 72 | + expect(refreshBody.ok).toBe(true); |
| 73 | + expect(refreshBody.accessToken).toBeTruthy(); |
| 74 | + expect(typeof refreshBody.accessToken).toBe('string'); |
| 75 | + expect(refreshBody.expiresIn).toBe(60 * 60 * 8); // 8 hours in seconds |
| 76 | + }); |
| 77 | + |
| 78 | + test('POST /api/session/refresh returns 400 for missing refreshToken', async ({ request }) => { |
| 79 | + const refreshResponse = await request.post(`${BASE_URL}/api/session/refresh`, { |
| 80 | + headers: { 'Content-Type': 'application/json' }, |
| 81 | + data: JSON.stringify({}), |
| 82 | + }); |
| 83 | + |
| 84 | + expect(refreshResponse.status()).toBe(400); |
| 85 | + const body = await refreshResponse.json(); |
| 86 | + expect(body.ok).toBe(false); |
| 87 | + expect(body.error).toBeTruthy(); |
| 88 | + }); |
| 89 | + |
| 90 | + test('POST /api/session/refresh returns 401 for invalid refreshToken', async ({ request }) => { |
| 91 | + const refreshResponse = await request.post(`${BASE_URL}/api/session/refresh`, { |
| 92 | + headers: { 'Content-Type': 'application/json' }, |
| 93 | + data: JSON.stringify({ refreshToken: 'invalid-token-that-does-not-exist' }), |
| 94 | + }); |
| 95 | + |
| 96 | + expect(refreshResponse.status()).toBe(401); |
| 97 | + const body = await refreshResponse.json(); |
| 98 | + expect(body.ok).toBe(false); |
| 99 | + expect(body.error).toBeTruthy(); |
| 100 | + }); |
| 101 | + |
| 102 | + test('POST /api/session/refresh returns 400 for invalid JSON body', async ({ request }) => { |
| 103 | + const refreshResponse = await request.post(`${BASE_URL}/api/session/refresh`, { |
| 104 | + headers: { 'Content-Type': 'application/json' }, |
| 105 | + data: 'not-valid-json', |
| 106 | + }); |
| 107 | + |
| 108 | + expect(refreshResponse.status()).toBe(400); |
| 109 | + const body = await refreshResponse.json(); |
| 110 | + expect(body.ok).toBe(false); |
| 111 | + }); |
| 112 | + |
| 113 | + test('new accessToken from refresh can be used for authenticated requests', async ({ request }) => { |
| 114 | + const { email, password } = DEFAULT_CREDENTIALS.admin; |
| 115 | + |
| 116 | + // Login with remember=true |
| 117 | + const loginResponse = await postForm(request, '/api/auth/login', { |
| 118 | + email, |
| 119 | + password, |
| 120 | + remember: '1', |
| 121 | + }); |
| 122 | + const { refreshToken } = await loginResponse.json(); |
| 123 | + |
| 124 | + // Get new access token via refresh |
| 125 | + const refreshResponse = await request.post(`${BASE_URL}/api/session/refresh`, { |
| 126 | + headers: { 'Content-Type': 'application/json' }, |
| 127 | + data: JSON.stringify({ refreshToken }), |
| 128 | + }); |
| 129 | + const { accessToken } = await refreshResponse.json(); |
| 130 | + |
| 131 | + // Use the new access token for an authenticated API call |
| 132 | + const profileResponse = await request.get(`${BASE_URL}/api/auth/profile`, { |
| 133 | + headers: { Authorization: `Bearer ${accessToken}` }, |
| 134 | + }); |
| 135 | + |
| 136 | + expect(profileResponse.status()).toBe(200); |
| 137 | + const profile = await profileResponse.json(); |
| 138 | + expect(profile.email).toBe(email); |
| 139 | + }); |
| 140 | +}); |
0 commit comments