import { provideHttpClient } from '@angular/common/http'; import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { API_CONFIG } from './app.constants'; import { ApiError, ApiService } from './api.service'; import { SessionService } from './session.service'; describe('ApiService', () => { let api: ApiService; let http: HttpTestingController; let sessionToken = ''; beforeEach(() => { sessionToken = ''; TestBed.configureTestingModule({ providers: [provideHttpClient(), provideHttpClientTesting(), { provide: SessionService, useValue: { token: () => sessionToken } }] }); api = TestBed.inject(ApiService); http = TestBed.inject(HttpTestingController); }); afterEach(() => http.verify()); it('routes migrated actions through the cloud gateway and parses JSON result strings', () => { let actual: unknown; api.get('node_list', { cpage: 1 }).subscribe((response) => actual = response.result); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.method).toBe('POST'); expect(request.request.headers.get('X-Parse-Application-Id')).toBe(API_CONFIG.parseAppId); expect(request.request.body).toEqual({ params: { action: 'node_list', cpage: 1 } }); request.flush({ retcode: 0, result: '[{"ID":1}]' }); expect(actual).toEqual([{ ID: 1 }]); }); it('keeps not-yet-migrated actions on the legacy gateway', () => { api.get('order_list', { cpage: 2 }).subscribe(); const request = http.expectOne((candidate) => candidate.url.includes(API_CONFIG.endpoint) && candidate.urlWithParams.includes('action=order_list')); expect(request.request.method).toBe('GET'); expect(request.request.params.get('cpage')).toBe('2'); request.flush({ retcode: 0, result: [] }); }); it('nests reserved id parameters inside params', () => { api.get('node_get', { id: 81 }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ params: { action: 'node_get', id: 81 } }); request.flush({ retcode: 0, result: { id: 81 } }); }); it('keeps private addon content without a Parse session and product queries on the legacy gateway', () => { api.get('content_list', { modelId: 56, myfield2: 'UserId=42' }).subscribe(); api.get('product_list', { cpage: 1 }).subscribe(); const content = http.expectOne((candidate) => candidate.urlWithParams.includes('action=content_list')); const products = http.expectOne((candidate) => candidate.urlWithParams.includes('action=product_list')); expect(content.request.url).toContain(API_CONFIG.endpoint); expect(products.request.url).toContain(API_CONFIG.endpoint); content.flush({ retcode: 0, result: [] }); products.flush({ retcode: 0, result: [] }); }); it('routes supported private addon content with a Parse session', () => { sessionToken = 'r:session-token'; api.get('content_list', { modelId: 56, myfield2: 'UserId=42|dqrq=20260818' }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'content_list', modelId: 56, myfield2: 'UserId=42|dqrq=20260818' } }); request.flush({ retcode: 0, result: [] }); }); it('routes verified learning detail reads with a Parse session', () => { sessionToken = 'r:session-token'; api.get('e_record_detail', { id: 931 }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'e_record_detail', id: 931 } }); request.flush({ retcode: 0, result: [{ GeneralID: 149, detail: [{ Title: 'beautiful' }] }] }); }); it('routes course word progress reads with a Parse session', () => { sessionToken = 'r:session-token'; api.get('e_ck_list', { uid: 58, nids: '40,41' }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'e_ck_list', uid: 58, nids: '40,41' } }); request.flush({ retcode: 0, result: [{ GeneralID: 2505, w_learned: 7 }] }); }); it('routes learning word state writes with a Parse session', () => { sessionToken = 'r:session-token'; api.post('e_add_words', { uid: 58, wordsId: '2505,2503', kcid: 16, ifnew: 1 }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'e_add_words', uid: 58, wordsId: '2505,2503', kcid: 16, ifnew: 1 } }); request.flush({ retcode: 0, result: { affected: 2, newWordState: 1 } }); }); it('routes study record upserts with a Parse session', () => { sessionToken = 'r:session-token'; api.post('stu_record_update_v2', { orderId: 918, uid: 23, addon: '{"xxqs":[]}' }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'stu_record_update_v2', orderId: 918, uid: 23, addon: '{"xxqs":[]}' } }); request.flush({ retcode: 0, result: { GeneralID: 123456, created: true } }); }); it('routes scheduled study record creation with a Parse session', () => { sessionToken = 'r:session-token'; api.post('content_add_zt', { content: '{"ModelID":56}', addon: '{"UserID":23,"xxqs":"[]"}' }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'content_add_zt', content: '{"ModelID":56}', addon: '{"UserID":23,"xxqs":"[]"}' } }); request.flush({ retcode: 0, result: '123456' }); }); it('routes appointment statistics with a Parse session', () => { sessionToken = 'r:session-token'; api.post('e_order_tongji', { uid: 23 }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ token: 'r:session-token', params: { action: 'e_order_tongji', uid: 23 } }); request.flush({ retcode: 0, result: { t30: '1', t_shichang: '0.5', t_total: '1' } }); }); it('keeps learning detail reads on legacy until a real Parse session exists', () => { sessionToken = 'legacy-session'; api.get('e_words_list', { uid: 58 }).subscribe(); const request = http.expectOne((candidate) => candidate.urlWithParams.includes('action=e_words_list')); expect(request.request.url).toContain(API_CONFIG.endpoint); request.flush({ retcode: 0, result: [] }); }); it('routes public vocabulary addon content without a session', () => { api.get('content_list', { modelId: 52, nid: 327 }).subscribe(); const request = http.expectOne(API_CONFIG.cloudFunctionUrl); expect(request.request.body).toEqual({ params: { action: 'content_list', modelId: 52, nid: 327 } }); request.flush({ retcode: 0, result: [] }); }); it('turns retcode -1 responses into ApiError', () => { let actual: unknown; api.post('user_login_passwd', {}).subscribe({ error: (error) => actual = error }); http.expectOne(API_CONFIG.cloudFunctionUrl).flush({ retcode: -1, retmsg: '账号或密码错误', result: null }, { status: 401, statusText: 'Unauthorized' }); http.expectOne((candidate) => candidate.urlWithParams.includes('action=user_login_passwd')).flush({ retcode: -1, retmsg: '登录失败', result: null }); expect(actual instanceof ApiError).toBeTrue(); expect((actual as ApiError).message).toBe('登录失败'); }); it('keeps a successful password login on the cloud response', () => { let actual: unknown; api.post('user_login_passwd', { name: 'tree', passwd: 'password' }).subscribe((response) => actual = response.result); http.expectOne(API_CONFIG.cloudFunctionUrl).flush({ retcode: 0, result: { userId: 42, sessionToken: 'r:token' } }); http.expectNone((candidate) => candidate.url.includes(API_CONFIG.endpoint)); expect(actual).toEqual({ userId: 42, sessionToken: 'r:token' }); }); });