| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import { PublishContentMode } from '../platforms/platforms.interface' |
| 3 | import { PublishValidationCombination, PublishValidationField, PublishValidationIssueCode } from '../platforms/publish.schema' |
| 4 | import { validatePublishContent } from './publish-content.utils' |
| 5 | |
| 6 | describe('validatePublishContent modes', () => { |
| 7 | it('rejects unsupported text mode', () => { |
| 8 | const issues = validatePublishContent( |
| 9 | { title: 'title', body: 'body', media: [] }, |
| 10 | { modes: [PublishContentMode.Video] }, |
| 11 | ) |
| 12 | |
| 13 | expect(issues).toContainEqual(expect.objectContaining({ |
| 14 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 15 | params: expect.objectContaining({ mode: PublishContentMode.Text }), |
| 16 | })) |
| 17 | }) |
| 18 | |
| 19 | it('rejects unsupported image-text mode', () => { |
| 20 | const issues = validatePublishContent( |
| 21 | { body: 'body', media: [{ url: 'https://assets.example.test/a.jpg' }] }, |
| 22 | { modes: [PublishContentMode.Video] }, |
| 23 | ) |
| 24 | |
| 25 | expect(issues).toContainEqual(expect.objectContaining({ |
| 26 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 27 | params: expect.objectContaining({ mode: PublishContentMode.ImageText }), |
| 28 | })) |
| 29 | }) |
| 30 | |
| 31 | it('rejects unsupported video mode', () => { |
| 32 | const issues = validatePublishContent( |
| 33 | { body: 'body', media: [{ url: 'https://assets.example.test/a.mp4' }] }, |
| 34 | { modes: [PublishContentMode.ImageText] }, |
| 35 | ) |
| 36 | |
| 37 | expect(issues).toContainEqual(expect.objectContaining({ |
| 38 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 39 | params: expect.objectContaining({ mode: PublishContentMode.Video }), |
| 40 | })) |
| 41 | }) |
| 42 | |
| 43 | it('allows declared content modes', () => { |
| 44 | const limits = { |
| 45 | modes: [PublishContentMode.Text, PublishContentMode.ImageText, PublishContentMode.Video], |
| 46 | } |
| 47 | |
| 48 | expect(validatePublishContent({ body: 'body', media: [] }, limits)).toEqual([]) |
| 49 | expect(validatePublishContent({ |
| 50 | body: 'body', |
| 51 | media: [], |
| 52 | cover: { url: 'https://assets.example.test/cover.jpg' }, |
| 53 | }, limits)).toEqual([]) |
| 54 | expect(validatePublishContent({ body: 'body', media: [{ url: 'https://assets.example.test/a.jpg' }] }, limits)).toEqual([]) |
| 55 | expect(validatePublishContent({ body: 'body', media: [{ url: 'https://assets.example.test/a.mp4' }] }, limits)).toEqual([]) |
| 56 | }) |
| 57 | |
| 58 | it('treats text with cover as image-text mode', () => { |
| 59 | const issues = validatePublishContent( |
| 60 | { |
| 61 | body: 'body', |
| 62 | media: [], |
| 63 | cover: { url: 'https://assets.example.test/cover.jpg' }, |
| 64 | }, |
| 65 | { modes: [PublishContentMode.Text] }, |
| 66 | ) |
| 67 | |
| 68 | expect(issues).toContainEqual(expect.objectContaining({ |
| 69 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 70 | params: expect.objectContaining({ mode: PublishContentMode.ImageText }), |
| 71 | })) |
| 72 | }) |
| 73 | |
| 74 | it('keeps mixed image/video as invalid combination instead of unsupported mode', () => { |
| 75 | const issues = validatePublishContent( |
| 76 | { |
| 77 | body: 'body', |
| 78 | media: [ |
| 79 | { url: 'https://assets.example.test/a.jpg' }, |
| 80 | { url: 'https://assets.example.test/a.mp4' }, |
| 81 | ], |
| 82 | }, |
| 83 | { modes: [PublishContentMode.ImageText, PublishContentMode.Video] }, |
| 84 | ) |
| 85 | |
| 86 | expect(issues).toContainEqual(expect.objectContaining({ |
| 87 | code: PublishValidationIssueCode.InvalidCombination, |
| 88 | params: expect.objectContaining({ combination: PublishValidationCombination.ImageVideo }), |
| 89 | })) |
| 90 | expect(issues).not.toContainEqual(expect.objectContaining({ |
| 91 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 92 | })) |
| 93 | }) |
| 94 | |
| 95 | it('classifies media by URL path extension instead of query string substrings', () => { |
| 96 | const issues = validatePublishContent( |
| 97 | { |
| 98 | body: 'body', |
| 99 | media: [{ url: 'https://assets.example.test/image.jpg?format=mp4' }], |
| 100 | }, |
| 101 | { modes: [PublishContentMode.ImageText] }, |
| 102 | ) |
| 103 | |
| 104 | expect(issues).toEqual([]) |
| 105 | }) |
| 106 | |
| 107 | it('classifies signed URL media by server metadata type before URL extension fallback', () => { |
| 108 | const issues = validatePublishContent( |
| 109 | { |
| 110 | body: 'body', |
| 111 | media: [{ url: 'https://assets.example.test/download?id=video-1', metadata: { type: 'video' } }], |
| 112 | }, |
| 113 | { modes: [PublishContentMode.Video] }, |
| 114 | ) |
| 115 | |
| 116 | expect(issues).toEqual([]) |
| 117 | }) |
| 118 | |
| 119 | it('detects image and video combinations from server metadata type', () => { |
| 120 | const issues = validatePublishContent( |
| 121 | { |
| 122 | body: 'body', |
| 123 | media: [ |
| 124 | { url: 'https://assets.example.test/download?id=image-1', metadata: { type: 'image' } }, |
| 125 | { url: 'https://assets.example.test/download?id=video-1', metadata: { type: 'video' } }, |
| 126 | ], |
| 127 | }, |
| 128 | { modes: [PublishContentMode.ImageText, PublishContentMode.Video] }, |
| 129 | ) |
| 130 | |
| 131 | expect(issues).toContainEqual(expect.objectContaining({ |
| 132 | code: PublishValidationIssueCode.InvalidCombination, |
| 133 | params: expect.objectContaining({ combination: PublishValidationCombination.ImageVideo }), |
| 134 | })) |
| 135 | expect(issues).not.toContainEqual(expect.objectContaining({ |
| 136 | code: PublishValidationIssueCode.UnsupportedContentMode, |
| 137 | })) |
| 138 | }) |
| 139 | |
| 140 | it('accepts extensionless covers when server metadata classifies them as images', () => { |
| 141 | const issues = validatePublishContent( |
| 142 | { |
| 143 | body: 'body', |
| 144 | media: [], |
| 145 | cover: { |
| 146 | url: 'https://assets.example.test/download?id=cover-1', |
| 147 | metadata: { type: 'image' }, |
| 148 | }, |
| 149 | }, |
| 150 | { modes: [PublishContentMode.ImageText] }, |
| 151 | ) |
| 152 | |
| 153 | expect(issues).toEqual([]) |
| 154 | }) |
| 155 | |
| 156 | it('rejects topics parsed from body when platform topic maxCount is exceeded', () => { |
| 157 | const issues = validatePublishContent( |
| 158 | { |
| 159 | body: '#a #b #c #d #e #f', |
| 160 | media: [], |
| 161 | }, |
| 162 | { modes: [PublishContentMode.Text] }, |
| 163 | { supported: true, maxCount: 5 }, |
| 164 | ) |
| 165 | |
| 166 | expect(issues).toContainEqual(expect.objectContaining({ |
| 167 | code: PublishValidationIssueCode.TooBig, |
| 168 | path: ['content', 'topics'], |
| 169 | params: expect.objectContaining({ |
| 170 | field: PublishValidationField.Topic, |
| 171 | maximum: 5, |
| 172 | }), |
| 173 | })) |
| 174 | }) |
| 175 | |
| 176 | it('uses stripped body length for platforms with native topic fields', () => { |
| 177 | expect(validatePublishContent( |
| 178 | { |
| 179 | body: '正文 #很长很长的话题', |
| 180 | media: [], |
| 181 | }, |
| 182 | { modes: [PublishContentMode.Text], maxBodyLength: 2 }, |
| 183 | { supported: true, nativeField: true }, |
| 184 | )).toEqual([]) |
| 185 | |
| 186 | expect(validatePublishContent( |
| 187 | { |
| 188 | title: '标题', |
| 189 | body: '#话题', |
| 190 | media: [], |
| 191 | }, |
| 192 | { modes: [PublishContentMode.Text], maxTotalTextLength: 2 }, |
| 193 | { supported: true, nativeField: true }, |
| 194 | )).toEqual([]) |
| 195 | }) |
| 196 | }) |
| 197 |