Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 33 additions & 83 deletions packages/backend/tests/unit/services/GuildAccessService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ const EMPTY_ACCESS = {
integrations: 'none',
}

const MANAGE_ALL_ACCESS = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}

async function expectRbacStorageUnavailable(
operation: Promise<unknown>,
) {
await expect(operation).rejects.toMatchObject({
statusCode: 503,
message:
'RBAC storage is unavailable. Run database migrations and retry.',
})
}

const SESSION: SessionData = {
userId: 'user-1',
accessToken: 'access-token',
Expand Down Expand Up @@ -203,14 +222,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds skips guilds that fail context resolution', async () => {
const guilds = [makeGuild('101', { owner: true }), makeGuild('202')]
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockGetUserGuilds.mockResolvedValue(guilds)
mockHasBotInGuild.mockImplementation(async (guildId: string) => {
Expand Down Expand Up @@ -284,14 +296,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds uses cached guilds on upstream 5xx failures', async () => {
const cachedGuild = makeGuild('515', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockRedisIsHealthy.mockReturnValue(true)
mockRedisGet.mockResolvedValue(JSON.stringify([cachedGuild]))
Expand Down Expand Up @@ -321,14 +326,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds skips guild when access resolution throws', async () => {
const guilds = [makeGuild('101', { owner: true }), makeGuild('202')]
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockGetUserGuilds.mockResolvedValue(guilds)
mockHasBotInGuild.mockResolvedValue(true)
Expand Down Expand Up @@ -358,11 +356,9 @@ describe('GuildAccessService', () => {
new MockGuildRoleGrantStorageError('missing table'),
)

await expect(guildAccessService.listAuthorizedGuilds(SESSION)).rejects.toMatchObject({
statusCode: 503,
message:
'RBAC storage is unavailable. Run database migrations and retry.',
})
await expectRbacStorageUnavailable(
guildAccessService.listAuthorizedGuilds(SESSION),
)
})

test('listAuthorizedGuilds returns retryable error when all context lookups fail', async () => {
Expand Down Expand Up @@ -434,14 +430,7 @@ describe('GuildAccessService', () => {

test('resolveGuildContext does not require bot lookup for admin guilds', async () => {
const adminGuild = makeGuild('909', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockGetUserGuilds.mockResolvedValue([adminGuild])
mockResolveEffectiveAccess.mockResolvedValue(adminAccess)
Expand Down Expand Up @@ -481,14 +470,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds throws when enriched guild has no context', async () => {
const guild = makeGuild('101', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockGetUserGuilds.mockResolvedValue([guild])
mockHasBotInGuild.mockResolvedValue(true)
Expand Down Expand Up @@ -576,25 +558,14 @@ describe('GuildAccessService', () => {
new MockGuildRoleGrantStorageError('missing table'),
)

await expect(
await expectRbacStorageUnavailable(
guildAccessService.resolveGuildContext(SESSION, guild.id),
).rejects.toMatchObject({
statusCode: 503,
message:
'RBAC storage is unavailable. Run database migrations and retry.',
})
)
})

test('resolveGuildContext short-circuits admin access without bot/member lookups', async () => {
const guild = makeGuild('909', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockGetUserGuilds.mockResolvedValue([guild])
mockHasBotInGuild.mockRejectedValue(new Error('discord unreachable'))
Expand All @@ -621,14 +592,7 @@ describe('GuildAccessService', () => {

test('resolveGuildContext does not authorize using cached guilds on upstream 429', async () => {
const guild = makeGuild('919', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS
const cachedGuilds = JSON.stringify([guild])

mockRedisIsHealthy.mockReturnValue(true)
Expand All @@ -651,14 +615,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds does not store raw access token bytes in redis key', async () => {
const guild = makeGuild('920', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockRedisIsHealthy.mockReturnValue(true)
mockGetUserGuilds.mockResolvedValue([guild])
Expand All @@ -675,14 +632,7 @@ describe('GuildAccessService', () => {

test('listAuthorizedGuilds keeps serving data when redis cache write fails', async () => {
const guild = makeGuild('930', { owner: true })
const adminAccess = {
overview: 'manage',
settings: 'manage',
moderation: 'manage',
automation: 'manage',
music: 'manage',
integrations: 'manage',
}
const adminAccess = MANAGE_ALL_ACCESS

mockRedisIsHealthy.mockReturnValue(true)
mockRedisSetex.mockRejectedValue(new Error('redis write failed'))
Expand Down
129 changes: 44 additions & 85 deletions packages/frontend/src/pages/AutoMod.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,35 +452,42 @@ describe('AutoModPage', () => {
expect(switches.every((s) => !s.hasAttribute('checked'))).toBe(true)
})

test('applies template and shows success toast', async () => {
const user = userEvent.setup()
const { toast } = await import('sonner')

const setTemplateContext = (template: {
id: string
name: string
description: string
}) => {
mockGuildStore(mockGuild)
vi.mocked(api.automod.getSettings).mockResolvedValue({
data: { settings: mockSettings },
} as any)
vi.mocked(api.automod.listTemplates).mockResolvedValue({
data: {
templates: [
{
id: 'balanced',
name: 'Balanced',
description: 'Safe defaults',
},
],
},
} as any)
vi.mocked(api.automod.applyTemplate).mockResolvedValue({
data: { settings: { ...mockSettings, linksEnabled: true } },
data: { templates: [template] },
} as any)
}

const clickTemplateApply = async (label: string) => {
const user = userEvent.setup()
renderPage()
await user.click(
await screen.findByRole('button', {
name: `Apply ${label} template`,
}),
)
}

const applyButton = await screen.findByRole('button', {
name: 'Apply Balanced template',
test('applies template and shows success toast', async () => {
const { toast } = await import('sonner')
setTemplateContext({
id: 'balanced',
name: 'Balanced',
description: 'Safe defaults',
})
await user.click(applyButton)
vi.mocked(api.automod.applyTemplate).mockResolvedValue({
data: { settings: { ...mockSettings, linksEnabled: true } },
} as any)

await clickTemplateApply('Balanced')

await waitFor(() => {
expect(api.automod.applyTemplate).toHaveBeenCalledWith(
Expand All @@ -493,75 +500,27 @@ describe('AutoModPage', () => {
})
})

test('shows API error message when template apply fails with ApiError', async () => {
const user = userEvent.setup()
const { toast } = await import('sonner')

mockGuildStore(mockGuild)
vi.mocked(api.automod.getSettings).mockResolvedValue({
data: { settings: mockSettings },
} as any)
vi.mocked(api.automod.listTemplates).mockResolvedValue({
data: {
templates: [
{
id: 'strict',
name: 'Strict',
description: 'Strict defaults',
},
],
},
} as any)
vi.mocked(api.automod.applyTemplate).mockRejectedValue(
new ApiError(404, 'Template not found'),
)

renderPage()

const applyButton = await screen.findByRole('button', {
name: 'Apply Strict template',
})
await user.click(applyButton)

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Template not found')
})
})

test('shows generic error when template apply fails unexpectedly', async () => {
const user = userEvent.setup()
test.each([
{
name: 'shows API error message when template apply fails with ApiError',
template: { id: 'strict', name: 'Strict', description: 'Strict defaults' },
error: new ApiError(404, 'Template not found'),
expectedToast: 'Template not found',
},
{
name: 'shows generic error when template apply fails unexpectedly',
template: { id: 'light', name: 'Light', description: 'Light defaults' },
error: new Error('boom'),
expectedToast: 'Failed to apply template',
},
])('$name', async ({ template, error, expectedToast }) => {
const { toast } = await import('sonner')

mockGuildStore(mockGuild)
vi.mocked(api.automod.getSettings).mockResolvedValue({
data: { settings: mockSettings },
} as any)
vi.mocked(api.automod.listTemplates).mockResolvedValue({
data: {
templates: [
{
id: 'light',
name: 'Light',
description: 'Light defaults',
},
],
},
} as any)
vi.mocked(api.automod.applyTemplate).mockRejectedValue(
new Error('boom'),
)

renderPage()

const applyButton = await screen.findByRole('button', {
name: 'Apply Light template',
})
await user.click(applyButton)
setTemplateContext(template)
vi.mocked(api.automod.applyTemplate).mockRejectedValue(error)
await clickTemplateApply(template.name)

await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
'Failed to apply template',
)
expect(toast.error).toHaveBeenCalledWith(expectedToast)
})
})
})
Loading
Loading