Skip to content

Conversation

@CodingOnStar
Copy link
Contributor

Summary

test: add comprehensive tests for plugin authentication components

  • Introduced unit tests for the components, including , , and .
  • Implemented tests for rendering, props handling, user interactions, and edge cases.
  • Ensured proper integration with mocked API hooks and context providers.
  • Enhanced coverage for credential handling and OAuth flows.

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran dev/reformat(backend) and cd web && npx lint-staged(frontend) to appease the lint gods

- Introduced unit tests for the  components, including , , and .
- Implemented tests for rendering, props handling, user interactions, and edge cases.
- Ensured proper integration with mocked API hooks and context providers.
- Enhanced coverage for credential handling and OAuth flows.
Copilot AI review requested due to automatic review settings December 24, 2025 08:40
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Dec 24, 2025
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @CodingOnStar, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the test coverage for the plugin authentication components. The added tests ensure the robustness and reliability of API key and OAuth credential handling, covering a wide array of scenarios from UI rendering and user interactions to complex data flows and error handling. This improvement aims to prevent regressions and maintain a high standard of quality for the authentication mechanisms.

Highlights

  • Comprehensive Test Coverage: Introduced extensive unit tests for core plugin authentication components including AddApiKeyButton, AddOAuthButton, ApiKeyModal, and OAuthClientSettings.
  • Component Integration Testing: Added integration tests for the main Authorize component and its sub-components, ensuring seamless interaction between them.
  • Hook and Utility Testing: Implemented tests for the usePluginAuth and usePluginAuthAction hooks, as well as the transformFormSchemasSecretInput utility, covering various data flows and edge cases.
  • UI Interaction Validation: Tests cover rendering, props handling, user interactions (e.g., button clicks, modal open/close), and disabled states across all authentication-related UI elements.
  • API Mocking and Edge Cases: Ensured proper integration with mocked API hooks and context providers, and validated behavior for various edge cases like empty payloads, undefined properties, and concurrent actions.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds an impressively comprehensive suite of tests for the plugin authentication components. The coverage of rendering, props, user interactions, edge cases, and branch-specific logic is excellent and greatly improves the reliability of these critical UI components.

While the test coverage is fantastic, the test files authorize-components.spec.tsx and index.spec.tsx are very large (over 2000 lines each). For better long-term maintainability, I recommend splitting these monolithic files into smaller, more focused test files. For instance, authorize-components.spec.tsx could be split into separate files for AddApiKeyButton, AddOAuthButton, etc. Similarly, index.spec.tsx could be broken down by the feature it's testing (e.g., hooks.spec.ts, utils.spec.ts, PluginAuth.spec.tsx). This will make the tests easier to navigate, understand, and maintain in the future.

