fix(ads): 终审修复——staging 前缀+运行时上限核验/失效绑定编辑/白名单去 heic/杂项
- verifyStaging 硬化(services/api/src/ads/ads.service.ts):新增键前缀核验 (仅 OBJECT_KEY_ROOT/ads/staging/ 布局,防照片域任意键被提升为公开广告) 与运行时 2MB 上限(head.bytes > AD_UPLOAD_MAX_BYTES → 400 报文含限额值 ——init 申明管不住实际 PUT 体,create/update 是唯一核验点);e2e ⑰ 以 测试 storage 直接 presignPut 真实对象覆盖两分支 - 失效绑定编辑 400(admin AdsFormDialog):PATCH 仅在 albumIds 实际变化时 携带(albumIdsChanged 序敏感深比较,纯函数落 adsLogic + vitest 用例); preloadOptions 解析失败的绑定 ID 加「已失效,可移除」后缀(zh/en 新增 adsManage.form.staleAlbum)——软删绑定不再挡住其它字段保存 - 白名单去 heic(原图直出无变体兜底,Chromium/Firefox 不渲染=破图): contracts 三处 enum 收窄 [image/jpeg, image/png] 并重生成类型;dto 定义 广告域独立 AD_IMAGE_CONTENT_TYPES(与照片域 UPLOAD_CONTENT_TYPES 分离), AD_IMAGE_EXTENSIONS 由其派生;init 对 .heic 文件名以广告域集合二次核验 拒绝(共享 extOfFilename 仍认 heic,杜绝 init 放行→create 中段断裂); e2e heic 矩阵 400;admin accept/文案/类型同步收窄 - searchAlbums 补 catch(http 层已弹 toast,本地静默保持旧选项) - ActivityPage 关闭记忆测试补位(activity.spec):dismissed 不拉取不渲染、 close 写 sessionStorage 并清空轮播两用例(getPublicAds 入替身防真实 fetch) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,14 @@ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
import App from '../App.vue';
|
||||
import { ApiError, getPublicAlbum, getPublicPhotos, type PublicAlbum } from '../api/client';
|
||||
import {
|
||||
ApiError,
|
||||
getPublicAds,
|
||||
getPublicAlbum,
|
||||
getPublicPhotos,
|
||||
type PublicAd,
|
||||
type PublicAlbum,
|
||||
} from '../api/client';
|
||||
import { createViewerI18n, type ViewerI18n } from '../i18n';
|
||||
import ActivityPage from '../pages/ActivityPage.vue';
|
||||
import { router } from '../router';
|
||||
@@ -12,13 +19,15 @@ import { makeAlbum, makeEnAlbum } from './fixtures';
|
||||
// getPublicPhotos(Task 7 起活动页内嵌 PhotoFeed,需喂空页避免真实 fetch),
|
||||
// 保证 ApiError/errors 映射等真实逻辑参与断言。Task 10 起活动页组装 WS/浏览
|
||||
// 上报:RealtimeClient 以无操作桩替换(jsdom WebSocket 会发起真实连接)、
|
||||
// postAlbumVisit 替身防真实 fetch。
|
||||
// postAlbumVisit 替身防真实 fetch;广告位轮播(Task 10)getPublicAds 同样
|
||||
// 替身(相册加载成功后页面会独立拉取广告,不替身会发起真实 fetch)。
|
||||
vi.mock('../api/client', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../api/client')>();
|
||||
return {
|
||||
...actual,
|
||||
getPublicAlbum: vi.fn(),
|
||||
getPublicPhotos: vi.fn().mockResolvedValue({ items: [], nextCursor: null, hasMore: false }),
|
||||
getPublicAds: vi.fn().mockResolvedValue({ items: [] }),
|
||||
postAlbumVisit: vi.fn().mockResolvedValue({ viewCount: 0, counted: false }),
|
||||
};
|
||||
});
|
||||
@@ -33,6 +42,7 @@ vi.mock('../realtime/ws', () => {
|
||||
|
||||
const mockedGetPublicAlbum = vi.mocked(getPublicAlbum);
|
||||
const mockedGetPublicPhotos = vi.mocked(getPublicPhotos);
|
||||
const mockedGetPublicAds = vi.mocked(getPublicAds);
|
||||
|
||||
// 逐挂载独立 i18n 实例 + 逐用例卸载:vue-i18n 在 app.unmount 时 dispose 共享实例
|
||||
// (见 i18n/index.ts 注释),复用单例会让后续用例的 locale 写入失效;残留组件的
|
||||
@@ -63,9 +73,11 @@ async function pickLanguage(wrapper: VueWrapper, option: '中文' | 'English'):
|
||||
describe('activity page (Task 6)', () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
i18n = createViewerI18n('zh-CN');
|
||||
document.title = '';
|
||||
mockedGetPublicAlbum.mockReset().mockResolvedValue(makeAlbum());
|
||||
mockedGetPublicAds.mockReset().mockResolvedValue({ items: [] });
|
||||
await router.push('/a/tok-activity');
|
||||
await router.isReady();
|
||||
});
|
||||
@@ -264,4 +276,37 @@ describe('activity page (Task 6)', () => {
|
||||
expect(chips[1].classes()).toContain('active');
|
||||
expect(chips[1].text()).toBe('Check-in Wall');
|
||||
});
|
||||
|
||||
// —— 广告位轮播(Task 10 终审补位):会话级关闭记忆在页面(组件只 emit
|
||||
// close,无 sessionStorage 副作用)—— dismissed 时不拉取不渲染、
|
||||
// close 后写键并清空,两段都在页面级守护 ——
|
||||
|
||||
function makeAd(adId: string): PublicAd {
|
||||
return { adId, imageUrl: `https://ad.test/${adId}.jpg`, linkUrl: `https://link.test/${adId}` };
|
||||
}
|
||||
|
||||
it('skips the ads request and carousel when the session dismissed flag is set', async () => {
|
||||
sessionStorage.setItem('viewer.adsDismissed', '1');
|
||||
mockedGetPublicAds.mockResolvedValue({ items: [makeAd('dismissed-ad')] });
|
||||
const wrapper = mountPage();
|
||||
await flushPromises();
|
||||
|
||||
// 命中会话级记忆 → 连请求都省(设计:关闭后本会话不再拉取)。
|
||||
expect(mockedGetPublicAds).not.toHaveBeenCalled();
|
||||
expect(wrapper.find('.ad-carousel').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('dismissing the carousel writes the sessionStorage flag and clears it from the page', async () => {
|
||||
mockedGetPublicAds.mockResolvedValue({ items: [makeAd('ad-1')] });
|
||||
const wrapper = mountPage();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('.ad-carousel').exists()).toBe(true);
|
||||
await wrapper.get('.ad-close').trigger('click');
|
||||
await nextTick();
|
||||
|
||||
// 页面 close 处理器:写会话级键 + 清空数据(轮播整块缺席)。
|
||||
expect(sessionStorage.getItem('viewer.adsDismissed')).toBe('1');
|
||||
expect(wrapper.find('.ad-carousel').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user