diff --git a/frontend/src/lib/services/contentEditor.test.ts b/frontend/src/lib/services/contentEditor.test.ts new file mode 100644 index 0000000..c0dc8df --- /dev/null +++ b/frontend/src/lib/services/contentEditor.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, vi } from 'vitest'; +import { deleteEntry, renameEntry } from './contentEditor'; + +describe('deleteEntry', () => { + it('mengirim force=true saat dipanggil dengan argumen force', async () => { + const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({ success: true }) }); + global.fetch = fetchMock; + + await deleteEntry('content', 'folder_x', false, true); + + const url = fetchMock.mock.calls[0][0] as string; + expect(url).toContain('force=true'); + expect(url).toContain('confirm_critical=false'); + }); + + it('mengirim confirm_critical=true untuk file navigasi kritis', async () => { + const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({ success: true }) }); + global.fetch = fetchMock; + + await deleteEntry('content', 'home.md', true); + + const url = fetchMock.mock.calls[0][0] as string; + expect(url).toContain('confirm_critical=true'); + expect(url).toContain('force=false'); + }); + + it('mengirim query string yang benar untuk folder biasa', async () => { + const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({ success: true }) }); + global.fetch = fetchMock; + + await deleteEntry('content', 'some_folder'); + + const url = fetchMock.mock.calls[0][0] as string; + expect(url).toContain('force=false'); + expect(url).toContain('confirm_critical=false'); + }); +}); + +describe('renameEntry', () => { + it('mengirim rename dengan argumen standar', async () => { + const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({ success: true }) }); + global.fetch = fetchMock; + + await renameEntry('content', 'old.md', 'new.md', false); + + expect(fetchMock.mock.calls[0][1]).toMatchObject({ + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + }); + }); + + it('mengirim confirm_critical=true untuk filename kritis', async () => { + const fetchMock = vi.fn().mockResolvedValue({ json: async () => ({ success: true }) }); + global.fetch = fetchMock; + + await renameEntry('content', 'home.md', 'renamed.md', true); + + const body = JSON.parse(fetchMock.mock.calls[0][1]?.body ?? '{}'); + expect(body.confirm_critical).toBe(true); + }); +}); \ No newline at end of file diff --git a/frontend/src/routes/teacher/content/+page.svelte b/frontend/src/routes/teacher/content/+page.svelte index 6c3040f..1f17647 100644 --- a/frontend/src/routes/teacher/content/+page.svelte +++ b/frontend/src/routes/teacher/content/+page.svelte @@ -528,7 +528,7 @@ } } else if (res.needs_force) { if (window.confirm(`Folder "${name}" tidak kosong. Hapus semua isi?`)) { - const forceRes = await deleteEntry(mgr.treeRoot, path, true); + const forceRes = await deleteEntry(mgr.treeRoot, path, PROTECTED_BASENAMES.has(name), true); if (forceRes.success) { await loadTrees(); if (mgr.activePath === path) { diff --git a/routes/content_editor.py b/routes/content_editor.py index ee89596..3691779 100644 --- a/routes/content_editor.py +++ b/routes/content_editor.py @@ -485,7 +485,7 @@ def upload_asset(): # Validasi isi benar-benar gambar (bukan cuma cek ekstensi) — SVG # dikecualikan (Pillow tidak parse SVG). - # Pakai .load() (decode sungguhan)而不是 .verify(): .verify() melakukan + # Pakai .load() (decode sungguhan) bukan .verify(): .verify() melakukan # CRC check strict pada IDAT yang sering FALSE-POSITIVE pada PNG valid # (terutama PNG sederhana / hasil re-encode) → backend 500. .load() # sudah cukup membuktikan file adalah gambar yang bisa di-decode.