blob: f2ac749c9d5858df4d989d46f5d113b2a0be4d0b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
import { getCsrfToken, setCsrfToken } from './csrf';
import { login, logout } from './auth';
function json(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { 'Content-Type': 'application/json', 'X-Request-Id': 'req-auth' },
});
}
describe('authentication API', () => {
afterEach(() => setCsrfToken(null));
it('sends Basic credentials only to login and keeps them out of browser storage', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
json({ code: 200, msg: 'Login success!' }),
).mockResolvedValueOnce(
json({
data: { user: 'root', csrfToken: 'csrf-login' },
requestId: 'req-auth',
}),
);
await login('root', 'secret');
const loginHeaders = new Headers(fetchSpy.mock.calls[0]?.[1]?.headers);
expect(fetchSpy.mock.calls[0]?.[0]).toBe('/rest/v1/login');
expect(loginHeaders.get('Authorization')).toBe(`Basic ${btoa('root:secret')}`);
expect(fetchSpy.mock.calls[1]?.[0]).toBe('/rest/v1/ui/me');
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(localStorage.length).toBe(0);
expect(sessionStorage.length).toBe(0);
expect(getCsrfToken()).toBe('csrf-login');
});
it('maps invalid credentials without revealing whether the user exists', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(json({
code: 401,
msg: 'Unauthorized',
data: 'Access denied for user missing',
}));
await expect(login('missing', 'wrong')).rejects.toMatchObject({
status: 401,
code: 'UI_LOGIN_FAILED',
message: 'Sign-in failed. Check the username and password.',
});
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
it('preserves the dedicated error for an authenticated non-admin user', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
json({ code: 200, msg: 'Login success!' }),
).mockResolvedValueOnce(
json(
{
code: 'UI_ADMIN_REQUIRED',
message: 'This account is authenticated but is not authorized to use the Doris Web Console.',
requestId: 'req-auth',
},
403,
),
).mockResolvedValueOnce(json({ code: 0, data: {} }));
await expect(login('analyst', 'secret')).rejects.toMatchObject({
status: 403,
code: 'UI_ADMIN_REQUIRED',
message: 'This account is authenticated but is not authorized to use the Doris Web Console.',
});
expect(fetchSpy.mock.calls[2]?.[0]).toBe('/rest/v1/logout');
});
it('uses the existing logout endpoint and clears the UI token afterward', async () => {
setCsrfToken('csrf-logout');
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
json({ data: { loggedOut: true }, requestId: 'req-auth' }),
);
await logout();
const headers = new Headers(fetchSpy.mock.calls[0]?.[1]?.headers);
expect(fetchSpy.mock.calls[0]?.[0]).toBe('/rest/v1/logout');
expect(headers.get('X-Doris-CSRF-Token')).toBeNull();
expect(getCsrfToken()).toBeNull();
});
});