tests/bundle-create.test.ts
Rama main · réplica de lectura del repositorio original.
import type { Database } from '../src/types/database.types'
import { randomUUID } from 'node:crypto'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { BASE_URL, getSupabaseClient, headers, resetAndSeedAppData, resetAppData, TEST_EMAIL, USER_ID } from './test-utils.ts'
const id = randomUUID()
const APPNAME = `com.bundle.create.${id}`
let testOrgId: string
type AppInsert = Database['public']['Tables']['apps']['Insert']
beforeAll(async () => {
await resetAndSeedAppData(APPNAME)
// Create test organization
const { data: orgData, error: orgError } = await getSupabaseClient().from('orgs').insert({
id: randomUUID(),
name: `Test Bundle Create Org ${id}`,
management_email: TEST_EMAIL,
created_by: USER_ID,
}).select().single()
if (orgError)
throw orgError
testOrgId = orgData.id
// Create test app
const appInsert: AppInsert = {
app_id: APPNAME,
name: `Test Bundle Create App`,
icon_url: 'https://example.com/icon.png',
owner_org: testOrgId,
}
await getSupabaseClient().from('apps').insert(appInsert)
})
afterAll(async () => {
await resetAppData(APPNAME)
await getSupabaseClient().from('orgs').delete().eq('id', testOrgId)
})
describe('[POST] /bundle - Create Bundle with External URL', () => {
it('should create bundle with valid GitHub ZIP URL', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
app_id: APPNAME,
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
version: '1.0.0-github-zip',
external_url: 'https://github.com/Cap-go/capgo/archive/refs/tags/v12.12.32.zip', // Valid ZIP file from GitHub
}),
})
// This should succeed as it's a valid HTTPS ZIP file
expect(response.status).toBe(200)
const data = await response.json() as { status: string, bundle: any }
expect(data.status).toBe('success')
expect(data.bundle).toBeTruthy()
expect(data.bundle.name).toBe('1.0.0-github-zip')
expect(data.bundle.external_url).toBe('https://github.com/Cap-go/capgo/archive/refs/tags/v12.12.32.zip')
expect(data.bundle.storage_provider).toBe('external')
})
// it.only('should reject non-ZIP files from GitHub', async () => {
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-tar-gz',
// external_url: 'https://github.com/Cap-go/capgo/archive/refs/tags/v12.12.32.tar.gz', // tar.gz instead of zip
// }),
// })
// // This should fail because .tar.gz is not a ZIP file
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// console.log(data)
// expect(data.error).toBe('url_not_zip')
// })
it('should return 400 when app_id is missing', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
version: '1.0.0',
external_url: 'https://example.com/test.zip',
// Missing app_id
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('missing_app_id')
})
it('should return 400 when version is missing', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: APPNAME,
external_url: 'https://example.com/test.zip',
// Missing version
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('missing_version')
})
it('should return 400 when external_url is missing', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: APPNAME,
version: '1.0.0',
// Missing external_url
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('missing_external_url')
})
it('should return 400 when URL is not HTTPS', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: APPNAME,
version: '1.0.0-http',
external_url: 'http://example.com/test.zip', // HTTP instead of HTTPS
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('invalid_protocol')
})
it('should return 400 when user cannot access the app', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: 'nonexistent.app',
version: '1.0.0',
external_url: 'https://example.com/test.zip',
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('cannot_create_bundle')
})
it('should return 400 when version already exists', async () => {
// First create a version
const capgo = getSupabaseClient()
await capgo
.from('app_versions')
.insert({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: APPNAME,
name: '1.0.0-duplicate',
owner_org: testOrgId,
storage_provider: 'r2',
})
// Try to create the same version again
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: JSON.stringify({
checksum: 'a1b2c3d4e5f6789abcdef123456789abcdef123456789abcdef123456789abcd',
app_id: APPNAME,
version: '1.0.0-duplicate',
external_url: 'https://example.com/test.zip',
}),
})
expect(response.status).toBe(400)
const data = await response.json() as { error: string }
expect(data.error).toBe('version_already_exists')
})
// it('should return 400 when URL does not point to ZIP file', async () => {
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-not-zip',
// external_url: 'https://httpbin.org/json', // Returns JSON, not ZIP
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// expect(['url_not_zip', 'url_fetch_error'].includes(data.error)).toBe(true)
// })
// it('should return 400 when URL points to HTML page', async () => {
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-html',
// external_url: 'https://httpbin.org/html', // Returns HTML
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// expect(['url_not_file', 'url_fetch_error'].includes(data.error)).toBe(true)
// })
it('should handle invalid JSON body', async () => {
const response = await fetch(`${BASE_URL}/bundle`, {
method: 'POST',
headers,
body: 'invalid json',
})
expect(response.status).toBeGreaterThanOrEqual(400)
})
// it('should validate URL accessibility', async () => {
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-404',
// external_url: 'https://httpbin.org/status/404', // Returns 404
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// expect(['url_not_accessible', 'url_fetch_error'].includes(data.error)).toBe(true)
// })
// it('should handle network errors gracefully', async () => {
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-network-error',
// external_url: 'https://nonexistent-domain-12345.com/test.zip',
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// expect(['url_fetch_error', 'url_not_accessible'].includes(data.error)).toBe(true)
// })
// it('should follow redirects properly', async () => {
// // Using httpbin.org/redirect-to which redirects to the target URL
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-redirect-test',
// external_url: 'https://httpbin.org/redirect-to?url=https://httpbin.org/json&status_code=302',
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// // Should fail with url_not_zip (because it redirects to JSON), not url_not_accessible
// // This proves the redirect was followed successfully
// expect(['url_not_zip', 'url_fetch_error'].includes(data.error)).toBe(true)
// })
// it('should handle multiple redirects (up to 5)', async () => {
// // Test with multiple redirects
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-multi-redirect',
// external_url: 'https://httpbin.org/redirect/3', // 3 redirects chain
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// // Should follow the redirects and then fail on final content validation
// expect(['url_not_zip', 'url_fetch_error', 'url_not_file'].includes(data.error)).toBe(true)
// })
// it('should reject too many redirects', async () => {
// // Test with more than 5 redirects
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-too-many-redirects',
// external_url: 'https://httpbin.org/redirect/10', // 10 redirects (should exceed limit)
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// // Should fail with too many redirects error
// expect(['url_fetch_error'].includes(data.error)).toBe(true)
// })
// it('should validate Content-Disposition header for filename', async () => {
// // This test demonstrates that Content-Disposition header checking is implemented
// // Most real-world URLs with proper Content-Disposition headers would be inaccessible in tests
// const response = await fetch(`${BASE_URL}/bundle`, {
// method: 'POST',
// headers,
// body: JSON.stringify({
// app_id: APPNAME,
// version: '1.0.0-content-disposition',
// external_url: 'https://httpbin.org/response-headers?Content-Disposition=attachment%3B%20filename%3D%22bundle.zip%22',
// }),
// })
// expect(response.status).toBe(400)
// const data = await response.json() as { error: string }
// // Should check Content-Disposition but still fail on content-type or accessibility
// expect(['url_not_zip', 'url_fetch_error', 'url_not_file'].includes(data.error)).toBe(true)
// })
})