| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- import { ComponentFixture, TestBed } from '@angular/core/testing';
- import { ActivatedRoute } from '@angular/router';
- import { of } from 'rxjs';
- import { AdminFieldSchema } from '../admin.models';
- import { CloudFunctionsService } from '../cloud-functions.service';
- import { AdminResourceComponent } from './admin-resource.component';
- describe('AdminResourceComponent', () => {
- let fixture: ComponentFixture<AdminResourceComponent>;
- let component: AdminResourceComponent;
- const functions = { admin: jasmine.createSpy('admin') };
- const objectField: AdminFieldSchema = { name: 'payload', type: 'Object', writable: true };
- beforeEach(async () => {
- functions.admin.calls.reset();
- await TestBed.configureTestingModule({
- imports: [AdminResourceComponent],
- providers: [
- { provide: ActivatedRoute, useValue: { paramMap: of({ get: () => 'CommonModel' }) } },
- { provide: CloudFunctionsService, useValue: functions },
- ],
- }).compileComponents();
- fixture = TestBed.createComponent(AdminResourceComponent);
- component = fixture.componentInstance;
- });
- it('keeps invalid JSON text visible and blocks saving it as a string', async () => {
- component.setField(objectField, '{invalid');
- expect(component.inputValue(objectField)).toBe('{invalid');
- expect(component.fieldErrors()['payload']).toContain('JSON 对象');
- await component.save();
- expect(functions.admin).not.toHaveBeenCalled();
- expect(component.error()).toContain('payload');
- });
- it('accepts a valid JSON object as structured data', () => {
- component.setField(objectField, '{"enabled":true}');
- expect(component.fieldErrors()['payload']).toBeUndefined();
- expect(component.draft()['payload']).toEqual({ enabled: true });
- });
- it('keeps preferred columns first and rounds page counts upward', () => {
- component.schema.set({
- className: 'CommonModel', label: '内容', writable: true, supportsSoftDelete: true,
- fields: [
- { name: 'misc', type: 'String', writable: true },
- { name: 'title', type: 'String', writable: true },
- { name: 'objectId', type: 'String', writable: false },
- ],
- });
- component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 21, results: [] });
- expect(component.columns().map((field) => field.name)).toEqual(['objectId', 'title', 'misc']);
- expect(component.pageCount()).toBe(2);
- });
- it('uses the dedicated cloud operation to reset an existing user password', async () => {
- functions.admin.and.resolveTo({ objectId: 'user-object' });
- component.className.set('_User');
- component.draftId.set('user-object');
- component.newPassword.set('SafePass123');
- component.confirmPassword.set('SafePass123');
- await component.resetPassword();
- expect(functions.admin).toHaveBeenCalledWith('resetPassword', { className: '_User', objectId: 'user-object', newPassword: 'SafePass123' });
- expect(component.passwordMessage()).toContain('已安全重置');
- });
- it('uses the dedicated ordinary-account operation without generic role fields', async () => {
- component.className.set('_User');
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- component.createUsername.set('student-1001');
- component.createPassword.set('SafePass123');
- component.createConfirmPassword.set('SafePass123');
- component.createDisplayName.set('新学员');
- component.createMobile.set('13800138000');
- functions.admin.and.callFake(async (operation: string) => operation === 'createUser'
- ? { objectId: 'new-user', userId: 1001, groupId: 1 }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.createUser();
- expect(functions.admin.calls.first().args).toEqual(['createUser', {
- username: 'student-1001', password: 'SafePass123', displayName: '新学员', mobile: '13800138000',
- }]);
- expect(component.editorOpen()).toBeFalse();
- });
- it('rejects mismatched account passwords before calling the cloud function', async () => {
- component.className.set('_User');
- component.createUsername.set('student-1002');
- component.createPassword.set('SafePass123');
- component.createConfirmPassword.set('Different123');
- await component.createUser();
- expect(functions.admin).not.toHaveBeenCalled();
- expect(component.createUserError()).toContain('不一致');
- });
- it('uses the protected batch operation to lock a user and reports revoked sessions', async () => {
- spyOn(window, 'confirm').and.returnValue(true);
- component.className.set('_User');
- component.draftId.set('member-object');
- component.draft.set({ objectId: 'member-object', isDisabled: false });
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
- ? { action: 'lock', updated: 1, revokedSessions: 2, results: [{ objectId: 'member-object', isDisabled: true }] }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.setUserDisabled(true);
- expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'lock', objectIds: ['member-object'], reason: '' }]);
- expect(component.draft()['isDisabled']).toBeTrue();
- expect(component.userActionMessage()).toContain('2 个会话');
- });
- it('moves a user through the dedicated group synchronization operation', async () => {
- component.className.set('_User');
- component.draftId.set('member-object');
- component.draft.set({ objectId: 'member-object', legacyGroupId: 1 });
- component.schema.set({ className: '_User', label: '用户管理', writable: true, supportsSoftDelete: true, fields: [] });
- component.selectedUserGroupId.set(3);
- functions.admin.and.callFake(async (operation: string) => operation === 'userBatch'
- ? { action: 'move', updated: 1, revokedSessions: 0, results: [{ objectId: 'member-object', legacyGroupId: 3, legacyUserData: { GroupID: 3 } }] }
- : { className: '_User', page: 1, pageSize: 20, total: 1, results: [] });
- await component.moveUserGroup();
- expect(functions.admin.calls.first().args).toEqual(['userBatch', { className: '_User', action: 'move', objectIds: ['member-object'], groupId: 3 }]);
- expect(component.draft()['legacyGroupId']).toBe(3);
- });
- it('routes group creation through the atomic group operation', async () => {
- component.className.set('Group');
- component.schema.set({ className: 'Group', label: '用户组', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ groupName: '普通会员', parentGroupId: 0, regSelect: true });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGroup'
- ? { objectId: 'group-object', groupId: 1, groupName: '普通会员' }
- : { className: 'Group', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGroup', {
- className: 'Group', objectId: undefined, fields: { groupName: '普通会员', parentGroupId: 0, regSelect: true },
- }]);
- });
- it('routes node creation through the structural cloud operation', async () => {
- component.className.set('Node');
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 });
- component.targetNodeParentId.set(16);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveNode'
- ? { objectId: 'node-object', nodeId: 401, nodeName: '新栏目', parentId: 16 }
- : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveNode', {
- className: 'Node', objectId: undefined, fields: { nodeName: '新栏目', nodeDir: 'new-node', parentId: 999, nodeId: 999 }, parentId: 16,
- }]);
- });
- it('moves a node through the cycle-protected batch operation', async () => {
- component.className.set('Node');
- component.draftId.set('node-object');
- component.draft.set({ objectId: 'node-object', nodeId: 40, parentId: 16, zstatus: 99 });
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.targetNodeParentId.set(29);
- functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
- ? { action: 'move', updated: 1, results: [{ objectId: 'node-object', nodeId: 40, parentId: 29, zstatus: 99 }] }
- : { className: 'Node', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyNodeAction('move');
- expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'move', objectIds: ['node-object'], parentId: 29 }]);
- expect(component.draft()['parentId']).toBe(29);
- expect(component.nodeActionMessage()).toContain('29');
- });
- it('routes special creation through the structural cloud operation', async () => {
- component.className.set('Special');
- component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
- component.draft.set({ specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 });
- component.targetSpecialParentId.set(1);
- functions.admin.and.callFake(async (operation: string) => operation === 'saveSpecial'
- ? { objectId: 'special-object', specId: 10, specName: '新专题', pid: 1 }
- : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveSpecial', {
- className: 'Special', objectId: undefined, fields: { specName: '新专题', specDir: 'new-special', specId: 999, pid: 999 }, pid: 1,
- }]);
- });
- it('moves a special through the dedicated batch operation', async () => {
- component.className.set('Special');
- component.draftId.set('special-object');
- component.draft.set({ objectId: 'special-object', specId: 10, pid: 1 });
- component.schema.set({ className: 'Special', label: '专题', writable: true, supportsSoftDelete: false, fields: [] });
- component.targetSpecialParentId.set(0);
- functions.admin.and.callFake(async (operation: string) => operation === 'specialBatch'
- ? { action: 'move', updated: 1, results: [{ objectId: 'special-object', specId: 10, pid: 0 }] }
- : { className: 'Special', page: 1, pageSize: 20, total: 1, results: [] });
- await component.moveSpecial();
- expect(functions.admin.calls.first().args).toEqual(['specialBatch', { className: 'Special', action: 'move', objectIds: ['special-object'], pid: 0 }]);
- expect(component.specialActionMessage()).toContain('0');
- });
- it('updates existing model metadata through the protected operation', async () => {
- component.className.set('Model');
- component.draftId.set('model-object');
- component.schema.set({ className: 'Model', label: '内容模型', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveModelMetadata'
- ? { objectId: 'model-object', modelId: 2, modelName: '文章模型', description: '安全说明' }
- : { className: 'Model', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveModelMetadata', {
- className: 'Model', objectId: 'model-object', fields: { objectId: 'model-object', modelId: 2, tableName: 'ZL_C_Article', modelName: '文章模型', description: '安全说明' },
- }]);
- });
- it('updates existing model-field presentation metadata without structural writes', async () => {
- component.className.set('ModelField');
- component.draftId.set('field-object');
- component.schema.set({ className: 'ModelField', label: '模型字段', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveModelFieldMetadata'
- ? { objectId: 'field-object', fieldId: 3, fieldAlias: '作者' }
- : { className: 'ModelField', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveModelFieldMetadata', {
- className: 'ModelField', objectId: 'field-object', fields: { objectId: 'field-object', fieldId: 3, modelId: 2, fieldName: 'author', fieldType: 'TextType', fieldAlias: '作者' },
- }]);
- });
- it('updates an existing guestbook entry through the dedicated editor', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- component.draft.set({ objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' });
- functions.admin.and.callFake(async (operation: string) => operation === 'saveGuestbook'
- ? { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' }
- : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
- await component.save();
- expect(functions.admin.calls.first().args).toEqual(['saveGuestbook', {
- className: 'Guestbook', objectId: 'guest-object', fields: { objectId: 'guest-object', gid: 10, status: 99, title: '留言标题', tcontent: '正文' },
- }]);
- });
- it('audits a guestbook entry through the workflow operation', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.draft.set({ objectId: 'guest-object', gid: 10, status: 0 });
- component.schema.set({ className: 'Guestbook', label: '留言', writable: true, creatable: false, supportsSoftDelete: false, fields: [] });
- functions.admin.and.callFake(async (operation: string) => operation === 'guestbookBatch'
- ? { action: 'audit', updated: 1, results: [{ objectId: 'guest-object', gid: 10, status: 99 }] }
- : { className: 'Guestbook', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyGuestbookAction('audit');
- expect(functions.admin.calls.first().args).toEqual(['guestbookBatch', { className: 'Guestbook', action: 'audit', objectIds: ['guest-object'] }]);
- expect(component.selectedGuestbookStatus()).toBe(99);
- });
- it('publishes an admin reply through the parent-safe cloud operation', async () => {
- component.className.set('Guestbook');
- component.draftId.set('guest-object');
- component.guestbookReplyTitle.set('回复标题');
- component.guestbookReplyContent.set('回复正文');
- functions.admin.and.resolveTo({ objectId: 'reply-object', gid: 11, parentid: 10, status: 99 });
- await component.replyGuestbook();
- expect(functions.admin).toHaveBeenCalledWith('guestbookReply', { className: 'Guestbook', parentObjectId: 'guest-object', title: '回复标题', content: '回复正文' });
- expect(component.guestbookActionMessage()).toContain('已发布');
- });
- it('updates content workflow state through the dedicated batch operation', async () => {
- component.className.set('CommonModel');
- component.draftId.set('content-object');
- component.draft.set({ objectId: 'content-object', status: 0, nodeId: 29 });
- component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
- component.selectedContentStatus.set(99);
- functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
- ? { action: 'status', updated: 1, results: [{ objectId: 'content-object', status: 99, nodeId: 29 }] }
- : { className: 'CommonModel', page: 1, pageSize: 20, total: 1, results: [] });
- await component.applyContentAction('status');
- expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-object'], status: 99 }]);
- expect(component.contentStatusValue()).toBe(99);
- expect(component.contentActionMessage()).toContain('已审核');
- });
- it('selects and clears all records on the current page', () => {
- component.className.set('CommonModel');
- component.page.set({ className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [{ objectId: 'content-a' }, { objectId: 'content-b' }] });
- component.togglePageSelection(true);
- expect(component.selectedIds()).toEqual(['content-a', 'content-b']);
- expect(component.allPageSelected()).toBeTrue();
- component.toggleSelection('content-a', false);
- expect(component.selectedIds()).toEqual(['content-b']);
- expect(component.allPageSelected()).toBeFalse();
- });
- it('sends all selected content ids through the bulk workflow operation', async () => {
- component.className.set('CommonModel');
- component.schema.set({ className: 'CommonModel', label: '内容', writable: true, creatable: false, supportsSoftDelete: true, fields: [] });
- component.selectedIds.set(['content-a', 'content-b']);
- component.selectedContentStatus.set(99);
- functions.admin.and.callFake(async (operation: string) => operation === 'contentBatch'
- ? { action: 'status', updated: 2, results: [] }
- : { className: 'CommonModel', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkContentAction('status');
- expect(functions.admin.calls.first().args).toEqual(['contentBatch', { className: 'CommonModel', action: 'status', objectIds: ['content-a', 'content-b'], status: 99 }]);
- expect(component.bulkMessage()).toContain('2 条内容');
- expect(component.selectedIds()).toEqual([]);
- });
- it('recovers all selected nodes through the dedicated batch operation', async () => {
- component.className.set('Node');
- component.schema.set({ className: 'Node', label: '栏目节点', writable: true, supportsSoftDelete: false, fields: [] });
- component.selectedIds.set(['node-a', 'node-b']);
- functions.admin.and.callFake(async (operation: string) => operation === 'nodeBatch'
- ? { action: 'recover', updated: 2, results: [] }
- : { className: 'Node', page: 1, pageSize: 20, total: 2, results: [] });
- await component.applyBulkNodeAction('recover');
- expect(functions.admin.calls.first().args).toEqual(['nodeBatch', { className: 'Node', action: 'recover', objectIds: ['node-a', 'node-b'] }]);
- expect(component.bulkMessage()).toContain('2 个栏目');
- });
- });
|