Files
sori.studio/tests/media-catalog.test.js

107 lines
3.6 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import {
detectMediaKind,
detectMediaRole,
escapePostgresLikeLiteral,
extractMediaReferences,
normalizeMediaCatalogUrl
} from '../lib/media-catalog.js'
import { normalizeMediaCatalogMode } from '../server/utils/media-catalog-mode.js'
test('미디어 카탈로그 모드는 운영 조회 모드만 허용한다', () => {
assert.equal(normalizeMediaCatalogMode('filesystem'), 'filesystem')
assert.equal(normalizeMediaCatalogMode('DATABASE'), 'database')
assert.equal(normalizeMediaCatalogMode(' shadow '), null)
assert.equal(normalizeMediaCatalogMode(''), null)
})
test('SQL LIKE wildcard 검색 문자를 리터럴로 이스케이프한다', () => {
assert.equal(escapePostgresLikeLiteral('100%_done!'), '100!%!_done!!')
})
test('미디어 URL을 저장 키로 정규화하고 쿼리를 제거한다', () => {
assert.deepEqual(
normalizeMediaCatalogUrl('/uploads/posts/2026/07/%ED%95%9C%EA%B8%80.png?v=1'),
{
url: '/uploads/posts/2026/07/한글.png',
storageKey: 'posts/2026/07/한글.png'
}
)
assert.deepEqual(
normalizeMediaCatalogUrl('/uploads/posts/literal-100%.png'),
{
url: '/uploads/posts/literal-100%.png',
storageKey: 'posts/literal-100%.png'
}
)
})
test('업로드 루트 이탈과 인코딩된 traversal을 차단한다', () => {
assert.throws(() => normalizeMediaCatalogUrl('/uploads/../secret.txt'), /MEDIA_URL_TRAVERSAL/)
assert.throws(() => normalizeMediaCatalogUrl('/uploads/%2e%2e/secret.txt'), /MEDIA_URL_TRAVERSAL/)
assert.throws(() => normalizeMediaCatalogUrl('/etc/passwd'), /MEDIA_URL_OUTSIDE_UPLOADS/)
})
test('확장자 종류와 시스템 역할 및 파생 원본 후보를 판별한다', () => {
assert.equal(detectMediaKind('photo.webp'), 'image')
assert.equal(detectMediaKind('clip.mp4'), 'video')
assert.equal(detectMediaKind('voice.m4a'), 'audio')
assert.equal(detectMediaKind('document.pdf'), 'file')
assert.deepEqual(detectMediaRole('exports/posts.zip'), {
role: 'export',
parentStorageKey: null
})
assert.deepEqual(detectMediaRole('posts/2026/07/thumbs/photo-card.webp'), {
role: 'card',
parentStorageKey: 'posts/2026/07/photo.jpg'
})
assert.equal(detectMediaRole('members/avatars/2026/07/profile.webp').role, 'avatar')
})
test('본문 720·1600 파생 URL을 레거시 body와 구분한다', async () => {
const {
isPostBody1600ThumbnailUrl,
isPostBody720ThumbnailUrl,
isPostBodyThumbnailUrl
} = await import('../lib/post-card-thumbnail-url.js')
assert.equal(
isPostBodyThumbnailUrl('/uploads/posts/2026/07/thumbs/photo-body.webp'),
true
)
assert.equal(
isPostBody1600ThumbnailUrl('/uploads/posts/2026/07/thumbs/photo-body.webp'),
false
)
assert.equal(
isPostBody720ThumbnailUrl('/uploads/posts/2026/07/thumbs/photo-body.webp'),
false
)
assert.equal(
isPostBody1600ThumbnailUrl('/uploads/posts/2026/07/thumbs/photo-body-1600.webp'),
true
)
assert.equal(
isPostBody720ThumbnailUrl('/uploads/posts/2026/07/thumbs/photo-body-720.webp'),
true
)
})
test('필드별 미디어 참조를 추출하고 같은 필드 중복을 제거한다', () => {
assert.deepEqual(extractMediaReferences({
content: '![a](/uploads/posts/a.png) 다시 /uploads/posts/a.png',
featuredImage: '/uploads/posts/a.png',
external: 'https://example.com/image.png'
}), [
{
mediaUrl: '/uploads/posts/a.png',
referenceField: 'content'
},
{
mediaUrl: '/uploads/posts/a.png',
referenceField: 'featuredImage'
}
])
})