刘光辉
3 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import type { FilePermissionPolicy } from './filePermissions';
 
import { reactive } from 'vue';
 
import { describe, expect, it } from 'vitest';
 
import {
  filePermissionDesignerContext,
  isEditableFilePermissionPolicy,
  retainFilePermissions,
  validateDesignerFilePermissions,
  validateFilePermissionPolicy,
} from './filePermissions';
 
const fields = { form: new Set(['files', 'rows-files']) };
const nodes = new Set(['node']);
function policy(): FilePermissionPolicy {
  return {
    version: 1,
    rules: [
      {
        id: 'r1',
        formId: 'form',
        field: 'rows-files',
        subject: 'PARTICIPANTS',
        trigger: 'ENTERED',
        triggerNodeId: 'node',
        onReturn: 'KEEP',
        actions: { VIEW: 'ALLOW', DOWNLOAD: 'DENY' },
      },
    ],
  };
}
 
describe('workflow file permission configuration', () => {
  it('keeps same-form reactive upload configuration without sharing mutable state', () => {
    const previous = reactive({ jnpfKey: 'uploadFile', filePermissions: { DOWNLOAD: 'DENY' } });
    const next = retainFilePermissions(previous, { __config__: { jnpfKey: 'uploadFile' } }, true);
    expect(next.filePermissions).toEqual({ DOWNLOAD: 'DENY' });
    next.filePermissions!.DOWNLOAD = 'ALLOW';
    expect(previous.filePermissions.DOWNLOAD).toBe('DENY');
  });
  it('does not carry permissions across forms or component types', () => {
    const previous = { jnpfKey: 'uploadFile', filePermissions: { DOWNLOAD: 'DENY' } };
    expect(retainFilePermissions(previous, { __config__: { jnpfKey: 'uploadFile' } }, false)).toEqual({});
    expect(retainFilePermissions(previous, { __config__: { jnpfKey: 'input' } }, true)).toEqual({});
    expect(retainFilePermissions({ ...previous, jnpfKey: 'input' }, { __config__: { jnpfKey: 'uploadFile' } }, true)).toEqual({});
  });
  it('retains malformed configuration for validation instead of dropping it', () => {
    expect(retainFilePermissions({ jnpfKey: 'uploadFile', filePermissions: null }, { __config__: { jnpfKey: 'uploadFile' } }, true)).toEqual({
      filePermissions: null,
    });
  });
  it('does not add configuration to legacy nodes', () => {
    expect(retainFilePermissions(undefined, { __config__: { jnpfKey: 'uploadFile' } }, true)).toEqual({});
  });
  it('round-trips persistent child-field rules through JSON', () => {
    expect(validateFilePermissionPolicy(structuredClone(policy()), fields, nodes)).toEqual([]);
  });
  it('rejects deleted fields and nodes', () => {
    expect(validateFilePermissionPolicy(policy(), { form: new Set(['files']) }, nodes)).toHaveLength(1);
    expect(validateFilePermissionPolicy(policy(), fields, new Set())).toHaveLength(1);
  });
  it('requires explicit behavior after return', () => {
    const value = policy();
    delete value.rules[0]!.onReturn;
    expect(validateFilePermissionPolicy(value, fields, nodes)).toHaveLength(1);
  });
  it('rejects duplicate rules and malformed actions', () => {
    const value = policy();
    value.rules.push({ ...value.rules[0]! });
    expect(validateFilePermissionPolicy(value, fields, nodes)).toHaveLength(1);
    (value.rules[0]!.actions as any).DOWNLOAD = true;
    expect(validateFilePermissionPolicy(value, fields, nodes).length).toBeGreaterThan(1);
  });
  it('validates subject nodes and version without silently resetting permissions', () => {
    const value = policy();
    value.rules[0]!.subject = 'NODE_PARTICIPANTS';
    expect(validateFilePermissionPolicy(value, fields, nodes)).toHaveLength(1);
    value.rules[0]!.subjectNodeId = 'node';
    expect(validateFilePermissionPolicy(value, fields, nodes)).toEqual([]);
    expect(validateFilePermissionPolicy({ ...value, version: 2 } as any, fields, nodes)).toHaveLength(1);
  });
  it('accepts the mutually exclusive non-current-handler subject without a subject node', () => {
    const value = policy();
    value.rules[0]!.subject = 'NON_CURRENT_HANDLERS';
    expect(validateFilePermissionPolicy(value, fields, nodes)).toEqual([]);
  });
});
 
