diff --git a/app/resources/js/components/admin/ImpersonationDialog.vue b/app/resources/js/components/admin/ImpersonationDialog.vue index 9a2dce08..c9c0f3f0 100644 --- a/app/resources/js/components/admin/ImpersonationDialog.vue +++ b/app/resources/js/components/admin/ImpersonationDialog.vue @@ -5,7 +5,7 @@ * 3-step state-machine: * 1. 'reason' — textarea для основания (≥30 chars) → POST /api/admin/impersonation/init. * 2. 'verify' — показ email клиента + ввод 6-значного кода → /api/admin/impersonation/verify. - * На dev показывается _dev_plain_code (на prod исчезнет после MailService). + * На dev показывается _dev_plain_code (за import.meta.env.DEV; на prod баннер не рендерится). * 3. 'active' — chip «Сессия активна», кнопка «Завершить» → /api/admin/impersonation/end. * * NB: на MVP saas-admin auth не реализован, requested_by передаётся параметром @@ -49,6 +49,10 @@ const expiresAt = ref(null); const devPlainCode = ref(null); const usedAtIso = ref(null); +// I4: явный frontend DEV-gate. import.meta.env.DEV статически заменяется Vite — +// в prod-сборке = false, баннер с плейн-кодом tree-shake'ится. +const isDevEnv = import.meta.env.DEV; + const reasonLength = computed(() => reason.value.trim().length); const reasonRemaining = computed(() => Math.max(0, 30 - reasonLength.value)); const reasonValid = computed(() => reasonLength.value >= 30); @@ -216,7 +220,7 @@ function close() { data-testid="code-input" /> { vi.clearAllMocks(); }); + afterEach(() => { + vi.unstubAllEnvs(); + }); + it('не рендерит content когда modelValue=false', () => { const wrapper = factory({ modelValue: false, tenant: sampleTenant }); expect(wrapper.find('.dialog-stub').exists()).toBe(false); @@ -198,4 +202,26 @@ describe('ImpersonationDialog.vue', () => { expect(events).toBeDefined(); expect(events?.[0]).toEqual([false]); }); + + it('I4 — dev-code-banner НЕ рендерится когда import.meta.env.DEV=false (prod)', async () => { + vi.stubEnv('DEV', false); + + vi.mocked(adminApi.impersonationInit).mockResolvedValue({ + token_id: 42, + expires_at: '2026-05-09T12:00:00Z', + sent_to_email: 'admin@okna-moscow.ru', + _dev_plain_code: '123456', + }); + + const wrapper = factory({ modelValue: true, tenant: sampleTenant }); + await wrapper.find('[data-testid="reason-input"] textarea').setValue( + 'Тикет SUP-12453: клиент сообщил, что в карточке сделки не сохраняется коммент.', + ); + await wrapper.find('[data-testid="submit-init-btn"]').trigger('click'); + await flushPromises(); + + // step verify достигнут — devPlainCode='123456' есть, но DEV=false → баннер не рендерится + expect(wrapper.text()).toContain('Код отправлен на email клиента'); + expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(false); + }); });