mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore: move api solution to storage and parallelize tests (#42565)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? - Move storage tests to run in parallel - Updated utils to use env <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Tests** * Improved test reliability by shifting storage bucket setup and cleanup from UI-based to API-backed operations. * Enhanced test isolation with streamlined prerequisite navigation steps. * **Chores** * Updated environment configuration to support dynamic API URL and service role key settings. * Refactored internal storage management utilities for improved maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
ba0f5d6e76
commit
c7f653f33f
6 files changed
+170
-49
No files matched your search
@@ -13,9 +13,12 @@ const toBoolean = (value?: string) => {
|
||||
return normalized === 'true'
|
||||
}
|
||||
|
||||
// Default service role key for local development
|
||||
const DEFAULT_SERVICE_ROLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU"
|
||||
|
||||
export const env = {
|
||||
STUDIO_URL: process.env.STUDIO_URL || 'http://localhost:8082',
|
||||
API_URL: process.env.API_URL || 'https://localhost:8080',
|
||||
API_URL: process.env.API_URL || 'http://127.0.0.1:54321',
|
||||
|
||||
IS_PLATFORM: toBoolean(process.env.IS_PLATFORM || 'false'),
|
||||
EMAIL: process.env.EMAIL,
|
||||
@@ -40,6 +43,9 @@ export const env = {
|
||||
|
||||
IS_APP_RUNNING_ON_LOCALHOST:
|
||||
process.env.STUDIO_URL?.includes('localhost') || process.env.STUDIO_URL?.includes('127.0.0.1'),
|
||||
|
||||
|
||||
SERVICE_ROLE_KEY: process.env.SERVICE_ROLE_KEY || DEFAULT_SERVICE_ROLE_KEY,
|
||||
}
|
||||
|
||||
export const STORAGE_STATE_PATH = path.join(import.meta.dirname, './playwright/.auth/user.json')
|
||||
@@ -6,21 +6,24 @@ import { waitForApiResponse } from '../utils/wait-for-response.js'
|
||||
import {
|
||||
createBucket,
|
||||
createFolder,
|
||||
deleteAllBuckets,
|
||||
deleteBucket,
|
||||
deleteItem,
|
||||
downloadFile,
|
||||
navigateToBucket,
|
||||
navigateToStorageFiles,
|
||||
renameItem,
|
||||
uploadFile,
|
||||
} from '../utils/storage-helpers.js'
|
||||
import { dismissToastsIfAny } from '../utils/dismiss-toast.js'
|
||||
import {
|
||||
createBucket as createBucketViaApi,
|
||||
deleteBucket as deleteBucketViaApi,
|
||||
} from '../utils/storage/index.js'
|
||||
|
||||
const bucketNamePrefix = 'pw_bucket'
|
||||
|
||||
test.describe.serial('Storage', () => {
|
||||
test.describe('Storage', () => {
|
||||
test.beforeEach(async ({ page, ref }) => {
|
||||
await deleteAllBuckets(page, ref)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
})
|
||||
|
||||
test('can navigate to storage page', async ({ page, ref }) => {
|
||||
@@ -36,6 +39,7 @@ test.describe.serial('Storage', () => {
|
||||
test('can create a private bucket', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_private`
|
||||
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
|
||||
// Verify it's marked as private (no "Public" badge should be visible)
|
||||
@@ -50,6 +54,7 @@ test.describe.serial('Storage', () => {
|
||||
test('can create a public bucket', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_public`
|
||||
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucket(page, ref, bucketName, true)
|
||||
|
||||
// Verify it's marked as public - wait for the badge to appear
|
||||
@@ -66,8 +71,10 @@ test.describe.serial('Storage', () => {
|
||||
test('can edit bucket settings', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_edit`
|
||||
|
||||
// Create a private bucket
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a fresh private bucket via API
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
|
||||
// Navigate to the bucket
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
@@ -96,12 +103,14 @@ test.describe.serial('Storage', () => {
|
||||
})
|
||||
|
||||
test('can delete a bucket', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_delete`
|
||||
const bucketName = `${bucketNamePrefix}_delbkt`
|
||||
|
||||
// Create a bucket
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
|
||||
// Delete it
|
||||
// Delete it via UI
|
||||
await deleteBucket(page, ref, bucketName)
|
||||
|
||||
// Verify it's gone
|
||||
@@ -115,10 +124,12 @@ test.describe.serial('Storage', () => {
|
||||
const bucketName1 = `${bucketNamePrefix}_search_1`
|
||||
const bucketName2 = `${bucketNamePrefix}_search_2`
|
||||
|
||||
// Create two buckets
|
||||
await createBucket(page, ref, bucketName1, false)
|
||||
await dismissToastsIfAny(page)
|
||||
await createBucket(page, ref, bucketName2, false)
|
||||
// Create two buckets via API
|
||||
await deleteBucketViaApi(bucketName1)
|
||||
await deleteBucketViaApi(bucketName2)
|
||||
await createBucketViaApi(bucketName1, false)
|
||||
await createBucketViaApi(bucketName2, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
|
||||
// Search for first bucket
|
||||
const searchInput = page.getByPlaceholder('Search for a bucket')
|
||||
@@ -152,24 +163,25 @@ test.describe.serial('Storage', () => {
|
||||
const bucketName = `${bucketNamePrefix}_upload`
|
||||
const fileName = 'test-file.txt'
|
||||
|
||||
// Create a bucket and navigate to it
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API and navigate to it
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
// Upload a file
|
||||
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
||||
await uploadFile(page, filePath, fileName)
|
||||
|
||||
// Clean up
|
||||
await deleteBucket(page, ref, bucketName)
|
||||
})
|
||||
|
||||
test('can create a folder', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_folder`
|
||||
const bucketName = `${bucketNamePrefix}_newfolder`
|
||||
const folderName = 'test_folder'
|
||||
|
||||
// Create a bucket and navigate to it
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API and navigate to it
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
// Create a folder
|
||||
@@ -181,8 +193,10 @@ test.describe.serial('Storage', () => {
|
||||
const fileName = 'test-file.txt'
|
||||
const newFileName = 'renamed-file.txt'
|
||||
|
||||
// Create a bucket, navigate to it, and upload a file
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and upload a file
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
||||
@@ -190,18 +204,17 @@ test.describe.serial('Storage', () => {
|
||||
|
||||
// Rename the file
|
||||
await renameItem(page, fileName, newFileName)
|
||||
|
||||
// Clean up
|
||||
await deleteBucket(page, ref, bucketName)
|
||||
})
|
||||
|
||||
test('can rename a folder', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_rename_folder`
|
||||
const bucketName = `${bucketNamePrefix}_mvdir`
|
||||
const folderName = 'old_folder'
|
||||
const newFolderName = 'new_folder'
|
||||
|
||||
// Create a bucket, navigate to it, and create a folder
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and create a folder
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
await createFolder(page, folderName)
|
||||
|
||||
@@ -210,11 +223,13 @@ test.describe.serial('Storage', () => {
|
||||
})
|
||||
|
||||
test('resets folder name when renaming with empty string', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_rename_folder_empty`
|
||||
const bucketName = `${bucketNamePrefix}_reset_enter`
|
||||
const folderName = 'folder_to_rename'
|
||||
|
||||
// Create a bucket, navigate to it, and create a folder
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and create a folder
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
await createFolder(page, folderName)
|
||||
|
||||
@@ -243,11 +258,13 @@ test.describe.serial('Storage', () => {
|
||||
})
|
||||
|
||||
test('resets folder name when clicking outside with empty string', async ({ page, ref }) => {
|
||||
const bucketName = `${bucketNamePrefix}_rename_folder_blur`
|
||||
const bucketName = `${bucketNamePrefix}_reset_blur`
|
||||
const folderName = 'folder_to_blur'
|
||||
|
||||
// Create a bucket, navigate to it, and create a folder
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and create a folder
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
await createFolder(page, folderName)
|
||||
|
||||
@@ -278,8 +295,10 @@ test.describe.serial('Storage', () => {
|
||||
const bucketName = `${bucketNamePrefix}_delete_file`
|
||||
const fileName = 'test-file.txt'
|
||||
|
||||
// Create a bucket, navigate to it, and upload a file
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and upload a file
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
||||
@@ -293,8 +312,10 @@ test.describe.serial('Storage', () => {
|
||||
const bucketName = `${bucketNamePrefix}_delete_folder`
|
||||
const folderName = 'test_folder'
|
||||
|
||||
// Create a bucket, navigate to it, and create a folder
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and create a folder
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
await createFolder(page, folderName)
|
||||
|
||||
@@ -306,8 +327,10 @@ test.describe.serial('Storage', () => {
|
||||
const bucketName = `${bucketNamePrefix}_download`
|
||||
const fileName = 'test-file.txt'
|
||||
|
||||
// Create a bucket, navigate to it, and upload a file
|
||||
await createBucket(page, ref, bucketName, false)
|
||||
// Create a bucket via API, navigate to it, and upload a file
|
||||
await deleteBucketViaApi(bucketName)
|
||||
await createBucketViaApi(bucketName, false)
|
||||
await navigateToStorageFiles(page, ref)
|
||||
await navigateToBucket(page, ref, bucketName)
|
||||
|
||||
const filePath = path.join(import.meta.dirname, 'files', fileName)
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
// Default Supabase CLI constants (hardcoded for local development)
|
||||
const SERVICE_ROLE_KEY =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU'
|
||||
const API_URL = 'http://127.0.0.1:54321'
|
||||
import { env } from "../../env.config.js"
|
||||
|
||||
/**
|
||||
* Execute a SQL query against the local Supabase database via pg-meta.
|
||||
@@ -15,11 +12,11 @@ const API_URL = 'http://127.0.0.1:54321'
|
||||
* @throws Error if the query fails
|
||||
*/
|
||||
export async function query<T>(sql: string, params?: Array<unknown>): Promise<Array<T>> {
|
||||
const response = await fetch(`${API_URL}/pg/query`, {
|
||||
const response = await fetch(`${env.API_URL}/pg/query`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
apikey: SERVICE_ROLE_KEY,
|
||||
apikey: env.SERVICE_ROLE_KEY,
|
||||
},
|
||||
body: JSON.stringify({ query: sql, parameters: params }),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { env } from "../../env.config.js";
|
||||
|
||||
/**
|
||||
* Make an HTTP request to the local Supabase Storage API.
|
||||
*
|
||||
* @param path - The path to append to the storage base URL (e.g., '/bucket')
|
||||
* @param options - Optional method and body
|
||||
* @returns Parsed JSON response
|
||||
* @throws Error if the request fails
|
||||
*/
|
||||
export async function storageRequest<T>(
|
||||
path: string,
|
||||
options?: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: Record<string, unknown> }
|
||||
): Promise<T> {
|
||||
const storageUrl = `${env.API_URL}/storage/v1`
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
apikey: env.SERVICE_ROLE_KEY,
|
||||
Authorization: `Bearer ${env.SERVICE_ROLE_KEY}`,
|
||||
}
|
||||
|
||||
if (options?.body) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
const response = await fetch(`${storageUrl}${path}`, {
|
||||
method: options?.method ?? 'GET',
|
||||
headers,
|
||||
body: options?.body ? JSON.stringify(options.body) : undefined,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(`Storage request failed (${response.status}): ${text}`)
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
return text ? JSON.parse(text) : ({} as T)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { storageRequest } from './client.js'
|
||||
export { createBucket, deleteBucket, deleteAllBuckets, listBuckets } from './queries.js'
|
||||
@@ -0,0 +1,54 @@
|
||||
import { storageRequest } from './client.js'
|
||||
|
||||
interface Bucket {
|
||||
id: string
|
||||
name: string
|
||||
public: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* List all storage buckets.
|
||||
*/
|
||||
export async function listBuckets(): Promise<Bucket[]> {
|
||||
return storageRequest<Bucket[]>('/bucket')
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a storage bucket. Idempotent — skips creation if the bucket already exists.
|
||||
*
|
||||
* @param name - Bucket name / id
|
||||
* @param isPublic - Whether the bucket should be public (default: false)
|
||||
*/
|
||||
export async function createBucket(name: string, isPublic: boolean = false): Promise<void> {
|
||||
const buckets = await listBuckets()
|
||||
if (buckets.some((b) => b.id === name)) return
|
||||
|
||||
await storageRequest('/bucket', {
|
||||
method: 'POST',
|
||||
body: { id: name, name, public: isPublic },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a storage bucket. Idempotent — empties the bucket first, then deletes it.
|
||||
* No-ops if the bucket does not exist.
|
||||
*
|
||||
* @param name - Bucket name / id
|
||||
*/
|
||||
export async function deleteBucket(name: string): Promise<void> {
|
||||
const buckets = await listBuckets()
|
||||
if (!buckets.some((b) => b.id === name)) return
|
||||
|
||||
await storageRequest(`/bucket/${name}/empty`, { method: 'POST' })
|
||||
await storageRequest(`/bucket/${name}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete every storage bucket.
|
||||
*/
|
||||
export async function deleteAllBuckets(): Promise<void> {
|
||||
const buckets = await listBuckets()
|
||||
for (const bucket of buckets) {
|
||||
await deleteBucket(bucket.id)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user