From ff96e2f7ad28fa417fc6430f1f49bada8f3fa49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Sun, 17 May 2026 06:45:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(admin):=20I4=20=E2=80=94=20devPlainCode-?= =?UTF-8?q?=D0=B1=D0=B0=D0=BD=D0=BD=D0=B5=D1=80=20=D0=B7=D0=B0=20import.me?= =?UTF-8?q?ta.env.DEV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/admin/ImpersonationDialog.vue | 8 ++++-- .../Frontend/ImpersonationDialog.spec.ts | 28 ++++++++++++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) 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); + }); });