| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- import { provideHttpClient } from '@angular/common/http';
- import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
- import { TestBed, fakeAsync, tick } from '@angular/core/testing';
- import { CLOUD_FUNCTION_BASE_URL } from './admin.constants';
- import { AdminSessionService } from './admin-session.service';
- import { AppMigrationStatus } from './admin.models';
- import { CloudFunctionsService } from './cloud-functions.service';
- describe('CloudFunctionsService', () => {
- let service: CloudFunctionsService;
- let http: HttpTestingController;
- beforeEach(() => {
- TestBed.configureTestingModule({
- providers: [
- provideHttpClient(),
- provideHttpClientTesting(),
- { provide: AdminSessionService, useValue: { session: () => ({ sessionToken: 'r:admin-session' }), logout: jasmine.createSpy('logout'), invalidateSession:jasmine.createSpy('invalidateSession'), verifySessionAfterFailure:jasmine.createSpy('verifySessionAfterFailure').and.resolveTo() } },
- ],
- });
- service = TestBed.inject(CloudFunctionsService);
- http = TestBed.inject(HttpTestingController);
- });
- afterEach(() => http.verify());
- it('loads the live app migration matrix with the admin Parse session', async () => {
- const pending = service.app<AppMigrationStatus>('migration_status');
- const request = http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/app/gateway`);
- expect(request.request.body).toEqual({ token: 'r:admin-session', params: { action: 'migration_status' } });
- request.flush({ retcode: 0, retmsg: '', result: { implemented: ['app_update'], blocked: { cart_list: '目标 Schema 无购物车类' } } });
- await expectAsync(pending).toBeResolvedTo({ implemented: ['app_update'], blocked: { cart_list: '目标 Schema 无购物车类' } });
- });
- it('calls the isolated operations gateway', async () => {
- const pending = service.operations('ops/users/list', { page: 1, pageSize: 20 });
- const request = http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);
- expect(request.request.body).toEqual({ token: 'r:admin-session', params: { operation: 'ops/users/list', page: 1, pageSize: 20 } });
- request.flush({ success: true, data: { page: 1, pageSize: 20, total: 0, items: [] } });
- await expectAsync(pending).toBeResolvedTo({ page: 1, pageSize: 20, total: 0, items: [] });
- });
- it('does not log out an authenticated user for a forbidden account-management action', async () => {
- const pending=service.admin('operatorAccounts/list');
- const assertion=expectAsync(pending).toBeRejected();
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/admin/gateway`).flush({message:'只有运营经理可以管理账号'},{status:403,statusText:'Forbidden'});
- await assertion; expect(TestBed.inject(AdminSessionService).logout).not.toHaveBeenCalled();
- });
- it('does not treat an app permission denial as a lost session', async()=>{
- const pending=service.app('migration_status'),assertion=expectAsync(pending).toBeRejected();
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/app/gateway`).flush({retmsg:'需要管理员权限'},{status:403,statusText:'Forbidden'});
- await assertion;const sessions=TestBed.inject(AdminSessionService);
- expect(sessions.invalidateSession).not.toHaveBeenCalled();expect(sessions.verifySessionAfterFailure).not.toHaveBeenCalled();expect(sessions.logout).not.toHaveBeenCalled();
- });
- it('verifies a gateway 401 using the token belonging to that request', async()=>{
- const pending=service.operations('ops/live/versions'),assertion=expectAsync(pending).toBeRejected();
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).flush({message:'管理员会话已失效'},{status:401,statusText:'Unauthorized'});
- await assertion;expect(TestBed.inject(AdminSessionService).verifySessionAfterFailure).toHaveBeenCalledWith('r:admin-session');
- expect(TestBed.inject(AdminSessionService).logout).not.toHaveBeenCalled();
- });
- it('honors an explicit disabled-account response',async()=>{
- const pending=service.app('migration_status'),assertion=expectAsync(pending).toBeRejected();
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/app/gateway`).flush({retmsg:'账号已停用'},{status:403,statusText:'Forbidden'});
- await assertion;expect(TestBed.inject(AdminSessionService).invalidateSession).toHaveBeenCalledWith('r:admin-session');
- });
- it('releases a hanging write after 20 seconds without retrying or logging out',fakeAsync(()=>{
- let failure:unknown;service.operations('ops/payroll/create',{idempotencyKey:'keep-this-key'}).catch(e=>failure=e);
- const req=http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);tick(20001);expect(req.cancelled).toBeTrue();expect(String(failure)).toContain('结果待核对');http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);expect(TestBed.inject(AdminSessionService).logout).not.toHaveBeenCalled();
- }));
- it('allows bounded vocabulary import calls to finish after the ordinary 20 second limit',fakeAsync(()=>{
- let result:unknown;service.operations('ops/vocabulary/import-preview').then(value=>result=value);
- const req=http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);tick(27500);expect(req.cancelled).toBeFalse();req.flush({success:true,data:{batchId:'batch'}});tick();expect(result).toEqual({batchId:'batch'});
- }));
- it('recovers one interrupted calendar read without losing the session',fakeAsync(()=>{
- let result:unknown;service.operations('ops/schedule/calendar').then(v=>result=v);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).error(new ProgressEvent('error'));
- tick(500);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).flush({success:true,data:{appointments:[]}});
- tick();expect(result).toEqual({appointments:[]});
- expect(TestBed.inject(AdminSessionService).verifySessionAfterFailure).not.toHaveBeenCalled();
- }));
- it('bounds retries for a temporarily unavailable member list and hides transport internals',fakeAsync(()=>{
- let failure:Error|undefined;service.operations('ops/users/list').catch(e=>failure=e);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).flush('unavailable',{status:503,statusText:'Service Unavailable'});
- tick(500);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).error(new ProgressEvent('error'));
- tick(1000);http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);
- expect(failure?.message).toContain('加载');expect(failure?.message).not.toContain('https://');expect(failure?.message).not.toContain('结果待核对');
- }));
- for(const operation of ['ops/appointments/create','ops/users/reset-password','ops/member-credits/post','ops/unknown/action']){
- it(`does not replay an uncertain write: ${operation}`,fakeAsync(()=>{
- let failure:Error|undefined;service.operations(operation,{idempotencyKey:'same-command'}).catch(e=>failure=e);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).error(new ProgressEvent('error'));
- tick(1000);http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);
- expect(failure?.message).toContain('结果待核对');expect(failure?.message).not.toContain('Unknown Error');
- }));
- }
- it('does not retry a slow read and labels it as a load failure',fakeAsync(()=>{
- let failure:Error|undefined;service.operations('ops/schedule/calendar').catch(e=>failure=e);
- const request=http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);tick(21000);
- expect(request.cancelled).toBeTrue();http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);
- expect(failure?.message).toContain('加载');expect(failure?.message).not.toContain('结果待核对');
- }));
- it('cannot turn a safe read into a retried write through params',fakeAsync(()=>{
- let result:unknown;service.operations('ops/users/list',{operation:'ops/users/create'}).then(v=>result=v);
- const request=http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);
- expect(request.request.body.params.operation).toBe('ops/users/list');
- request.flush({success:true,data:[]});tick();expect(result).toEqual([]);
- }));
- it('does not retry with an old session when the user logs out during backoff',fakeAsync(()=>{
- let failure:unknown;service.operations('ops/users/list').catch(e=>failure=e);
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).error(new ProgressEvent('error'));
- spyOn(TestBed.inject(AdminSessionService),'session').and.returnValue(null);
- tick(500);http.expectNone(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);expect(failure).toBeDefined();
- }));
- it('coalesces batch mutation notifications until the batch ends',async()=>{
- let changes=0;service.dataChanges$.subscribe(()=>changes++);
- await service.batchDataChanges(async()=>{
- for(let i=0;i<3;i++){
- const pending=service.operations('ops/appointments/cancel');
- http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`).flush({success:true,data:{}});
- await pending;expect(changes).toBe(0);
- }
- });
- expect(changes).toBe(1);
- });
- it('announces one final reconciliation even when a whole batch fails',async()=>{
- let changes=0;service.dataChanges$.subscribe(()=>changes++);
- await expectAsync(service.batchDataChanges(async()=>{throw Error('uncertain');})).toBeRejectedWithError('uncertain');
- expect(changes).toBe(1);
- });
- });
- describe('课时查询与记账区分',()=>{
- for(const operation of ['ops/users/credit-preview','ops/users/credit-accounts','ops/users/credit-ledger','ops/users/credit-status'])it(`${operation} 超时不提示已经提交`,fakeAsync(()=>{TestBed.configureTestingModule({providers:[CloudFunctionsService,provideHttpClient(),provideHttpClientTesting(),{provide:AdminSessionService,useValue:{session:()=>({sessionToken:'token'})}}]});const service=TestBed.inject(CloudFunctionsService),http=TestBed.inject(HttpTestingController);let message='';service.operations(operation).catch(e=>message=e.message);http.expectOne(`${CLOUD_FUNCTION_BASE_URL}/xiaoshu/ops/gateway-v3`);tick(20001);expect(message).toContain('加载');expect(message).not.toContain('提交结果');http.verify();}));
- });
|