|
1 | 1 | import { test, expect } from '@playwright/test'; |
| 2 | +import { getLongTermToken, revokeLongTermToken } from '../../helpers/auth.js'; |
2 | 3 |
|
3 | 4 | /** |
4 | 5 | * E2E tests for the Datasets API. |
@@ -131,13 +132,88 @@ test.describe.serial('Datasets API — recreate and query lifecycle', () => { |
131 | 132 | }); |
132 | 133 | }); |
133 | 134 |
|
134 | | -test.describe('Datasets API — upload', () => { |
| 135 | +test.describe.serial('Datasets API — upload', () => { |
135 | 136 | const baseURL = process.env.TEST_BASE_URL || 'http://localhost:18888'; |
| 137 | + let longTermToken = null; |
136 | 138 |
|
137 | | - test('POST /api/datasets/upload — multipart CSV upload', async ({ request }) => { |
138 | | - // Upload requires isLongTermToken (API key auth), which the test user may not have. |
139 | | - // We still validate the endpoint doesn't crash. |
140 | | - test.skip(true, 'SKIP: Dataset upload requires long-term API token (isLongTermToken) — not available in standard test auth'); |
| 139 | + test.beforeAll(async ({ request }) => { |
| 140 | + longTermToken = await getLongTermToken(request); |
| 141 | + }); |
| 142 | + |
| 143 | + test.afterAll(async ({ request }) => { |
| 144 | + if (longTermToken) await revokeLongTermToken(request, longTermToken); |
| 145 | + }); |
| 146 | + |
| 147 | + // --- Crash regression tests (busboy error handling) --- |
| 148 | + |
| 149 | + test('POST /api/datasets/upload — invalid Content-Type does not crash server (returns 400, not 500)', async ({ request }) => { |
| 150 | + // Sending a non-multipart Content-Type previously caused busboy to throw |
| 151 | + // synchronously with no try/catch, crashing the process. After the fix it |
| 152 | + // must return 400 (when a valid token is present) or a non-5xx auth failure. |
| 153 | + const response = await request.post(`${baseURL}/api/datasets/upload`, { |
| 154 | + headers: { |
| 155 | + ...(longTermToken ? { Authorization: `Bearer ${longTermToken}` } : {}), |
| 156 | + 'Content-Type': 'text/plain', |
| 157 | + }, |
| 158 | + data: 'not a multipart body', |
| 159 | + }); |
| 160 | + expect(response.status()).not.toBe(500); |
| 161 | + if (longTermToken) { |
| 162 | + expect(response.status()).toBe(400); |
| 163 | + const body = await response.json(); |
| 164 | + expect(body.status).toBe('failure'); |
| 165 | + } |
| 166 | + }); |
| 167 | + |
| 168 | + test('POST /api/datasets/upload — missing Content-Type does not crash server', async ({ request }) => { |
| 169 | + // Busboy throws 'Missing Content-Type' synchronously when the header is absent. |
| 170 | + const response = await request.post(`${baseURL}/api/datasets/upload`, { |
| 171 | + headers: { |
| 172 | + ...(longTermToken ? { Authorization: `Bearer ${longTermToken}` } : {}), |
| 173 | + 'Content-Type': '', |
| 174 | + }, |
| 175 | + data: '', |
| 176 | + }); |
| 177 | + expect(response.status()).not.toBe(500); |
| 178 | + }); |
| 179 | + |
| 180 | + // --- Auth gate --- |
| 181 | + |
| 182 | + test('POST /api/datasets/upload — without token returns failure, not 5xx', async ({ request }) => { |
| 183 | + const response = await request.post(`${baseURL}/api/datasets/upload`, { |
| 184 | + multipart: { |
| 185 | + name: `upload_noauth_${Date.now()}`, |
| 186 | + header: JSON.stringify(['name', 'value']), |
| 187 | + upsert: 'false', |
| 188 | + file: { name: 'data.csv', mimeType: 'text/csv', buffer: Buffer.from('name,value\nfoo,1\n') }, |
| 189 | + }, |
| 190 | + }); |
| 191 | + expect(response.status()).not.toBe(500); |
| 192 | + const body = await response.json(); |
| 193 | + expect(body.status).toBe('failure'); |
| 194 | + }); |
| 195 | + |
| 196 | + // --- Happy-path upload (only when a long-term token was obtained) --- |
| 197 | + |
| 198 | + test('POST /api/datasets/upload — valid multipart CSV upload does not return 5xx', async ({ request }) => { |
| 199 | + if (!longTermToken) { |
| 200 | + test.skip(true, 'SKIP: Could not obtain a long-term token'); |
| 201 | + return; |
| 202 | + } |
| 203 | + |
| 204 | + const csvContent = ['name,score', 'alpha,10', 'beta,20', 'gamma,30'].join('\n') + '\n'; |
| 205 | + const response = await request.post(`${baseURL}/api/datasets/upload`, { |
| 206 | + headers: { Authorization: `Bearer ${longTermToken}` }, |
| 207 | + multipart: { |
| 208 | + name: `upload_e2e_${Date.now()}`, |
| 209 | + header: JSON.stringify(['name', 'score']), |
| 210 | + upsert: 'false', |
| 211 | + file: { name: 'data.csv', mimeType: 'text/csv', buffer: Buffer.from(csvContent) }, |
| 212 | + }, |
| 213 | + }); |
| 214 | + expect(response.status()).not.toBe(500); |
| 215 | + const body = await response.json(); |
| 216 | + expect(body).toHaveProperty('status'); |
141 | 217 | }); |
142 | 218 | }); |
143 | 219 |
|
|
0 commit comments