Files
supabase__supabase/apps/studio/components/interfaces/ConfigDrift/github-config-drift.test.ts
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

260 lines
8.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
formatGitHubConfigDecodeMessage,
getConfigDriftSummary,
type ConfigDriftResult,
type GitHubConfigDriftSummary,
} from './github-config-drift'
/**
* Asserts `result` is the `success` branch of `ConfigDriftResult` and returns its summary, so the
* existing drift-comparison tests below can keep asserting on the summary shape directly.
*/
function summaryOf(result: ConfigDriftResult): GitHubConfigDriftSummary {
if (result.status !== 'success') {
throw new Error(`Expected a successful decode, got ${result.status}`)
}
return result.summary
}
describe('getConfigDriftSummary', () => {
it('counts a matching field as managed', () => {
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: { api: { max_rows: 1000 } },
githubConfig: { api: { max_rows: 1000 } },
})
)
expect(summary).toEqual({
driftedFields: [],
matchedFields: [{ section: 'api', configPath: 'api.max_rows', value: 1000 }],
unmanagedFields: [],
})
})
it('reports a differing field as drifted, keeping raw (non-normalized) display values', () => {
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: { auth: { enable_signup: false } },
githubConfig: { auth: { enable_signup: true } },
})
)
expect(summary.matchedFields).toEqual([])
expect(summary.driftedFields).toEqual([
{
section: 'auth',
configPath: 'auth.enable_signup',
settingHref: expect.any(Function),
dashboardValue: false,
githubValue: true,
},
])
})
it('reports a field missing from config.toml as drifted against the hosted value', () => {
const drifted = summaryOf(
getConfigDriftSummary({
dashboardConfig: { auth: { site_url: 'https://example.com' } },
githubConfig: { auth: {} },
})
)
expect(drifted.driftedFields).toHaveLength(1)
// site_url defaults to http://127.0.0.1:3000
expect(drifted.driftedFields[0].githubValue).toEqual('http://127.0.0.1:3000')
})
describe('auth.additional_redirect_urls', () => {
it('counts an identical list as managed', () => {
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: {
auth: { additional_redirect_urls: ['https://a.com', 'https://b.com'] },
},
githubConfig: { auth: { additional_redirect_urls: ['https://a.com', 'https://b.com'] } },
})
)
expect(summary).toEqual({
driftedFields: [],
matchedFields: [
{
section: 'auth',
configPath: 'auth.additional_redirect_urls',
value: ['https://a.com', 'https://b.com'],
},
],
unmanagedFields: [],
})
})
it('counts a list that differs only in order as managed', () => {
const summary = summaryOf(
getConfigDriftSummary({
// `fromApiProjectConfig` sorts the dashboard list and `normalizeGithubValue` sorts the
// config.toml one, so ordering can never register as drift.
dashboardConfig: {
auth: { additional_redirect_urls: ['https://a.com', 'https://b.com'] },
},
githubConfig: { auth: { additional_redirect_urls: ['https://b.com', 'https://a.com'] } },
})
)
expect(summary).toEqual({
driftedFields: [],
matchedFields: [
{
section: 'auth',
configPath: 'auth.additional_redirect_urls',
value: ['https://a.com', 'https://b.com'],
},
],
unmanagedFields: [],
})
})
it('ignores duplicate and untrimmed entries in config.toml', () => {
// `auth.additional_redirect_urls` is a registry "set"-equality field whose document-side
// canonicalization re-joins-and-splits the array (mirroring a push/pull round trip), which
// trims each entry — so padding and exact-duplicate entries never register as drift.
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: { auth: { additional_redirect_urls: ['https://a.com'] } },
githubConfig: {
auth: { additional_redirect_urls: [' https://a.com ', 'https://a.com'] },
},
})
)
expect(summary.matchedFields).toHaveLength(1)
expect(summary.driftedFields).toEqual([])
})
it('reports a list the dashboard adds to as drifted, keeping the raw dashboard list', () => {
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: {
auth: { additional_redirect_urls: ['https://a.com', 'https://b.com'] },
},
githubConfig: { auth: { additional_redirect_urls: ['https://a.com'] } },
})
)
expect(summary.driftedFields).toEqual([
{
section: 'auth',
configPath: 'auth.additional_redirect_urls',
settingHref: expect.any(Function),
dashboardValue: ['https://a.com', 'https://b.com'],
githubValue: ['https://a.com'],
},
])
})
it('reports a list missing from config.toml as drifted', () => {
const drifted = summaryOf(
getConfigDriftSummary({
dashboardConfig: { auth: { additional_redirect_urls: ['https://a.com'] } },
githubConfig: { auth: {} },
})
)
expect(drifted.driftedFields).toHaveLength(1)
// additional_redirect_urls defaults to ['https://127.0.0.1:3000']
expect(drifted.driftedFields[0].githubValue).toEqual(['https://127.0.0.1:3000'])
expect(drifted.driftedFields[0].dashboardValue).toEqual(['https://a.com'])
})
})
describe('invalid config.toml', () => {
it('reports a wrong scalar type with its dotted path', () => {
const result = getConfigDriftSummary({
dashboardConfig: { api: { max_rows: 1000 } },
githubConfig: { api: { max_rows: 'abc' } },
})
expect(result).toEqual({
status: 'invalid-config',
issues: [{ path: 'api.max_rows', message: 'Expected number' }],
})
})
it('reports an explicit null the same way as a wrong scalar type', () => {
const result = getConfigDriftSummary({
dashboardConfig: { api: { max_rows: 1000 } },
githubConfig: { api: { max_rows: null } },
})
expect(result).toEqual({
status: 'invalid-config',
issues: [{ path: 'api.max_rows', message: 'Expected number' }],
})
})
it('reports a nested path', () => {
const result = getConfigDriftSummary({
dashboardConfig: { auth: { enable_signup: true } },
githubConfig: { auth: { external: { github: { enabled: 'yes' } } } },
})
expect(result).toEqual({
status: 'invalid-config',
issues: [{ path: 'auth.external.github.enabled', message: 'Expected boolean' }],
})
})
it('reports every bad field in one pass', () => {
const result = getConfigDriftSummary({
dashboardConfig: { api: { max_rows: 1000 } },
githubConfig: { api: { max_rows: 'abc', port: 'x' } },
})
expect(result.status).toEqual('invalid-config')
expect(result).toMatchObject({
issues: expect.arrayContaining([
{ path: 'api.max_rows', message: 'Expected number' },
{ path: 'api.port', message: 'Expected number' },
]),
})
if (result.status === 'invalid-config') {
expect(result.issues).toHaveLength(2)
}
})
it('still decodes successfully when the document has unknown sections or keys', () => {
const summary = summaryOf(
getConfigDriftSummary({
dashboardConfig: { api: { max_rows: 1000 } },
githubConfig: { api: { max_rows: 1000, made_up_key: true }, made_up_section: {} },
})
)
expect(summary.matchedFields).toEqual([
{ section: 'api', configPath: 'api.max_rows', value: 1000 },
])
})
})
})
describe('formatGitHubConfigDecodeMessage', () => {
it('formats a single issue as one sentence', () => {
const message = formatGitHubConfigDecodeMessage([
{ path: 'api.max_rows', message: 'Expected number' },
])
expect(message).toEqual('config.toml has an invalid value at api.max_rows: expected number.')
})
it('formats multiple issues as a comma-separated list', () => {
const message = formatGitHubConfigDecodeMessage([
{ path: 'api.max_rows', message: 'Expected number' },
{ path: 'api.port', message: 'Expected number' },
])
expect(message).toEqual(
'config.toml has invalid values: api.max_rows (expected number), api.port (expected number).'
)
})
})