Files
supabase/apps/studio/components/interfaces/ConfigDrift/ConfigurationDriftPage.test.tsx
Ivan Vasilov 8422045b86 chore: Use @supabase/config for the code configuration page (#50398)
How to test:
1. Connect a project to GH repo
2. Deploy the `config.toml` once
3. Change some setting in Auth
4. You should see a change in
`/dashboard/project/_/settings/code-configuration`

<img width="1271" height="1186" alt="Screenshot 2026-09-16 at 16 26 39"
src="https://github.com/user-attachments/assets/dfc135a4-e495-489e-88fd-b760383793b4"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Configuration drift comparisons now use a consistent project
configuration schema.
- Drift details display complete current-environment and `config.toml`
values, grouped by section.
- Matching and unmanaged settings are organized into dedicated sections.
  - Configuration fields link directly to relevant Studio settings.
  - Added a warning that GitHub deployments overwrite local changes.

- **Bug Fixes**
- Configuration updates now refresh project configuration data
automatically.
  - Improved labels and formatting for boolean and redirect URL values.
- Drift errors identify invalid configuration paths and provide
corrective guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 16:38:37 +02:00

305 lines
9.7 KiB
TypeScript

import { screen } from '@testing-library/react'
import { apiV1Components, apiV2Components, platformComponents } from 'api-types'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
import { ConfigurationDriftPage } from './ConfigurationDriftPage'
import type { ProfileContextType } from '@/lib/profile'
import { createMockOrganizationResponse } from '@/tests/helpers'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
type ProjectDetailResponse = platformComponents['schemas']['ProjectDetailResponse_Output']
type OrganizationResponse = platformComponents['schemas']['OrganizationResponse_Output']
type ListGitHubConnectionsResponse =
platformComponents['schemas']['ListGitHubConnectionsResponse_Output']
type GetGitHubConnectionConfigResponse =
platformComponents['schemas']['GetGitHubConnectionConfigResponse_Output']
type BranchResponse = apiV1Components['schemas']['BranchResponse_Output']
type V2ProjectConfigResponse = apiV2Components['schemas']['V2ProjectConfigResponse_Output']
const PROJECT_REF = 'default'
const ORGANIZATION_ID = 1
const CONNECTION_ID = 42
const PROFILE_CONTEXT: ProfileContextType = {
profile: {
id: 1,
auth0_id: 'auth0|test',
gotrue_id: 'gotrue-test',
username: 'testuser',
primary_email: 'test@example.com',
first_name: null,
last_name: null,
mobile: null,
is_alpha_user: false,
is_sso_user: false,
disabled_features: [],
free_project_limit: null,
},
error: null,
isLoading: false,
isError: false,
isSuccess: true,
}
vi.mock('common', async (importOriginal) => {
const actual = await importOriginal<typeof import('common')>()
return {
...actual,
useIsLoggedIn: () => true,
useParams: () => ({ ref: PROJECT_REF }),
}
})
vi.mock('@/lib/constants', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/constants')>()
return {
...actual,
IS_PLATFORM: true,
}
})
const PROJECT: ProjectDetailResponse = {
cloud_provider: 'AWS',
db_host: `db.${PROJECT_REF}.supabase.co`,
high_availability: false,
id: 1,
inserted_at: '2025-01-01T00:00:00Z',
integration_source: null,
is_branch_enabled: true,
is_physical_backups_enabled: false,
name: 'Test project',
organization_id: ORGANIZATION_ID,
ref: PROJECT_REF,
region: 'us-east-1',
restUrl: `https://${PROJECT_REF}.supabase.co`,
status: 'ACTIVE_HEALTHY',
subscription_id: 'subscription-1',
updated_at: '2025-01-01T00:00:00Z',
connectionString: '',
}
const BRANCH: BranchResponse = {
created_at: '2025-01-01T00:00:00Z',
id: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
is_default: true,
name: 'main',
parent_project_ref: PROJECT_REF,
persistent: true,
project_ref: PROJECT_REF,
status: 'MIGRATIONS_PASSED',
updated_at: '2025-01-01T00:00:00Z',
with_data: false,
}
const CONNECTION: ListGitHubConnectionsResponse['connections'][number] = {
branch_limit: 0,
id: CONNECTION_ID,
inserted_at: '2025-01-01T00:00:00Z',
installation_id: 1,
new_branch_per_pr: false,
project: { id: 1, name: 'Test project', ref: PROJECT_REF },
repository: { id: 1, name: 'acme/repo' },
supabase_changes_only: false,
updated_at: '2025-01-01T00:00:00Z',
user: null,
workdir: '',
}
// Field values here are chosen to match @supabase/config's CLI schema defaults (see
// `getDefaultCliConfig`), so the only drift the tests below see is the one they introduce via
// the `auth` param — everything else round-trips as "matched" or "unmanaged".
function createProjectConfigResponse(auth: Record<string, unknown>): V2ProjectConfigResponse {
return {
data: {
id: PROJECT_REF,
type: 'project_config',
attributes: {
api: {
db_extra_search_path: 'public,extensions',
db_pool: null,
db_pool_acquisition_timeout: 10,
db_schema: 'public,graphql_public',
max_rows: 1000,
},
auth,
database: {
major_version: 17,
network_restrictions: {
allowed_cidrs: [
{ address: '0.0.0.0/0', type: 'v4' },
{ address: '::/0', type: 'v6' },
],
entitlement: 'disallowed',
status: 'stored',
},
postgres_settings: {},
ssl_enforced: false,
},
pooler: {
default_pool_size: 20,
ignore_startup_parameters: '',
max_client_conn: 100,
pool_mode: 'transaction',
query_wait_timeout: 120,
reserve_pool_size: 0,
server_idle_timeout: 0,
server_lifetime: 3600,
},
realtime: {
connection_pool: 20,
max_bytes_per_second: 100_000,
max_channels_per_client: 100,
max_concurrent_users: 200,
max_events_per_second: 100,
max_joins_per_second: 100,
max_payload_size_in_kb: 1000,
max_presence_events_per_second: 100,
postgres_changes_pool: null,
presence_enabled: true,
private_only: false,
suspend: false,
},
storage: {
capabilities: { iceberg_catalog: false, list_v2: true, object_versioning: false },
features: {
iceberg_catalog: { enabled: false, max_catalogs: 0, max_namespaces: 0, max_tables: 0 },
image_transformation: { enabled: false },
purge_cache: { enabled: false },
s3_protocol: { enabled: true },
vector_buckets: { enabled: true, max_buckets: 10, max_indexes: 5 },
},
file_size_limit: 52_428_800,
migration_version: '0',
upstream_target: 'main',
},
},
},
}
}
function mockProject() {
addAPIMock({ method: 'get', path: '/platform/projects/:ref', response: PROJECT })
addAPIMock({
method: 'get',
path: '/platform/organizations',
response: () =>
HttpResponse.json<OrganizationResponse[]>([
createMockOrganizationResponse({ id: ORGANIZATION_ID }),
]),
})
}
function mockConnectedProject() {
mockProject()
addAPIMock({
method: 'get',
path: '/v1/projects/:ref/branches',
response: () => HttpResponse.json<BranchResponse[]>([BRANCH]),
})
addAPIMock({
method: 'get',
path: '/platform/integrations/github/connections',
response: () => HttpResponse.json<ListGitHubConnectionsResponse>({ connections: [CONNECTION] }),
})
}
function mockProjectConfig(auth: Record<string, unknown>) {
addAPIMock({
method: 'get',
path: '/v2/projects/:ref/config',
response: () => HttpResponse.json<V2ProjectConfigResponse>(createProjectConfigResponse(auth)),
})
}
function mockGitHubConfig(config: Record<string, unknown>) {
addAPIMock({
method: 'get',
path: '/platform/integrations/github/connections/:connection_id/config',
response: () =>
HttpResponse.json<GetGitHubConnectionConfigResponse>({
config,
path: 'supabase/config.toml',
ref: null,
sha: 'abc123',
}),
})
}
describe('ConfigurationDriftPage', () => {
test('prompts to connect GitHub when the project has no connection', async () => {
mockProject()
addAPIMock({
method: 'get',
path: '/v1/projects/:ref/branches',
response: () => HttpResponse.json<BranchResponse[]>([]),
})
addAPIMock({
method: 'get',
path: '/platform/integrations/github/connections',
response: () => HttpResponse.json<ListGitHubConnectionsResponse>({ connections: [] }),
})
customRender(<ConfigurationDriftPage />, { profileContext: PROFILE_CONTEXT })
expect(await screen.findByText(/Connect a GitHub repo/)).toBeInTheDocument()
})
test('shows an error state when a query fails', async () => {
mockProject()
addAPIMock({
method: 'get',
path: '/v1/projects/:ref/branches',
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Boom from the backend' }, { status: 500 }),
})
addAPIMock({
method: 'get',
path: '/platform/integrations/github/connections',
response: () => HttpResponse.json<ListGitHubConnectionsResponse>({ connections: [] }),
})
customRender(<ConfigurationDriftPage />, { profileContext: PROFILE_CONTEXT })
expect(await screen.findByText('Could not check configuration drift')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Refresh' })).toBeInTheDocument()
})
test('shows a success state when all comparable settings match', async () => {
mockConnectedProject()
mockProjectConfig({ disable_signup: false })
mockGitHubConfig({ auth: { enable_signup: true } })
customRender(<ConfigurationDriftPage />, { profileContext: PROFILE_CONTEXT })
expect(await screen.findByText('All compared settings match')).toBeInTheDocument()
})
test('shows a config.toml decode error with the offending path', async () => {
mockConnectedProject()
mockProjectConfig({ disable_signup: false })
mockGitHubConfig({ api: { max_rows: 'abc' } })
customRender(<ConfigurationDriftPage />, { profileContext: PROFILE_CONTEXT })
expect(await screen.findByText('Could not read config.toml')).toBeInTheDocument()
expect(screen.getByText(/api\.max_rows/)).toBeInTheDocument()
})
test('renders a drift row when the dashboard and config.toml disagree', async () => {
mockConnectedProject()
mockProjectConfig({ disable_signup: false })
mockGitHubConfig({ auth: { enable_signup: false } })
customRender(<ConfigurationDriftPage />, { profileContext: PROFILE_CONTEXT })
expect(await screen.findByText('New user signups')).toBeInTheDocument()
expect(screen.getByText('1 setting differs')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /Open setting/ })).toHaveAttribute(
'href',
`/project/${PROJECT_REF}/auth/providers`
)
})
})