// @vitest-environment jsdom import { render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { ResolvedAssetImage } from '../components/ResolvedAssetImage'; import { clearStoredAccessToken, setStoredAccessToken } from '../services/apiClient'; import { clearSignedAssetReadUrlCache } from '../services/assetReadUrlService'; describe('useResolvedAssetReadUrl', () => { beforeEach(() => { clearSignedAssetReadUrlCache(); clearStoredAccessToken({ emit: false }); setStoredAccessToken('test-access-token', { emit: false }); vi.restoreAllMocks(); }); afterEach(() => { clearStoredAccessToken({ emit: false }); }); test('generated 私有资源签名完成前不会把裸路径写入 img', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response( JSON.stringify({ ok: true, data: { read: { objectKey: 'generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png', signedUrl: 'https://signed.example.com/puzzle.png', expiresAt: '2099-01-01T00:10:00Z', }, }, error: null, meta: { apiVersion: '2026-04-08', routeVersion: '2026-04-08', latencyMs: 1, timestamp: '2099-01-01T00:00:00Z', }, }), { status: 200, headers: { 'Content-Type': 'application/json', }, }, ), ); render( , ); expect(screen.queryByRole('img', { name: '候选图' })).toBeNull(); const image = await screen.findByRole('img', { name: '候选图' }); expect(image.getAttribute('src')).toBe('https://signed.example.com/puzzle.png'); expect(globalThis.fetch).toHaveBeenCalledTimes(1); expect(String(vi.mocked(globalThis.fetch).mock.calls[0]?.[0])).toContain( 'legacyPublicPath=%2Fgenerated-puzzle-assets%2Fpuzzle-session-1%2Fcandidate-1%2Fasset-1%2Fimage.png', ); }); test('无前导斜杠的 generated 私有资源也不会裸写入 img', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response( JSON.stringify({ ok: true, data: { read: { objectKey: 'generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png', signedUrl: 'https://signed.example.com/puzzle.png', expiresAt: '2099-01-01T00:10:00Z', }, }, error: null, meta: { apiVersion: '2026-04-08', routeVersion: '2026-04-08', latencyMs: 1, timestamp: '2099-01-01T00:00:00Z', }, }), { status: 200, headers: { 'Content-Type': 'application/json', }, }, ), ); render( , ); expect(screen.queryByRole('img', { name: '候选图' })).toBeNull(); const image = await screen.findByRole('img', { name: '候选图' }); expect(image.getAttribute('src')).toBe('https://signed.example.com/puzzle.png'); }); test('refreshKey changes force a fresh signed url request without mutating OSS signature query', async () => { vi.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response( JSON.stringify({ ok: true, data: { read: { objectKey: 'generated-puzzle-assets/puzzle-session-1/candidate-1/asset-1/image.png', signedUrl: 'https://signed.example.com/puzzle.png', expiresAt: '2099-01-01T00:10:00Z', }, }, error: null, meta: { apiVersion: '2026-04-08', routeVersion: '2026-04-08', latencyMs: 1, timestamp: '2099-01-01T00:00:00Z', }, }), { status: 200, headers: { 'Content-Type': 'application/json', }, }, ), ); const { rerender } = render( , ); const firstImage = await screen.findByRole('img', { name: '候选图' }); expect(firstImage.getAttribute('src')).toBe('https://signed.example.com/puzzle.png'); rerender( , ); await waitFor(() => { expect(globalThis.fetch).toHaveBeenCalledTimes(2); }); expect(screen.getByRole('img', { name: '候选图' }).getAttribute('src')).toBe( 'https://signed.example.com/puzzle.png', ); }); test('generated 私有资源签名失败时保持空图像而不是回退裸路径', async () => { vi.spyOn(globalThis, 'fetch').mockResolvedValue( new Response( JSON.stringify({ ok: false, data: null, error: { code: 'NOT_FOUND', message: '对象不存在', }, meta: { apiVersion: '2026-04-08', routeVersion: '2026-04-08', latencyMs: 1, timestamp: '2099-01-01T00:00:00Z', }, }), { status: 400, headers: { 'Content-Type': 'application/json', }, }, ), ); render( , ); await waitFor(() => { expect(globalThis.fetch).toHaveBeenCalledTimes(1); }); expect(screen.queryByRole('img', { name: '候选图' })).toBeNull(); }); });