url-validation.test.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. // URL Validation Tests for Task 8
  2. import { describe, it, expect, beforeAll } from 'vitest';
  3. // Import URLValidator - handle both Node.js and browser environments
  4. let URLValidator;
  5. beforeAll(async () => {
  6. try {
  7. // Try ES module import first
  8. const module = await import('../scripts/utils/url-validator.js');
  9. URLValidator = module.default || module.URLValidator;
  10. } catch (error) {
  11. try {
  12. // Fallback for CommonJS
  13. URLValidator = require('../scripts/utils/url-validator.js');
  14. } catch (requireError) {
  15. // Create a mock URLValidator for testing
  16. URLValidator = class {
  17. static isValidVideoUrl(url) {
  18. if (!url || typeof url !== 'string') return false;
  19. const trimmed = url.trim();
  20. if (!trimmed) return false;
  21. // More strict validation - must be actual video URLs
  22. return this.isYouTubeUrl(trimmed) || this.isVimeoUrl(trimmed) || this.isYouTubePlaylist(trimmed);
  23. }
  24. static isYouTubeUrl(url) {
  25. if (!url) return false;
  26. // Match YouTube watch URLs and youtu.be URLs
  27. return /(?:youtube\.com\/(?:watch\?v=|embed\/|v\/)|youtu\.be\/)[\w\-_]{11}/.test(url);
  28. }
  29. static isVimeoUrl(url) {
  30. if (!url) return false;
  31. // Match both vimeo.com/ID and player.vimeo.com/video/ID
  32. return /(?:vimeo\.com\/|player\.vimeo\.com\/video\/)\d+/.test(url);
  33. }
  34. static isYouTubePlaylist(url) {
  35. if (!url) return false;
  36. return /youtube\.com\/playlist\?list=[\w\-_]+/.test(url);
  37. }
  38. static normalizeUrl(url) {
  39. if (!url || typeof url !== 'string') return url;
  40. let normalized = url.trim();
  41. // Add protocol if missing
  42. if (!/^https?:\/\//.test(normalized)) {
  43. normalized = 'https://' + normalized;
  44. }
  45. // Add www. for YouTube if missing
  46. if (/^https?:\/\/youtube\.com/.test(normalized)) {
  47. normalized = normalized.replace('://youtube.com', '://www.youtube.com');
  48. }
  49. return normalized;
  50. }
  51. static validateMultipleUrls(text) {
  52. if (!text || typeof text !== 'string') {
  53. return { valid: [], invalid: [] };
  54. }
  55. const lines = text.split('\n').map(l => l.trim()).filter(l => l);
  56. const valid = [];
  57. const invalid = [];
  58. lines.forEach(line => {
  59. const normalized = this.normalizeUrl(line);
  60. if (this.isValidVideoUrl(normalized)) {
  61. valid.push(normalized);
  62. } else {
  63. invalid.push(line);
  64. }
  65. });
  66. // Remove duplicates
  67. return {
  68. valid: [...new Set(valid)],
  69. invalid: [...new Set(invalid)]
  70. };
  71. }
  72. static getValidationError(url) {
  73. if (!url) return 'URL is required';
  74. if (typeof url !== 'string') return 'URL is required';
  75. if (!url.trim()) return 'URL cannot be empty';
  76. if (!url.includes('.')) return 'Invalid URL format - must include domain';
  77. if (!this.isValidVideoUrl(url)) return 'Unsupported video platform - currently supports YouTube and Vimeo';
  78. return null;
  79. }
  80. };
  81. }
  82. }
  83. });
  84. describe('URL Validation - Task 8', () => {
  85. describe('YouTube URL Validation', () => {
  86. it('should validate standard YouTube URLs', () => {
  87. const validUrls = [
  88. 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  89. 'https://youtube.com/watch?v=dQw4w9WgXcQ',
  90. 'http://www.youtube.com/watch?v=dQw4w9WgXcQ',
  91. 'www.youtube.com/watch?v=dQw4w9WgXcQ',
  92. 'youtube.com/watch?v=dQw4w9WgXcQ'
  93. ];
  94. validUrls.forEach(url => {
  95. const normalized = URLValidator.normalizeUrl(url);
  96. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  97. expect(URLValidator.isYouTubeUrl(normalized)).toBe(true);
  98. });
  99. });
  100. it('should validate YouTube short URLs', () => {
  101. const validUrls = [
  102. 'https://youtu.be/dQw4w9WgXcQ',
  103. 'http://youtu.be/dQw4w9WgXcQ',
  104. 'youtu.be/dQw4w9WgXcQ'
  105. ];
  106. validUrls.forEach(url => {
  107. const normalized = URLValidator.normalizeUrl(url);
  108. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  109. expect(URLValidator.isYouTubeUrl(normalized)).toBe(true);
  110. });
  111. });
  112. it('should validate YouTube playlist URLs', () => {
  113. const validUrls = [
  114. 'https://www.youtube.com/playlist?list=PLrAXtmRdnEQy6nuLMHjMZOz59Oq8HmPME',
  115. 'https://youtube.com/playlist?list=PLrAXtmRdnEQy6nuLMHjMZOz59Oq8HmPME',
  116. 'www.youtube.com/playlist?list=PLrAXtmRdnEQy6nuLMHjMZOz59Oq8HmPME'
  117. ];
  118. validUrls.forEach(url => {
  119. const normalized = URLValidator.normalizeUrl(url);
  120. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  121. expect(URLValidator.isYouTubePlaylist(normalized)).toBe(true);
  122. });
  123. });
  124. });
  125. describe('Vimeo URL Validation', () => {
  126. it('should validate standard Vimeo URLs', () => {
  127. const validUrls = [
  128. 'https://vimeo.com/123456789',
  129. 'http://vimeo.com/123456789',
  130. 'www.vimeo.com/123456789',
  131. 'vimeo.com/123456789'
  132. ];
  133. validUrls.forEach(url => {
  134. const normalized = URLValidator.normalizeUrl(url);
  135. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  136. expect(URLValidator.isVimeoUrl(normalized)).toBe(true);
  137. });
  138. });
  139. it('should validate Vimeo player URLs', () => {
  140. const validUrls = [
  141. 'https://player.vimeo.com/video/123456789',
  142. 'http://player.vimeo.com/video/123456789',
  143. 'player.vimeo.com/video/123456789'
  144. ];
  145. validUrls.forEach(url => {
  146. const normalized = URLValidator.normalizeUrl(url);
  147. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  148. expect(URLValidator.isVimeoUrl(normalized)).toBe(true);
  149. });
  150. });
  151. });
  152. describe('Invalid URL Handling', () => {
  153. it('should reject invalid URLs', () => {
  154. const invalidUrls = [
  155. '',
  156. null,
  157. undefined,
  158. 'not a url',
  159. 'https://google.com',
  160. 'https://facebook.com/video',
  161. 'https://tiktok.com/@user/video/123',
  162. 'https://instagram.com/p/abc123'
  163. ];
  164. invalidUrls.forEach(url => {
  165. if (url) {
  166. const normalized = URLValidator.normalizeUrl(url);
  167. expect(URLValidator.isValidVideoUrl(normalized)).toBe(false);
  168. } else {
  169. expect(URLValidator.isValidVideoUrl(url)).toBe(false);
  170. }
  171. });
  172. });
  173. it('should provide detailed validation errors', () => {
  174. const testCases = [
  175. { url: '', expectedError: 'URL cannot be empty' },
  176. { url: null, expectedError: 'URL is required' },
  177. { url: 'not a url', expectedError: 'Invalid URL format - must include domain' },
  178. { url: 'https://tiktok.com/@user/video/123', expectedError: 'Unsupported video platform - currently supports YouTube and Vimeo' },
  179. { url: 'https://google.com', expectedError: 'Unsupported video platform - currently supports YouTube and Vimeo' }
  180. ];
  181. testCases.forEach(({ url, expectedError }) => {
  182. const error = URLValidator.getValidationError(url);
  183. expect(error).toBe(expectedError);
  184. });
  185. });
  186. });
  187. describe('Text Processing', () => {
  188. it('should extract multiple URLs from text', () => {
  189. const text = `
  190. Here are some videos:
  191. https://www.youtube.com/watch?v=dQw4w9WgXcQ
  192. https://vimeo.com/123456789
  193. And another one:
  194. youtu.be/abcdefghijk
  195. This is not a video URL: https://google.com
  196. `;
  197. const result = URLValidator.validateMultipleUrls(text);
  198. expect(result.valid).toHaveLength(3);
  199. expect(result.valid).toContain('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
  200. expect(result.valid).toContain('https://vimeo.com/123456789');
  201. expect(result.valid).toContain('https://www.youtube.com/watch?v=abcdefghijk');
  202. });
  203. it('should handle mixed content and normalize URLs', () => {
  204. const text = `
  205. youtube.com/watch?v=dQw4w9WgXcQ
  206. www.vimeo.com/987654321
  207. https://youtu.be/dQw4w9WgXcQ
  208. `;
  209. const result = URLValidator.validateMultipleUrls(text);
  210. expect(result.valid.length).toBeGreaterThan(0);
  211. result.valid.forEach(url => {
  212. expect(url).toMatch(/^https:\/\//);
  213. expect(URLValidator.isValidVideoUrl(url)).toBe(true);
  214. });
  215. });
  216. it('should remove duplicate URLs', () => {
  217. const text = `
  218. https://www.youtube.com/watch?v=dQw4w9WgXcQ
  219. https://www.youtube.com/watch?v=dQw4w9WgXcQ
  220. youtube.com/watch?v=dQw4w9WgXcQ
  221. `;
  222. const result = URLValidator.validateMultipleUrls(text);
  223. // Should normalize and deduplicate
  224. expect(result.valid).toHaveLength(1);
  225. expect(result.valid[0]).toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ');
  226. });
  227. });
  228. describe('URL Normalization', () => {
  229. it('should add https protocol to URLs without protocol', () => {
  230. const testCases = [
  231. { input: 'youtube.com/watch?v=dQw4w9WgXcQ', expected: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
  232. { input: 'www.vimeo.com/123456789', expected: 'https://vimeo.com/123456789' },
  233. { input: 'youtu.be/dQw4w9WgXcQ', expected: 'https://youtu.be/dQw4w9WgXcQ' }
  234. ];
  235. testCases.forEach(({ input, expected }) => {
  236. const normalized = URLValidator.normalizeUrl(input);
  237. expect(normalized).toMatch(/^https:\/\//);
  238. // Check that it's a valid video URL after normalization
  239. expect(URLValidator.isValidVideoUrl(normalized)).toBe(true);
  240. });
  241. });
  242. });
  243. describe('YouTube Shorts Support', () => {
  244. it('should validate YouTube Shorts URLs', () => {
  245. const shortsUrls = [
  246. 'https://www.youtube.com/shorts/abc12345678',
  247. 'https://youtube.com/shorts/xyz98765432',
  248. 'youtube.com/shorts/test1234567'
  249. ];
  250. shortsUrls.forEach(url => {
  251. expect(URLValidator.isYouTubeUrl(url)).toBe(true);
  252. expect(URLValidator.isYouTubeShorts(url)).toBe(true);
  253. });
  254. });
  255. it('should detect Shorts URLs correctly', () => {
  256. // Positive cases
  257. expect(URLValidator.isYouTubeShorts('https://www.youtube.com/shorts/abc12345678')).toBe(true);
  258. expect(URLValidator.isYouTubeShorts('https://youtube.com/shorts/xyz98765432')).toBe(true);
  259. expect(URLValidator.isYouTubeShorts('youtube.com/shorts/test1234567')).toBe(true);
  260. // Negative cases
  261. expect(URLValidator.isYouTubeShorts('https://www.youtube.com/watch?v=abc12345678')).toBe(false);
  262. expect(URLValidator.isYouTubeShorts('https://youtu.be/abc12345678')).toBe(false);
  263. expect(URLValidator.isYouTubeShorts('https://vimeo.com/123456789')).toBe(false);
  264. });
  265. it('should extract video ID from Shorts URLs', () => {
  266. const url = 'https://www.youtube.com/shorts/abc12345678';
  267. const videoId = URLValidator.extractYouTubeId(url);
  268. expect(videoId).toBe('abc12345678');
  269. });
  270. it('should extract video ID from various Shorts URL formats', () => {
  271. const testCases = [
  272. { url: 'https://www.youtube.com/shorts/abc12345678', expected: 'abc12345678' },
  273. { url: 'https://youtube.com/shorts/xyz98765432', expected: 'xyz98765432' },
  274. { url: 'youtube.com/shorts/test1234567', expected: 'test1234567' }
  275. ];
  276. testCases.forEach(({ url, expected }) => {
  277. const normalized = URLValidator.normalizeUrl(url);
  278. const videoId = URLValidator.extractYouTubeId(normalized);
  279. expect(videoId).toBe(expected);
  280. });
  281. });
  282. it('should normalize Shorts URLs to watch URLs', () => {
  283. const shortsUrl = 'https://www.youtube.com/shorts/abc12345678';
  284. const normalized = URLValidator.normalizeUrl(shortsUrl);
  285. expect(normalized).toBe('https://www.youtube.com/watch?v=abc12345678');
  286. });
  287. it('should extract Shorts URLs from multi-line text', () => {
  288. const text = `
  289. Check out this short: https://youtube.com/shorts/abc12345678
  290. And this one: https://www.youtube.com/shorts/xyz98765432
  291. Regular video: https://www.youtube.com/watch?v=test1234567
  292. `;
  293. const { valid } = URLValidator.validateMultipleUrls(text);
  294. expect(valid.length).toBe(3);
  295. // All should be normalized to watch URLs
  296. valid.forEach(url => {
  297. expect(url).toMatch(/youtube\.com\/watch\?v=/);
  298. });
  299. });
  300. it('should handle Shorts URLs with additional parameters', () => {
  301. const urlWithParams = 'https://www.youtube.com/shorts/abc12345678?feature=share';
  302. expect(URLValidator.isYouTubeUrl(urlWithParams)).toBe(true);
  303. expect(URLValidator.isYouTubeShorts(urlWithParams)).toBe(true);
  304. const videoId = URLValidator.extractYouTubeId(urlWithParams);
  305. expect(videoId).toBe('abc12345678');
  306. });
  307. it('should deduplicate Shorts URLs that point to the same video', () => {
  308. const text = `
  309. https://www.youtube.com/shorts/abc12345678
  310. https://youtube.com/shorts/abc12345678
  311. https://www.youtube.com/watch?v=abc12345678
  312. `;
  313. const { valid } = URLValidator.validateMultipleUrls(text);
  314. // All three URLs point to the same video, should be deduplicated to 1
  315. expect(valid.length).toBe(1);
  316. expect(valid[0]).toBe('https://www.youtube.com/watch?v=abc12345678');
  317. });
  318. });
  319. });