ProjectNeedsSecuring.test.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { fireEvent, screen } from '@testing-library/react'
  2. import { mockAnimationsApi } from 'jsdom-testing-mocks'
  3. import type { MouseEventHandler, ReactNode } from 'react'
  4. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
  5. import { ProjectNeedsSecuring } from './ProjectNeedsSecuring'
  6. import { render } from '@/tests/helpers'
  7. const {
  8. mockUseFlag,
  9. mockUseProjectLintsQuery,
  10. mockUseSelectedProjectQuery,
  11. mockUseTablesQuery,
  12. mockUseProjectPostgrestConfigQuery,
  13. mockUseTablePrivilegesQuery,
  14. mockUseLocalStorageQuery,
  15. mockUseRouter,
  16. mockRouterPush,
  17. } = vi.hoisted(() => ({
  18. mockUseFlag: vi.fn(),
  19. mockUseProjectLintsQuery: vi.fn(),
  20. mockUseSelectedProjectQuery: vi.fn(),
  21. mockUseTablesQuery: vi.fn(),
  22. mockUseProjectPostgrestConfigQuery: vi.fn(),
  23. mockUseTablePrivilegesQuery: vi.fn(),
  24. mockUseLocalStorageQuery: vi.fn(),
  25. mockUseRouter: vi.fn(),
  26. mockRouterPush: vi.fn(),
  27. }))
  28. vi.mock('common', async () => {
  29. const actual = await vi.importActual<typeof import('common')>('common')
  30. return {
  31. ...actual,
  32. useFlag: mockUseFlag,
  33. useParams: () => ({ ref: 'project-ref' }),
  34. }
  35. })
  36. vi.mock('next/router', () => ({
  37. useRouter: () => mockUseRouter(),
  38. }))
  39. vi.mock('next/link', () => ({
  40. default: ({
  41. href,
  42. children,
  43. onClick,
  44. ...props
  45. }: {
  46. href: string
  47. children: ReactNode
  48. onClick?: MouseEventHandler<HTMLAnchorElement>
  49. [key: string]: unknown
  50. }) => (
  51. <a href={href} onClick={onClick} {...props}>
  52. {children}
  53. </a>
  54. ),
  55. }))
  56. vi.mock('@/data/lint/lint-query', () => ({
  57. useProjectLintsQuery: mockUseProjectLintsQuery,
  58. }))
  59. vi.mock('@/hooks/misc/useSelectedProject', () => ({
  60. useSelectedProjectQuery: mockUseSelectedProjectQuery,
  61. }))
  62. vi.mock('@/data/tables/tables-query', () => ({
  63. useTablesQuery: mockUseTablesQuery,
  64. }))
  65. vi.mock('@/data/config/project-postgrest-config-query', () => ({
  66. parseDbSchemaString: vi.fn((value: string) => value.split(',').map((schema) => schema.trim())),
  67. useProjectPostgrestConfigQuery: mockUseProjectPostgrestConfigQuery,
  68. }))
  69. vi.mock('@/data/privileges/table-privileges-query', () => ({
  70. useTablePrivilegesQuery: mockUseTablePrivilegesQuery,
  71. }))
  72. vi.mock('@/hooks/misc/useLocalStorage', () => ({
  73. useLocalStorageQuery: mockUseLocalStorageQuery,
  74. }))
  75. vi.mock('sonner', () => ({
  76. toast: {
  77. error: vi.fn(),
  78. },
  79. }))
  80. const issueLint = {
  81. cache_key: 'lint-1',
  82. name: 'rls_disabled_in_public',
  83. detail: 'RLS is disabled on public.invoices',
  84. description: 'RLS disabled',
  85. level: 'ERROR',
  86. categories: ['SECURITY'],
  87. metadata: {
  88. schema: 'public',
  89. name: 'invoices',
  90. },
  91. }
  92. const tables = [
  93. {
  94. id: 1,
  95. name: 'invoices',
  96. schema: 'public',
  97. rls_enabled: false,
  98. },
  99. {
  100. id: 2,
  101. name: 'profiles',
  102. schema: 'public',
  103. rls_enabled: false,
  104. },
  105. {
  106. id: 3,
  107. name: 'customers',
  108. schema: 'public',
  109. rls_enabled: true,
  110. },
  111. ]
  112. const tablePrivileges = [
  113. {
  114. schema: 'public',
  115. name: 'invoices',
  116. privileges: [
  117. {
  118. grantee: 'anon',
  119. privilege_type: 'SELECT',
  120. },
  121. ],
  122. },
  123. ]
  124. describe('ProjectNeedsSecuring', () => {
  125. beforeEach(() => {
  126. mockAnimationsApi()
  127. mockUseFlag.mockReturnValue(true)
  128. mockUseRouter.mockReturnValue({ pathname: '/project/[ref]', push: mockRouterPush })
  129. mockUseSelectedProjectQuery.mockReturnValue({
  130. data: { connectionString: 'postgresql://example' },
  131. })
  132. mockUseProjectLintsQuery.mockReturnValue({
  133. data: [issueLint],
  134. isPending: false,
  135. isError: false,
  136. })
  137. mockUseTablesQuery.mockReturnValue({
  138. data: tables,
  139. isPending: false,
  140. isError: false,
  141. })
  142. mockUseProjectPostgrestConfigQuery.mockReturnValue({
  143. data: 'public',
  144. isPending: false,
  145. isError: false,
  146. })
  147. mockUseTablePrivilegesQuery.mockReturnValue({
  148. data: tablePrivileges,
  149. isPending: false,
  150. isError: false,
  151. })
  152. mockUseLocalStorageQuery.mockReturnValue([null, vi.fn(), { isLoading: false }])
  153. })
  154. afterEach(() => {
  155. vi.clearAllMocks()
  156. window.localStorage.clear()
  157. })
  158. it('renders the security gate when an exposed table has RLS disabled and the project has not been dismissed', () => {
  159. render(
  160. <ProjectNeedsSecuring>
  161. <div data-testid="project-children">Project content</div>
  162. </ProjectNeedsSecuring>
  163. )
  164. expect(screen.getByText('Your project needs securing')).toBeInTheDocument()
  165. expect(screen.getByText('Review and fix')).toBeInTheDocument()
  166. expect(screen.getByRole('link', { name: 'Open Data API settings' })).toHaveAttribute(
  167. 'href',
  168. '/project/project-ref/integrations/data_api/settings'
  169. )
  170. expect(screen.queryByRole('columnheader', { name: 'Action' })).not.toBeInTheDocument()
  171. expect(screen.queryByRole('link', { name: 'View policies' })).not.toBeInTheDocument()
  172. expect(screen.queryByText('profiles')).not.toBeInTheDocument()
  173. expect(screen.queryByText('customers')).not.toBeInTheDocument()
  174. expect(screen.getByText('Skip to home')).toBeInTheDocument()
  175. expect(screen.queryByTestId('project-children')).not.toBeInTheDocument()
  176. })
  177. it('navigates to the table policies page when a table row is clicked', () => {
  178. render(
  179. <ProjectNeedsSecuring>
  180. <div data-testid="project-children">Project content</div>
  181. </ProjectNeedsSecuring>
  182. )
  183. fireEvent.click(screen.getByText('invoices'))
  184. expect(mockRouterPush).toHaveBeenCalledWith(
  185. '/project/project-ref/auth/policies?schema=public&search=invoices'
  186. )
  187. })
  188. it('renders the project content when the security gate has been dismissed', () => {
  189. mockUseLocalStorageQuery.mockReturnValue([
  190. '2026-04-21T00:00:00.000Z',
  191. vi.fn(),
  192. { isLoading: false },
  193. ])
  194. render(
  195. <ProjectNeedsSecuring>
  196. <div data-testid="project-children">Project content</div>
  197. </ProjectNeedsSecuring>
  198. )
  199. expect(screen.queryByText('Your project needs securing')).not.toBeInTheDocument()
  200. expect(screen.getByTestId('project-children')).toBeInTheDocument()
  201. })
  202. it('renders the project content when there are no RLS issues', () => {
  203. mockUseProjectLintsQuery.mockReturnValue({
  204. data: [],
  205. isPending: false,
  206. isError: false,
  207. })
  208. render(
  209. <ProjectNeedsSecuring>
  210. <div data-testid="project-children">Project content</div>
  211. </ProjectNeedsSecuring>
  212. )
  213. expect(screen.queryByText('Your project needs securing')).not.toBeInTheDocument()
  214. expect(screen.getByTestId('project-children')).toBeInTheDocument()
  215. })
  216. it('renders the project content on non-home project routes', () => {
  217. mockUseRouter.mockReturnValue({ pathname: '/project/[ref]/database/tables' })
  218. render(
  219. <ProjectNeedsSecuring>
  220. <div data-testid="project-children">Project content</div>
  221. </ProjectNeedsSecuring>
  222. )
  223. expect(screen.queryByText('Your project needs securing')).not.toBeInTheDocument()
  224. expect(screen.getByTestId('project-children')).toBeInTheDocument()
  225. })
  226. it('renders the project content when the feature flag is disabled', () => {
  227. mockUseFlag.mockReturnValue(false)
  228. render(
  229. <ProjectNeedsSecuring>
  230. <div data-testid="project-children">Project content</div>
  231. </ProjectNeedsSecuring>
  232. )
  233. expect(screen.queryByText('Your project needs securing')).not.toBeInTheDocument()
  234. expect(screen.getByTestId('project-children')).toBeInTheDocument()
  235. })
  236. })