@tbox.cn/app-provider-mock 0.2.0
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/server/index.d.ts +246 -0
- package/dist/server/index.js +643 -0
- package/package.json +51 -0
- package/src/index.ts +5 -0
- package/src/server/index.ts +148 -0
- package/src/server/mall-info.ts +34 -0
- package/src/server/member.ts +180 -0
- package/src/server/mock-enroll-route.ts +84 -0
- package/src/server/parking.ts +182 -0
- package/src/server/promotion.ts +148 -0
- package/src/server/shopping.ts +222 -0
- package/tbox.module.json +149 -0
- package/tests/integrations-e2e.test.ts +219 -0
- package/tests/mall-info.test.ts +47 -0
- package/tests/mock-impl.test.ts +348 -0
- package/tsconfig.json +7 -0
- package/tsup.config.ts +26 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider 拆包 F2 端到端槽位解析测试:provider-mock serverModule register →
|
|
3
|
+
* createIntegrationsRegistry.resolveService → 模块 ports 形态消费。
|
|
4
|
+
* 锁死链路:ctx.integrations 同引用传递(registerServerModules 语义模拟)+ local 短路 +
|
|
5
|
+
* mock 实现经槽位轴产出(demo ref 厂商中立)。
|
|
6
|
+
*/
|
|
7
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { createIntegrationsRegistry, createAuthStrategyRegistry, createEnvironmentSecretProvider, createServerContext } from '@tbox.cn/app-sdk/server';
|
|
9
|
+
import { serverModule, MOCK_SHELL_USER_ID, enrollMockMember, logoutMockMember, __resetMockMemberState } from '../src/server/index';
|
|
10
|
+
import type { IntegrationsConfig } from '@tbox.cn/app-contracts';
|
|
11
|
+
import type { RequestContext } from '@tbox.cn/app-contracts';
|
|
12
|
+
|
|
13
|
+
const integrations: IntegrationsConfig = {
|
|
14
|
+
services: {
|
|
15
|
+
'member.account': { provider: 'mock', implementation: 'mock-member@1', instances: { 'mall-demo': { enabled: true } } },
|
|
16
|
+
'member.profile': { provider: 'mock', implementation: 'mock-member@1', instances: { 'mall-demo': { enabled: true } } },
|
|
17
|
+
'member.card': { provider: 'mock', implementation: 'mock-member@1', instances: { 'mall-demo': { enabled: true } } },
|
|
18
|
+
'member.benefits': { provider: 'mock', implementation: 'mock-member@1', instances: { 'mall-demo': { enabled: true } } },
|
|
19
|
+
'member.enrollment': { provider: 'mock', implementation: 'mock-member@1', instances: { 'mall-demo': { enabled: true } } },
|
|
20
|
+
'parking.query': { provider: 'mock', implementation: 'mock-parking@1', instances: { 'mall-demo': { enabled: true } } },
|
|
21
|
+
'parking.payment': { provider: 'mock', implementation: 'mock-parking@1', instances: { 'mall-demo': { enabled: true } } },
|
|
22
|
+
'parking.vehicle': { provider: 'mock', implementation: 'mock-parking@1', instances: { 'mall-demo': { enabled: true } } },
|
|
23
|
+
'promotion.offers': { provider: 'mock', implementation: 'mock-promotion@1', instances: { 'mall-demo': { enabled: true } } },
|
|
24
|
+
'promotion.entitlements': { provider: 'mock', implementation: 'mock-promotion@1', instances: { 'mall-demo': { enabled: true } } },
|
|
25
|
+
'promotion.activities': { provider: 'mock', implementation: 'mock-promotion@1', instances: { 'mall-demo': { enabled: true } } },
|
|
26
|
+
'promotion.acquisition': { provider: 'mock', implementation: 'mock-promotion@1', instances: { 'mall-demo': { enabled: true } } },
|
|
27
|
+
'shopping-guide.catalog': { provider: 'mock', implementation: 'mock-shopping@1', instances: { 'mall-demo': { enabled: true } } },
|
|
28
|
+
'shopping-guide.shops': { provider: 'mock', implementation: 'mock-shopping@1', instances: { 'mall-demo': { enabled: true } } },
|
|
29
|
+
'shopping-guide.categories': { provider: 'mock', implementation: 'mock-shopping@1', instances: { 'mall-demo': { enabled: true } } },
|
|
30
|
+
'shopping-guide.directory': { provider: 'mock', implementation: 'mock-shopping@1', instances: { 'mall-demo': { enabled: true } } },
|
|
31
|
+
'mall.info': { provider: 'mock', implementation: 'mock-mall-info@1', instances: { 'mall-demo': { enabled: true } } },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const catalog = {
|
|
36
|
+
providers: {
|
|
37
|
+
mock: {
|
|
38
|
+
'mock-member@1': { provider: 'mock', implementation: 'mock-member@1', credentialType: '-', owner: '@tbox.cn/app-provider-mock', local: true, services: ['member.account', 'member.profile', 'member.card', 'member.benefits', 'member.enrollment'] },
|
|
39
|
+
'mock-parking@1': { provider: 'mock', implementation: 'mock-parking@1', credentialType: '-', owner: '@tbox.cn/app-provider-mock', local: true, services: ['parking.query', 'parking.payment', 'parking.vehicle'] },
|
|
40
|
+
'mock-promotion@1': { provider: 'mock', implementation: 'mock-promotion@1', credentialType: '-', owner: '@tbox.cn/app-provider-mock', local: true, services: ['promotion.offers', 'promotion.entitlements', 'promotion.activities', 'promotion.acquisition'] },
|
|
41
|
+
'mock-shopping@1': { provider: 'mock', implementation: 'mock-shopping@1', credentialType: '-', owner: '@tbox.cn/app-provider-mock', local: true, services: ['shopping-guide.catalog', 'shopping-guide.shops', 'shopping-guide.categories', 'shopping-guide.directory'] },
|
|
42
|
+
'mock-mall-info@1': { provider: 'mock', implementation: 'mock-mall-info@1', credentialType: '-', owner: '@tbox.cn/app-provider-mock', local: true, services: ['mall.info'] },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
services: {},
|
|
46
|
+
credentialTypes: {},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function requestContext(mallId = 'mall-demo'): RequestContext {
|
|
50
|
+
return {
|
|
51
|
+
identity: { userId: 'u_1', source: 'dev' },
|
|
52
|
+
credentials: { provider: 'dev', token: 't' },
|
|
53
|
+
attributes: { mall: { mallId } },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fixtureCtx() {
|
|
58
|
+
const registry = createIntegrationsRegistry({
|
|
59
|
+
integrations,
|
|
60
|
+
catalog,
|
|
61
|
+
authStrategies: createAuthStrategyRegistry(),
|
|
62
|
+
secretProvider: createEnvironmentSecretProvider({ source: {}, production: false }),
|
|
63
|
+
instanceIdOf: (scope) => scope.mallId ?? '',
|
|
64
|
+
});
|
|
65
|
+
const ctx = createServerContext({ integrations: registry });
|
|
66
|
+
serverModule.register(ctx); // provider register 期注册 adapter(经 ctx.integrations 同引用)
|
|
67
|
+
return { ctx, registry };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('F2 端到端:provider-mock 经槽位轴解析', () => {
|
|
71
|
+
it('register(经 ctx.integrations)→ resolveService 三槽产出 mock 实现', async () => {
|
|
72
|
+
const { ctx, registry } = fixtureCtx();
|
|
73
|
+
expect(registry.listAdapters()).toEqual(['mock\u0000mock-member@1', 'mock\u0000mock-parking@1', 'mock\u0000mock-promotion@1', 'mock\u0000mock-shopping@1', 'mock\u0000mock-mall-info@1']);
|
|
74
|
+
|
|
75
|
+
const member = (await ctx.integrations!.resolveService({
|
|
76
|
+
service: 'member.account',
|
|
77
|
+
scope: { mallId: 'mall-demo' },
|
|
78
|
+
request: requestContext(),
|
|
79
|
+
})) as { getAccount(u: string): Promise<{ state: string }> };
|
|
80
|
+
await expect(member.getAccount('u1')).resolves.toMatchObject({ state: 'not_enrolled' });
|
|
81
|
+
|
|
82
|
+
const parking = (await ctx.integrations!.resolveService({
|
|
83
|
+
service: 'parking.query',
|
|
84
|
+
scope: { mallId: 'mall-demo' },
|
|
85
|
+
request: requestContext(),
|
|
86
|
+
})) as { queryFee(q: unknown, input: { plateNo: string }): Promise<{ quoteRef: string; payableCents: number }[]> };
|
|
87
|
+
const quotes = await parking.queryFee({ scope: {} } as never, { plateNo: 'demo-plate' });
|
|
88
|
+
expect(quotes[0]?.quoteRef.startsWith('demo:quote:')).toBe(true);
|
|
89
|
+
|
|
90
|
+
const promotion = (await ctx.integrations!.resolveService({
|
|
91
|
+
service: 'promotion.offers',
|
|
92
|
+
scope: { mallId: 'mall-demo' },
|
|
93
|
+
request: requestContext(),
|
|
94
|
+
})) as { searchOffers(q: unknown, input: { keyword: string }): Promise<{ items: unknown[] }> };
|
|
95
|
+
const page = await promotion.searchOffers({ scope: {} } as never, { keyword: '' });
|
|
96
|
+
expect(page.items.length).toBeGreaterThanOrEqual(2);
|
|
97
|
+
|
|
98
|
+
// 017 槽位原子化:guide 四槽各自产出单 Port(同一 impl 实例族)
|
|
99
|
+
const shoppingShop = (await ctx.integrations!.resolveService({
|
|
100
|
+
service: 'shopping-guide.shops',
|
|
101
|
+
scope: { mallId: 'mall-demo' },
|
|
102
|
+
request: requestContext(),
|
|
103
|
+
})) as { searchShops(input: unknown): Promise<{ items: unknown[]; total: number }> };
|
|
104
|
+
const shopPage = await shoppingShop.searchShops({ ctx: { scope: {} }, page: 1, pageSize: 10 });
|
|
105
|
+
expect(shopPage.total).toBeGreaterThan(0);
|
|
106
|
+
const shoppingCategory = (await ctx.integrations!.resolveService({
|
|
107
|
+
service: 'shopping-guide.categories',
|
|
108
|
+
scope: { mallId: 'mall-demo' },
|
|
109
|
+
request: requestContext(),
|
|
110
|
+
})) as { listCategories(): Promise<readonly unknown[]> };
|
|
111
|
+
expect((await shoppingCategory.listCategories()).length).toBeGreaterThan(0);
|
|
112
|
+
const shoppingDirectory = (await ctx.integrations!.resolveService({
|
|
113
|
+
service: 'shopping-guide.directory',
|
|
114
|
+
scope: { mallId: 'mall-demo' },
|
|
115
|
+
request: requestContext(),
|
|
116
|
+
})) as { getDirectory(ctx: unknown): Promise<{ mallId: string; buildings: unknown[] } | null> };
|
|
117
|
+
expect((await shoppingDirectory.getDirectory({ scope: { mallId: 'mall-demo' } } as never))?.mallId).toBe('mall-demo');
|
|
118
|
+
|
|
119
|
+
// mall.info(融合语义:动态别名 + demo 位置字段——mall-info agent tool 演示数据面)
|
|
120
|
+
const mallInfo = (await ctx.integrations!.resolveService({
|
|
121
|
+
service: 'mall.info',
|
|
122
|
+
scope: { mallId: 'mall-demo' },
|
|
123
|
+
request: requestContext(),
|
|
124
|
+
})) as { getMallInfo(mallId: string): Promise<{ mallId: string; name: string } | undefined> };
|
|
125
|
+
await expect(mallInfo.getMallInfo('mall-demo')).resolves.toMatchObject({
|
|
126
|
+
mallId: 'mall-demo',
|
|
127
|
+
name: '示例商场(mall-demo)',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('local 短路:transport 为 InMemory(免凭据链)——mock 无 credentialRef 而解析成功', async () => {
|
|
132
|
+
const { ctx } = fixtureCtx();
|
|
133
|
+
await expect(
|
|
134
|
+
ctx.integrations!.resolveService({
|
|
135
|
+
service: 'member.account',
|
|
136
|
+
scope: { mallId: 'mall-demo' },
|
|
137
|
+
request: requestContext(),
|
|
138
|
+
}),
|
|
139
|
+
).resolves.toBeDefined();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('槽未配置(shopping-guide 无实例的 mall)→ INSTANCE_NOT_FOUND 上抛', async () => {
|
|
143
|
+
const { ctx } = fixtureCtx();
|
|
144
|
+
await expect(
|
|
145
|
+
ctx.integrations!.resolveService({
|
|
146
|
+
service: 'member.account',
|
|
147
|
+
scope: { mallId: 'mall-other' },
|
|
148
|
+
request: requestContext('mall-other'),
|
|
149
|
+
}),
|
|
150
|
+
).rejects.toMatchObject({ code: 'PROVIDER_INSTANCE_NOT_FOUND' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('C5:scoped mock 换码器(入会闭环身份供给)', () => {
|
|
155
|
+
beforeEach(() => __resetMockMemberState());
|
|
156
|
+
|
|
157
|
+
it('matches 绑定语义:member.account 绑 mock 的 mall 命中;未选择商场命中自身;绑他牌/未知 mall 不命中', () => {
|
|
158
|
+
const { ctx } = fixtureCtx();
|
|
159
|
+
const hit = ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-demo' } });
|
|
160
|
+
expect(hit?.provider).toBe('alipay');
|
|
161
|
+
// 未选择商场(无 mallId)→ 命中本候选,exchangeByCode 显性抛 AUTH_NOT_CONFIGURED(不落内置本地 alipay)
|
|
162
|
+
const noMall = ctx.authExchanges.get('alipay');
|
|
163
|
+
expect(noMall?.provider).toBe('alipay');
|
|
164
|
+
// 未绑 mock 的 mall(无 member.account 实例)→ scoped 不命中,落到后续层(此处无兜底 = undefined)
|
|
165
|
+
expect(ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-other' } })).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('未选择商场(无 mallId)→ exchangeByCode 抛 AUTH_NOT_CONFIGURED 未选择商场', async () => {
|
|
169
|
+
const { ctx } = fixtureCtx();
|
|
170
|
+
const exchanger = ctx.authExchanges.get('alipay')!;
|
|
171
|
+
await expect(exchanger.exchangeByCode('any-authcode', 'alipay')).rejects.toThrow(/未选择商场/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('GUEST 形态(对齐真实档潜客分支):token="" + openid 身份——合法登录未入会', async () => {
|
|
175
|
+
const { ctx } = fixtureCtx();
|
|
176
|
+
const exchanger = ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-demo' } })!;
|
|
177
|
+
const result = await exchanger.exchangeByCode('any-authcode', 'alipay', { mall: { mallId: 'mall-demo' } });
|
|
178
|
+
expect(result).toEqual({
|
|
179
|
+
identity: { userId: 'demo-user', source: 'external' },
|
|
180
|
+
credentials: { token: '', provider: 'mock', attributes: { openid: 'demo:openid:demo-user' } },
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('MEMBER 形态(对齐真实档会员换码输出):入会后冷登 → token + memberId', async () => {
|
|
185
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
186
|
+
const { ctx } = fixtureCtx();
|
|
187
|
+
const exchanger = ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-demo' } })!;
|
|
188
|
+
const result = await exchanger.exchangeByCode('any-authcode', 'alipay', { mall: { mallId: 'mall-demo' } });
|
|
189
|
+
expect(result).toEqual({
|
|
190
|
+
identity: { userId: 'demo-user', source: 'external' },
|
|
191
|
+
credentials: { token: 'demo:token:demo-user', provider: 'mock', attributes: { memberId: 'demo-user' } },
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('刻意不实现 devExternalCredentials:dev-login 走协议 fail-soft 无 claim 签发', () => {
|
|
196
|
+
const { ctx } = fixtureCtx();
|
|
197
|
+
const exchanger = ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-demo' } });
|
|
198
|
+
// 能力口缺席(typeof 守卫语义——login-routes 同款消费形态)
|
|
199
|
+
expect(typeof (exchanger as { devExternalCredentials?: unknown })?.devExternalCredentials).toBe('undefined');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('登出态换码降级 GUEST 形态(对齐真实档 silentLogin 非 200 → 访客结果,2026-09-02 真机实测)', async () => {
|
|
203
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
204
|
+
logoutMockMember(MOCK_SHELL_USER_ID);
|
|
205
|
+
const { ctx } = fixtureCtx();
|
|
206
|
+
const exchanger = ctx.authExchanges.get('alipay', { mall: { mallId: 'mall-demo' } })!;
|
|
207
|
+
const degraded = await exchanger.exchangeByCode('any-authcode', 'alipay', { mall: { mallId: 'mall-demo' } });
|
|
208
|
+
expect(degraded.identity).toEqual({ userId: MOCK_SHELL_USER_ID, source: 'external' });
|
|
209
|
+
expect(degraded.credentials).toEqual({
|
|
210
|
+
token: '',
|
|
211
|
+
provider: 'mock',
|
|
212
|
+
attributes: { openid: 'demo:openid:demo-user' },
|
|
213
|
+
});
|
|
214
|
+
// 重登录(enroll 清登出态)后恢复 MEMBER 形态——H5 半屏成功登录后冷登即恢复
|
|
215
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
216
|
+
const result = await exchanger.exchangeByCode('any-authcode', 'alipay', { mall: { mallId: 'mall-demo' } });
|
|
217
|
+
expect(result.credentials?.token).toBe('demo:token:demo-user');
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mock mall.info adapter 单测(mall.info 槽供给;对齐真实厂商 mall-info 单测通用形态)。
|
|
3
|
+
* 请求形状断言(记录≠校验教训):mallId 形状守卫——合法 id 恒返回确定性别名;
|
|
4
|
+
* 畸形 id(注入类字符/超长)→ undefined,零上游调用(mock 无 IO,守卫即终态)。
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { mockMallInfoFactory, MOCK_MALL_INFO_PROVIDER, MOCK_MALL_INFO_IMPL } from '../src/server/mall-info';
|
|
8
|
+
|
|
9
|
+
function makePort() {
|
|
10
|
+
return mockMallInfoFactory({
|
|
11
|
+
provider: MOCK_MALL_INFO_PROVIDER,
|
|
12
|
+
implementation: MOCK_MALL_INFO_IMPL,
|
|
13
|
+
instanceId: '',
|
|
14
|
+
config: {},
|
|
15
|
+
logger: { info: () => undefined, warn: () => undefined, error: () => undefined },
|
|
16
|
+
service: 'mall.info',
|
|
17
|
+
} as never) as { getMallInfo(mallId: string): Promise<{ mallId: string; name: string } | undefined> };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('mockMallInfoFactory:请求形状守卫', () => {
|
|
21
|
+
it('合法 mallId → 确定性别名 + demo 位置(动态商场品牌区真源 + agent tool 演示数据面)', async () => {
|
|
22
|
+
const port = makePort();
|
|
23
|
+
const info = await port.getMallInfo('1102588');
|
|
24
|
+
expect(info).toEqual({
|
|
25
|
+
mallId: '1102588',
|
|
26
|
+
name: '示例商场(1102588)',
|
|
27
|
+
address: '示例商场地址(demo)',
|
|
28
|
+
latitude: 30.0,
|
|
29
|
+
longitude: 120.0,
|
|
30
|
+
});
|
|
31
|
+
const info2 = await port.getMallInfo('demo-mall_1');
|
|
32
|
+
expect(info2?.name).toBe('示例商场(demo-mall_1)');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('畸形 mallId → undefined(形状守卫——防注入类字符/超长)', async () => {
|
|
36
|
+
const port = makePort();
|
|
37
|
+
expect(await port.getMallInfo('../etc/passwd')).toBeUndefined();
|
|
38
|
+
expect(await port.getMallInfo('')).toBeUndefined();
|
|
39
|
+
expect(await port.getMallInfo('a'.repeat(33))).toBeUndefined();
|
|
40
|
+
expect(await port.getMallInfo('id with space')).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('实现常量契约(provider=' + MOCK_MALL_INFO_PROVIDER + ' impl=' + MOCK_MALL_INFO_IMPL + ')', () => {
|
|
44
|
+
expect(MOCK_MALL_INFO_PROVIDER).toBe('mock');
|
|
45
|
+
expect(MOCK_MALL_INFO_IMPL).toBe('mock-mall-info@1');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-mock 单测(F1b):mock 实现满足 TIER1 Port 面 + demo ref 厂商中立 + adapter 注册形态。
|
|
3
|
+
* 会员态为模块级共享 store(C5 入会闭环前提)——文件级复位保证用例顺序无关
|
|
4
|
+
* (「初始 not_enrolled」类断言不依赖前序用例是否 enroll 过同一 userId)。
|
|
5
|
+
*/
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { MockMemberServiceImpl } from '../src/server/member';
|
|
8
|
+
import { MockParkingServiceImpl } from '../src/server/parking';
|
|
9
|
+
import { MockPromotionServiceImpl } from '../src/server/promotion';
|
|
10
|
+
import { MockShoppingServiceImpl } from '../src/server/shopping';
|
|
11
|
+
import { serverModule, MOCK_SHELL_USER_ID, enrollMockMember, logoutMockMember, isMockMemberEnrolled, isMockMemberLoggedOut, __resetMockMemberState } from '../src/server/index';
|
|
12
|
+
import { resolveMockEnroll, resolveMockLogout } from '../src/server/mock-enroll-route';
|
|
13
|
+
import { merchantLocationSchema, merchantStatusSchema } from '@tbox.cn/app-contracts-mall';
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
__resetMockMemberState();
|
|
17
|
+
});
|
|
18
|
+
import type { ParkingQueryContext, PromotionQueryContext, ShopRecord, ShoppingQueryContext } from '@tbox.cn/app-contracts-mall';
|
|
19
|
+
|
|
20
|
+
const parkCtx: ParkingQueryContext = {
|
|
21
|
+
scope: { miniAppId: 'demo-app', mallId: 'mall-demo' },
|
|
22
|
+
requestContext: { credentials: {} },
|
|
23
|
+
} as ParkingQueryContext;
|
|
24
|
+
const promoCtx = parkCtx as unknown as PromotionQueryContext;
|
|
25
|
+
const shopCtx: ShoppingQueryContext = {
|
|
26
|
+
scope: { miniAppId: 'demo-app', mallId: 'mall-demo' },
|
|
27
|
+
requestContext: { credentials: {} },
|
|
28
|
+
} as ShoppingQueryContext;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 四域消费面契约锁:mock 实现必须覆盖模块消费的每个方法/属性。
|
|
32
|
+
* dag 铁律禁 provider import 模块类型 → 编译期强校验不可行;此处为运行时断言
|
|
33
|
+
* (架构约束下最优)。清单 = 各模块工具/服务消费面 grep 实况(时点快照,
|
|
34
|
+
* 模块新增消费方法需同步补清单)。
|
|
35
|
+
*/
|
|
36
|
+
function assertConsumedMethods(impl: unknown, methods: readonly string[], props: readonly string[] = []) {
|
|
37
|
+
for (const m of methods) {
|
|
38
|
+
expect(typeof (impl as Record<string, unknown>)[m], `mock 缺消费方法 ${m}`).toBe('function');
|
|
39
|
+
}
|
|
40
|
+
for (const p of props) {
|
|
41
|
+
expect((impl as Record<string, unknown>)[p], `mock 缺消费属性 ${p}`).not.toBeUndefined();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MEMBER_CONSUMED_METHODS = [
|
|
46
|
+
'getAccount', 'getProfile', 'getCard', 'queryPoints', 'getBenefits', 'listPointUsage',
|
|
47
|
+
'queryDiscount', 'deductPoints', 'refundPoints', 'awardPoints', 'enroll',
|
|
48
|
+
'getEnrollmentStatus', 'getEnrollmentFormDefinition',
|
|
49
|
+
] as const;
|
|
50
|
+
const PARKING_CONSUMED_METHODS = [
|
|
51
|
+
'queryFee', 'listVehicles', 'listPaymentRecords', 'getPaymentRecordDetail', 'getInvoiceSettings',
|
|
52
|
+
'listInvoiceableOrders', 'queryParkingInfo', 'queryApplicableBenefits', 'locateVehicle',
|
|
53
|
+
'createOrder', 'getOrderInfo', 'getPaymentSign', 'uploadResult', 'reconcile', 'reprice',
|
|
54
|
+
'applyInvoice', 'bindVehicle', 'unbindVehicle',
|
|
55
|
+
] as const;
|
|
56
|
+
const PROMOTION_CONSUMED_METHODS = [
|
|
57
|
+
'searchOffers', 'getOfferDetail', 'recommendOffers', 'listEntitlements', 'getEntitlementDetail',
|
|
58
|
+
'listActivities', 'getActivityDetail', 'listActivityCategories', 'acquire',
|
|
59
|
+
] as const;
|
|
60
|
+
const SHOPPING_CONSUMED_METHODS = [
|
|
61
|
+
'searchShops', 'getShop', 'listCategories', 'getDirectory', 'searchProducts', 'getProduct', 'getFacets',
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
describe('provider-mock:四域消费面契约锁(防少方法回归)', () => {
|
|
65
|
+
it('member 消费方法全存在(含 getEnrollmentFormDefinition,enrollUrl 属性)', () => {
|
|
66
|
+
assertConsumedMethods(new MockMemberServiceImpl(), MEMBER_CONSUMED_METHODS, ['enrollUrl']);
|
|
67
|
+
});
|
|
68
|
+
it('parking 消费方法全存在', () => {
|
|
69
|
+
assertConsumedMethods(new MockParkingServiceImpl('mall-demo'), PARKING_CONSUMED_METHODS);
|
|
70
|
+
});
|
|
71
|
+
it('promotion 消费方法全存在', () => {
|
|
72
|
+
assertConsumedMethods(new MockPromotionServiceImpl(), PROMOTION_CONSUMED_METHODS);
|
|
73
|
+
});
|
|
74
|
+
it('shopping 消费方法全存在', () => {
|
|
75
|
+
assertConsumedMethods(new MockShoppingServiceImpl(), SHOPPING_CONSUMED_METHODS);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('provider-mock:mock 实现 Port 面与厂商中立', () => {
|
|
80
|
+
it('member:开户/卡面/积分/流水/入会闭环,ref 恒 demo: 前缀', async () => {
|
|
81
|
+
const member = new MockMemberServiceImpl();
|
|
82
|
+
await expect(member.getAccount('u1')).resolves.toMatchObject({ state: 'not_enrolled' });
|
|
83
|
+
await member.enroll({ userId: 'u1', identity: 'enrollment_submission', fieldValues: {}, acceptedConsentIds: [], idempotencyKey: 'k1' });
|
|
84
|
+
await expect(member.getAccount('u1')).resolves.toMatchObject({ state: 'active', memberRef: 'demo:member:u1' });
|
|
85
|
+
await expect(member.getCard('u1')).resolves.toMatchObject({ cardDisplay: '示例会员卡', cardRef: 'demo:card:u1' });
|
|
86
|
+
await expect(member.queryPoints('u1')).resolves.toMatchObject({ balance: 300 });
|
|
87
|
+
const usage = await member.listPointUsage('u1', 1, 10);
|
|
88
|
+
expect(usage.items[0].usageRecordRef.startsWith('demo:')).toBe(true);
|
|
89
|
+
// CTA 卡 schema enrollUrl min(1):mock 自持非空 demo 深链(厂商中立 + alipays:// 形态)
|
|
90
|
+
expect(member.enrollUrl).toMatch(/^alipays:\/\//);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('parking:报价/权益/reprice/支付三步链(reconcile 恒 PAID),ref 恒 demo: 前缀', async () => {
|
|
94
|
+
const parking = new MockParkingServiceImpl('mall-demo');
|
|
95
|
+
const quote = (await parking.queryFee(parkCtx, { plateNo: 'demo-plate' }))[0];
|
|
96
|
+
expect(quote).toMatchObject({ plateNo: 'demo-plate', payableCents: 1500, paymentState: 'UNPAID' });
|
|
97
|
+
expect(quote?.quoteRef.startsWith('demo:quote:')).toBe(true);
|
|
98
|
+
const benefits = await parking.queryApplicableBenefits(parkCtx, 'demo-plate');
|
|
99
|
+
expect(benefits).toHaveLength(2);
|
|
100
|
+
const repriced = await parking.reprice(parkCtx, quote!.quoteRef, [benefits[0].benefitRef]);
|
|
101
|
+
expect(repriced.discountCents).toBeGreaterThan(0);
|
|
102
|
+
const order = await parking.createOrder(parkCtx, quote!.quoteRef);
|
|
103
|
+
expect(order.paymentRef.startsWith('demo:payment:')).toBe(true);
|
|
104
|
+
await expect(parking.reconcile(parkCtx, order.paymentRef)).resolves.toBe('PAID');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('promotion:搜索/精查/推荐/资产/领取闭环,ref 恒 demo: 前缀', async () => {
|
|
108
|
+
const promotion = new MockPromotionServiceImpl();
|
|
109
|
+
const page = await promotion.searchOffers(promoCtx, { keyword: '' });
|
|
110
|
+
expect(page.items.length).toBeGreaterThanOrEqual(2);
|
|
111
|
+
expect(page.items.every((o) => o.offerRef.startsWith('demo:'))).toBe(true);
|
|
112
|
+
const detail = await promotion.getOfferDetail(promoCtx, { offerRef: 'demo:coupon:1001' });
|
|
113
|
+
expect(detail?.title).toContain('满 100 减 20');
|
|
114
|
+
const op = await promotion.acquire(promoCtx, { offerRef: 'demo:coupon:1001', externalMemberRef: 'demo:member:u1', idempotencyKey: 'k1' });
|
|
115
|
+
expect(op.status).toBe('SUCCEEDED');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('provider-mock:serverModule 形态(无卡模块 + adapter 注册)', () => {
|
|
120
|
+
it('cards 空 map + register 注册五 adapter/换码器/路由(真实 ServerContext 面)', () => {
|
|
121
|
+
expect(Object.keys(serverModule.cards)).toHaveLength(0);
|
|
122
|
+
expect(serverModule.declaration.moduleId).toBe('provider-mock');
|
|
123
|
+
const registered: Array<[string, string, unknown]> = [];
|
|
124
|
+
const exchangers: string[] = [];
|
|
125
|
+
const routers: string[] = [];
|
|
126
|
+
const ctx = {
|
|
127
|
+
integrations: { registerAdapter: (p: string, i: string, f: unknown) => registered.push([p, i, f]) },
|
|
128
|
+
authExchanges: { register: (e: { provider: string }) => exchangers.push(e.provider) },
|
|
129
|
+
routes: { register: (id: string) => routers.push(id) },
|
|
130
|
+
};
|
|
131
|
+
serverModule.register(ctx as never);
|
|
132
|
+
expect(registered.map(([p, i]) => `${p}\u0000${i}`)).toEqual([
|
|
133
|
+
'mock\u0000mock-member@1',
|
|
134
|
+
'mock\u0000mock-parking@1',
|
|
135
|
+
'mock\u0000mock-promotion@1',
|
|
136
|
+
'mock\u0000mock-shopping@1',
|
|
137
|
+
'mock\u0000mock-mall-info@1',
|
|
138
|
+
]);
|
|
139
|
+
// 入会闭环接线:scoped alipay 换码器 + demo 入会路由
|
|
140
|
+
expect(exchangers).toEqual(['alipay']);
|
|
141
|
+
expect(routers).toEqual(['provider-mock']);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('provider-mock:shopping mock 行为(seed 派生 + 过滤矩阵)', () => {
|
|
146
|
+
const impl = new MockShoppingServiceImpl();
|
|
147
|
+
|
|
148
|
+
it('无参搜索 = 全量 12 家,分页 hasMore 正确', async () => {
|
|
149
|
+
const p1 = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10 });
|
|
150
|
+
expect(p1.total).toBe(12);
|
|
151
|
+
expect(p1.items).toHaveLength(10);
|
|
152
|
+
expect(p1.hasMore).toBe(true);
|
|
153
|
+
const p2 = await impl.searchShops({ ctx: shopCtx, page: 2, pageSize: 10 });
|
|
154
|
+
expect(p2.items).toHaveLength(2);
|
|
155
|
+
expect(p2.hasMore).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('关键词大小写归一:NIKE 与 nike 均命中 2 家', async () => {
|
|
159
|
+
const a = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, keywords: ['NIKE'] });
|
|
160
|
+
const b = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, keywords: ['nike'] });
|
|
161
|
+
expect(a.total).toBe(2);
|
|
162
|
+
expect(b.total).toBe(2);
|
|
163
|
+
expect(a.items.map((s) => s.name).sort()).toEqual(['NIKE 专卖店', 'NIKE 跑步体验店']);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('关键词多字段命中(名称/品牌/类目)', async () => {
|
|
167
|
+
const byCat = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, keywords: ['运动'] });
|
|
168
|
+
expect(byCat.total).toBe(3); // 运动服饰三店(类目命中)
|
|
169
|
+
const byBrand = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, keywords: ['NIKE'] });
|
|
170
|
+
expect(byBrand.total).toBe(2); // 品牌命中
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('类目等值过滤', async () => {
|
|
174
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, businessCategory: '餐饮' });
|
|
175
|
+
expect(page.total).toBe(4);
|
|
176
|
+
expect(page.items.every((s) => s.businessCategories.includes('餐饮'))).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('楼层等值过滤', async () => {
|
|
180
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, floorCode: '3 楼' });
|
|
181
|
+
expect(page.total).toBe(3);
|
|
182
|
+
expect(page.items.every((s) => s.location.floorName === '3 楼')).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('楼栋等值过滤', async () => {
|
|
186
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, buildingCode: 'A 栋' });
|
|
187
|
+
expect(page.total).toBe(12);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('组合过滤 AND 语义', async () => {
|
|
191
|
+
const hit = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, businessCategory: '运动服饰', floorCode: '3 楼' });
|
|
192
|
+
expect(hit.total).toBe(3);
|
|
193
|
+
const miss = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, businessCategory: '餐饮', floorCode: '3 楼' });
|
|
194
|
+
expect(miss.total).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('无命中 = total 0', async () => {
|
|
198
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10, keywords: ['不存在的品牌'] });
|
|
199
|
+
expect(page.total).toBe(0);
|
|
200
|
+
expect(page.items).toHaveLength(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('getShop:命中/未命中/入参 trim', async () => {
|
|
204
|
+
const hit = await impl.getShop('demo-shop-1007', shopCtx);
|
|
205
|
+
expect(hit?.name).toBe('NIKE 专卖店');
|
|
206
|
+
const trimmed = await impl.getShop(' demo-shop-1007 ', shopCtx);
|
|
207
|
+
expect(trimmed?.merchantId).toBe('demo-shop-1007');
|
|
208
|
+
await expect(impl.getShop('demo-shop-9999', shopCtx)).resolves.toBeNull();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('listCategories:6 类目,code=name,merchantCount 正确', async () => {
|
|
212
|
+
const cats = await impl.listCategories(shopCtx);
|
|
213
|
+
expect(cats).toHaveLength(6);
|
|
214
|
+
expect(cats.every((c) => c.code === c.name)).toBe(true);
|
|
215
|
+
expect(cats.find((c) => c.name === '餐饮')?.merchantCount).toBe(4);
|
|
216
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10 });
|
|
217
|
+
const shopCats = new Set(page.items.flatMap((s) => s.businessCategories));
|
|
218
|
+
expect(cats.map((c) => c.code).every((c) => shopCats.has(c))).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('getDirectory:mallId 来自 ctx,楼层计数 = 门店数,floorCode 派生同值', async () => {
|
|
222
|
+
const dir = await impl.getDirectory(shopCtx);
|
|
223
|
+
expect(dir?.mallId).toBe('mall-demo');
|
|
224
|
+
expect(dir?.mallName).toBe('示例商场');
|
|
225
|
+
expect(dir?.buildings).toHaveLength(1);
|
|
226
|
+
expect(dir?.buildings[0].floors).toHaveLength(5);
|
|
227
|
+
expect(dir?.buildings[0].floors.find((f) => f.floorName === '3 楼')?.merchantCount).toBe(3);
|
|
228
|
+
expect(dir?.buildings[0].floors.every((f) => f.floorCode === f.floorName)).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('派生 ShopRecord 过契约 schema(location/status)', async () => {
|
|
232
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 10 });
|
|
233
|
+
for (const s of page.items as readonly ShopRecord[]) {
|
|
234
|
+
expect(merchantLocationSchema.safeParse(s.location).success).toBe(true);
|
|
235
|
+
expect(merchantStatusSchema.safeParse(s.status).success).toBe(true);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('member:getEnrollmentFormDefinition 返回合法表单定义(queryEnrollmentForm 工具消费)', async () => {
|
|
240
|
+
const member = new MockMemberServiceImpl();
|
|
241
|
+
const def = await member.getEnrollmentFormDefinition();
|
|
242
|
+
expect(def.formVersion).toBe('demo-v1');
|
|
243
|
+
expect(def.fields).toHaveLength(2);
|
|
244
|
+
expect(def.consents).toHaveLength(1);
|
|
245
|
+
expect(def.authorizationStatus).toBe('required');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('provider-mock:入会闭环共享 store(C5)', () => {
|
|
250
|
+
// 模块级共享状态:用例间复位,防串扰
|
|
251
|
+
|
|
252
|
+
it('状态跨实例共享:A 实例入会 → B 实例(factory 每请求新建)读到 active', async () => {
|
|
253
|
+
const a = new MockMemberServiceImpl();
|
|
254
|
+
const b = new MockMemberServiceImpl();
|
|
255
|
+
await a.enroll({ userId: 'u1', identity: 'enrollment_submission', fieldValues: {}, acceptedConsentIds: [], idempotencyKey: 'k1' });
|
|
256
|
+
await expect(b.getAccount('u1')).resolves.toMatchObject({ state: 'active' });
|
|
257
|
+
await expect(b.getCard('u1')).resolves.toMatchObject({ cardRef: 'demo:card:u1' });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('默认访客:demo-user 初始 not_enrolled(无种子——首进首页即「立即加入会员」)', async () => {
|
|
261
|
+
const member = new MockMemberServiceImpl();
|
|
262
|
+
await expect(member.getAccount(MOCK_SHELL_USER_ID)).resolves.toMatchObject({
|
|
263
|
+
authState: 'authenticated',
|
|
264
|
+
state: 'not_enrolled',
|
|
265
|
+
});
|
|
266
|
+
await expect(member.getCard(MOCK_SHELL_USER_ID)).resolves.toBeNull();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('enrollMockMember 幂等翻状态 + 余额缺省 300', async () => {
|
|
270
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
271
|
+
enrollMockMember(MOCK_SHELL_USER_ID); // 幂等
|
|
272
|
+
const member = new MockMemberServiceImpl();
|
|
273
|
+
await expect(member.getAccount(MOCK_SHELL_USER_ID)).resolves.toMatchObject({
|
|
274
|
+
state: 'active',
|
|
275
|
+
memberRef: 'demo:member:demo-user',
|
|
276
|
+
});
|
|
277
|
+
await expect(member.queryPoints(MOCK_SHELL_USER_ID)).resolves.toMatchObject({ balance: 300 });
|
|
278
|
+
expect(isMockMemberEnrolled(MOCK_SHELL_USER_ID)).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('provider-mock:mock 入会落地路由(resolveMockEnroll 纯决策)', () => {
|
|
283
|
+
|
|
284
|
+
it('有身份 + 无参 → 缺省 demo-user 翻状态返 200', () => {
|
|
285
|
+
const outcome = resolveMockEnroll({ body: {}, identity: { source: 'dev' } });
|
|
286
|
+
expect(outcome).toEqual({ status: 200, body: { state: 'active', memberRef: 'demo:member:demo-user' } });
|
|
287
|
+
expect(isMockMemberEnrolled('demo-user')).toBe(true);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('定向 userId(curl 翻浏览器 dev 用户)', () => {
|
|
291
|
+
const outcome = resolveMockEnroll({ body: { userId: 'dev_h5' }, identity: { source: 'dev' } });
|
|
292
|
+
expect(outcome).toEqual({ status: 200, body: { state: 'active', memberRef: 'demo:member:dev_h5' } });
|
|
293
|
+
expect(isMockMemberEnrolled('dev_h5')).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('幂等:同 userId 两次均 200', () => {
|
|
297
|
+
const identity = { source: 'dev' as const };
|
|
298
|
+
expect(resolveMockEnroll({ body: {}, identity }).status).toBe(200);
|
|
299
|
+
expect(resolveMockEnroll({ body: {}, identity }).status).toBe(200);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('匿名/无身份 → 401(不随 AUTH_MODE 放宽)', () => {
|
|
303
|
+
expect(resolveMockEnroll({ body: {}, identity: null }).status).toBe(401);
|
|
304
|
+
expect(resolveMockEnroll({ body: {}, identity: { source: 'anonymous' } }).status).toBe(401);
|
|
305
|
+
expect(isMockMemberEnrolled('demo-user')).toBe(false);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('NODE_ENV=production → 503(mock 不进生产装配,双保险)', () => {
|
|
309
|
+
expect(resolveMockEnroll({ body: {}, identity: { source: 'dev' }, nodeEnv: 'production' }).status).toBe(503);
|
|
310
|
+
expect(isMockMemberEnrolled('demo-user')).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('provider-mock:登出态(logout 路由 + 换码降级 GUEST 形态 + getAccount 感知)', () => {
|
|
315
|
+
it('resolveMockLogout:有身份 → 200 翻登出态;enroll 清除登出态', () => {
|
|
316
|
+
__resetMockMemberState();
|
|
317
|
+
enrollMockMember('demo-user');
|
|
318
|
+
expect(isMockMemberLoggedOut('demo-user')).toBe(false);
|
|
319
|
+
const outcome = resolveMockLogout({ body: {}, identity: { source: 'dev' } });
|
|
320
|
+
expect(outcome).toEqual({ status: 200, body: { state: 'logged_out' } });
|
|
321
|
+
expect(isMockMemberLoggedOut('demo-user')).toBe(true);
|
|
322
|
+
// 重登录(enroll)清除登出态——与真实链同构(gate 页登录成功后厂商侧登录态恢复)
|
|
323
|
+
enrollMockMember('demo-user');
|
|
324
|
+
expect(isMockMemberLoggedOut('demo-user')).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('resolveMockLogout:匿名/无身份 → 401;production → 503(与 enroll 同形)', () => {
|
|
328
|
+
__resetMockMemberState();
|
|
329
|
+
expect(resolveMockLogout({ body: {}, identity: null }).status).toBe(401);
|
|
330
|
+
expect(resolveMockLogout({ body: {}, identity: { source: 'anonymous' } }).status).toBe(401);
|
|
331
|
+
expect(resolveMockLogout({ body: {}, identity: { source: 'dev' }, nodeEnv: 'production' }).status).toBe(503);
|
|
332
|
+
expect(isMockMemberLoggedOut('demo-user')).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('登出态 getAccount → not_enrolled(入会半屏触发判据);enroll 清登出态 → active', async () => {
|
|
336
|
+
// 对齐真实档降级会话语义(token 空 + visitortoken → not_enrolled):登出态不得误报 active,
|
|
337
|
+
// 否则降级后会员查询直接放行、半屏闭环永不触发。
|
|
338
|
+
__resetMockMemberState();
|
|
339
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
340
|
+
logoutMockMember(MOCK_SHELL_USER_ID);
|
|
341
|
+
const member = new MockMemberServiceImpl();
|
|
342
|
+
await expect(member.getAccount(MOCK_SHELL_USER_ID)).resolves.toMatchObject({ state: 'not_enrolled' });
|
|
343
|
+
await expect(member.getCard(MOCK_SHELL_USER_ID)).resolves.toBeNull();
|
|
344
|
+
// enroll 即清登出态(mock-enrollment 页「同意协议并登录」回调)→ 冷登回 MEMBER → active
|
|
345
|
+
enrollMockMember(MOCK_SHELL_USER_ID);
|
|
346
|
+
await expect(member.getAccount(MOCK_SHELL_USER_ID)).resolves.toMatchObject({ state: 'active' });
|
|
347
|
+
});
|
|
348
|
+
});
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: {
|
|
5
|
+
index: 'src/index.ts',
|
|
6
|
+
'server/index': 'src/server/index.ts',
|
|
7
|
+
},
|
|
8
|
+
format: ['esm'],
|
|
9
|
+
target: 'node20',
|
|
10
|
+
platform: 'node',
|
|
11
|
+
outDir: 'dist',
|
|
12
|
+
clean: true,
|
|
13
|
+
sourcemap: false,
|
|
14
|
+
// dts worker 以命令行式传参会撞 TS5074(incremental 需单文件 emit 或 tsBuildInfoFile);
|
|
15
|
+
// 声明产物一次性构建无需增量——仅 dts 关闭,typecheck 的增量缓存不受影响。
|
|
16
|
+
dts: { compilerOptions: { incremental: false } },
|
|
17
|
+
external: [
|
|
18
|
+
'@tbox.cn/app-sdk',
|
|
19
|
+
'@tbox.cn/app-contracts',
|
|
20
|
+
'@tbox.cn/app-contracts-mall',
|
|
21
|
+
// mock-enroll-route 挂 express Router(devDep 不自动外部化——沿 module-member 先例显式声明,
|
|
22
|
+
// 防 express 被 bundle 进 dist(体积膨胀 + 宿主双实例))
|
|
23
|
+
'express',
|
|
24
|
+
'zod',
|
|
25
|
+
],
|
|
26
|
+
});
|