import { describe, it, expect, vi } from 'vitest' import { ErrorCode } from '@d3ro/core/errors' import { IPC_CHANNELS } from '@d3ro/core/ipc-channels' import { configGet, configSet } from '../../src/main/services/ConfigService' import { getOnlineLLMService } from '../../src/main/services/OnlineLLMService' import { getCloudSyncService } from '../../src/main/services/CloudSyncService' import { registerLLMHandlers } from '../../src/main/ipc/llm-handlers' import { registerCloudSyncHandlers } from '../../src/main/ipc/cloud-sync-handlers' import { invokeIpc, useRedHarness } from './harness' import { FX } from './fixtures' useRedHarness() describe('유스케이스: 온라인 로그인/가입/로그아웃 + 클라우드 로그인 실패', () => { it('로그인 전에는 GET_USER 가 비인증이다', async () => { registerLLMHandlers() const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.GET_USER) expect(res.success).toBe(true) if (res.success) { expect(res.data.isAuthenticated).toBe(false) expect(res.data.email).toBeNull() } }) it('로그인 성공 시 토큰이 저장되고 인증 상태가 true 다', async () => { registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: true, json: async () => ({ token: FX.AUTH_TOKEN, email: FX.AUTH_EMAIL }), })), ) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGIN, { email: FX.AUTH_EMAIL, password: 'fx-not-the-asserted-secret', }) expect(res.success).toBe(true) expect(configGet('authToken')).toBe(FX.AUTH_TOKEN) expect(configGet('userEmail')).toBe(FX.AUTH_EMAIL) expect(configGet('llmBackend')).toBe('online') const user = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.GET_USER) if (user.success) expect(user.data.isAuthenticated).toBe(true) }) it('로그인 실패(4xx)는 토큰을 남기지 않고 에러를 반환한다', async () => { registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: false, json: async () => ({ message: FX.AUTH_FAIL_MSG }), })), ) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGIN, { email: FX.AUTH_EMAIL, password: 'wrong', }) expect(res.success).toBe(false) if (!res.success) { expect(res.error.code).toBe(ErrorCode.LLMProcessingFailed) expect(res.error.message).toContain(FX.AUTH_FAIL_MSG) } expect(configGet('authToken')).toBeNull() }) it('서버 연결 실패는 LLMServerUnreachable 이다', async () => { registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => { throw new Error('ECONNREFUSED') }), ) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGIN, { email: FX.AUTH_EMAIL, password: 'x', }) expect(res.success).toBe(false) if (!res.success) expect(res.error.code).toBe(ErrorCode.LLMServerUnreachable) }) it('로그인 실패 후 재시도 성공이 토큰을 저장한다', async () => { registerLLMHandlers() const fetchMock = vi .fn() .mockResolvedValueOnce({ ok: false, json: async () => ({ message: FX.AUTH_FAIL_MSG }), }) .mockResolvedValueOnce({ ok: true, json: async () => ({ token: FX.AUTH_TOKEN, email: FX.AUTH_EMAIL }), }) vi.stubGlobal('fetch', fetchMock) const fail = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGIN, { email: 'a', password: 'b' }) expect(fail.success).toBe(false) const ok = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGIN, { email: 'a', password: 'b' }) expect(ok.success).toBe(true) expect(configGet('authToken')).toBe(FX.AUTH_TOKEN) }) it('가입 성공은 로그인과 같이 토큰을 저장한다', async () => { registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: true, json: async () => ({ token: FX.AUTH_TOKEN, email: FX.AUTH_EMAIL }), })), ) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.REGISTER, { email: FX.AUTH_EMAIL, password: 'pw', }) expect(res.success).toBe(true) expect(configGet('authToken')).toBe(FX.AUTH_TOKEN) }) it('가입 실패는 토큰을 쓰지 않는다', async () => { registerLLMHandlers() vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: false, json: async () => ({ message: 'email taken' }), })), ) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.REGISTER, { email: FX.AUTH_EMAIL, password: 'pw', }) expect(res.success).toBe(false) expect(configGet('authToken')).toBeNull() }) it('로그아웃은 토큰과 이메일을 지운다', async () => { registerLLMHandlers() configSet('authToken', FX.AUTH_TOKEN) configSet('userEmail', FX.AUTH_EMAIL) const res = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.LOGOUT) expect(res.success).toBe(true) expect(configGet('authToken')).toBeNull() expect(configGet('userEmail')).toBeNull() const user = await invokeIpc(IPC_CHANNELS.ONLINE_AUTH.GET_USER) if (user.success) expect(user.data.isAuthenticated).toBe(false) }) it('토큰 없이 OnlineLLM.processText 는 로그인 필요 에러다', async () => { await expect( getOnlineLLMService().processText('hello', 'refine'), ).rejects.toMatchObject({ code: ErrorCode.LLMServerUnreachable }) }) it('isAvailable 은 토큰이 있을 때만 true 다', () => { expect(getOnlineLLMService().isAvailable()).toBe(false) configSet('authToken', FX.AUTH_TOKEN) expect(getOnlineLLMService().isAvailable()).toBe(true) }) it('OnlineLLM 이 401 을 받으면 토큰을 지우고 에러를 던진다', async () => { configSet('authToken', FX.AUTH_TOKEN) vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: false, status: 401, json: async () => ({}), })), ) await expect(getOnlineLLMService().processText('hi', 'refine')).rejects.toMatchObject({ code: ErrorCode.LLMServerUnreachable, }) expect(configGet('authToken')).toBeNull() }) it('OnlineLLM 이 빈 텍스트를 반환하면 성공으로 위장하지 않는다', async () => { configSet('authToken', FX.AUTH_TOKEN) vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ text: '' }), })), ) await expect(getOnlineLLMService().processText('hi', 'refine')).rejects.toMatchObject({ code: ErrorCode.LLMProcessingFailed, }) }) it('CloudSync 미초기화 상태에서 startSignIn 은 설정 안 됨 에러다', async () => { await expect(getCloudSyncService().startSignIn('google')).rejects.toMatchObject({ code: ErrorCode.LLMServerUnreachable, }) }) it('CloudSync handleAuthCallback 빈 코드는 실패한다', async () => { await expect(getCloudSyncService().handleAuthCallback('')).rejects.toBeTruthy() }) it('CloudSync 미인증 getState 는 authenticated=false', () => { const state = getCloudSyncService().getState() expect(state.authenticated).toBe(false) expect(state.userEmail).toBeNull() }) it('미인증 pushAll 은 로그인 필요 에러다', async () => { await expect(getCloudSyncService().pushAll()).rejects.toMatchObject({ code: ErrorCode.LLMServerUnreachable, }) }) it('미인증 pullAll 은 로그인 필요 에러다', async () => { await expect(getCloudSyncService().pullAll()).rejects.toMatchObject({ code: ErrorCode.LLMServerUnreachable, }) }) it('IPC cloudSync:signIn 은 미설정에서 실패를 숨기지 않는다', async () => { registerCloudSyncHandlers() const res = await invokeIpc(IPC_CHANNELS.CLOUD_SYNC.SIGN_IN, { provider: 'google' }) expect(res.success).toBe(false) }) it('IPC cloudSync:getState 는 비인증이다', async () => { registerCloudSyncHandlers() const res = await invokeIpc(IPC_CHANNELS.CLOUD_SYNC.GET_STATE) expect(res.success).toBe(true) if (res.success) expect(res.data.authenticated).toBe(false) }) })