Comment on lines +617 to +627
const settingsIcon = screen.getByRole('button').querySelector('[class*="shrink-0"][class*="w-8"]')
if (settingsIcon) {
fireEvent.click(settingsIcon)
}
else {
// Alternative: click by finding the RiEqualizer2Line icon's parent
const icons = screen.getByRole('button').querySelectorAll('svg')
const settingsButton = icons[icons.length - 1]?.parentElement
if (settingsButton)
fireEvent.click(settingsButton)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This selector for the settings icon is quite brittle as it relies on specific CSS class names (shrink-0, w-8) and the internal structure of the button (assuming the last SVG is the settings icon). This could cause the test to fail on unrelated styling changes.

A more robust approach would be to add a data-testid attribute to the settings icon's clickable area in the AddOAuthButton component. For example: data-testid="oauth-settings-button". Then you could simply use screen.getByTestId('oauth-settings-button') to select it.

Since the component source is not part of this PR, I understand you might not be able to change it right now. However, it's a good practice to keep in mind for future improvements to make tests more resilient.

Comment on lines +1141 to +1142
// Wait a bit and verify API was not called
await new Promise(resolve => setTimeout(resolve, 100))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This setTimeout is unnecessary. Since the form validation is mocked to fail, the handleConfirm function should return synchronously before any async API calls are made. You can remove this delay, and the test will be faster and more robust.

Suggested change
// Wait a bit and verify API was not called
await new Promise(resolve => setTimeout(resolve, 100))
// Since validation fails, the async part of handleConfirm is not reached and the check can be performed immediately.

Comment on lines +1202 to +1203
// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using setTimeout with an arbitrary delay to wait for state updates can lead to flaky tests. A more robust and idiomatic way to handle this with @testing-library/react is to use waitFor to assert on an expected change in the DOM.

In this case, when doingAction becomes true, the confirm button should become disabled. You can wait for this to happen instead of using a timeout. This makes the test more reliable and less dependent on timing. This principle applies to other similar tests in this file as well (e.g., lines 1744, 1805).

Suggested change
// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))
// Wait for the button to become disabled, indicating the action is in progress
await waitFor(() => {
expect(confirmButton).toBeDisabled()
})

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive test coverage for plugin authentication components in the Dify web application. The tests cover authentication flows, credential management, and OAuth integration for plugin systems.

Purpose

Add unit and integration tests for plugin authentication components to improve code quality and prevent regressions.

Key Changes

  • Added 2035 lines of tests for core plugin auth functionality (index.spec.tsx)
  • Added 786 lines of tests for the Authorize component (authorize/index.spec.tsx)
  • Added 2264 lines of tests for authorize subcomponents like AddApiKeyButton, AddOAuthButton, ApiKeyModal, and OAuthClientSettings (authorize/authorize-components.spec.tsx)

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
web/app/components/plugins/plugin-auth/index.spec.tsx Tests for main plugin auth exports, types, utils, hooks (usePluginAuth, usePluginAuthAction, useCredential hooks), and core components (PluginAuth, PluginAuthInAgent, PluginAuthInDataSourceNode, AuthorizedInNode, AuthorizedInDataSourceNode) with edge case coverage
web/app/components/plugins/plugin-auth/authorize/index.spec.tsx Tests for the Authorize component covering rendering, props handling, memoization, accessibility, and various authorization state combinations
web/app/components/plugins/plugin-auth/authorize/authorize-components.spec.tsx Tests for authorize subcomponents including AddApiKeyButton, AddOAuthButton, ApiKeyModal, and OAuthClientSettings with form submission, modal interactions, OAuth flows, and edge cases

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1759 to +1817
it('should return early from handleRemove if doingActionRef is true', async () => {
const pluginPayload = createPluginPayload()
let resolveFirstCall: (value?: unknown) => void
let deleteCallCount = 0

mockDeletePluginOAuthCustomClient.mockImplementation(() => {
deleteCallCount++
if (deleteCallCount === 1) {
return new Promise((resolve) => {
resolveFirstCall = resolve
})
}
return Promise.resolve({})
})

const schemasWithOAuthClient: FormSchema[] = [
{
name: '__oauth_client__',
label: 'OAuth Client',
type: 'radio' as FormSchema['type'],
options: [
{ label: 'Default', value: 'default' },
{ label: 'Custom', value: 'custom' },
],
default: 'custom',
required: false,
},
...defaultSchemas,
]

render(
<OAuthClientSettings
pluginPayload={pluginPayload}
schemas={schemasWithOAuthClient}
editValues={{ __oauth_client__: 'custom', client_id: 'id', client_secret: 'secret' }}
hasOriginalClientParams={true}
/>,
{ wrapper: createWrapper() },
)

const removeButton = screen.getByText('common.operation.remove')

// First click starts the delete request
fireEvent.click(removeButton)

// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))

// Second click while first request is pending
fireEvent.click(removeButton)

// Wait a bit more
await new Promise(resolve => setTimeout(resolve, 10))

// Only one delete call should have been made
expect(deleteCallCount).toBe(1)

// Clean up
resolveFirstCall!()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable resolveFirstCall is used with a non-null assertion operator but may be undefined. This same issue appears in multiple test cases. Add a safety check or initialize the variable.

Copilot uses AI. Check for mistakes.
Comment on lines +1202 to +1213
// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))

// Second click while first request is still pending
fireEvent.click(confirmButton)

// Wait a bit more
await new Promise(resolve => setTimeout(resolve, 10))

// Only one API call should have been made
expect(apiCallCount).toBe(1)

Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses arbitrary setTimeout delays (10ms, 100ms) to wait for state updates and async operations. This makes tests fragile and can lead to flaky tests in slower environments. Consider using waitFor with appropriate conditions or Jest's timer mocks to make tests more deterministic and reliable.

Suggested change
// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))
// Second click while first request is still pending
fireEvent.click(confirmButton)
// Wait a bit more
await new Promise(resolve => setTimeout(resolve, 10))
// Only one API call should have been made
expect(apiCallCount).toBe(1)
// Wait until the first API call has been triggered
await waitFor(() => {
expect(apiCallCount).toBe(1)
})
// Second click while first request is still pending
fireEvent.click(confirmButton)
// Ensure no additional API call was made
await waitFor(() => {
expect(apiCallCount).toBe(1)
})

Copilot uses AI. Check for mistakes.
Comment on lines +1141 to +1143
// Wait a bit and verify API was not called
await new Promise(resolve => setTimeout(resolve, 100))
expect(mockAddPluginCredential).not.toHaveBeenCalled()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple test cases use new Promise(resolve => setTimeout(resolve, 100)) for arbitrary waits instead of proper assertions. This pattern appears throughout the test suite and makes tests slower and less reliable. Use waitFor with explicit conditions to check for expected states rather than hoping that 100ms is enough time.

Suggested change
// Wait a bit and verify API was not called
await new Promise(resolve => setTimeout(resolve, 100))
expect(mockAddPluginCredential).not.toHaveBeenCalled()
// Verify API is not called while validation fails
await waitFor(() => {
expect(mockAddPluginCredential).not.toHaveBeenCalled()
})

Copilot uses AI. Check for mistakes.
Comment on lines +170 to +171

expect(screen.getByRole('button').className).toContain('btn-secondary-accent')
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded expectation for button class 'btn-secondary-accent' may fail if CSS class naming changes. Consider testing behavior/functionality rather than implementation details like specific CSS classes.

Suggested change
expect(screen.getByRole('button').className).toContain('btn-secondary-accent')
const defaultButton = screen.getByRole('button')
render(
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant="secondary-accent"
/>,
{ wrapper: createWrapper() },
)
const explicitSecondaryAccentButton = screen.getByRole('button')
expect(defaultButton.className).toBe(explicitSecondaryAccentButton.className)

Copilot uses AI. Check for mistakes.
Comment on lines +362 to +364
// null is falsy, so it won't be transformed
expect(result.api_key).toBe(null)
// 0 is falsy, so it won't be transformed
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "null is falsy, so it won't be transformed" and "0 is falsy, so it won't be transformed", but this is misleading. Looking at the test expectations, both null and 0 are NOT transformed (they remain as-is), which is correct. However, the comment implies this is the expected behavior because they are falsy, but this might indicate the implementation only transforms truthy non-empty values. The comment should be clearer about whether this is the intended behavior or a potential bug, especially since 0 could be a valid secret value in some contexts.

Suggested change
// null is falsy, so it won't be transformed
expect(result.api_key).toBe(null)
// 0 is falsy, so it won't be transformed
// null is preserved as-is to represent an explicitly unset secret, not masked as [__HIDDEN__]
expect(result.api_key).toBe(null)
// numeric values like 0 are also preserved; only non-empty string secrets are transformed

Copilot uses AI. Check for mistakes.
Comment on lines +1910 to +1915
// Should not throw, error is caught
await expect(
act(async () => {
await result.current.handleConfirm()
}),
).rejects.toThrow('Delete failed')
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test expects the API to throw an error with message 'Delete failed', but the test comment says "Should not throw, error is caught". The expectation rejects.toThrow('Delete failed') actually expects the promise to reject, meaning the error is NOT caught. This indicates the test implementation contradicts the comment, or the error handling in the actual code doesn't catch the error as intended. The comment should be corrected or the test should be updated to verify proper error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +1177 to +1215
render(
<PluginAuthInAgent
pluginPayload={pluginPayload}
onAuthorizationItemClick={onAuthorizationItemClick}
/>,
{ wrapper: createWrapper() },
)

// Click trigger button to open popup
const triggerButton = screen.getByRole('button')
fireEvent.click(triggerButton)

// Find and click the workspace default item in the dropdown
// There will be multiple elements with this text, we need the one in the popup (not the trigger)
const workspaceDefaultItems = screen.getAllByText('plugin.auth.workspaceDefault')
// The second one is in the popup list (first one is the trigger button)
const popupItem = workspaceDefaultItems.length > 1 ? workspaceDefaultItems[1] : workspaceDefaultItems[0]
fireEvent.click(popupItem)

// Verify onAuthorizationItemClick was called with empty string for workspace default
expect(onAuthorizationItemClick).toHaveBeenCalledWith('')
})

it('should call onAuthorizationItemClick with credential id when specific credential is clicked', async () => {
const PluginAuthInAgent = (await import('./plugin-auth-in-agent')).default

const onAuthorizationItemClick = vi.fn()
const credential = createCredential({
id: 'specific-cred-id',
name: 'Specific Credential',
credential_type: CredentialTypeEnum.API_KEY,
})
mockGetPluginCredentialInfo.mockReturnValue({
credentials: [credential],
supported_credential_types: [CredentialTypeEnum.API_KEY],
allow_custom_token: true,
})

const pluginPayload = createPluginPayload()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable resolveFirstCall is used with a non-null assertion operator (resolveFirstCall!()) but it's only assigned inside a conditional callback that may or may not execute immediately. If the mock implementation path is not taken, resolveFirstCall will be undefined at cleanup time, causing a runtime error. Add a safety check or initialize the variable to prevent potential undefined reference errors.

Copilot uses AI. Check for mistakes.
Comment on lines +1715 to +1756
it('should return early from handleConfirm if doingActionRef is true', async () => {
const pluginPayload = createPluginPayload()
let resolveFirstCall: (value?: unknown) => void
let apiCallCount = 0

mockSetPluginOAuthCustomClient.mockImplementation(() => {
apiCallCount++
if (apiCallCount === 1) {
return new Promise((resolve) => {
resolveFirstCall = resolve
})
}
return Promise.resolve({})
})

render(
<OAuthClientSettings
pluginPayload={pluginPayload}
schemas={defaultSchemas}
/>,
{ wrapper: createWrapper() },
)

const saveButton = screen.getByText('plugin.auth.saveOnly')

// First click starts the request
fireEvent.click(saveButton)

// Wait a tick to ensure state updates
await new Promise(resolve => setTimeout(resolve, 10))

// Second click while first request is pending
fireEvent.click(saveButton)

// Wait a bit more
await new Promise(resolve => setTimeout(resolve, 10))

// Only one API call should have been made
expect(apiCallCount).toBe(1)

// Clean up
resolveFirstCall!()
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable resolveFirstCall is used with a non-null assertion operator but may be undefined if the code path doesn't execute as expected. Add a safety check or initialize the variable.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants