@things-factory/organization 10.1.104 → 10.1.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/organization",
3
- "version": "10.1.104",
3
+ "version": "10.1.105",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -37,9 +37,9 @@
37
37
  "@operato/popup": "^10.15.0",
38
38
  "@operato/shell": "^10.0.0",
39
39
  "@things-factory/auth-base": "^10.1.79",
40
- "@things-factory/code-ui": "^10.1.104",
41
- "@things-factory/contact": "^10.1.79",
40
+ "@things-factory/code-ui": "^10.1.105",
41
+ "@things-factory/contact": "^10.1.105",
42
42
  "@things-factory/shell": "^10.1.79"
43
43
  },
44
- "gitHead": "0d05c7573220c6da2dec2654a4b86d7bd8e3c4fb"
44
+ "gitHead": "c8ad9b40d57709beefd5e3d6167fd9cab6efea3a"
45
45
  }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * A department's manager and an employee's supervisor, department and user are looked up inside the caller's domain.
3
+ *
4
+ * The lookups asked by id alone, so a mutation run in one domain could link a row of another: an employee of
5
+ * domain B as the manager of a department of domain A, a user who is not a member of A as an employee of A.
6
+ * This runs the real mutations on sqlite and reads the links back from the table.
7
+ */
8
+ import 'reflect-metadata'
9
+
10
+ const { existsSync } = require('fs')
11
+ const { join } = require('path')
12
+ const { DataSource } = require('typeorm')
13
+
14
+ const shell = require('@things-factory/shell')
15
+ const authBase = require('@things-factory/auth-base')
16
+ const contact = require('@things-factory/contact')
17
+
18
+ function loadCompiled(modulePath: string): any {
19
+ const resolved = join(__dirname, '..', 'dist-server', `${modulePath}.js`)
20
+ if (!existsSync(resolved)) {
21
+ throw new Error(`organization build output is missing: ${resolved}\nBuild it first: npm run build:server`)
22
+ }
23
+ return require(resolved)
24
+ }
25
+
26
+ let dataSource: any
27
+ let Department: any
28
+ let Employee: any
29
+ let DepartmentMutation: any
30
+ let EmployeeMutation: any
31
+
32
+ let domain: any
33
+ let other: any
34
+ let user: any
35
+ let member: any
36
+ let outsider: any
37
+ let ours: any
38
+ let theirs: any
39
+ let ourTeam: any
40
+ let theirTeam: any
41
+
42
+ beforeAll(async () => {
43
+ const organization = loadCompiled('service/index')
44
+ Department = loadCompiled('service/department/department').Department
45
+ Employee = loadCompiled('service/employee/employee').Employee
46
+ DepartmentMutation = loadCompiled('service/department/department-mutation').DepartmentMutation
47
+ EmployeeMutation = loadCompiled('service/employee/employee-mutation').EmployeeMutation
48
+
49
+ dataSource = new DataSource({
50
+ type: 'sqlite',
51
+ database: ':memory:',
52
+ synchronize: true,
53
+ dropSchema: true,
54
+ namingStrategy: new shell.NamingStrategy(),
55
+ entities: [...shell.entities, ...authBase.entities, ...contact.entities, ...organization.entities],
56
+ logging: false
57
+ })
58
+ await dataSource.initialize()
59
+ shell.addDataSource('default', dataSource)
60
+ shell.addDataSource('tx', dataSource)
61
+ })
62
+
63
+ beforeEach(async () => {
64
+ await dataSource.synchronize(true)
65
+ const domains = dataSource.getRepository(shell.Domain)
66
+ domain = await domains.save({ name: 'ours', subdomain: 'ours' })
67
+ other = await domains.save({ name: 'theirs', subdomain: 'theirs' })
68
+
69
+ const users = dataSource.getRepository(authBase.User)
70
+ user = await users.save({ name: 'tester', email: 'tester@domain-reference.test', domains: [domain] })
71
+ member = await users.save({ name: 'member', email: 'member@domain-reference.test', domains: [domain] })
72
+ outsider = await users.save({ name: 'outsider', email: 'outsider@domain-reference.test', domains: [other] })
73
+
74
+ const employees = dataSource.getRepository(Employee)
75
+ ours = await employees.save({ domain, controlNo: 'E1', name: 'ours' })
76
+ theirs = await employees.save({ domain: other, controlNo: 'E1', name: 'theirs' })
77
+
78
+ const departments = dataSource.getRepository(Department)
79
+ ourTeam = await departments.save({ domain, controlNo: 'T1', name: 'our team' })
80
+ theirTeam = await departments.save({ domain: other, controlNo: 'T1', name: 'their team' })
81
+ })
82
+
83
+ afterAll(async () => {
84
+ if (!dataSource) return
85
+ shell.removeDataSource('default')
86
+ shell.removeDataSource('tx')
87
+ await dataSource.destroy()
88
+ })
89
+
90
+ const context = () => ({ state: { domain, user, tx: dataSource.manager } })
91
+
92
+ async function managerOf(id: string) {
93
+ return (await dataSource.getRepository(Department).findOne({ where: { id }, relations: ['manager'] }))?.manager?.id ?? null
94
+ }
95
+
96
+ async function linksOf(id: string) {
97
+ const row = await dataSource.getRepository(Employee).findOne({ where: { id }, relations: ['supervisor', 'department', 'user'] })
98
+ return { supervisor: row?.supervisor?.id ?? null, department: row?.department?.id ?? null, user: row?.user?.id ?? null }
99
+ }
100
+
101
+ describe("a department's manager comes from the caller's domain", () => {
102
+ it('an employee of this domain is accepted', async () => {
103
+ const created = await new DepartmentMutation().createDepartment({ controlNo: 'T2', name: 'new', manager: { id: ours.id } }, context())
104
+ expect(await managerOf(created.id)).toBe(ours.id)
105
+ })
106
+
107
+ it('createDepartment refuses an employee of another domain', async () => {
108
+ await expect(
109
+ new DepartmentMutation().createDepartment({ controlNo: 'T2', name: 'new', manager: { id: theirs.id } }, context())
110
+ ).rejects.toThrow(/manager not found in this domain/)
111
+ expect(await dataSource.getRepository(Department).countBy({ name: 'new' })).toBe(0)
112
+ })
113
+
114
+ it('updateDepartment refuses an employee of another domain', async () => {
115
+ await expect(
116
+ new DepartmentMutation().updateDepartment(ourTeam.id, { manager: { id: theirs.id } }, context())
117
+ ).rejects.toThrow(/manager not found in this domain/)
118
+ expect(await managerOf(ourTeam.id)).toBeNull()
119
+ })
120
+
121
+ it('updateMultipleDepartment refuses it on a new row and on an edited one', async () => {
122
+ const mutation = new DepartmentMutation()
123
+ await expect(
124
+ mutation.updateMultipleDepartment([{ cuFlag: '+', controlNo: 'T2', name: 'new', manager: { id: theirs.id } }], context())
125
+ ).rejects.toThrow(/manager not found in this domain/)
126
+ await expect(
127
+ mutation.updateMultipleDepartment([{ cuFlag: 'M', id: ourTeam.id, manager: { id: theirs.id } }], context())
128
+ ).rejects.toThrow(/manager not found in this domain/)
129
+ expect(await managerOf(ourTeam.id)).toBeNull()
130
+ })
131
+
132
+ it('null still clears the manager', async () => {
133
+ await new DepartmentMutation().updateDepartment(ourTeam.id, { manager: { id: ours.id } }, context())
134
+ await new DepartmentMutation().updateDepartment(ourTeam.id, { manager: null }, context())
135
+ expect(await managerOf(ourTeam.id)).toBeNull()
136
+ })
137
+ })
138
+
139
+ describe("an employee's supervisor, department and user come from the caller's domain", () => {
140
+ it('rows of this domain, and a member of it, are accepted', async () => {
141
+ const created = await new EmployeeMutation().createEmployee(
142
+ { controlNo: 'E2', name: 'new', supervisor: { id: ours.id }, department: { id: ourTeam.id }, user: { id: member.id } },
143
+ context()
144
+ )
145
+ expect(await linksOf(created.id)).toEqual({ supervisor: ours.id, department: ourTeam.id, user: member.id })
146
+ })
147
+
148
+ it.each([
149
+ ['supervisor', () => ({ supervisor: { id: theirs.id } })],
150
+ ['department', () => ({ department: { id: theirTeam.id } })],
151
+ ['user', () => ({ user: { id: outsider.id } })]
152
+ ])('createEmployee refuses a %s of another domain', async (what, patch) => {
153
+ await expect(
154
+ new EmployeeMutation().createEmployee({ controlNo: 'E2', name: 'new', ...patch() }, context())
155
+ ).rejects.toThrow(new RegExp(`${what} not found in this domain`))
156
+ expect(await dataSource.getRepository(Employee).countBy({ controlNo: 'E2' })).toBe(0)
157
+ })
158
+
159
+ it.each([
160
+ ['supervisor', () => ({ supervisor: { id: theirs.id } })],
161
+ ['department', () => ({ department: { id: theirTeam.id } })],
162
+ ['user', () => ({ user: { id: outsider.id } })]
163
+ ])('updateEmployee refuses a %s of another domain', async (what, patch) => {
164
+ await expect(new EmployeeMutation().updateEmployee(ours.id, patch(), context())).rejects.toThrow(
165
+ new RegExp(`${what} not found in this domain`)
166
+ )
167
+ expect(await linksOf(ours.id)).toEqual({ supervisor: null, department: null, user: null })
168
+ })
169
+
170
+ it.each([
171
+ ['supervisor', () => ({ supervisor: { id: theirs.id } })],
172
+ ['department', () => ({ department: { id: theirTeam.id } })],
173
+ ['user', () => ({ user: { id: outsider.id } })]
174
+ ])('updateMultipleEmployee refuses a %s of another domain on a new row and on an edited one', async (what, patch) => {
175
+ const mutation = new EmployeeMutation()
176
+ await expect(
177
+ mutation.updateMultipleEmployee([{ cuFlag: '+', controlNo: 'E2', name: 'new', ...patch() }], context())
178
+ ).rejects.toThrow(new RegExp(`${what} not found in this domain`))
179
+ await expect(
180
+ mutation.updateMultipleEmployee([{ cuFlag: 'M', id: ours.id, ...patch() }], context())
181
+ ).rejects.toThrow(new RegExp(`${what} not found in this domain`))
182
+ expect(await linksOf(ours.id)).toEqual({ supervisor: null, department: null, user: null })
183
+ })
184
+ })