세션·로그인 제한, ZIP/업로드 스트리밍, CSP 최소 강제와 Report-Only, Playwright 격리 검증을 추가하고 미디어 목록을 reconcile 후 database 모드로 고정한다. Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.1 KiB
JavaScript
99 lines
3.1 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import {
|
|
detectMediaKind,
|
|
detectMediaRole,
|
|
escapePostgresLikeLiteral,
|
|
extractMediaReferences,
|
|
normalizeMediaCatalogUrl
|
|
} from '../lib/media-catalog.js'
|
|
|
|
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: ' 다시 /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'
|
|
}
|
|
])
|
|
})
|