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:
Ali Waseem authored and GitHub committed 2026-02-06 10:50:41 -07:00
1 parent ba0f5d6e76
commit c7f653f33f
6 files changed
+170 -49

No files matched your search

+7 -1
View File
@@ -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')
+65 -42
View File
@@ -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)
+3 -6
View File
@@ -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 }),
})
+39
View File
@@ -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)
}
+2
View File
@@ -0,0 +1,2 @@
export { storageRequest } from './client.js'
export { createBucket, deleteBucket, deleteAllBuckets, listBuckets } from './queries.js'
+54
View File
@@ -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)
}
}