Skip to content

Commit 5172e29

Browse files
committed
test(org): fail seeded reads at their source and follow the default group
1 parent 6d4cbf0 commit 5172e29

2 files changed

Lines changed: 150 additions & 92 deletions

File tree

‎apps/sim/scripts/org-redesign-seed.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ await db.transaction(async (tx) => {
248248
name: 'Standard Access',
249249
config: STANDARD_GROUP_CONFIG,
250250
createdBy: OWNER.userId,
251-
membershipMode: 'explicit',
251+
isDefault: true,
252252
},
253253
{
254254
id: NARROW_GROUP_ID,

‎apps/sim/scripts/test-org-redesign-e2e.ts‎

Lines changed: 149 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ import {
3333
NARROW,
3434
ORGANIZATION_ID,
3535
ORGANIZATION_NAME,
36+
OUTSIDE_ORGANIZATION_ID,
3637
OUTSIDER,
3738
OWNER,
3839
PASSWORD,
3940
SEEDED_SOURCE,
41+
STANDARD_GROUP_ID,
4042
WORKSPACES,
4143
} from '@/scripts/org-redesign-e2e/fixture'
4244
import { SIDEBAR_WIDTH } from '@/stores/constants'
@@ -387,6 +389,29 @@ function notProvable(name: string, reason: string) {
387389
checks.push({ name, section: currentSection, status: 'skipped', durationMs: 0, error: reason })
388390
}
389391

392+
/**
393+
* Fails a page's own read at its source, so its server seed stays out of hydration and the client
394+
* read fails too: the only way a seeded page reaches its error state. `restore` lets a check retry.
395+
*/
396+
async function withTableHidden(
397+
table: string,
398+
run: (restore: () => Promise<void>) => Promise<void>
399+
) {
400+
const hidden = `${table}_e2e_hidden`
401+
await sql.unsafe(`alter table "${table}" rename to "${hidden}"`)
402+
let restored = false
403+
const restore = async () => {
404+
if (restored) return
405+
restored = true
406+
await sql.unsafe(`alter table "${hidden}" rename to "${table}"`)
407+
}
408+
try {
409+
await run(restore)
410+
} finally {
411+
await restore()
412+
}
413+
}
414+
390415
const menuEntry = (page: Page, name: string | RegExp) =>
391416
page.getByRole('menuitem', { name, exact: typeof name === 'string' })
392417

@@ -411,8 +436,8 @@ async function expectPageNotFound(page: Page) {
411436
const railLink = (page: Page, label: string) =>
412437
sidebar(page).getByRole('link', { name: new RegExp(`(^| )${escapeRegExp(label)}$`) })
413438

414-
const accountChip = (page: Page, name: string) =>
415-
sidebar(page).getByRole('button', { name: new RegExp(`(^| )${escapeRegExp(name)}$`) })
439+
const accountChip = (page: Page) =>
440+
sidebar(page).getByRole('button', { name: 'Account menu', exact: true })
416441

417442
/** The row around a rail link: its status glyph sits beside the link, not in it. */
418443
const railRow = (page: Page, label: string) => railLink(page, label).locator('xpath=..')
@@ -518,32 +543,22 @@ async function landingChecks() {
518543
},
519544
{ allow: [/404/] }
520545
)
521-
await check('signed out goes to login, and an expired session keeps its way back', async () => {
522-
await withSession(null, async ({ page, context }) => {
523-
await page.goto(url(`${root}/home`))
524-
await expect(page).toHaveURL((address) => address.pathname === '/login')
525-
const stale = 'org-redesign-expired-session'
526-
await context.addCookies([
527-
{
528-
name: 'better-auth.session_token',
529-
value: encodeURIComponent(`${stale}.${await makeSignature(stale, authSecret)}`),
530-
url: origin,
531-
httpOnly: true,
532-
sameSite: 'Lax',
533-
},
534-
])
546+
await check('signed out, every organization route goes to login', async () => {
547+
await withSession(null, async ({ page }) => {
535548
for (const path of [
536549
'/home',
537550
'/links',
538551
'/integrations?connectorType=gmail&provider=gmail&connectionMode=live&optionId=gmail',
539552
]) {
540553
await page.goto(url(`${root}${path}`))
541554
await expect(page).toHaveURL((address) => address.pathname === '/login')
542-
const callback = new URL(page.url()).searchParams.get('callbackUrl') ?? ''
543-
assert(callback.startsWith(root), `${path} returns to ${callback}`)
544555
}
545556
})
546557
})
558+
notProvable(
559+
'Signed out: callbackUrl keeps the org root or page',
560+
'proxy.ts answers a cookieless request with a bare /login first; unchanged from staging'
561+
)
547562
await check('a non-member sees the denial and a way to their workspaces', async () => {
548563
await withSession(OUTSIDER, async ({ page }) => {
549564
await page.goto(url(`${root}/home`))
@@ -861,30 +876,54 @@ async function sidebarChecks() {
861876
},
862877
{ allow: [/\/api\/files\//] }
863878
)
864-
await check(
865-
'Invite is hidden for a member, and an admin invites with partial failures kept',
866-
async () => {
867-
await withSession(ADMIN, async ({ page }) => {
868-
await page.goto(url(`${root}/home`))
869-
await page.getByRole('button', { name: 'Organization menu' }).click()
870-
await page
879+
await check('Invite follows the default group, and an admin invites when it allows', async () => {
880+
await withSession(ADMIN, async ({ page }) => {
881+
await page.goto(url(`${root}/home`))
882+
await page.getByRole('button', { name: 'Organization menu' }).click()
883+
await expect(
884+
page
871885
.getByRole('dialog', { name: 'Organization menu' })
872886
.getByRole('button', { name: 'Invite', exact: true })
873-
.click()
874-
const modal = dialog(page, 'Invite people')
875-
await expect(modal).toBeVisible()
876-
const field = modal.getByRole('textbox', { name: 'Email addresses' })
877-
await field.fill(`invitee@org-redesign.test`)
878-
await field.press('Enter')
879-
await expect(
880-
modal.getByRole('combobox', { name: 'Role' }).or(modal.getByLabel('Role'))
881-
).toBeVisible()
882-
await expect(modal.getByRole('button', { name: 'Send invites' })).toBeEnabled()
883-
await modal.getByRole('button', { name: 'Cancel' }).click()
884-
await expect(modal).toHaveCount(0)
885-
})
886-
}
887-
)
887+
).toHaveCount(0)
888+
await sql`update permission_group set config = config - 'disableInvitations' where id = ${STANDARD_GROUP_ID}`
889+
await page.reload()
890+
await page.getByRole('button', { name: 'Organization menu' }).click()
891+
await page
892+
.getByRole('dialog', { name: 'Organization menu' })
893+
.getByRole('button', { name: 'Invite', exact: true })
894+
.click()
895+
const modal = dialog(page, 'Invite people')
896+
await expect(modal).toBeVisible()
897+
const field = modal.getByRole('textbox', { name: 'Email addresses' })
898+
await field.fill(`invitee@org-redesign.test`)
899+
await field.press('Enter')
900+
await expect(
901+
modal.getByRole('combobox', { name: 'Role' }).or(modal.getByLabel('Role'))
902+
).toBeVisible()
903+
await field.fill('refused@org-redesign.test')
904+
await field.press('Enter')
905+
await expect(modal.getByRole('button', { name: 'Send invites' })).toBeEnabled()
906+
await page.route('**/api/workspaces/invitations/batch', (route) =>
907+
route.fulfill({
908+
json: {
909+
success: false,
910+
successful: ['invitee@org-redesign.test'],
911+
added: [],
912+
failed: [{ email: 'refused@org-redesign.test', error: 'Seat limit reached' }],
913+
},
914+
})
915+
)
916+
await modal.getByRole('button', { name: 'Send invites' }).click()
917+
await expect(modal.getByRole('alert')).toBeVisible()
918+
await expect(modal.getByText('refused@org-redesign.test')).toBeVisible()
919+
await expect(modal.getByText('invitee@org-redesign.test')).toHaveCount(0)
920+
await modal.getByRole('button', { name: 'Cancel' }).click()
921+
await expect(modal).toHaveCount(0)
922+
}).finally(
923+
() =>
924+
sql`update permission_group set config = config || '{"disableInvitations":true}'::jsonb where id = ${STANDARD_GROUP_ID}`
925+
)
926+
})
888927

889928
section('2 Sidebar: workspaces')
890929
await check('a member sees only their workspaces; + is disabled and says why', async () => {
@@ -1127,7 +1166,8 @@ async function sidebarChecks() {
11271166
await check('account chip and menu: Settings and Sign out', async () => {
11281167
await withSession(MEMBER, async ({ page }) => {
11291168
await page.goto(url(`${root}/home`))
1130-
const chip = accountChip(page, MEMBER.name)
1169+
const chip = accountChip(page)
1170+
await expect(chip).toContainText(MEMBER.name)
11311171
await chip.click()
11321172
const menu = page.getByRole('dialog', { name: 'Account menu' })
11331173
await expect(menu.getByRole('button', { name: 'Sign out', exact: true })).toBeVisible()
@@ -1138,7 +1178,7 @@ async function sidebarChecks() {
11381178
await check('sign out ends the session', async () => {
11391179
await withSession(CONNECTED, async ({ page }) => {
11401180
await page.goto(url(`${root}/home`))
1141-
await accountChip(page, CONNECTED.name).click()
1181+
await accountChip(page).click()
11421182
await page
11431183
.getByRole('dialog', { name: 'Account menu' })
11441184
.getByRole('button', { name: 'Sign out', exact: true })
@@ -1816,15 +1856,14 @@ async function searchChecks() {
18161856
'history states: failure retries, Clear history empties',
18171857
async () => {
18181858
await withSession(ADMIN, async ({ page }) => {
1819-
await page.route(`**/api/organizations/${ORGANIZATION_ID}/search/history**`, (route) =>
1820-
route.request().method() === 'GET'
1821-
? route.fulfill({ status: 500, json: { error: 'No' } })
1822-
: route.continue()
1823-
)
1824-
await page.goto(url(`${root}/search?mode=search`))
1825-
await expect(page.getByText('Recent activity couldn’t load.')).toBeVisible()
1826-
await page.unrouteAll()
1827-
await page.getByRole('button', { name: 'Try again', exact: true }).click()
1859+
await withTableHidden('organization_search_history', async (restore) => {
1860+
await page.goto(url(`${root}/search?mode=search`))
1861+
await expect(page.getByText('Recent activity couldn’t load.')).toBeVisible({
1862+
timeout: 40_000,
1863+
})
1864+
await restore()
1865+
await page.getByRole('button', { name: 'Try again', exact: true }).click()
1866+
})
18281867
const clear = page.getByRole('button', { name: 'Clear history', exact: true })
18291868
await expect(clear).toBeVisible()
18301869
await clear.click()
@@ -2027,28 +2066,30 @@ async function integrationChecks() {
20272066
})
20282067
})
20292068
await check(
2030-
'three reads fail separately, each with its retry',
2069+
'the inventory and Search reads fail separately, each with its retry',
20312070
async () => {
2071+
const retry = (page: Page) => page.getByRole('button', { name: /Retry|Try again/ }).first()
20322072
await withSession(MEMBER, async ({ page }) => {
2033-
let fail = true
2034-
await page.route(`**/api/organizations/${ORGANIZATION_ID}/connected-accounts**`, (route) =>
2035-
fail ? route.fulfill({ status: 500, json: { error: 'No' } }) : route.continue()
2036-
)
2037-
await page.goto(url(`${root}/integrations`))
2038-
await expect(page.getByText('Could not load your connections')).toBeVisible()
2039-
fail = false
2040-
await page
2041-
.getByRole('button', { name: /Retry|Try again/ })
2042-
.first()
2043-
.click()
2044-
await expect(page.getByRole('link', { name: /^Gmail/ })).toBeVisible()
2073+
await withTableHidden('credential_group', async (restore) => {
2074+
await page.goto(url(`${root}/integrations`))
2075+
await expect(page.getByText('Could not load your connections')).toBeVisible({
2076+
timeout: 40_000,
2077+
})
2078+
await restore()
2079+
await retry(page).click()
2080+
await expect(page.getByRole('link', { name: /^Gmail/ })).toBeVisible()
2081+
})
20452082
})
20462083
await withSession(MEMBER, async ({ page }) => {
2047-
await page.route('**/api/knowledge/sim-search/integrations**', (route) =>
2048-
route.fulfill({ status: 500, json: { error: 'No' } })
2049-
)
2050-
await page.goto(url(`${root}/integrations`))
2051-
await expect(page.getByText('Could not load Search connections')).toBeVisible()
2084+
await withTableHidden('organization_search_integration', async (restore) => {
2085+
await page.goto(url(`${root}/integrations`))
2086+
await expect(page.getByText('Could not load Search connections')).toBeVisible({
2087+
timeout: 40_000,
2088+
})
2089+
await restore()
2090+
await retry(page).click()
2091+
await expect(page.getByText('Could not load Search connections')).toHaveCount(0)
2092+
})
20522093
})
20532094
},
20542095
{ allow: [/500/] }
@@ -2413,27 +2454,33 @@ async function linksChecks() {
24132454
'loading, failure with Retry, and the empty state',
24142455
async () => {
24152456
await withSession(MEMBER, async ({ page }) => {
2457+
await page.goto(url(`${root}/home`))
24162458
const hold = createDeferred<void>()
2417-
let mode: 'hold' | 'fail' | 'empty' | 'real' = 'hold'
2418-
await page.route(`**/api/organizations/${ORGANIZATION_ID}/sim-links**`, async (route) => {
2419-
if (mode === 'hold') await hold.promise
2420-
if (mode === 'fail')
2421-
return route.fulfill({ status: 500, json: { error: 'Failed to load links' } })
2422-
if (mode === 'empty') return route.fulfill({ json: { links: [], nextCursor: null } })
2423-
return route.continue()
2424-
})
2425-
void page.goto(linksUrl)
2426-
await expect(
2427-
page
2428-
.getByRole('status', { name: 'Loading links' })
2429-
.or(page.getByRole('heading', { name: 'Links' }))
2430-
.first()
2431-
).toBeVisible()
2432-
mode = 'fail'
2459+
await page.route(
2460+
(address) => address.pathname === `${root}/links` && address.searchParams.has('_rsc'),
2461+
async (route) => {
2462+
await hold.promise
2463+
await route.continue()
2464+
}
2465+
)
2466+
await mainNav(page).getByRole('link', { name: 'Links', exact: true }).click()
2467+
await expect(page.getByRole('status', { name: 'Loading links' })).toBeVisible()
2468+
await expect(page.getByRole('button', { name: 'New link', exact: true })).toBeVisible()
24332469
hold.resolve()
2434-
await expect(page.getByRole('button', { name: /Retry/ })).toBeVisible({ timeout: 40_000 })
2435-
mode = 'empty'
2436-
await page.getByRole('button', { name: /Retry/ }).click()
2470+
await expect(rowFor(page, NAMED_LINKS.handbook.name)).toBeVisible()
2471+
})
2472+
await withSession(MEMBER, async ({ page }) => {
2473+
await withTableHidden('sim_link', async (restore) => {
2474+
await page.goto(linksUrl)
2475+
const retry = page.getByRole('button', { name: /Retry/ })
2476+
await expect(retry).toBeVisible({ timeout: 40_000 })
2477+
await restore()
2478+
await retry.click()
2479+
await expect(rowFor(page, NAMED_LINKS.handbook.name)).toBeVisible()
2480+
})
2481+
})
2482+
await withSession(OUTSIDER, async ({ page }) => {
2483+
await page.goto(url(`/o/${OUTSIDE_ORGANIZATION_ID}/links`))
24372484
await expect(page.getByText('Create short links like handbook')).toBeVisible()
24382485
await page.getByRole('button', { name: 'New link', exact: true }).last().click()
24392486
await expect(dialog(page, 'New link')).toBeVisible()
@@ -2556,7 +2603,7 @@ async function settingsChecks() {
25562603
await check('Back from Settings returns to the remembered page', async () => {
25572604
await withSession(MEMBER, async ({ page }) => {
25582605
await page.goto(url(`${root}/links`))
2559-
await accountChip(page, MEMBER.name).click()
2606+
await accountChip(page).click()
25602607
await page
25612608
.getByRole('dialog', { name: 'Account menu' })
25622609
.getByRole('button', { name: 'Settings', exact: true })
@@ -2774,8 +2821,16 @@ async function blastRadiusChecks() {
27742821
MEMBER,
27752822
async (session) => {
27762823
const { page } = session
2824+
const consoleErrors: string[] = []
2825+
page.on('console', (message) => {
2826+
if (message.type() === 'error') consoleErrors.push(message.text())
2827+
})
27772828
await page.goto(url(`/extension/o/${ORGANIZATION_ID}`))
2778-
await expect(composer(page)).toBeVisible()
2829+
await expect(composer(page))
2830+
.toBeVisible()
2831+
.catch((error) => {
2832+
throw new Error(`${getErrorMessage(error)}\nConsole: ${consoleErrors.join('\n')}`)
2833+
})
27792834
await session.settle()
27802835
await expect(page.locator('[data-platform-ui]')).toHaveCount(0)
27812836
const home = session.requests.filter(({ path }) => /\/home\/|\/usage\/activity/.test(path))
@@ -2835,7 +2890,7 @@ async function blastRadiusChecks() {
28352890
}
28362891

28372892
const PREFETCHED_ON_LAYOUT = [
2838-
{ label: 'chat list', pattern: /^\/api\/mothership\/chats\?/ },
2893+
{ label: 'chat list', pattern: /^\/api\/mothership\/chats\?(?=.*organizationId=)/ },
28392894
{ label: 'profile', pattern: /^\/api\/users\/me\/profile/ },
28402895
{ label: 'workspace list', pattern: /^\/api\/workspaces(\?|$)/ },
28412896
]
@@ -3142,6 +3197,8 @@ async function walkOne(persona: Persona, viewport: Viewport, theme: Theme) {
31423197
async function walkRoutes(personas: Persona[]) {
31433198
section('Route walk')
31443199
currentCheck = 'Route walk'
3200+
// Hundreds of loads from one address reach Better Auth's own limiter, which a member never does.
3201+
allowedFailures = [/429 [A-Z]+ \/api\/auth\//]
31453202
const plan = personas.flatMap((persona) =>
31463203
[DESKTOP, PHONE].flatMap((viewport) =>
31473204
(['light', 'dark'] as const).map((theme) => ({ persona, viewport, theme }))
@@ -3156,6 +3213,7 @@ async function walkRoutes(personas: Persona[]) {
31563213
}
31573214
})
31583215
)
3216+
allowedFailures = []
31593217
}
31603218

31613219
async function peoplePhaseChecks() {

0 commit comments

Comments
 (0)