UNPKG

matterbridge-roborock-vacuum-plugin

Version:
314 lines 17.3 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { RoborockAuthenticateApi } from '../../../roborockCommunication/api/authClient.js'; describe('RoborockAuthenticateApi', () => { let mockLogger; let mockAxiosFactory; let mockAxiosInstance; let api; beforeEach(() => { mockLogger = { info: vi.fn(), error: vi.fn(), debug: vi.fn(), notice: vi.fn() }; mockAxiosInstance = { post: vi.fn(), get: vi.fn(), interceptors: { request: { use: vi.fn(), }, response: { use: vi.fn(), }, }, }; mockAxiosFactory = { create: vi.fn(() => mockAxiosInstance), }; api = new RoborockAuthenticateApi(mockLogger, mockAxiosFactory); }); it('loginWithUserData should call loginWithAuthToken and return userData', async () => { const spy = vi.spyOn(api, 'loginWithAuthToken'); const userData = { token: 'abc', other: 'data' }; const result = await api.loginWithUserData('user', userData); expect(spy).toHaveBeenCalledWith('user', 'abc'); expect(result).toBe(userData); }); it('loginWithPassword should call auth and return userData', async () => { const userData = { token: 'tok', other: 'data' }; const response = { data: { data: userData } }; vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue(response); vi.spyOn(api, 'auth').mockReturnValue(userData); const result = await api.loginWithPassword('user', 'pass'); expect(result).toBe(userData); expect(mockAxiosInstance.post).toHaveBeenCalled(); expect(api.getAPIFor).toHaveBeenCalledWith('user'); expect(api.auth).toHaveBeenCalledWith('user', response.data); }); it('loginWithPassword should throw error if token missing', async () => { const response = { data: { data: null, msg: 'fail', code: 401 } }; vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue(response); vi.spyOn(api, 'auth').mockImplementation(() => { throw new Error('Authentication failed: fail code: 401'); }); await expect(api.loginWithPassword('user', 'pass')).rejects.toThrow('Authentication failed: fail code: 401'); }); it('getHomeDetails should return undefined if username/authToken missing', async () => { api.username = undefined; api.authToken = undefined; const result = await api.getBasicHomeInfo(); expect(result).toBeUndefined(); expect(mockLogger.error).toHaveBeenCalled(); }); it('getHomeDetails should throw error if response.data missing', async () => { api.username = 'user'; api.authToken = 'tok'; vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.get.mockResolvedValue({ data: { data: null } }); await expect(api.getBasicHomeInfo()).rejects.toThrow('Failed to retrieve the home details'); }); it('getHomeDetails should return HomeInfo if present', async () => { api.username = 'user'; api.authToken = 'tok'; const homeInfo = { home: 'info' }; vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.get.mockResolvedValue({ data: { data: homeInfo } }); const result = await api.getBasicHomeInfo(); expect(result).toBe(homeInfo); }); it('getBaseUrl should throw error if response.data missing', async () => { vi.spyOn(api, 'apiForUser').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { data: null, msg: 'fail' } }); await expect(api.getBaseUrl('user')).rejects.toThrow('Failed to retrieve base URL: fail'); }); it('getBaseUrl should return url if present', async () => { vi.spyOn(api, 'apiForUser').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { data: { url: 'http://base.url' } } }); const result = await api.getBaseUrl('user'); expect(result).toBe('http://base.url'); }); it('apiForUser should create AxiosInstance with correct headers', async () => { const username = 'user'; const baseUrl = 'http://base.url'; const spy = vi.spyOn(mockAxiosFactory, 'create'); await api.apiForUser(username, baseUrl); expect(spy).toHaveBeenCalledWith(expect.objectContaining({ baseURL: baseUrl, headers: expect.objectContaining({ header_clientid: expect.any(String), Authorization: undefined, }), })); }); it('auth should call loginWithAuthToken and return userData', () => { const spy = vi.spyOn(api, 'loginWithAuthToken'); const response = { data: { token: 'tok', other: 'data' }, msg: '', code: 0 }; const result = api.auth('user', response); expect(spy).toHaveBeenCalledWith('user', 'tok'); expect(result).toBe(response.data); }); it('auth should throw error if token missing', () => { const response = { data: null, msg: 'fail', code: 401 }; expect(() => api.auth('user', response)).toThrow('Authentication failed: fail code: 401'); }); it('loginWithAuthToken should set username and authToken', () => { api.loginWithAuthToken('user', 'tok'); expect(api.username).toBe('user'); expect(api.authToken).toBe('tok'); }); describe('requestCodeV4', () => { it('should successfully request verification code', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { code: 200 } }); await api.requestCodeV4('test@example.com'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('api/v4/email/code/send', expect.any(URLSearchParams), expect.objectContaining({ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } })); expect(mockLogger.debug).toHaveBeenCalledWith('Verification code requested successfully'); }); it('should throw error if account not found', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { code: 2008 } }); // AccountNotFound await expect(api.requestCodeV4('notfound@example.com')).rejects.toThrow('Account not found for email'); }); it('should throw error if rate limited', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { code: 9002 } }); // RateLimited await expect(api.requestCodeV4('rate@example.com')).rejects.toThrow('Rate limited'); }); it('should throw error for other failures', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); mockAxiosInstance.post.mockResolvedValue({ data: { code: 500, msg: 'Server error' } }); await expect(api.requestCodeV4('fail@example.com')).rejects.toThrow('Failed to send verification code'); }); }); describe('loginWithCodeV4', () => { it('should successfully login with code', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('1234567890abcdef'); const signedKey = 'signedKey123'; const userData = { token: 'userToken', rriot: {} }; mockAxiosInstance.post .mockResolvedValueOnce({ data: { data: { k: signedKey }, code: 200 } }) // signKeyV3 .mockResolvedValueOnce({ data: { data: userData, code: 200 } }); // actual login vi.spyOn(api, 'auth').mockReturnValue(userData); const result = await api.loginWithCodeV4('test@example.com', '123456'); expect(result).toBe(userData); expect(mockAxiosInstance.post).toHaveBeenCalledTimes(2); }); it('should throw error when authentication returns no user data', async () => { vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('1234567890abcdef'); const signedKey = 'signedKey123'; mockAxiosInstance.post .mockResolvedValueOnce({ data: { data: { k: signedKey }, code: 200 } }) // signKeyV3 .mockResolvedValueOnce({ data: { data: null, code: 500, msg: 'Server error' } }); // login fails await expect(api.loginWithCodeV4('test@example.com', '123456')).rejects.toThrow('Authentication failed: Server error code: 500'); }); }); describe('Country fallback logic', () => { it('should use fallback for euiot when country/countryCode not cached', async () => { api.cachedCountry = undefined; api.cachedCountryCode = undefined; api.baseUrl = 'https://euiot.example.com'; vi.spyOn(api, 'signKeyV3').mockResolvedValue('signedKey'); vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('randStr'); const userData = { token: 'tok', rriot: {} }; mockAxiosInstance.post.mockResolvedValue({ data: { data: userData, code: 200 } }); vi.spyOn(api, 'authV4').mockReturnValue(userData); await api.loginWithCodeV4('test@example.com', '123456'); // The request should include fallback country 'Germany' and countryCode 'DE' in params expect(mockAxiosInstance.post).toHaveBeenCalledWith('api/v4/auth/email/login/code', null, expect.objectContaining({ params: expect.objectContaining({ country: 'Germany', countryCode: 'DE', }), })); }); it('should use fallback for usiot when country/countryCode not cached', async () => { api.cachedCountry = undefined; api.cachedCountryCode = undefined; api.baseUrl = 'https://usiot.example.com'; vi.spyOn(api, 'signKeyV3').mockResolvedValue('signedKey'); vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('randStr'); const userData = { token: 'tok', rriot: {} }; mockAxiosInstance.post.mockResolvedValue({ data: { data: userData, code: 200 } }); vi.spyOn(api, 'authV4').mockReturnValue(userData); await api.loginWithCodeV4('test@example.com', '123456'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('api/v4/auth/email/login/code', null, expect.objectContaining({ params: expect.objectContaining({ country: 'United States', countryCode: 'US', }), })); }); it('should use fallback for cniot when country/countryCode not cached', async () => { api.cachedCountry = undefined; api.cachedCountryCode = undefined; api.baseUrl = 'https://cniot.example.com'; vi.spyOn(api, 'signKeyV3').mockResolvedValue('signedKey'); vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('randStr'); const userData = { token: 'tok', rriot: {} }; mockAxiosInstance.post.mockResolvedValue({ data: { data: userData, code: 200 } }); vi.spyOn(api, 'authV4').mockReturnValue(userData); await api.loginWithCodeV4('test@example.com', '123456'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('api/v4/auth/email/login/code', null, expect.objectContaining({ params: expect.objectContaining({ country: 'China', countryCode: 'CN', }), })); }); it('should use fallback for ruiot when country/countryCode not cached', async () => { api.cachedCountry = undefined; api.cachedCountryCode = undefined; api.baseUrl = 'https://ruiot.example.com'; vi.spyOn(api, 'signKeyV3').mockResolvedValue('signedKey'); vi.spyOn(api, 'getAPIFor').mockResolvedValue(mockAxiosInstance); vi.spyOn(api, 'generateRandomString').mockReturnValue('randStr'); const userData = { token: 'tok', rriot: {} }; mockAxiosInstance.post.mockResolvedValue({ data: { data: userData, code: 200 } }); vi.spyOn(api, 'authV4').mockReturnValue(userData); await api.loginWithCodeV4('test@example.com', '123456'); expect(mockAxiosInstance.post).toHaveBeenCalledWith('api/v4/auth/email/login/code', null, expect.objectContaining({ params: expect.objectContaining({ country: 'Russia', countryCode: 'RU', }), })); }); }); describe('Interceptor logging', () => { it('should log request and response via interceptors', async () => { // Call apiForUser to trigger interceptor setup await api.apiForUser('user', 'http://test.com'); const requestInterceptor = mockAxiosInstance.interceptors.request.use.mock.calls[0][0]; const responseInterceptor = mockAxiosInstance.interceptors.response.use.mock.calls[0][0]; // Test request interceptor const config = { baseURL: 'http://test.com', url: '/api/test', method: 'POST', params: { key: 'value' }, data: { foo: 'bar' }, headers: { 'Content-Type': 'application/json' }, }; const result = requestInterceptor(config); expect(result).toBe(config); // Test response interceptor const response = { status: 200, statusText: 'OK', data: { success: true }, config: { baseURL: 'http://test.com', url: '/api/test', method: 'POST', }, headers: {}, }; const responseResult = responseInterceptor(response); expect(responseResult).toBe(response); expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('[Roborock Authenticate API][Request]')); expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('[Roborock Authenticate API][Response]')); expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('200:OK')); }); it('should log error via error interceptor', async () => { // Call apiForUser to trigger interceptor setup await api.apiForUser('user', 'http://test.com'); const errorInterceptor = mockAxiosInstance.interceptors.response.use.mock.calls[0][1]; const error = new Error('Request failed'); await expect(errorInterceptor(error)).rejects.toBeInstanceOf(Error); await expect(errorInterceptor(error)).rejects.toThrow('Request failed'); }); it('should log error message when response is missing', async () => { // Call apiForUser to trigger interceptor setup await api.apiForUser('user', 'http://test.com'); const errorInterceptor = mockAxiosInstance.interceptors.response.use.mock.calls[0][1]; const error = new Error('Network error'); await expect(errorInterceptor(error)).rejects.toBeInstanceOf(Error); await expect(errorInterceptor(error)).rejects.toThrow('Network error'); }); }); describe('getBaseUrl caching', () => { it('should return cached baseUrl when username matches', async () => { api.cachedBaseUrl = 'http://cached.url'; api.username = 'user123'; const result = await api.getBaseUrl('user123'); expect(result).toBe('http://cached.url'); // Verify apiForUser was not called expect(mockAxiosInstance.post).not.toHaveBeenCalled(); }); }); describe('getAPIFor', () => { it('should call getBaseUrl and apiForUser', async () => { vi.spyOn(api, 'getBaseUrl').mockResolvedValue('http://test.url'); vi.spyOn(api, 'apiForUser').mockResolvedValue(mockAxiosInstance); const result = await api.getAPIFor('user'); expect(result).toBe(mockAxiosInstance); expect(api.getBaseUrl).toHaveBeenCalledWith('user'); expect(api.apiForUser).toHaveBeenCalledWith('user', 'http://test.url'); }); }); }); //# sourceMappingURL=authClient.test.js.map