- 新增地址选择插件,支持地图位置选择并返回地址和经纬度 - 新增微信订阅消息发送功能,支持审核结果、提现通知等场景 - 新增订单支付状态查询、提现收款人姓名保存等接口 - 新增服务区域管理,支持按省市区划分并同步到信息发布 - 新增资金流水和营收报表导出功能 - 新增可用优惠券查询、订单退款拆分处理等功能 - 优化后台角标显示、首页数据统计、UI配置下发等 - 修复订单支付并发问题、地区管理逻辑等
266 行
10 KiB
JavaScript
266 行
10 KiB
JavaScript
/**
|
||
* 接口契约测试(用户端)
|
||
* 目的:验证「前端 api 模块 ↔ 后端响应结构」字段级契约一致。
|
||
*/
|
||
import { describe, it, expect, expectReject } from './harness.mjs'
|
||
import { authApi, homeApi, postApi, memberApi, distributionApi, messageApi } from '../common/api/index.js'
|
||
|
||
describe('用户端 · 认证与登录', () => {
|
||
it('微信手机号快捷获取返回真实号码;缺 code 时报错(v1.0.8)', async () => {
|
||
const res = await authApi.wxPhone({ code: 'wx_phone_code_001' })
|
||
expect(res.phone).toMatch(/^1\d{10}$/)
|
||
await expectReject(() => authApi.wxPhone({}), '缺少手机号授权 code')
|
||
})
|
||
|
||
it('手机号绑定:格式校验 + 落库回显', async () => {
|
||
await expectReject(() => authApi.bindPhone({ phone: '1380000' }), '手机号格式不正确')
|
||
const res = await authApi.bindPhone({ phone: '13800001111' })
|
||
expect(res.phone).toBe('13800001111')
|
||
})
|
||
|
||
it('微信昵称/头像可随登录带上并回显(v1.0.9)', async () => {
|
||
const res = await authApi.wxLogin({ code: 'wx_code_profile', nickname: '微信小明', avatar: '/uploads/mock/avatar.png' })
|
||
expect(res.userInfo.nickname).toBe('微信小明')
|
||
expect(res.userInfo.avatar).toContain('/uploads/')
|
||
})
|
||
|
||
it('头像上传返回可持久化路径与完整地址', async () => {
|
||
const up = await authApi.uploadAvatar('/tmp/avatar.png')
|
||
expect(up.avatar).toContain('/uploads/')
|
||
expect(up.url).toContain('http')
|
||
await expectReject(() => authApi.uploadAvatar(''), '未接收到上传文件')
|
||
})
|
||
|
||
it('资料更新:可补填昵称/头像,空提交被拦截', async () => {
|
||
const info = await authApi.saveProfile({ nickname: '昵称补充' })
|
||
expect(info.nickname).toBe('昵称补充')
|
||
await expectReject(() => authApi.saveProfile({}), '没有需要更新的资料')
|
||
})
|
||
|
||
it('微信登录返回 token 与用户信息', async () => {
|
||
const res = await authApi.wxLogin({ code: 'wx_code_001' })
|
||
expect(res.token).toContain('mock_token')
|
||
expect(res.userInfo.nickname).toBeDefined()
|
||
})
|
||
|
||
it('手机号绑定校验格式', async () => {
|
||
const ok = await authApi.bindPhone({ phone: '13800001111' })
|
||
expect(ok.phone).toBe('13800001111')
|
||
await expectReject(() => authApi.bindPhone({ phone: '123' }), '手机号格式不正确')
|
||
})
|
||
|
||
it('用户类型切换:商家必须带资质', async () => {
|
||
const res = await authApi.switchType({ userType: 1 })
|
||
expect(res.userType).toBe(1)
|
||
expect(res.needAudit).toBe(false)
|
||
const biz = await authApi.switchType({ userType: 2, license: '营业执照.jpg' })
|
||
expect(biz.needAudit).toBe(true)
|
||
await expectReject(() => authApi.switchType({ userType: 2 }), '需上传营业执照')
|
||
await expectReject(() => authApi.switchType({ userType: 9 }), '用户类型不合法')
|
||
})
|
||
|
||
it('获取用户资料', async () => {
|
||
const res = await authApi.profile()
|
||
expect(res.phone).toBeDefined()
|
||
expect(res.userType).toBeDefined()
|
||
})
|
||
})
|
||
|
||
describe('用户端 · 首页与展示', () => {
|
||
it('轮播广告位返回颜色与跳转链接', async () => {
|
||
const list = await homeApi.banners()
|
||
expect(list).toBeArray()
|
||
expect(list[0].link).toContain('/pages/')
|
||
})
|
||
|
||
it('分类导航返回 8 个一级分类', async () => {
|
||
const list = await homeApi.categories()
|
||
expect(list.length).toBe(8)
|
||
expect(list[0].iconKey).toBeDefined()
|
||
expect(list[0].name).toBeTruthy()
|
||
})
|
||
|
||
it('信息流支持分类 / 区域 / 排序 / 分页', async () => {
|
||
const byCat = await homeApi.feed({ categoryId: 3, page: 1, size: 10 })
|
||
expect(byCat.list.every((p) => p.categoryId === 3)).toBe(true)
|
||
const byArea = await homeApi.feed({ area: '云岩区' })
|
||
expect(byArea.list.every((p) => p.area === '云岩区')).toBe(true)
|
||
// 排序口径 v1.0.7:后台置顶的信息始终排在最前,其余按所选排序
|
||
const hot = await homeApi.feed({ sort: 'hot' })
|
||
const hotRest = hot.list.filter((p) => !p.isTop)
|
||
for (let i = 1; i < hotRest.length; i++) {
|
||
expect(hotRest[i - 1].views).toBeGreaterThan(hotRest[i].views - 1)
|
||
}
|
||
if (hot.list.some((p) => p.isTop)) {
|
||
expect(hot.list[0].isTop).toBe(true)
|
||
}
|
||
const page1 = await homeApi.feed({ page: 1, size: 2 })
|
||
expect(page1.list).toHaveLength(2)
|
||
expect(page1.hasMore).toBe(true)
|
||
expect(page1.total).toBeGreaterThan(2)
|
||
})
|
||
|
||
it('置顶信息在信息流中排首位(后台手动置顶)', async () => {
|
||
const res = await homeApi.feed({ page: 1, size: 10 })
|
||
const pinned = res.list.filter((p) => p.isTop)
|
||
expect(pinned.length).toBeGreaterThan(0)
|
||
// 所有置顶项都排在非置顶项之前
|
||
const firstUnpinned = res.list.findIndex((p) => !p.isTop)
|
||
const lastPinned = res.list.map((p) => !!p.isTop).lastIndexOf(true)
|
||
expect(lastPinned).toBeLessThanOrEqual(firstUnpinned - 1)
|
||
expect(res.list[0].isTop).toBe(true)
|
||
})
|
||
|
||
it('关键词搜索命中标题', async () => {
|
||
const res = await homeApi.feed({ keyword: '快递' })
|
||
expect(res.total).toBeGreaterThan(0)
|
||
expect(res.list[0].title).toContain('快递')
|
||
})
|
||
|
||
it('热门搜索词可用', async () => {
|
||
const list = await homeApi.hotKeywords()
|
||
expect(list.length).toBeGreaterThan(0)
|
||
})
|
||
})
|
||
|
||
describe('用户端 · 信息发布与详情', () => {
|
||
it('免费发布返回待审核状态且无订单', async () => {
|
||
const res = await postApi.create({ title: '测试标题', content: '内容', paid: false })
|
||
expect(res.postId).toBeDefined()
|
||
expect(res.auditStatus).toBe(0)
|
||
expect(res.order).toBeFalsy()
|
||
})
|
||
|
||
it('付费发布返回支付订单', async () => {
|
||
const res = await postApi.create({ title: '付费标题', content: '内容', paid: true, amount: 5, payType: 'wxpay' })
|
||
expect(res.order.orderNo).toContain('PO')
|
||
expect(res.order.amount).toBe(5)
|
||
})
|
||
|
||
it('必填校验:标题 / 内容', async () => {
|
||
await expectReject(() => postApi.create({ content: 'x' }), '标题不能为空')
|
||
await expectReject(() => postApi.create({ title: 'x' }), '内容不能为空')
|
||
})
|
||
|
||
it('草稿箱保存与读取', async () => {
|
||
const d = await postApi.saveDraft({ title: '草稿A', content: 'bbb' })
|
||
expect(d.draftId).toBeDefined()
|
||
const list = await postApi.drafts()
|
||
expect(list).toBeArray()
|
||
})
|
||
|
||
it('详情返回发布者与互动数据', async () => {
|
||
const res = await postApi.detail(102)
|
||
expect(res.id).toBe(102)
|
||
expect(res.publisher.name).toBeDefined()
|
||
expect(res.views).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('详情:会员/已解锁直接返回明文联系方式(v1.1.1 修复「开通会员仍显示已受保护」)', async () => {
|
||
const res = await postApi.detail(102)
|
||
expect(res.unlocked).toBe(true)
|
||
expect(res.contact).toMatch(/^1\d{10}$/)
|
||
expect(res.wechat).toBeTruthy()
|
||
})
|
||
|
||
it('付费查看:支持微信 / 余额,非法方式被拦截', async () => {
|
||
const wx = await postApi.payView(101, { payType: 'wxpay' })
|
||
// 与后端 Post::payView 对齐:微信支付只返回待支付订单(不含 contact/unlocked),解锁由支付回调写入
|
||
expect(wx.order.orderNo).toBeDefined()
|
||
expect(wx.unlocked).toBeFalsy()
|
||
const bal = await postApi.payView(101, { payType: 'balance' })
|
||
expect(bal.unlocked).toBe(true)
|
||
await expectReject(() => postApi.payView(101, { payType: 'alipay' }), '支付方式不支持')
|
||
})
|
||
|
||
it('评价互动:评分 1-5 校验', async () => {
|
||
const ok = await postApi.comment(101, { score: 5, content: '很好' })
|
||
expect(ok.commentId).toBeDefined()
|
||
await expectReject(() => postApi.comment(101, { score: 9, content: 'x' }), '评分需在 1-5 之间')
|
||
await expectReject(() => postApi.comment(101, { score: 5 }), '评价内容不能为空')
|
||
})
|
||
})
|
||
|
||
describe('用户端 · 会员中心', () => {
|
||
it('会员套餐:月/季/年三档', async () => {
|
||
const plans = await memberApi.plans()
|
||
expect(plans).toHaveLength(3)
|
||
expect(plans[0].name).toBe('月卡')
|
||
expect(plans[2].name).toBe('年卡')
|
||
})
|
||
|
||
it('会员权益可读取', async () => {
|
||
const b = await memberApi.benefits()
|
||
expect(b.freeView).toBeGreaterThan(0)
|
||
expect(b.benefits.length).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('开通会员:套餐必选、支付方式受控', async () => {
|
||
const res = await memberApi.open({ planId: 'M12', payType: 'wxpay' })
|
||
expect(res.status).toBe('paid')
|
||
expect(res.orderNo).toContain('MO')
|
||
await expectReject(() => memberApi.open({ payType: 'wxpay' }), '请选择会员套餐')
|
||
await expectReject(() => memberApi.open({ planId: 'M1', payType: 'card' }), '支付方式不支持')
|
||
})
|
||
|
||
it('订单管理包含发布 / 会员 / 充值三类', async () => {
|
||
const res = await memberApi.orders({ page: 1, size: 50 })
|
||
const types = res.list.map((o) => o.type)
|
||
expect(types).toContain('post')
|
||
expect(types).toContain('member')
|
||
expect(types).toContain('recharge')
|
||
})
|
||
|
||
it('我的资产包含余额 / 优惠券 / 积分', async () => {
|
||
const a = await memberApi.assets()
|
||
expect(a.balance).toBeDefined()
|
||
expect(a.coupons).toBeArray()
|
||
expect(a.points).toBeDefined()
|
||
})
|
||
})
|
||
|
||
describe('用户端 · 分销中心', () => {
|
||
it('佣金概览字段完整', async () => {
|
||
const o = await distributionApi.overview()
|
||
expect(o.withdrawable).toBeDefined()
|
||
expect(o.totalCommission).toBeDefined()
|
||
expect(o.inviteCode).toBeDefined()
|
||
})
|
||
|
||
it('团队管理按一级/二级筛选', async () => {
|
||
const all = await distributionApi.team({ page: 1, size: 50 })
|
||
expect(all.list.length).toBeGreaterThan(0)
|
||
const l1 = await distributionApi.team({ level: 1 })
|
||
expect(l1.list.every((t) => t.level === 1)).toBe(true)
|
||
const l2 = await distributionApi.team({ level: 2 })
|
||
expect(l2.list.every((t) => t.level === 2)).toBe(true)
|
||
})
|
||
|
||
it('提现校验:金额合法且不超过可提现余额', async () => {
|
||
const res = await distributionApi.withdraw({ amount: 10, channel: 'wxpay' })
|
||
expect(res.status).toBe('auditing')
|
||
await expectReject(() => distributionApi.withdraw({ amount: 0 }), '提现金额不合法')
|
||
await expectReject(() => distributionApi.withdraw({ amount: 999999 }), '超过可提现余额')
|
||
})
|
||
|
||
it('提现记录可查询', async () => {
|
||
const list = await distributionApi.withdrawRecords()
|
||
expect(list).toBeArray()
|
||
})
|
||
})
|
||
|
||
describe('用户端 · 消息中心', () => {
|
||
it('系统通知与互动消息分类正确', async () => {
|
||
const sys = await messageApi.list({ type: 'system' })
|
||
expect(sys.list.every((m) => m.type === 'system')).toBe(true)
|
||
const inter = await messageApi.list({ type: 'interact' })
|
||
expect(inter.list.every((m) => m.type === 'interact')).toBe(true)
|
||
})
|
||
|
||
it('标记已读', async () => {
|
||
const res = await messageApi.read(1)
|
||
expect(res.read).toBe(true)
|
||
})
|
||
})
|
||
|