Files
twenty/packages/twenty-server/test/integration/graphql/suites/settings-permissions/api-key-webhooks.integration-spec.ts
T
Félix Malfait 75df1f3997 chore(settings): address review comments from PR 21072 (#21121)
## Summary

Round through bosiraphael's 31 review threads on the merged PR #21072
(discovery hero + ephemeral playground token). The user asked to apply
each suggestion only where it adds value, so this PR is split into three
buckets.

### Comments (~17 threads)

- Tightened security-rationale / CSS-gotcha / API-doc comments to one or
two factual lines
- Kept (shortened) the comments above `RequireAccessTokenGuard` call
sites — without them a future reader could remove the guard and silently
reopen the escalation hole
- Kept (shortened) the in-memory-only rationale on
`playgroundApiKeyState` for the same reason
- Kept `flex: 1 + min-height: 0` CSS gotcha on `SubMenuTopBarContainer`
— non-obvious and easy to break

### Structure / extraction

- Move `WEBHOOK_TABLE_ROW_GRID_TEMPLATE_COLUMNS` to its own constants
file (one-export-per-file)
- Split `SettingsAgentToolsTab` and `SettingsAgentToolsTable` across
queries/, hooks/, types/, utils/:
  - `graphql/queries/findManyApplicationsForToolTable.ts`
  - `graphql/queries/findManyMarketplaceAppsForToolTable.ts`
  - `hooks/useSettingsAgentToolsTable.ts` (data loading + index merging)
  - `types/SettingsAgentToolItem|Application|MarketplaceApp`
  - `utils/getToolApplicationId|getToolLink`
- Extract `SettingsAiModelsTab` optimistic mutations into
`hooks/useSettingsAiModelsActions` (handleModelFieldChange,
handleUseRecommendedToggle, handleModelToggle,
handleToggleAllVisibleModels)
- Extract `SettingsAI.handleCreateTool` into `hooks/useCreateTool`
- Drop unnecessary `useMemo` wrappers on `heroTabs` arrays
(SettingsObjects, SettingsLayout)
- Simplify `MenuItemToggle` handler in SettingsAgentSkillsTab:
`onToggleChange={setShowDeactivated}` (no longer wrapping with arrow +
read of stale `!showDeactivated`)

### Hero assets

- Replace placeholder `customize-illustration` with per-page exports
- Rename `layout/customize-illustration-{light,dark}.png` →
`layout/cover-{light,dark}.png`
- Add `cover-{light,dark}.png` for **applications** and **members**
(they were both pointing at the layout placeholder as a TODO)
- Overwrite `data-model/cover-*.png`, `playground/cover-*.png`,
`ai/ai-tools-cover-*.png` with the new exports

## Test plan

- [ ] `npx nx typecheck twenty-front` 
- [ ] `npx nx typecheck twenty-server` 
- [ ] `npx nx lint twenty-front`  (oxlint + oxfmt, 0 warnings/errors)
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the new hero illustration (light + dark)
- [ ] AI tab: tool list still loads, search + Custom/Managed/Standard
filters still work, "New Tool" still navigates to detail
- [ ] AI tab: Models tab — smart/fast model select, "Use best models
only" toggle, per-model checkboxes, toggle-all all still
optimistic+revert on error
- [ ] Skills tab: "Deactivated" toggle still flips show/hide
- [ ] Webhooks table still uses the 1fr 28px grid
2026-06-02 07:23:14 +02:00

87 lines
2.8 KiB
TypeScript

import request from 'supertest';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
const client = request(`http://localhost:${APP_PORT}`);
describe('api key and webhooks permissions', () => {
describe('generateApiKeyToken', () => {
it('should throw a permission error when user does not have permission (member role)', async () => {
const queryData = {
query: `
mutation generateApiKeyToken {
generateApiKeyToken(apiKeyId: "test-api-key-id", expiresAt: "2025-01-01T00:00:00Z") {
token
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${APPLE_JONY_MEMBER_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
});
// Non-ACCESS tokens (API_KEY here, PLAYGROUND same path) must never mint
// an API key — enforced by RequireAccessTokenGuard.
it('should reject a non-ACCESS token even with API key permission', async () => {
const queryData = {
query: `
mutation generateApiKeyToken {
generateApiKeyToken(apiKeyId: "test-api-key-id", expiresAt: "2025-01-01T00:00:00Z") {
token
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${API_KEY_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
});
});
describe('createApiKey', () => {
it('should reject a non-ACCESS token even with API key permission', async () => {
const queryData = {
query: `
mutation createApiKey {
createApiKey(input: { name: "escalation", expiresAt: "2025-01-01T00:00:00Z", roleId: "20202020-0000-4000-8000-000000000000" }) {
id
}
}
`,
};
await client
.post('/metadata')
.set('Authorization', `Bearer ${API_KEY_ACCESS_TOKEN}`)
.send(queryData)
.expect(200)
.expect((res) => {
expect(res.body.data).toBeNull();
expect(res.body.errors).toBeDefined();
expect(res.body.errors[0].extensions.code).toBe(ErrorCode.FORBIDDEN);
});
});
});
});