@things-factory/auth-base 10.0.0 → 10.0.4

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.
Files changed (36) hide show
  1. package/dist-server/index.d.ts +3 -0
  2. package/dist-server/index.js +3 -0
  3. package/dist-server/index.js.map +1 -1
  4. package/dist-server/middlewares/domain-authenticate-middleware.js +26 -29
  5. package/dist-server/middlewares/domain-authenticate-middleware.js.map +1 -1
  6. package/dist-server/service/domain-generator/domain-generator-mutation.js +8 -4
  7. package/dist-server/service/domain-generator/domain-generator-mutation.js.map +1 -1
  8. package/dist-server/service/index.d.ts +1 -1
  9. package/dist-server/service/user/user-mutation.js +9 -5
  10. package/dist-server/service/user/user-mutation.js.map +1 -1
  11. package/dist-server/service/user/user-query.js +4 -1
  12. package/dist-server/service/user/user-query.js.map +1 -1
  13. package/dist-server/service/user/user.d.ts +12 -0
  14. package/dist-server/service/user/user.js +22 -9
  15. package/dist-server/service/user/user.js.map +1 -1
  16. package/dist-server/tsconfig.tsbuildinfo +1 -1
  17. package/dist-server/utils/can-user-checkin-domain.d.ts +15 -0
  18. package/dist-server/utils/can-user-checkin-domain.js +28 -0
  19. package/dist-server/utils/can-user-checkin-domain.js.map +1 -0
  20. package/dist-server/utils/checkin-domain-facts.d.ts +37 -0
  21. package/dist-server/utils/checkin-domain-facts.js +223 -0
  22. package/dist-server/utils/checkin-domain-facts.js.map +1 -0
  23. package/dist-server/utils/checkin-domain-rule.d.ts +72 -0
  24. package/dist-server/utils/checkin-domain-rule.js +76 -0
  25. package/dist-server/utils/checkin-domain-rule.js.map +1 -0
  26. package/dist-server/utils/domain-ownership.d.ts +36 -0
  27. package/dist-server/utils/domain-ownership.js +64 -0
  28. package/dist-server/utils/domain-ownership.js.map +1 -0
  29. package/dist-server/utils/get-user-domains.d.ts +16 -0
  30. package/dist-server/utils/get-user-domains.js +47 -89
  31. package/dist-server/utils/get-user-domains.js.map +1 -1
  32. package/package.json +4 -4
  33. package/spec/unit/checkin-domain.spec.ts +307 -0
  34. package/spec/unit/checkin-privilege.spec.ts +205 -0
  35. package/spec/unit/domain-ownership.spec.ts +188 -0
  36. package/tests/checkin-domain-rule.test.ts +128 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * 권한 판정의 도메인 상속 — 실제 엔티티·실제 쿼리 검증.
