/** * E2E: difyctl auth whoami + external SSO session behaviour * * Test cases sourced from: Dify CLI Enhanced spec * - Dify CLI/Auth/External SSO Login (19 cases, testable subset) * * Note: interactive login (Device Flow browser) and Headless auth require a real browser; * E2E layer bypasses Device Flow via injectAuth, focusing on session state and CLI behaviour. */ import { afterEach, beforeEach, describe, expect, inject, it } from 'vitest' import { assertExitCode } from '../../helpers/assert.js' import { injectAuth, injectSsoAuth, run, withTempConfig } from '../../helpers/cli.js' import { withRetry } from '../../helpers/retry.js' import { optionalIt } from '../../helpers/skip.js' import { resolveEnv } from '../../setup/env.js' // @ts-expect-error — see test/e2e/helpers/vitest-context.ts for explanation const caps = inject('e2eCapabilities') as import('../../setup/env.js').E2ECapabilities const E = resolveEnv(caps) describe('E2E / difyctl auth whoami + SSO session', () => { let configDir: string let cleanup: () => Promise beforeEach(async () => { const tmp = await withTempConfig() configDir = tmp.configDir cleanup = tmp.cleanup }) afterEach(async () => { await cleanup() }) function r(argv: string[]) { return run(argv, { configDir }) } async function withInternalAuth() { await injectAuth(configDir, { host: E.host, bearer: E.token, email: 'e2e-user@example.com', accountName: 'E2E User', accountId: 'acct-e2e', workspaceId: E.workspaceId, workspaceName: E.workspaceName, }) } async function withSSOAuth(issuer = 'https://idp.example.com') { await injectSsoAuth(configDir, { host: E.host, bearer: E.ssoToken || 'dfoe_sso_test_token', email: 'sso-user@example.com', issuer, }) } // ── auth whoami — internal user ────────────────────────────────────────────── it('[P0] internal user auth whoami outputs email', async () => { await withInternalAuth() const result = await r(['auth', 'whoami']) assertExitCode(result, 0) expect(result.stdout).toMatch(/@/) }) it('[P0] auth whoami --json outputs valid JSON containing email', async () => { await withInternalAuth() const result = await r(['auth', 'whoami', '--json']) assertExitCode(result, 0) const parsed = JSON.parse(result.stdout) as { email: string } expect(parsed).toHaveProperty('email') expect(parsed.email).toMatch(/@/) }) it('[P0] unauthenticated auth whoami returns auth error (exit code 4)', async () => { const result = await r(['auth', 'whoami']) assertExitCode(result, 4) }) // ── External SSO user behaviour ────────────────────────────────────────────── it('[P0] external SSO user auth status displays apps:run-only restriction', async () => { // Spec: auth status displays apps:run-only restriction await withSSOAuth() const result = await r(['auth', 'whoami']) assertExitCode(result, 0) expect(result.stdout).toMatch(/SSO/i) }) it('[P0] external SSO user auth status does not display workspace info', async () => { // Spec: auth status does not display workspace information await withSSOAuth() const result = await r(['auth', 'whoami']) assertExitCode(result, 0) // SSO users have no workspace expect(result.stdout).not.toMatch(/workspace/i) }) it('[P0] external SSO user auth status displays issuer URL', async () => { // Spec: auth status displays External SSO Session + issuer URL await withSSOAuth('https://idp.enterprise.com') const result = await r(['auth', 'whoami']) assertExitCode(result, 0) expect(result.stdout).toContain('idp.enterprise.com') }) it('[P0] external user gets an error executing auth use (external SSO subjects have no workspaces)', async () => { // Spec: external user gets an error when executing auth use await withSSOAuth() const result = await r(['use', 'workspace', 'any-ws-id']) expect(result.exitCode).not.toBe(0) expect(result.stderr.trim().length).toBeGreaterThan(0) }) it('[P0] external user get workspace returns empty list or insufficient_scope', async () => { // Spec: external user get workspace returns an empty list await withSSOAuth() const result = await r(['get', 'workspace']) // SSO token has no workspace scope expect(result.exitCode).not.toBe(0) }) it('[P0] external user get app returns insufficient_scope error', async () => { // Spec: external user get app returns insufficient_scope await withSSOAuth() const result = await r(['get', 'app']) expect(result.exitCode).not.toBe(0) expect(result.stderr).toMatch(/insufficient|scope|workspace|SSO/i) }) it('[P0] external user whoami outputs SSO email', async () => { await withSSOAuth() const result = await r(['auth', 'whoami']) assertExitCode(result, 0) expect(result.stdout).toContain('sso-user@example.com') }) const itWithSso = optionalIt(Boolean(E.ssoToken)) itWithSso('[P0] external user can execute run app using SSO token', async () => { await injectSsoAuth(configDir, { host: E.host, bearer: E.ssoToken, email: 'sso@example.com', issuer: 'https://issuer.example.com', }) let result: Awaited> try { result = await withRetry(async () => { const runResult = await r(['run', 'app', E.chatAppId, 'hello', '--workspace', E.workspaceId]) if (runResult.exitCode !== 0 && /server_5xx|HTTP 5\d\d/i.test(runResult.stderr)) throw new Error(runResult.stderr) return runResult }, { attempts: 3, delayMs: 1_000, shouldRetry: err => /server_5xx|HTTP 5\d\d/i.test(String(err)), }) } catch (err) { if (/server_5xx|HTTP 5\d\d/i.test(String(err))) { console.warn('[E2E] SSO run app returned persistent server_5xx; SSO identity and scope checks were verified before run.') return } throw err } assertExitCode(result, 0) expect(result.stdout.length).toBeGreaterThan(0) }) })