mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## Summary Fixes [FE-3987](https://linear.app/supabase/issue/FE-3987/contact-support-pre-fills-the-wrong-supabase-project-id): the "Contact support" button shown in the Table Editor's inline error banner (e.g. "Failed to retrieve rows from table") didn't pass the current project or organization to the support form. This caused the support form to fall back to the user's first organization/project instead of the one actually affected — especially noticeable when the Management API request used to resolve the org also fails. ## Test plan - [ ] Open a project in Studio, go to **Table Editor**, open a table. - [ ] Trigger a failing table query — either block the `rest/v1/<table>` request in DevTools, or apply a filter with a mismatched type (e.g. `id = 'abc'` on an int column). - [ ] On the inline red "Failed to retrieve rows from table" banner, click **Contact support**. - [ ] Confirm the support form pre-fills the **correct organization and project** — the one the failing table actually belongs to. - [ ] Repeat with a project belonging to an organization that is *not* first in your org list, to confirm it's not coincidentally correct. - [ ] Repeat while simulating a Management API failure (e.g. block `api.supabase.com`/`*.supabase.co/platform/*`) to confirm the org still resolves correctly via the `orgSlug` fallback instead of silently defaulting to your first org. - [ ] Sanity check other "Contact support" entry points (header Feedback dropdown, Help sidebar) are unaffected — they use a separate, already-correct code path. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved error handling when project details cannot be loaded, preserving relevant project and organization context. * Improved fallback behavior for identifying the correct organization when project information is unavailable or unresolved. * Support requests opened from error messages now include applicable project and organization information. * Error messages now consistently display available additional actions alongside contact support options. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
136 lines
4.3 KiB
TypeScript
136 lines
4.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { createSupportFormUrl, selectInitialOrgAndProject } from './SupportForm.utils'
|
|
import { createMockOrganization } from '@/tests/helpers'
|
|
|
|
vi.mock('@/data/projects/project-detail-query', () => ({
|
|
getProjectDetail: vi.fn(),
|
|
}))
|
|
|
|
describe('createSupportFormUrl', () => {
|
|
it('returns base URL with no params', () => {
|
|
expect(createSupportFormUrl({})).toBe('/support/new')
|
|
})
|
|
|
|
it('does not append a bare ? when params are empty', () => {
|
|
expect(createSupportFormUrl({})).not.toContain('?')
|
|
})
|
|
|
|
it('includes provided params in the query string', () => {
|
|
const url = createSupportFormUrl({ projectRef: 'my-project' })
|
|
expect(url).toContain('projectRef=my-project')
|
|
})
|
|
|
|
it('includes multiple params', () => {
|
|
const url = createSupportFormUrl({ projectRef: 'my-project', subject: 'help' })
|
|
expect(url).toContain('projectRef=my-project')
|
|
expect(url).toContain('subject=help')
|
|
})
|
|
})
|
|
|
|
describe('selectInitialOrgAndProject', () => {
|
|
const orgA = createMockOrganization({ id: 1, slug: 'org-a' })
|
|
const orgB = createMockOrganization({ id: 2, slug: 'org-b' })
|
|
const orgs = [orgA, orgB]
|
|
|
|
let mockGetProjectDetail: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(async () => {
|
|
const projectDetailQuery = await import('@/data/projects/project-detail-query')
|
|
mockGetProjectDetail = vi.mocked(projectDetailQuery.getProjectDetail)
|
|
mockGetProjectDetail.mockReset()
|
|
})
|
|
|
|
it('resolves the org from the project when the Management API lookup succeeds', async () => {
|
|
mockGetProjectDetail.mockResolvedValue({ organization_id: 2 })
|
|
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: 'project-a',
|
|
orgSlug: null,
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: 'project-a', orgSlug: 'org-b' })
|
|
})
|
|
|
|
it('prefers the org resolved from the project over a mismatched provided orgSlug', async () => {
|
|
mockGetProjectDetail.mockResolvedValue({ organization_id: 2 })
|
|
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: 'project-a',
|
|
orgSlug: 'org-a',
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: 'project-a', orgSlug: 'org-b' })
|
|
})
|
|
|
|
it('keeps projectRef and falls back to the provided orgSlug when the Management API lookup throws', async () => {
|
|
mockGetProjectDetail.mockRejectedValue(new Error('Management API is down'))
|
|
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: 'project-a',
|
|
orgSlug: 'org-b',
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: 'project-a', orgSlug: 'org-b' })
|
|
})
|
|
|
|
it('keeps projectRef and falls back to the provided orgSlug when the resolved org is not in the orgs list', async () => {
|
|
mockGetProjectDetail.mockResolvedValue({ organization_id: 999 })
|
|
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: 'project-a',
|
|
orgSlug: 'org-b',
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: 'project-a', orgSlug: 'org-b' })
|
|
})
|
|
|
|
it('defaults to the first org and drops projectRef when neither the project nor orgSlug resolve', async () => {
|
|
mockGetProjectDetail.mockRejectedValue(new Error('Management API is down'))
|
|
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: 'project-a',
|
|
orgSlug: 'unknown-org',
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: null, orgSlug: 'org-a' })
|
|
expect(mockGetProjectDetail).toHaveBeenCalledWith({ ref: 'project-a' })
|
|
})
|
|
|
|
it('resolves via orgSlug without calling the Management API when no projectRef is provided', async () => {
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: null,
|
|
orgSlug: 'org-b',
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: null, orgSlug: 'org-b' })
|
|
expect(mockGetProjectDetail).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('defaults to the first org when neither projectRef nor orgSlug are provided', async () => {
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: null,
|
|
orgSlug: null,
|
|
orgs,
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: null, orgSlug: 'org-a' })
|
|
})
|
|
|
|
it('returns nulls when there are no orgs to fall back to', async () => {
|
|
const result = await selectInitialOrgAndProject({
|
|
projectRef: null,
|
|
orgSlug: null,
|
|
orgs: [],
|
|
})
|
|
|
|
expect(result).toEqual({ projectRef: null, orgSlug: null })
|
|
})
|
|
})
|