3
+ *
4
+ * 역할은 부모 도메인으로부터 상속되므로, 상속 역할을 부여받아 자식 도메인에 체크인한 사용자는
5
+ * 그 역할의 권한을 자식 도메인에서 행사할 수 있어야 한다. 체크인만 열리고 권한이 0 이면
6
+ * 상속 부여는 무의미하다.
7
+ *
8
+ * 주의: 이 스펙은 빌드 산출물(`dist-server`)을 대상으로 한다.
9
+ */
10
+
11
+ import { DataSource } from 'typeorm'
12
+
13
+ import { NamingStrategy, addDataSource, entities as shellEntities, Domain } from '@things-factory/shell'
14
+ import {
15
+ DomainOwner,
16
+ Privilege,
17
+ Role,
18
+ User,
19
+ entities as authEntities,
20
+ getDomainsWithPrivilege
21
+ } from '@things-factory/auth-base'
22
+
23
+ let dataSource: DataSource
24
+
25
+ async function domain(name: string, parent?: Domain): Promise<Domain> {
26
+ return await dataSource.getRepository(Domain).save({ name, subdomain: name, parent } as any)
27
+ }
28
+
29
+ async function privilege(name: string, category: string): Promise<Privilege> {
30
+ return await dataSource.getRepository(Privilege).save({ name, category } as any)
31
+ }
32
+
33
+ async function role(name: string, roleDomain: Domain, privileges: Privilege[]): Promise<Role> {
34
+ return await dataSource.getRepository(Role).save({ name, domain: roleDomain, privileges } as any)
35
+ }
36
+
37
+ async function user(name: string, domains: Domain[], roles: Role[]): Promise<User> {
38
+ return await dataSource.getRepository(User).save({
39
+ name,
40
+ email: `${name}@test.com`,
41
+ username: name,
42
+ domains,
43
+ roles
44
+ } as any)
45
+ }
46
+
47
+ async function reload(target: Domain): Promise<Domain> {
48
+ return await dataSource.getRepository(Domain).findOneBy({ id: target.id })
49
+ }
50
+
51
+ async function reloadUser(target: User): Promise<User> {
52
+ return await dataSource.getRepository(User).findOne({ where: { id: target.id }, relations: ['domains'] })
53
+ }
54
+
55
+ beforeAll(async () => {
56
+ dataSource = new DataSource({
57
+ type: 'sqlite',
58
+ database: ':memory:',
59
+ synchronize: true,
60
+ dropSchema: true,
61
+ namingStrategy: new NamingStrategy(),
62
+ entities: [...shellEntities, ...authEntities],
63
+ logging: false
64
+ })
65
+
66
+ await dataSource.initialize()
67
+ addDataSource('default', dataSource)
68
+ })
69
+
70
+ afterAll(async () => {
71
+ await dataSource?.destroy()
72
+ })
73
+
74
+ beforeEach(async () => {
75
+ for (const junction of dataSource.entityMetadatas.filter(metadata => metadata.isJunction)) {
76
+ await dataSource.query(`DELETE FROM "${junction.tableName}"`)
77
+ }
78
+
79
+ for (const target of [DomainOwner, Role, Privilege, User, Domain]) {
80
+ await dataSource.query(`DELETE FROM "${dataSource.getMetadata(target).tableName}"`)
81
+ }
82
+ })
83
+
84
+ describe('User.hasPrivilege — 도메인 상속', () => {
85
+ it('자기 도메인에 정의된 역할의 권한을 행사한다', async () => {
86
+ const target = await domain('acme')
87
+ const query = await privilege('query', 'board')
88
+ const operator = await role('operator', target, [query])
89
+ const member = await user('kim', [target], [operator])
90
+
91
+ expect(await User.hasPrivilege('query', 'board', await reload(target), member)).toBe(true)
92
+ })
93
+
94
+ it('부모 도메인에 정의된 상속 역할의 권한을 자식 도메인에서 행사한다', async () => {
95
+ const parent = await domain('parent')
96
+ const child = await domain('child', parent)
97
+ const query = await privilege('query', 'board')
98
+ const inherited = await role('operator', parent, [query])
99
+ const member = await user('kim', [child], [inherited])
100
+
101
+ expect(await User.hasPrivilege('query', 'board', await reload(child), member)).toBe(true)
102
+ })
103
+
104
+ it('무관한 도메인에 정의된 역할의 권한은 행사하지 못한다', async () => {
105
+ const target = await domain('acme')
106
+ const other = await domain('other')
107
+ const query = await privilege('query', 'board')
108
+ const otherRole = await role('operator', other, [query])
109
+ const member = await user('kim', [target], [otherRole])
110
+
111
+ expect(await User.hasPrivilege('query', 'board', await reload(target), member)).toBe(false)
112
+ })
113
+
114
+ it('역할이 갖지 않은 권한은 행사하지 못한다', async () => {
115
+ const parent = await domain('parent')
116
+ const child = await domain('child', parent)
117
+ const query = await privilege('query', 'board')
118
+ await privilege('mutation', 'board')
119
+ const inherited = await role('viewer', parent, [query])
120
+ const member = await user('kim', [child], [inherited])
121
+
122
+ expect(await User.hasPrivilege('mutation', 'board', await reload(child), member)).toBe(false)
123
+ })
124
+
125
+ it('조부모 도메인의 역할까지 거슬러 올라가지는 않는다 (상속은 1단)', async () => {
126
+ const grandParent = await domain('grand')
127
+ const parent = await domain('parent', grandParent)
128
+ const child = await domain('child', parent)
129
+ const query = await privilege('query', 'board')
130
+ const grandRole = await role('operator', grandParent, [query])
131
+ const member = await user('kim', [child], [grandRole])
132
+
133
+ expect(await User.hasPrivilege('query', 'board', await reload(child), member)).toBe(false)
134
+ })
135
+ })
136
+
137
+ describe('User.getPrivilegesByDomain — 도메인 상속', () => {
138
+ it('상속 역할의 권한이 목록에 포함된다', async () => {
139
+ const parent = await domain('parent')
140
+ const child = await domain('child', parent)
141
+ const inheritedPrivilege = await privilege('query', 'board')
142
+ const ownPrivilege = await privilege('mutation', 'worklist')
143
+ const inherited = await role('operator', parent, [inheritedPrivilege])
144
+ const own = await role('editor', child, [ownPrivilege])
145
+ const member = await user('kim', [child], [inherited, own])
146
+
147
+ const privileges = await User.getPrivilegesByDomain(member, await reload(child))
148
+
149
+ expect(privileges).toEqual(
150
+ expect.arrayContaining([
151
+ { category: 'board', privilege: 'query' },
152
+ { category: 'worklist', privilege: 'mutation' }
153
+ ])
154
+ )
155
+ })
156
+
157
+ it('무관한 도메인 역할의 권한은 목록에서 제외된다', async () => {
158
+ const target = await domain('acme')
159
+ const other = await domain('other')
160
+ const otherPrivilege = await privilege('query', 'secret')
161
+ const otherRole = await role('operator', other, [otherPrivilege])
162
+ const member = await user('kim', [target, other], [otherRole])
163
+
164
+ const privileges = await User.getPrivilegesByDomain(member, await reload(target))
165
+
166
+ expect(privileges).toEqual([])
167
+ })
168
+ })
169
+
170
+ describe('getDomainsWithPrivilege — 부여 도메인 + 상속', () => {
171
+ it('상속 역할로 권한을 가진 자식 도메인을 반환한다', async () => {
172
+ const parent = await domain('parent')
173
+ const child = await domain('child', parent)
174
+ const query = await privilege('query', 'board')
175
+ const inherited = await role('operator', parent, [query])
176
+ const member = await user('kim', [child], [inherited])
177
+
178
+ const domains = await getDomainsWithPrivilege(await reloadUser(member), 'query', 'board')
179
+
180
+ expect(domains.map(d => d.subdomain)).toEqual(['child'])
181
+ })
182
+
183
+ it('권한 없는 역할만 부여된 도메인은 제외한다', async () => {
184
+ const withPrivilege = await domain('with')
185
+ const without = await domain('without')
186
+ const query = await privilege('query', 'board')
187
+ const granted = await role('operator', withPrivilege, [query])
188
+ const plain = await role('guest', without, [])
189
+ const member = await user('kim', [withPrivilege, without], [granted, plain])
190
+
191
+ const domains = await getDomainsWithPrivilege(await reloadUser(member), 'query', 'board')
192
+
193
+ expect(domains.map(d => d.subdomain)).toEqual(['with'])
194
+ })
195
+
196
+ it('소유 도메인은 권한 판정 없이 포함한다', async () => {
197
+ const owned = await domain('owned')
198
+ const owner = await user('boss', [], [])
199
+ await dataSource.getRepository(Domain).update(owned.id, { owner: owner.id })
200
+
201
+ const domains = await getDomainsWithPrivilege(await reloadUser(owner), 'query', 'board')
202
+
203
+ expect(domains.map(d => d.subdomain)).toEqual(['owned'])
204
+ })
205
+ })
@@ -0,0 +1,188 @@
1
+ /**
2
+ * 도메인 소유권 판정 — 실제 엔티티·실제 쿼리 검증.
3
+ *
4
+ * 도메인 관리자는 여러 명일 수 있다(`DomainOwner` 테이블). `Domain.owner` 컬럼은 대표 오너
5
+ * 표시용 캐시이므로, 도메인 범위의 판정은 두 경로를 함께 봐야 한다.
6
+ *
7
+ * 주의: 이 스펙은 빌드 산출물(`dist-server`)을 대상으로 한다.
8
+ */
9
+
10
+ import { DataSource } from 'typeorm'
11
+
12
+ import { NamingStrategy, addDataSource, entities as shellEntities, Domain } from '@things-factory/shell'
13
+ import {
14
+ DomainOwner,
15
+ User,
16
+ entities as authEntities,
17
+ findDomainOwnerUserIds,
18
+ isDomainOwner,
19
+ setPrimaryDomainOwner
20
+ } from '@things-factory/auth-base'
21
+
22
+ let dataSource: DataSource
23
+
24
+ async function domain(name: string): Promise<Domain> {
25
+ return await dataSource.getRepository(Domain).save({ name, subdomain: name } as any)
26
+ }
27
+
28
+ async function usr(name: string): Promise<User> {
29
+ return await dataSource.getRepository(User).save({
30
+ name,
31
+ email: `${name}@test.com`,
32
+ username: name
33
+ } as any)
34
+ }
35
+
36
+ async function reload(target: Domain): Promise<Domain> {
37
+ return await dataSource.getRepository(Domain).findOneBy({ id: target.id })
38
+ }
39
+
40
+ async function ownerEntryCount(target: Domain): Promise<number> {
41
+ return await dataSource.getRepository(DomainOwner).count({ where: { domain: { id: target.id } } })
42
+ }
43
+
44
+ beforeAll(async () => {
45
+ dataSource = new DataSource({
46
+ type: 'sqlite',
47
+ database: ':memory:',
48
+ synchronize: true,
49
+ dropSchema: true,
50
+ namingStrategy: new NamingStrategy(),
51
+ entities: [...shellEntities, ...authEntities],
52
+ logging: false
53
+ })
54
+
55
+ await dataSource.initialize()
56
+ addDataSource('default', dataSource)
57
+ })
58
+
59
+ afterAll(async () => {
60
+ await dataSource?.destroy()
61
+ })
62
+
63
+ beforeEach(async () => {
64
+ for (const junction of dataSource.entityMetadatas.filter(metadata => metadata.isJunction)) {
65
+ await dataSource.query(`DELETE FROM "${junction.tableName}"`)
66
+ }
67
+
68
+ for (const target of [DomainOwner, User, Domain]) {
69
+ await dataSource.query(`DELETE FROM "${dataSource.getMetadata(target).tableName}"`)
70
+ }
71
+ })
72
+
73
+ describe('isDomainOwner', () => {
74
+ it('대표 오너 컬럼으로 인정한다', async () => {
75
+ const target = await domain('acme')
76
+ const owner = await usr('boss')
77
+ await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
78
+
79
+ expect(await isDomainOwner(await reload(target), owner)).toBe(true)
80
+ })
81
+
82
+ it('DomainOwner 테이블의 공동 오너도 인정한다', async () => {
83
+ const target = await domain('acme')
84
+ const primary = await usr('boss')
85
+ const coOwner = await usr('deputy')
86
+ await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
87
+ await dataSource.getRepository(DomainOwner).save({ domain: target, user: coOwner } as any)
88
+
89
+ expect(await isDomainOwner(await reload(target), coOwner)).toBe(true)
90
+ })
91
+
92
+ it('오너가 아닌 사용자는 인정하지 않는다', async () => {
93
+ const target = await domain('acme')
94
+ const owner = await usr('boss')
95
+ const other = await usr('lee')
96
+ await dataSource.getRepository(Domain).update(target.id, { owner: owner.id })
97
+
98
+ expect(await isDomainOwner(await reload(target), other)).toBe(false)
99
+ })
100
+
101
+ it('다른 도메인의 오너 엔트리는 인정하지 않는다', async () => {
102
+ const target = await domain('acme')
103
+ const another = await domain('other')
104
+ const coOwner = await usr('deputy')
105
+ await dataSource.getRepository(DomainOwner).save({ domain: another, user: coOwner } as any)
106
+
107
+ expect(await isDomainOwner(await reload(target), coOwner)).toBe(false)
108
+ })
109
+
110
+ it('도메인이나 사용자가 없으면 인정하지 않는다', async () => {
111
+ const target = await domain('acme')
112
+ const owner = await usr('boss')
113
+
114
+ expect(await isDomainOwner(null, owner)).toBe(false)
115
+ expect(await isDomainOwner(await reload(target), null)).toBe(false)
116
+ })
117
+ })
118
+
119
+ describe('findDomainOwnerUserIds', () => {
120
+ it('대표 오너와 공동 오너를 합쳐서 반환한다', async () => {
121
+ const target = await domain('acme')
122
+ const primary = await usr('boss')
123
+ const coOwner = await usr('deputy')
124
+ const plain = await usr('lee')
125
+ await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
126
+ await dataSource.getRepository(DomainOwner).save({ domain: target, user: coOwner } as any)
127
+
128
+ const ownerIds = await findDomainOwnerUserIds(await reload(target))
129
+
130
+ expect(ownerIds.has(primary.id)).toBe(true)
131
+ expect(ownerIds.has(coOwner.id)).toBe(true)
132
+ expect(ownerIds.has(plain.id)).toBe(false)
133
+ expect(ownerIds.size).toBe(2)
134
+ })
135
+
136
+ it('대표 오너가 테이블에도 있으면 중복 계산하지 않는다', async () => {
137
+ const target = await domain('acme')
138
+ const primary = await usr('boss')
139
+ await dataSource.getRepository(Domain).update(target.id, { owner: primary.id })
140
+ await dataSource.getRepository(DomainOwner).save({ domain: target, user: primary } as any)
141
+
142
+ expect((await findDomainOwnerUserIds(await reload(target))).size).toBe(1)
143
+ })
144
+ })
145
+
146
+ describe('setPrimaryDomainOwner', () => {
147
+ it('컬럼과 테이블을 함께 세팅한다', async () => {
148
+ const target = await domain('acme')
149
+ const owner = await usr('boss')
150
+
151
+ await setPrimaryDomainOwner(target, owner.id)
152
+
153
+ expect((await reload(target)).owner).toBe(owner.id)
154
+ expect(await ownerEntryCount(target)).toBe(1)
155
+ expect(await isDomainOwner(await reload(target), owner)).toBe(true)
156
+ })
157
+
158
+ it('기존 공동 오너 엔트리를 제거하지 않는다', async () => {
159
+ const target = await domain('acme')
160
+ const previous = await usr('boss')
161
+ const next = await usr('deputy')
162
+ await setPrimaryDomainOwner(target, previous.id)
163
+
164
+ await setPrimaryDomainOwner(target, next.id)
165
+
166
+ expect((await reload(target)).owner).toBe(next.id)
167
+ expect(await ownerEntryCount(target)).toBe(2)
168
+ /* 이관 후에도 이전 오너는 공동 오너로 남는다 — 제거는 removeDomainOwner 의 책임. */
169
+ expect(await isDomainOwner(await reload(target), previous)).toBe(true)
170
+ })
171
+
172
+ it('같은 오너를 다시 지정해도 엔트리를 중복 생성하지 않는다', async () => {
173
+ const target = await domain('acme')
174
+ const owner = await usr('boss')
175
+
176
+ await setPrimaryDomainOwner(target, owner.id)
177
+ await setPrimaryDomainOwner(target, owner.id)
178
+
179
+ expect(await ownerEntryCount(target)).toBe(1)
180
+ })
181
+
182
+ it('도메인이나 사용자가 없으면 명시적으로 실패한다', async () => {
183
+ const target = await domain('acme')
184
+
185
+ await expect(setPrimaryDomainOwner(target, '')).rejects.toThrow()
186
+ await expect(setPrimaryDomainOwner(null as any, 'some-user')).rejects.toThrow()
187
+ })
188
+ })
@@ -0,0 +1,128 @@
1
+ /**
2
+ * 체크인 대상 도메인 판정 규칙 테스트.
3
+ *
4
+ * 핵심 회귀: 부모 도메인에 정의된 역할을 자식 도메인에서 부여받은 사용자는
5
+ * "역할을 부여한 도메인(자식)" 에 체크인해야 하고, "역할을 정의한 도메인(부모)" 에는 체크인하지 못한다.
6
+ */
7
+
8
+ import { CheckinFacts, checkinGrantsOf, isCheckinAllowed } from '../server/utils/checkin-domain-rule.js'
9
+
10
+ const USER = 'user-1'
11
+ const PARENT = { id: 'domain-parent', parentId: null }
12
+ const CHILD = { id: 'domain-child', parentId: 'domain-parent' }
13
+ const SIBLING = { id: 'domain-sibling', parentId: 'domain-parent' }
14
+ const SUPPLIER = { id: 'domain-supplier', parentId: null }
15
+
16
+ function facts(partial: Partial<CheckinFacts> = {}): CheckinFacts {
17
+ return {
18
+ userId: USER,
19
+ memberDomainIds: [],
20
+ roleDomainIds: [],
21
+ ...partial
22
+ }
23
+ }
24
+
25
+ describe('checkin domain rule', () => {
26
+ describe('상속받은 역할을 부여한 도메인', () => {
27
+ /* 자식 도메인 관리자가 부모 정의 역할(상속)을 자기 도메인 사용자에게 부여한 상황. */
28
+ const inheritedGrant = facts({
29
+ memberDomainIds: [CHILD.id],
30
+ roleDomainIds: [PARENT.id]
31
+ })
32
+
33
+ it('부여가 일어난 자식 도메인에 체크인할 수 있다', () => {
34
+ expect(isCheckinAllowed(CHILD, inheritedGrant)).toBe(true)
35
+ expect(checkinGrantsOf(CHILD, inheritedGrant)).toEqual([{ via: 'inherited-role', roleDomainId: PARENT.id }])
36
+ })
37
+
38
+ it('역할을 정의한 부모 도메인에는 체크인할 수 없다', () => {
39
+ expect(isCheckinAllowed(PARENT, inheritedGrant)).toBe(false)
40
+ })
41
+
42
+ it('멤버가 아닌 형제 자식 도메인에는 체크인할 수 없다', () => {
43
+ /* 부모 역할은 모든 자식이 상속하므로, 멤버십 없이 열리면 테넌트 격리가 깨진다. */
44
+ expect(isCheckinAllowed(SIBLING, inheritedGrant)).toBe(false)
45
+ })
46
+ })
47
+
48
+ describe('자기 도메인에 정의된 역할', () => {
49
+ const ownGrant = facts({
50
+ memberDomainIds: [CHILD.id],
51
+ roleDomainIds: [CHILD.id]
52
+ })
53
+
54
+ it('그 도메인에 체크인할 수 있다', () => {
55
+ expect(checkinGrantsOf(CHILD, ownGrant)).toEqual([{ via: 'role', roleDomainId: CHILD.id }])
56
+ })
57
+
58
+ it('부모 도메인에는 체크인할 수 없다', () => {
59
+ expect(isCheckinAllowed(PARENT, ownGrant)).toBe(false)
60
+ })
61
+ })
62
+
63
+ describe('멤버십과 역할', () => {
64
+ it('멤버이지만 어떤 역할도 없으면 체크인할 수 없다', () => {
65
+ expect(isCheckinAllowed(CHILD, facts({ memberDomainIds: [CHILD.id] }))).toBe(false)
66
+ })
67
+
68
+ it('역할만 있고 멤버가 아니면 체크인할 수 없다', () => {
69
+ expect(isCheckinAllowed(CHILD, facts({ roleDomainIds: [CHILD.id] }))).toBe(false)
70
+ })
71
+
72
+ it('최상위 도메인은 상속 경로가 없다', () => {
73
+ const rootFacts = facts({ memberDomainIds: [PARENT.id], roleDomainIds: ['domain-other'] })
74
+ expect(isCheckinAllowed(PARENT, rootFacts)).toBe(false)
75
+ })
76
+ })
77
+
78
+ describe('파트너십으로 grant 된 역할', () => {
79
+ /* 공급자 도메인이 자기 역할을 고객 도메인에 grant 하고, 고객 사용자가 그 역할을 부여받았다. */
80
+ const partnerGrant = facts({
81
+ memberDomainIds: [CHILD.id],
82
+ roleDomainIds: [SUPPLIER.id],
83
+ partnerRoleDomainIds: [SUPPLIER.id]
84
+ })
85
+
86
+ it('멤버가 아니어도 공급자 도메인에 체크인할 수 있다', () => {
87
+ expect(checkinGrantsOf(SUPPLIER, partnerGrant)).toEqual([{ via: 'partner', roleDomainId: SUPPLIER.id }])
88
+ })
89
+
90
+ it('grant 되지 않은 역할은 파트너 경로를 열지 않는다', () => {
91
+ const notGranted = facts({
92
+ memberDomainIds: [CHILD.id],
93
+ roleDomainIds: [SUPPLIER.id]
94
+ })
95
+ expect(isCheckinAllowed(SUPPLIER, notGranted)).toBe(false)
96
+ })
97
+ })
98
+
99
+ describe('소유권', () => {
100
+ it('하위 호환 owner 캐시 컬럼으로 체크인할 수 있다', () => {
101
+ expect(checkinGrantsOf({ ...PARENT, owner: USER }, facts())).toEqual([{ via: 'owner' }])
102
+ })
103
+
104
+ it('DomainOwner 테이블 기준으로도 체크인할 수 있다', () => {
105
+ expect(checkinGrantsOf(PARENT, facts({ ownedDomainIds: [PARENT.id] }))).toEqual([{ via: 'owner' }])
106
+ })
107
+
108
+ it('다른 사용자가 owner 인 도메인은 체크인할 수 없다', () => {
109
+ expect(isCheckinAllowed({ ...PARENT, owner: 'user-2' }, facts())).toBe(false)
110
+ })
111
+ })
112
+
113
+ describe('여러 근거가 동시에 성립하는 경우', () => {
114
+ it('근거를 모두 반환한다 (권한 판정이 그중 권한 있는 역할을 골라야 한다)', () => {
115
+ const both = facts({
116
+ memberDomainIds: [CHILD.id],
117
+ roleDomainIds: [CHILD.id, PARENT.id],
118
+ ownedDomainIds: [CHILD.id]
119
+ })
120
+
121
+ expect(checkinGrantsOf(CHILD, both)).toEqual([
122
+ { via: 'owner' },
123
+ { via: 'role', roleDomainId: CHILD.id },
124
+ { via: 'inherited-role', roleDomainId: PARENT.id }
125
+ ])
126
+ })
127
+ })
128
+ })