Skip to content

Commit 7cb07b5

Browse files
authored
fix: add regression tests for error codes (#990)
1 parent 2fd71cc commit 7cb07b5

3 files changed

Lines changed: 177 additions & 14 deletions

File tree

src/test/iceberg.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,34 @@ describe('Iceberg Catalog', () => {
210210
})
211211
})
212212

213+
it('returns InvalidParameter for invalid namespace names', async () => {
214+
const bucketName = t.random.name('ice-bucket')
215+
await t.storage.createIcebergBucket({
216+
name: bucketName,
217+
})
218+
219+
const response = await app.inject({
220+
method: 'POST',
221+
url: `/iceberg/v1/${bucketName}/namespaces`,
222+
headers: {
223+
'Content-Type': 'application/json',
224+
Authorization: `Bearer ${await serviceKeyAsync}`,
225+
},
226+
payload: {
227+
namespace: 'awsnamespace',
228+
},
229+
})
230+
231+
expect(response.statusCode).toBe(400)
232+
expect(await response.json()).toEqual({
233+
error: {
234+
code: 400,
235+
message: 'Resource name must not start with the reserved prefix "aws"',
236+
type: 'InvalidParameter',
237+
},
238+
})
239+
})
240+
213241
it('can list namespaces', async () => {
214242
const bucketName = t.random.name('ice-bucket')
215243

src/test/s3-error-code.test.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { randomUUID } from 'node:crypto'
2+
import { mkdtemp, rm } from 'node:fs/promises'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { ListPartsCommand, S3Client } from '@aws-sdk/client-s3'
6+
import { KnexMetastore } from '@storage/protocols/iceberg/knex'
7+
import { FastifyInstance } from 'fastify'
8+
import { getConfig } from '../config'
9+
import { useStorage } from './utils/storage'
10+
11+
const {
12+
icebergBucketDetectionSuffix,
13+
s3ProtocolAccessKeyId,
14+
s3ProtocolAccessKeySecret,
15+
storageS3Region,
16+
} = getConfig()
17+
18+
async function createFileBackedApp(fileBackendPath: string) {
19+
jest.resetModules()
20+
21+
const configModule = await import('../config')
22+
23+
configModule.getConfig({ reload: true })
24+
configModule.mergeConfig({
25+
storageBackendType: 'file',
26+
storageFilePath: fileBackendPath,
27+
})
28+
29+
return (await import('../app')).default()
30+
}
31+
32+
describe('S3 protocol error code', () => {
33+
const t = useStorage()
34+
35+
let testApp: FastifyInstance
36+
let client: S3Client
37+
let icebergMetastore: KnexMetastore
38+
let fileBackendPath: string
39+
40+
beforeAll(async () => {
41+
fileBackendPath = await mkdtemp(join(tmpdir(), 'storage-file-backend-'))
42+
testApp = await createFileBackedApp(fileBackendPath)
43+
icebergMetastore = new KnexMetastore(t.database.connection.pool.acquire(), {
44+
multiTenant: false,
45+
schema: 'storage',
46+
})
47+
48+
const listener = await testApp.listen()
49+
50+
client = new S3Client({
51+
endpoint: `${listener.replace('[::1]', 'localhost')}/s3`,
52+
forcePathStyle: true,
53+
region: storageS3Region,
54+
credentials: {
55+
accessKeyId: s3ProtocolAccessKeyId!,
56+
secretAccessKey: s3ProtocolAccessKeySecret!,
57+
},
58+
})
59+
})
60+
61+
afterAll(async () => {
62+
client?.destroy()
63+
await testApp?.close()
64+
65+
jest.resetModules()
66+
await rm(fileBackendPath, { recursive: true, force: true })
67+
})
68+
69+
it('returns NotSupported for iceberg list-parts on non-S3 backends', async () => {
70+
const nonce = randomUUID().replaceAll('-', '')
71+
const analyticsBucket = await t.storage.createIcebergBucket({
72+
name: `ice_bucket_${nonce}`,
73+
})
74+
const namespaceName = `namespace${nonce}`
75+
const tableName = `table${nonce}`
76+
const internalBucketName = `internal-${nonce}${icebergBucketDetectionSuffix}`
77+
78+
const namespace = await icebergMetastore.createNamespace({
79+
name: namespaceName,
80+
bucketName: analyticsBucket.name,
81+
bucketId: analyticsBucket.id,
82+
tenantId: '',
83+
metadata: {},
84+
})
85+
86+
await icebergMetastore.createTable({
87+
name: tableName,
88+
bucketName: analyticsBucket.name,
89+
bucketId: analyticsBucket.id,
90+
location: `s3://${internalBucketName}`,
91+
namespaceId: namespace.id,
92+
})
93+
94+
await expect(
95+
client.send(
96+
new ListPartsCommand({
97+
Bucket: internalBucketName,
98+
Key: `${namespaceName}/${tableName}/data.parquet`,
99+
UploadId: 'upload-id',
100+
})
101+
)
102+
).rejects.toMatchObject({
103+
$metadata: expect.objectContaining({ httpStatusCode: 409 }),
104+
name: 'NotSupported',
105+
})
106+
})
107+
})

src/test/vectors.test.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -283,24 +283,52 @@ describe('Vectors API', () => {
283283
})
284284

285285
it('should handle vector service not configured', async () => {
286-
// Mock app without s3Vector service
287-
const appWithoutVector = app()
288286
mergeConfig({ vectorEnabled: false })
289287

290-
const response = await appWithoutVector.inject({
291-
method: 'POST',
292-
url: '/vector/CreateIndex',
293-
headers: {
294-
authorization: `Bearer ${serviceToken}`,
295-
},
296-
payload: validCreateIndexRequest,
297-
})
288+
const appWithoutVector = app()
298289

299-
expect(response.statusCode).toBe(404)
300-
const body = JSON.parse(response.body)
301-
expect(body.error).toBe('Not Found')
290+
try {
291+
const response = await appWithoutVector.inject({
292+
method: 'POST',
293+
url: '/vector/CreateIndex',
294+
headers: {
295+
authorization: `Bearer ${serviceToken}`,
296+
},
297+
payload: validCreateIndexRequest,
298+
})
299+
300+
expect(response.statusCode).toBe(404)
301+
const body = JSON.parse(response.body)
302+
expect(body.error).toBe('Not Found')
303+
} finally {
304+
await appWithoutVector.close()
305+
}
306+
})
302307

303-
await appWithoutVector.close()
308+
it('should return FeatureNotEnabled when the vector backend is not configured', async () => {
309+
mergeConfig({ vectorEnabled: true, vectorS3Buckets: [] })
310+
311+
const appWithoutVector = app()
312+
313+
try {
314+
const response = await appWithoutVector.inject({
315+
method: 'POST',
316+
url: '/vector/CreateIndex',
317+
headers: {
318+
authorization: `Bearer ${serviceToken}`,
319+
},
320+
payload: validCreateIndexRequest,
321+
})
322+
323+
expect(response.statusCode).toBe(409)
324+
expect(JSON.parse(response.body)).toMatchObject({
325+
statusCode: '409',
326+
code: 'FeatureNotEnabled',
327+
error: 'FeatureNotEnabled',
328+
})
329+
} finally {
330+
await appWithoutVector.close()
331+
}
304332
})
305333

306334
it('should handle S3Vector service errors', async () => {

0 commit comments

Comments
 (0)