@allbluecn/web-app 0.4.17 → 0.4.19
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 +11 -11
- package/src/components/billing/checkout-redirect.ts +1 -1
- package/src/components/changelog/changelog-page.tsx +1 -1
- package/src/components/layout/sidebar-03/user-menu.tsx +1 -1
- package/src/components/notifications/notification-list.tsx +232 -0
- package/src/components/notifications/notification-preferences.tsx +156 -0
- package/src/components/settings/ai/create-custom-provider-dialog.tsx +145 -0
- package/src/components/settings/ai/provider-grid.tsx +46 -25
- package/src/components/settings/personal-team/team/team-client.tsx +118 -15
- package/src/components/settings/personal-team/team/team-invitations.tsx +887 -416
- package/src/components/settings/personal-team/team/team-member-list.tsx +122 -136
- package/src/config/sidebar-routes.tsx +16 -0
- package/src/hooks/use-notifications.ts +119 -0
- package/src/lib/app-version.ts +4 -1
- package/src/lib/changelog/sdk-versions-types.ts +1 -0
- package/src/lib/changelog/sdk-versions.ts +29 -19
- package/src/lib/collections/ai/index.ts +0 -1
- package/src/lib/collections/index.ts +0 -1
- package/src/plugins/.gen-manifest.json +13 -0
- package/src/plugins/modules.gen.ts +20 -0
- package/src/plugins/plugins.gen.ts +20 -0
- package/src/plugins/ui.gen.ts +8 -0
- package/src/routeTree.gen.ts +47 -0
- package/src/routes/_login/login/route.tsx +10 -1
- package/src/routes/_login/register/route.lazy.tsx +3 -2
- package/src/routes/_login/register/route.tsx +5 -0
- package/src/routes/_nav/inspiration/route.tsx +8 -0
- package/src/routes/_nav/prd/$id/route.tsx +9 -0
- package/src/routes/_nav/prd/index.tsx +9 -0
- package/src/routes/_nav/prd/route.tsx +8 -0
- package/src/routes/_nav/settings/-settings-tabs.tsx +2 -1
- package/src/routes/_nav/settings/design/route.lazy.tsx +1 -1
- package/src/routes/_nav/settings/notifications/route.lazy.tsx +37 -0
- package/src/routes/_nav/settings/notifications/route.tsx +20 -0
- package/src/routes/_nav/settings/privacy/route.lazy.tsx +2 -2
- package/src/routes/_nav/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/tags/$id/route.tsx +8 -0
- package/src/routes/_nav/tags/index.tsx +8 -0
- package/src/routes/_nav/tags/route.tsx +8 -0
- package/src/routes/_nav/tags/settings/route.tsx +9 -0
- package/src/routes/api/notifications/stream.ts +88 -0
- package/src/routes/invite/$token/route.lazy.tsx +308 -44
- package/src/routes/invite/$token/route.tsx +16 -8
- package/src/routes/share/prd/$prdId/route.tsx +10 -0
- package/src/server/notifications/event-bus.ts +16 -0
- package/src/server/notifications/notification-service.ts +86 -0
- package/src/server/resources.ts +10 -2
- package/src/server/serverFns/__tests__/compliance.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-accept.test.ts +100 -12
- package/src/server/serverFns/__tests__/team-invitation.test.ts +130 -60
- package/src/server/serverFns/__tests__/team-join-request.test.ts +281 -0
- package/src/server/serverFns/__tests__/team-members.test.ts +39 -118
- package/src/{lib/collections/ai/ai-providers-collection.ts → server/serverFns/auth/setup.ts} +7 -12
- package/src/server/serverFns/notifications.ts +155 -0
- package/src/server/serverFns/team.ts +521 -53
- package/vite.shared.ts +13 -0
package/src/server/resources.ts
CHANGED
|
@@ -83,6 +83,7 @@ export async function requireTeamSeats(
|
|
|
83
83
|
ownerId: string,
|
|
84
84
|
seatType: SeatType,
|
|
85
85
|
amount = 1,
|
|
86
|
+
opts?: { excludeInvitationId?: string },
|
|
86
87
|
): Promise<void> {
|
|
87
88
|
const licensed = await verifyActiveLicense()
|
|
88
89
|
const { limits, addonSeats } = await resolveAccountPlanLimits(ownerId)
|
|
@@ -97,7 +98,14 @@ export async function requireTeamSeats(
|
|
|
97
98
|
if (limit === 'unlimited') return
|
|
98
99
|
const [members, pending] = await Promise.all([
|
|
99
100
|
prisma.teamMembership.count({ where: { ownerId, seatType } }),
|
|
100
|
-
prisma.teamInvitation.count({
|
|
101
|
+
prisma.teamInvitation.count({
|
|
102
|
+
where: {
|
|
103
|
+
ownerId,
|
|
104
|
+
seatType,
|
|
105
|
+
status: { in: ['pending', 'pending_approval'] },
|
|
106
|
+
...(opts?.excludeInvitationId ? { id: { not: opts.excludeInvitationId } } : {}),
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
101
109
|
])
|
|
102
110
|
if (members + pending + amount > limit) {
|
|
103
111
|
throw new ResourceLimitError(seatType === 'member' ? 'seats' : 'guestSeats', limit, members + pending)
|
|
@@ -141,7 +149,7 @@ export async function getResourceSnapshots(
|
|
|
141
149
|
where: { ownerId: userId },
|
|
142
150
|
}),
|
|
143
151
|
prisma.teamInvitation.count({
|
|
144
|
-
where: { ownerId: userId, status: 'pending' },
|
|
152
|
+
where: { ownerId: userId, status: { in: ['pending', 'pending_approval'] } },
|
|
145
153
|
}),
|
|
146
154
|
])
|
|
147
155
|
|
|
@@ -92,8 +92,8 @@ describe('compliance serverFn 三路径权益门控', () => {
|
|
|
92
92
|
|
|
93
93
|
it('路径 1:OSS 部署(proModule.compliance 为 null)→ FeatureUnavailableError', async () => {
|
|
94
94
|
setProCompliance(null)
|
|
95
|
-
await expect(requestMyExportFn()).rejects.toBeInstanceOf(FeatureUnavailableError)
|
|
96
|
-
await expect(requestMyDeletionFn()).rejects.toBeInstanceOf(FeatureUnavailableError)
|
|
95
|
+
await expect(requestMyExportFn({ data: {} })).rejects.toBeInstanceOf(FeatureUnavailableError)
|
|
96
|
+
await expect(requestMyDeletionFn({ data: {} })).rejects.toBeInstanceOf(FeatureUnavailableError)
|
|
97
97
|
expect(mocks.createExportJob).not.toHaveBeenCalled()
|
|
98
98
|
})
|
|
99
99
|
|
|
@@ -109,8 +109,8 @@ describe('compliance serverFn 三路径权益门控', () => {
|
|
|
109
109
|
planName: 'pro',
|
|
110
110
|
features: { byokAi: true, hostedAi: true },
|
|
111
111
|
})
|
|
112
|
-
await expect(requestMyExportFn()).rejects.toBeInstanceOf(FeatureForbiddenError)
|
|
113
|
-
await expect(requestMyDeletionFn()).rejects.toBeInstanceOf(FeatureForbiddenError)
|
|
112
|
+
await expect(requestMyExportFn({ data: {} })).rejects.toBeInstanceOf(FeatureForbiddenError)
|
|
113
|
+
await expect(requestMyDeletionFn({ data: {} })).rejects.toBeInstanceOf(FeatureForbiddenError)
|
|
114
114
|
expect(mocks.createExportJob).not.toHaveBeenCalled()
|
|
115
115
|
})
|
|
116
116
|
|
|
@@ -127,11 +127,11 @@ describe('compliance serverFn 三路径权益门控', () => {
|
|
|
127
127
|
features: { compliance: true },
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
-
const exported = await requestMyExportFn()
|
|
130
|
+
const exported = await requestMyExportFn({ data: {} })
|
|
131
131
|
expect(exported).toEqual({ id: 'job-1' })
|
|
132
132
|
expect(mocks.createExportJob).toHaveBeenCalledWith('user', 'u1', 'u1')
|
|
133
133
|
|
|
134
|
-
const deleted = await requestMyDeletionFn()
|
|
134
|
+
const deleted = await requestMyDeletionFn({ data: {} })
|
|
135
135
|
expect(deleted).toEqual({ id: 'job-2' })
|
|
136
136
|
expect(mocks.createDeleteJob).toHaveBeenCalledWith('user', 'u1', 'u1')
|
|
137
137
|
})
|
|
@@ -29,8 +29,10 @@ vi.mock('@tanstack/react-start/server', () => ({
|
|
|
29
29
|
|
|
30
30
|
vi.mock('@allbluecn/database', () => ({
|
|
31
31
|
prisma: {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
user: { findUnique: vi.fn() },
|
|
33
|
+
teamInvitation: { findUnique: vi.fn(), update: vi.fn() },
|
|
34
|
+
teamMembership: { findUnique: vi.fn(), create: vi.fn() },
|
|
35
|
+
teamJoinRequest: { findUnique: vi.fn(), upsert: vi.fn() },
|
|
34
36
|
$transaction: vi.fn(),
|
|
35
37
|
},
|
|
36
38
|
}))
|
|
@@ -43,8 +45,13 @@ vi.mock('@/server/audit', () => ({
|
|
|
43
45
|
auditLog: vi.fn(),
|
|
44
46
|
}))
|
|
45
47
|
|
|
48
|
+
vi.mock('@/server/resources', () => ({
|
|
49
|
+
requireTeamSeats: vi.fn().mockResolvedValue(undefined),
|
|
50
|
+
}))
|
|
51
|
+
|
|
46
52
|
import { prisma } from '@allbluecn/database'
|
|
47
|
-
import {
|
|
53
|
+
import { requireTeamSeats } from '@/server/resources'
|
|
54
|
+
import { acceptTeamInvitationFn, getTeamInvitationByTokenFn, approveTeamInvitationFn } from '../team'
|
|
48
55
|
|
|
49
56
|
describe('acceptTeamInvitationFn', () => {
|
|
50
57
|
beforeEach(() => vi.clearAllMocks())
|
|
@@ -68,18 +75,33 @@ describe('acceptTeamInvitationFn', () => {
|
|
|
68
75
|
.rejects.toMatchObject({ message: expect.stringContaining('已加入') })
|
|
69
76
|
})
|
|
70
77
|
|
|
71
|
-
it('link
|
|
78
|
+
it('link 邀请任意登录用户申请 → 创建 TeamJoinRequest,不占用邀请行', async () => {
|
|
72
79
|
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
73
80
|
id: 'inv1', token: 't1', status: 'pending', seatType: 'guest', channel: 'link',
|
|
74
|
-
email:
|
|
81
|
+
email: null, expiresAt: new Date(Date.now() + 60000), ownerId: 'owner1',
|
|
75
82
|
} as never)
|
|
76
|
-
vi.mocked(prisma.teamMembership.findUnique).mockResolvedValue(null)
|
|
77
|
-
vi.mocked(prisma
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
vi.mocked(prisma.teamMembership.findUnique).mockResolvedValue(null as never)
|
|
84
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue({ email: 'b@x.com' } as never)
|
|
85
|
+
vi.mocked(prisma.teamJoinRequest.findUnique).mockResolvedValue(null as never)
|
|
86
|
+
vi.mocked(prisma.teamJoinRequest.upsert).mockResolvedValue({ id: 'jr1' } as never)
|
|
87
|
+
|
|
88
|
+
const res = await acceptTeamInvitationFn({ data: { token: 't1', userId: null } })
|
|
89
|
+
|
|
90
|
+
expect(res).toMatchObject({ success: true, requiresApproval: true })
|
|
91
|
+
expect(prisma.teamJoinRequest.upsert).toHaveBeenCalledWith(expect.objectContaining({
|
|
92
|
+
where: { invitationId_email: { invitationId: 'inv1', email: 'b@x.com' } },
|
|
80
93
|
}))
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
expect(prisma.teamInvitation.update).not.toHaveBeenCalled()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('申请人是 owner 的 owner(互为成员)→ 拒绝', async () => {
|
|
98
|
+
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
99
|
+
id: 'inv1', token: 't1', status: 'pending', seatType: 'member', channel: 'link',
|
|
100
|
+
email: null, expiresAt: new Date(Date.now() + 60000), ownerId: 'owner1',
|
|
101
|
+
} as never)
|
|
102
|
+
vi.mocked(prisma.teamMembership.findUnique).mockResolvedValue({ id: 'm1', ownerId: 'u2' } as never)
|
|
103
|
+
await expect(acceptTeamInvitationFn({ data: { token: 't1', userId: null } }))
|
|
104
|
+
.rejects.toMatchObject({ message: expect.stringContaining('已加入团队') })
|
|
83
105
|
})
|
|
84
106
|
|
|
85
107
|
it('email 邀请邮箱不匹配 → 拒绝', async () => {
|
|
@@ -98,6 +120,72 @@ describe('acceptTeamInvitationFn', () => {
|
|
|
98
120
|
channel: 'email', email: 'b@x.com', owner: { id: 'o1', username: 'owner', email: 'o@x.com' },
|
|
99
121
|
} as never)
|
|
100
122
|
const result = await getTeamInvitationByTokenFn({ data: { token: 't1' } })
|
|
101
|
-
expect(result
|
|
123
|
+
expect(result?.effectiveStatus).toBe('pending')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('approveTeamInvitationFn', () => {
|
|
128
|
+
beforeEach(() => vi.clearAllMocks())
|
|
129
|
+
|
|
130
|
+
it('批准前复查席位,并用 acceptedBy 创建 membership', async () => {
|
|
131
|
+
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
132
|
+
id: 'inv1', ownerId: 'u2', status: 'pending_approval', seatType: 'member',
|
|
133
|
+
acceptedBy: 'u2', token: 't1', channel: 'email', email: 'b@x.com',
|
|
134
|
+
expiresAt: new Date(Date.now() + 60000), createdAt: new Date(),
|
|
135
|
+
} as never)
|
|
136
|
+
vi.mocked(prisma.$transaction).mockImplementation(async (fn: any) => {
|
|
137
|
+
const tx = {
|
|
138
|
+
teamMembership: { create: vi.fn().mockResolvedValue({ id: 'm1' }) },
|
|
139
|
+
teamInvitation: { update: vi.fn().mockResolvedValue({}) },
|
|
140
|
+
}
|
|
141
|
+
await fn(tx)
|
|
142
|
+
return tx
|
|
143
|
+
})
|
|
144
|
+
const result = await approveTeamInvitationFn({ data: { invitationId: 'inv1', seatType: 'member' } })
|
|
145
|
+
expect(result.success).toBe(true)
|
|
146
|
+
expect(requireTeamSeats).toHaveBeenCalledWith('u2', 'member', 1, { excludeInvitationId: 'inv1' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('批准后邮箱邀请置 accepted,重置链接邀请现在走 approveTeamJoinRequestFn', async () => {
|
|
150
|
+
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
151
|
+
id: 'inv1', ownerId: 'u2', status: 'pending_approval', seatType: 'member',
|
|
152
|
+
acceptedBy: 'u2', token: 't1', channel: 'link', email: null,
|
|
153
|
+
expiresAt: new Date(Date.now() + 60000), createdAt: new Date(),
|
|
154
|
+
} as never)
|
|
155
|
+
await expect(approveTeamInvitationFn({ data: { invitationId: 'inv1', seatType: 'member' } }))
|
|
156
|
+
.rejects.toMatchObject({ message: expect.stringContaining('approveTeamJoinRequestFn') })
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('acceptedBy 为 null 时用 applicantEmail 回填申请人', async () => {
|
|
160
|
+
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
161
|
+
id: 'inv1', ownerId: 'u2', status: 'pending_approval', seatType: 'member',
|
|
162
|
+
acceptedBy: null, applicantEmail: 'b@x.com', token: 't1', channel: 'email', email: 'b@x.com',
|
|
163
|
+
expiresAt: new Date(Date.now() + 60000), createdAt: new Date(),
|
|
164
|
+
} as never)
|
|
165
|
+
vi.mocked(prisma.user.findUnique).mockResolvedValue({ id: 'u2' } as never)
|
|
166
|
+
let txRef: any
|
|
167
|
+
vi.mocked(prisma.$transaction).mockImplementation(async (fn: any) => {
|
|
168
|
+
const tx = {
|
|
169
|
+
teamMembership: { create: vi.fn().mockResolvedValue({ id: 'm1' }) },
|
|
170
|
+
teamInvitation: { update: vi.fn().mockResolvedValue({}) },
|
|
171
|
+
}
|
|
172
|
+
txRef = tx
|
|
173
|
+
await fn(tx)
|
|
174
|
+
return tx
|
|
175
|
+
})
|
|
176
|
+
await approveTeamInvitationFn({ data: { invitationId: 'inv1', seatType: 'member' } })
|
|
177
|
+
expect(txRef.teamMembership.create).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({ data: expect.objectContaining({ memberId: 'u2' }) }),
|
|
179
|
+
)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('过期邀请拒绝批准', async () => {
|
|
183
|
+
vi.mocked(prisma.teamInvitation.findUnique).mockResolvedValue({
|
|
184
|
+
id: 'inv1', ownerId: 'u2', status: 'pending_approval', seatType: 'member',
|
|
185
|
+
acceptedBy: 'u2', token: 't1', channel: 'email', email: 'b@x.com',
|
|
186
|
+
expiresAt: new Date(Date.now() - 1000), createdAt: new Date(),
|
|
187
|
+
} as never)
|
|
188
|
+
await expect(approveTeamInvitationFn({ data: { invitationId: 'inv1', seatType: 'member' } }))
|
|
189
|
+
.rejects.toMatchObject({ message: expect.stringContaining('过期') })
|
|
102
190
|
})
|
|
103
191
|
})
|
|
@@ -39,7 +39,10 @@ vi.mock('@allbluecn/database', () => ({
|
|
|
39
39
|
findMany: vi.fn(),
|
|
40
40
|
update: vi.fn(),
|
|
41
41
|
count: vi.fn(),
|
|
42
|
+
deleteMany: vi.fn(),
|
|
42
43
|
},
|
|
44
|
+
teamJoinRequest: { updateMany: vi.fn(), findMany: vi.fn() },
|
|
45
|
+
$transaction: vi.fn(),
|
|
43
46
|
},
|
|
44
47
|
}))
|
|
45
48
|
|
|
@@ -60,84 +63,151 @@ vi.mock('@/server/email/sender', () => ({
|
|
|
60
63
|
}))
|
|
61
64
|
|
|
62
65
|
import { prisma } from '@allbluecn/database'
|
|
63
|
-
import {
|
|
66
|
+
import {
|
|
67
|
+
createTeamInvitationFn,
|
|
68
|
+
refreshTeamInvitationLinkFn,
|
|
69
|
+
listTeamInvitationsFn,
|
|
70
|
+
clearExpiredInvitationsFn,
|
|
71
|
+
} from '../team'
|
|
64
72
|
|
|
65
|
-
describe('
|
|
73
|
+
describe('refreshTeamInvitationLinkFn', () => {
|
|
66
74
|
beforeEach(() => vi.clearAllMocks())
|
|
67
75
|
|
|
68
|
-
it('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
76
|
+
it('生成新链接,删除旧链接', async () => {
|
|
77
|
+
vi.mocked(prisma.$transaction).mockImplementation(async (fn: any) => {
|
|
78
|
+
const tx = {
|
|
79
|
+
teamInvitation: {
|
|
80
|
+
deleteMany: vi.fn().mockResolvedValue({}),
|
|
81
|
+
create: vi.fn().mockResolvedValue({ id: 'inv-new', token: 'abc123' }),
|
|
82
|
+
},
|
|
83
|
+
teamJoinRequest: {
|
|
84
|
+
updateMany: vi.fn().mockResolvedValue({}),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
return fn(tx)
|
|
72
88
|
})
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
).
|
|
89
|
+
const result = await refreshTeamInvitationLinkFn({ data: {} })
|
|
90
|
+
expect(result.success).toBe(true)
|
|
91
|
+
expect(result.invitationId).toBe('inv-new')
|
|
92
|
+
expect(result.token).toBeTruthy()
|
|
93
|
+
expect(result.token.length).toBeGreaterThan(0)
|
|
76
94
|
})
|
|
77
95
|
|
|
78
|
-
it('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
96
|
+
it('支持自定义有效期', async () => {
|
|
97
|
+
vi.mocked(prisma.$transaction).mockImplementation(async (fn: any) => {
|
|
98
|
+
const tx = {
|
|
99
|
+
teamInvitation: {
|
|
100
|
+
deleteMany: vi.fn().mockResolvedValue({}),
|
|
101
|
+
create: vi.fn().mockResolvedValue({ id: 'inv-new', token: 'xyz' }),
|
|
102
|
+
},
|
|
103
|
+
teamJoinRequest: {
|
|
104
|
+
updateMany: vi.fn().mockResolvedValue({}),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
return fn(tx)
|
|
83
108
|
})
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} as never)
|
|
87
|
-
await expect(
|
|
88
|
-
createTeamInvitationFn({ data: { email: 'b@x.com', seatType: 'member', channel: 'email' } }),
|
|
89
|
-
).rejects.toMatchObject({ message: expect.stringContaining('已加入') })
|
|
109
|
+
const result = await refreshTeamInvitationLinkFn({ data: { expiresInDays: 14 } })
|
|
110
|
+
expect(result.success).toBe(true)
|
|
90
111
|
})
|
|
91
112
|
|
|
92
|
-
it('
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
it('默认有效期 7 天', async () => {
|
|
114
|
+
vi.mocked(prisma.$transaction).mockImplementation(async (fn: any) => {
|
|
115
|
+
const tx = {
|
|
116
|
+
teamInvitation: {
|
|
117
|
+
deleteMany: vi.fn().mockResolvedValue({}),
|
|
118
|
+
create: vi.fn().mockResolvedValue({ id: 'inv-new', token: 'tok' }),
|
|
119
|
+
},
|
|
120
|
+
teamJoinRequest: {
|
|
121
|
+
updateMany: vi.fn().mockResolvedValue({}),
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
return fn(tx)
|
|
97
125
|
})
|
|
98
|
-
|
|
99
|
-
vi.mocked(prisma.
|
|
100
|
-
|
|
101
|
-
await expect(
|
|
102
|
-
createTeamInvitationFn({ data: { email: 'b@x.com', seatType: 'member', channel: 'email' } }),
|
|
103
|
-
).rejects.toMatchObject({ message: expect.stringContaining('待处理') })
|
|
126
|
+
await refreshTeamInvitationLinkFn({ data: {} })
|
|
127
|
+
const createCall = vi.mocked(prisma.$transaction).mock.calls[0][0]
|
|
128
|
+
expect(createCall).toBeDefined()
|
|
104
129
|
})
|
|
130
|
+
})
|
|
105
131
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
vi.mocked(prisma.teamInvitation.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
describe('listTeamInvitationsFn', () => {
|
|
133
|
+
beforeEach(() => vi.clearAllMocks())
|
|
134
|
+
|
|
135
|
+
it('返回所有邀请(含已过期链接)', async () => {
|
|
136
|
+
vi.mocked(prisma.teamInvitation.updateMany).mockResolvedValue({ count: 0 } as never)
|
|
137
|
+
vi.mocked(prisma.teamInvitation.findMany).mockResolvedValue([
|
|
138
|
+
{
|
|
139
|
+
id: 'inv2',
|
|
140
|
+
ownerId: 'owner1',
|
|
141
|
+
channel: 'email',
|
|
142
|
+
status: 'pending',
|
|
143
|
+
token: 'tok2',
|
|
144
|
+
email: 'b@x.com',
|
|
145
|
+
expiresAt: new Date(Date.now() + 86400000),
|
|
146
|
+
createdAt: new Date(Date.now() + 1000),
|
|
147
|
+
owner: { id: 'owner1', username: 'A', email: 'a@x.com' },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'inv1',
|
|
151
|
+
ownerId: 'owner1',
|
|
152
|
+
channel: 'link',
|
|
153
|
+
status: 'expired',
|
|
154
|
+
token: 'old',
|
|
155
|
+
expiresAt: new Date(Date.now() - 86400000),
|
|
156
|
+
createdAt: new Date(Date.now() - 86400000),
|
|
157
|
+
owner: { id: 'owner1', username: 'A', email: 'a@x.com' },
|
|
158
|
+
},
|
|
159
|
+
] as never)
|
|
160
|
+
vi.mocked(prisma.teamJoinRequest.updateMany).mockResolvedValue({ count: 0 } as never)
|
|
161
|
+
vi.mocked(prisma.teamJoinRequest.findMany).mockResolvedValue([] as never)
|
|
162
|
+
const result = await listTeamInvitationsFn()
|
|
163
|
+
expect(result.invitations).toHaveLength(2)
|
|
164
|
+
expect(result.invitations[0].channel).toBe('email')
|
|
165
|
+
expect(result.invitations[1].channel).toBe('link')
|
|
166
|
+
expect(result.joinRequests).toEqual([])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('过期 pending/pending_approval 邀请自动标记为 expired', async () => {
|
|
170
|
+
vi.mocked(prisma.teamInvitation.updateMany).mockResolvedValue({ count: 1 } as never)
|
|
171
|
+
vi.mocked(prisma.teamInvitation.findMany).mockResolvedValue([])
|
|
172
|
+
vi.mocked(prisma.teamJoinRequest.updateMany).mockResolvedValue({ count: 0 } as never)
|
|
173
|
+
vi.mocked(prisma.teamJoinRequest.findMany).mockResolvedValue([])
|
|
174
|
+
await listTeamInvitationsFn()
|
|
175
|
+
expect(prisma.teamInvitation.updateMany).toHaveBeenCalledWith(
|
|
117
176
|
expect.objectContaining({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
seatType: 'guest',
|
|
121
|
-
token: expect.any(String),
|
|
122
|
-
expiresAt: expect.any(Date),
|
|
177
|
+
where: expect.objectContaining({
|
|
178
|
+
status: { in: ['pending', 'pending_approval'] },
|
|
123
179
|
}),
|
|
124
180
|
}),
|
|
125
181
|
)
|
|
126
|
-
expect(result.success).toBe(true)
|
|
127
182
|
})
|
|
183
|
+
})
|
|
128
184
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
await expect(
|
|
132
|
-
createTeamInvitationFn({ data: { seatType: 'member', channel: 'link' } }),
|
|
133
|
-
).rejects.toMatchObject({ message: expect.stringContaining('邀请链接') })
|
|
134
|
-
})
|
|
185
|
+
describe('createTeamInvitationFn link 邀请', () => {
|
|
186
|
+
beforeEach(() => vi.clearAllMocks())
|
|
135
187
|
|
|
136
|
-
it('
|
|
188
|
+
it('link 邀请:无旧 pending 链接 → 创建成功', async () => {
|
|
137
189
|
vi.mocked(prisma.teamInvitation.findFirst).mockResolvedValue(null)
|
|
138
|
-
vi.mocked(prisma.teamInvitation.create).
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
190
|
+
vi.mocked(prisma.teamInvitation.create).mockResolvedValue(
|
|
191
|
+
{ id: 'inv1', token: 'abc' } as never,
|
|
192
|
+
)
|
|
193
|
+
const result = await createTeamInvitationFn({ data: { seatType: 'member', channel: 'link' } })
|
|
194
|
+
expect(result.success).toBe(true)
|
|
195
|
+
expect(result.invitationId).toBe('inv1')
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('clearExpiredInvitationsFn', () => {
|
|
200
|
+
beforeEach(() => vi.clearAllMocks())
|
|
201
|
+
|
|
202
|
+
it('清除所有过期邀请', async () => {
|
|
203
|
+
vi.mocked(prisma.teamInvitation.deleteMany).mockResolvedValue({ count: 2 } as never)
|
|
204
|
+
const result = await clearExpiredInvitationsFn({ data: {} })
|
|
205
|
+
expect(result.success).toBe(true)
|
|
206
|
+
expect(result.count).toBe(2)
|
|
207
|
+
expect(prisma.teamInvitation.deleteMany).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
where: expect.objectContaining({ status: 'expired' }),
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
142
212
|
})
|
|
143
|
-
})
|
|
213
|
+
})
|