describe('designer file permission boundary', () => {
  const live = new Set(['gateway', 'node', 'start']);
  function data(): any {
    return {
      global: {
        allFormMap: {
          form_form: [
            { id: 'files', fullName: '主文件', __config__: { jnpfKey: 'uploadFile' } },
            { id: 'rows-files', fullName: '记录-文件', __config__: { jnpfKey: 'uploadFile' } },
            { id: 'name', __config__: { jnpfKey: 'input' } },
          ],
          form_foreign: [{ id: 'files', __config__: { jnpfKey: 'uploadFile' } }],
        },
      },
      start: { type: 'start', formId: 'form', formName: '审核表单' },
      node: { type: 'approver', nodeName: '确认' },
      gateway: { type: 'gateway', formId: 'foreign' },
      deleted: { type: 'approver', formId: 'foreign' },
    };
  }
  it('offers native main/child upload fields only from live human node forms', () => {
    const context = filePermissionDesignerContext(data(), live);
    expect(context.attachments.map((item) => [item.formId, item.field])).toEqual([
      ['form', 'files'],
      ['form', 'rows-files'],
    ]);
    expect(context.nodeIds).toEqual(new Set(['node', 'start']));
    expect(context.attachments[1]!.label).toBe('审核表单 / 记录-文件');
  });
  it('keeps unconfigured legacy flows of every type unchanged', () => {
    const value = data();
    const original = JSON.stringify(value);
    for (const type of [0, 1, 2, 3]) expect(validateDesignerFilePermissions(value, live, type)).toEqual([]);
    expect(JSON.stringify(value)).toBe(original);
  });
  it('validates native serialized config and blocks stale fields after a form switch', () => {
    const value = data();
    value.global.filePermissionPolicy = policy();
    expect(validateDesignerFilePermissions(structuredClone(value), live, 0)).toEqual([]);
    value.start.formId = 'foreign';
    expect(validateDesignerFilePermissions(value, live, 0)).toContain('附件字段不存在:r1');
  });
  it('rejects duplicate IDs across nodes but ignores removed nodes', () => {
    const value = data();
    value.global.filePermissionPolicy = policy();
    value.deleted.filePermissionPolicy = policy();
    expect(validateDesignerFilePermissions(value, live, 1)).toEqual([]);
    value.node.filePermissionPolicy = policy();
    expect(validateDesignerFilePermissions(value, live, 1)).toContain('文件规则 ID 重复:r1');
  });
  it('blocks unsupported flow types and reserved rule IDs while accepting return release', () => {
    const value = data();
    value.global.filePermissionPolicy = policy();
    expect(validateDesignerFilePermissions(value, live, 3)).toContain('当前流程类型暂不支持文件权限规则');
    value.global.filePermissionPolicy.rules[0].onReturn = 'RELEASE';
    value.global.filePermissionPolicy.rules[0].id = '@field/node/files';
    expect(validateDesignerFilePermissions(value, live, 0)).toEqual(['文件规则使用了保留标识']);
  });
  it('rejects malformed data without mutating or throwing', () => {
    for (const broken of [
      { version: 2, rules: [] },
      { version: 1, rules: [null] },
      { version: 1, rules: [{ id: 'r1', actions: null }] },
    ]) {
      const value = data();
      value.global.filePermissionPolicy = broken;
      const before = JSON.stringify(value);
      expect(isEditableFilePermissionPolicy(broken)).toBe(false);
      expect(validateDesignerFilePermissions(value, live, 0).length).toBeGreaterThan(0);
      expect(JSON.stringify(value)).toBe(before);
    }
  });
});