Skip to content
Open
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
7 changes: 4 additions & 3 deletions src/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export async function login(
let serverConfig: ServerConfig | undefined;
try {
({ serverConfig, user } = await verifyToken(client, account));
} catch {
// Token valid but couldn't fetch user details - not fatal
} catch (error) {
// Authentication failure is fatal; enrichment failures remain best-effort.
if (error instanceof ZentaoError && error.code === '1004') throw error;
}

return { token, user, serverConfig };
Expand All @@ -50,7 +51,7 @@ export async function login(
/**
* 拉取服务器配置与用户列表,用于验证 Token 是否可用。
* - 服务端配置失败抛 E1002(服务不可达)
* - /users 401 由 SDK 映射为 E1004(Token 失效)
* - /users 401 或 HTTP 200 登录页映射为 E1004(Token 失效)
* - /users 返回空列表也按 E1004 处理
*/
export async function verifyToken(
Expand Down
42 changes: 41 additions & 1 deletion tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, test, expect, afterEach, beforeEach } from 'bun:test';
import { createClient } from '../src/api/index';
import { verifyToken, getEnvCredentials } from '../src/auth/login';
import { login, verifyToken, getEnvCredentials } from '../src/auth/login';
import { ZentaoError } from '../src/errors';

type RouteHandler = (req: Request, url: URL) => Response | Promise<Response>;
Expand Down Expand Up @@ -31,6 +31,46 @@ function makeClient(server: { url: URL }, token = 'test-token') {
return createClient(server.url.toString(), token);
}

describe('login', () => {
test('does not accept a token when post-login verification returns a login page', async () => {
const server = createMockServer({
users: () => new Response(
'<!DOCTYPE html><html><body><form id="loginForm"><input name="account"></form></body></html>',
{ headers: { 'Content-Type': 'text/html' } },
),
fallback: (_req, url) => url.pathname === '/api.php/v2/users/login'
? Response.json({ status: 'success', token: 'immediately-invalid-token' })
: new Response('not found', { status: 404 }),
});

try {
await expect(login(server.url.toString(), 'admin', 'password'))
.rejects.toMatchObject({ code: '1004' });
} finally {
server.stop();
}
});

test('keeps user and server details best-effort after a valid login', async () => {
const server = createMockServer({
serverConfig: () => new Response('unavailable', { status: 503 }),
fallback: (_req, url) => url.pathname === '/api.php/v2/users/login'
? Response.json({ status: 'success', token: 'valid-token' })
: new Response('not found', { status: 404 }),
});

try {
await expect(login(server.url.toString(), 'admin', 'password')).resolves.toEqual({
token: 'valid-token',
user: undefined,
serverConfig: undefined,
});
} finally {
server.stop();
}
});
});

describe('verifyToken', () => {
test('返回 serverConfig 和匹配账号的 user', async () => {
const server = createMockServer({
Expand Down