diff --git a/web/app/device/components/__tests__/authorize-sso.spec.tsx b/web/app/device/components/__tests__/authorize-sso.spec.tsx new file mode 100644 index 0000000000..7f1e4a98c8 --- /dev/null +++ b/web/app/device/components/__tests__/authorize-sso.spec.tsx @@ -0,0 +1,51 @@ +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import AuthorizeSSO from '../authorize-sso' + +const mockCtx = { + subject_email: 'gareth@company.com', + subject_issuer: 'Okta (okta.company.com)', + user_code: 'ABCD-3456', + csrf_token: 'tok', + expires_at: '2099-01-01T00:00:00Z', +} + +const mockFetchApprovalContext = vi.fn().mockResolvedValue(mockCtx) +const mockApproveExternal = vi.fn().mockResolvedValue(undefined) + +vi.mock('@/service/device-flow', () => ({ + fetchApprovalContext: () => mockFetchApprovalContext(), + approveExternal: (...args: unknown[]) => mockApproveExternal(...args), + DeviceFlowError: class extends Error { + code: string + status: number + constructor(code: string, status = 400) { + super(code) + this.code = code + this.status = status + } + }, +})) + +describe('AuthorizeSSO', () => { + it('renders subject_email and issuer after context loads', async () => { + render() + await screen.findByText('gareth@company.com') + expect(screen.getByText('Okta (okta.company.com)')).toBeInTheDocument() + }) + + it('renders single Authorize button with no Cancel', async () => { + render() + await screen.findByRole('button', { name: /Authorize/i }) + expect(screen.queryByRole('button', { name: /Cancel/i })).not.toBeInTheDocument() + }) + + it('calls approveExternal on Authorize click then calls onApproved', async () => { + const onApproved = vi.fn() + render() + await screen.findByRole('button', { name: /Authorize/i }) + await userEvent.click(screen.getByRole('button', { name: /Authorize/i })) + await waitFor(() => expect(onApproved).toHaveBeenCalled()) + }) +}) diff --git a/web/app/device/components/authorize-sso.tsx b/web/app/device/components/authorize-sso.tsx index 722052498d..3499465a91 100644 --- a/web/app/device/components/authorize-sso.tsx +++ b/web/app/device/components/authorize-sso.tsx @@ -1,8 +1,10 @@ 'use client' import type { FC } from 'react' -import { useEffect, useState } from 'react' import type { ApprovalContext } from '@/service/device-flow' +import { Avatar } from '@langgenius/dify-ui/avatar' +import { Button } from '@langgenius/dify-ui/button' +import { useEffect, useState } from 'react' import { approveExternal, fetchApprovalContext } from '@/service/device-flow' import { approveErrorCopy } from '../utils/error-copy' @@ -30,16 +32,22 @@ const AuthorizeSSO: FC = ({ onApproved, onError }) => { useEffect(() => { let cancelled = false fetchApprovalContext() - .then((c) => { if (!cancelled) setCtx(c) }) + .then((c) => { + if (!cancelled) + setCtx(c) + }) .catch((e) => { if (!cancelled) setLoadErr(approveErrorCopy(e)) }) - return () => { cancelled = true } + return () => { + cancelled = true + } }, []) const approve = async () => { - if (!ctx) return + if (!ctx) + return setBusy(true) try { await approveExternal(ctx, ctx.user_code) @@ -53,12 +61,19 @@ const AuthorizeSSO: FC = ({ onApproved, onError }) => { } } + // loadErr and loading states render without the icon-circle pattern intentionally — + // they occur before the SSO identity is established, so there is no terminal + // state to decorate. The page.tsx error_* states cover post-lookup failures. if (loadErr) { return (

This session is no longer valid

- Run difyctl auth login again to start a new sign-in. + Run + {' '} + difyctl auth login + {' '} + again to start a new sign-in.

) @@ -68,29 +83,40 @@ const AuthorizeSSO: FC = ({ onApproved, onError }) => { } return ( -
+

Authorize Dify CLI

- Dify CLI (difyctl) is requesting access via SSO. If you did not start - this from your terminal, close this tab. + difyctl is requesting access via SSO. If you didn't start this from your terminal, close this tab.

-
-

- Signed in as {ctx.subject_email} -

-

- Issuer: {ctx.subject_issuer} -

+
+ +
+

{ctx.subject_email}

+

via SSO

+
-
+ )} + +
) }