forked from
j4ck.xyz/tweets2bsky
A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
1#!/usr/bin/env node
2
3let testsPassed = 0;
4let testsFailed = 0;
5
6function assert(condition, message) {
7 if (condition) {
8 console.log(` ✓ ${message}`);
9 testsPassed++;
10 } else {
11 console.log(` ✗ ${message}`);
12 testsFailed++;
13 }
14}
15
16console.log('Running logic tests...\n');
17
18// Test 1: Twitter URL Manipulation
19console.log('Test 1: Twitter URL Manipulation (High Quality Download)');
20{
21 const url1 = 'https://pbs.twimg.com/media/ABC123.jpg';
22 const highQuality1 = url1.includes('?') ? url1.replace('?', ':orig?') : url1 + ':orig';
23 assert(highQuality1 === 'https://pbs.twimg.com/media/ABC123.jpg:orig', 'Should append :orig to plain URLs');
24
25 const url2 = 'https://pbs.twimg.com/media/ABC123.jpg?format=jpg&name=small';
26 const highQuality2 = url2.includes('?') ? url2.replace('?', ':orig?') : url2 + ':orig';
27 assert(
28 highQuality2 === 'https://pbs.twimg.com/media/ABC123.jpg:orig?format=jpg&name=small',
29 'Should replace ? with :orig? for query URLs',
30 );
31
32 const url3 = 'https://pbs.twimg.com/media/DEF456.png?name=large';
33 const highQuality3 = url3.includes('?') ? url3.replace('?', ':orig?') : url3 + ':orig';
34 assert(highQuality3 === 'https://pbs.twimg.com/media/DEF456.png:orig?name=large', 'Should work with PNGs too');
35 console.log();
36}
37
38// Test 2: Text Splitting Logic
39console.log('Test 2: Text Splitting Logic');
40{
41 function splitText(text, limit = 300) {
42 if (text.length <= limit) return [text];
43 const chunks = [];
44 let remaining = text;
45 while (remaining.length > 0) {
46 if (remaining.length <= limit) {
47 chunks.push(remaining);
48 break;
49 }
50 let splitIndex = remaining.lastIndexOf('\n\n', limit);
51 if (splitIndex === -1) {
52 splitIndex = remaining.lastIndexOf('. ', limit);
53 if (splitIndex === -1) {
54 splitIndex = remaining.lastIndexOf(' ', limit);
55 if (splitIndex === -1) {
56 splitIndex = limit;
57 }
58 } else {
59 splitIndex += 1;
60 }
61 }
62 chunks.push(remaining.substring(0, splitIndex).trim());
63 remaining = remaining.substring(splitIndex).trim();
64 }
65 return chunks;
66 }
67
68 const text1 = 'Hello world';
69 const result1 = splitText(text1, 300);
70 assert(result1.length === 1, 'Short text should not be split');
71 assert(result1[0] === 'Hello world', 'Content should be preserved');
72
73 const text2 = 'First paragraph.\n\nSecond paragraph.\n\nThird paragraph.';
74 const result2 = splitText(text2, 50);
75 assert(result2.length >= 2, `Should split at paragraph breaks (got ${result2.length} chunks)`);
76 const allHaveContent = result2.every((c) => c.length > 0);
77 assert(allHaveContent, 'All chunks have content');
78 console.log();
79}
80
81// Test 3: MIME Type Detection
82console.log('Test 3: MIME Type Detection');
83{
84 const isPng = (mimeType) => mimeType === 'image/png';
85 const isJpeg = (mimeType) => mimeType === 'image/jpeg' || mimeType === 'image/jpg';
86 const isWebp = (mimeType) => mimeType === 'image/webp';
87 const isGif = (mimeType) => mimeType === 'image/gif';
88 const isAnimation = (mimeType) => isGif(mimeType) || isWebp(mimeType);
89
90 assert(isPng('image/png') === true, 'PNG detection works');
91 assert(isPng('image/jpeg') === false, 'JPEG is not PNG');
92 assert(isJpeg('image/jpeg') === true, 'JPEG detection works');
93 assert(isJpeg('image/jpg') === true, 'JPEG with JPG extension detection works');
94 assert(isWebp('image/webp') === true, 'WebP detection works');
95 assert(isGif('image/gif') === true, 'GIF detection works');
96 assert(isAnimation('image/webp') === true, 'WebP is animation');
97 assert(isAnimation('image/gif') === true, 'GIF is animation');
98 assert(isAnimation('image/jpeg') === false, 'JPEG is not animation');
99 console.log();
100}
101
102// Test 4: Aspect Ratio Calculation
103console.log('Test 4: Aspect Ratio Calculation');
104{
105 const sizes = {
106 large: { w: 1200, h: 800 },
107 medium: { w: 600, h: 400 },
108 small: { w: 300, h: 200 },
109 };
110
111 const getAspectRatio = (mediaSizes, originalInfo) => {
112 if (mediaSizes?.large) {
113 return { width: mediaSizes.large.w, height: mediaSizes.large.h };
114 } else if (originalInfo) {
115 return { width: originalInfo.width, height: originalInfo.height };
116 }
117 return undefined;
118 };
119
120 const ratio1 = getAspectRatio(sizes, undefined);
121 assert(ratio1.width === 1200 && ratio1.height === 800, 'Uses large size when available');
122
123 const ratio2 = getAspectRatio(undefined, { width: 1920, height: 1080 });
124 assert(ratio2.width === 1920 && ratio2.height === 1080, 'Falls back to original_info');
125
126 const ratio3 = getAspectRatio(undefined, undefined);
127 assert(ratio3 === undefined, 'Returns undefined when no data');
128 console.log();
129}
130
131// Test 5: Video Variant Sorting
132console.log('Test 5: Video Variant Sorting (Highest Quality First)');
133{
134 const variants = [
135 { content_type: 'video/mp4', url: 'low.mp4', bitrate: 500000 },
136 { content_type: 'video/mp4', url: 'high.mp4', bitrate: 2000000 },
137 { content_type: 'video/mp4', url: 'medium.mp4', bitrate: 1000000 },
138 { content_type: 'audio/mp4', url: 'audio.mp4', bitrate: 128000 },
139 ];
140
141 const mp4s = variants
142 .filter((v) => v.content_type === 'video/mp4')
143 .sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0));
144
145 assert(mp4s.length === 3, 'Should filter to only MP4 videos');
146 assert(mp4s[0].url === 'high.mp4', 'Highest bitrate first');
147 assert(mp4s[1].url === 'medium.mp4', 'Medium bitrate second');
148 assert(mp4s[2].url === 'low.mp4', 'Low bitrate last');
149 console.log();
150}
151
152// Test 6: Size Formatting
153console.log('Test 6: Size Formatting');
154{
155 const formatSize = (bytes) => (bytes / 1024).toFixed(2) + ' KB';
156 const formatSizeMB = (bytes) => (bytes / 1024 / 1024).toFixed(2) + ' MB';
157
158 assert(formatSize(1024) === '1.00 KB', '1KB formats correctly');
159 assert(formatSize(1536) === '1.50 KB', '1.5KB formats correctly');
160 assert(formatSizeMB(1048576) === '1.00 MB', '1MB formats correctly');
161 console.log();
162}
163
164// Test 7: Fixed Delay (10 seconds)
165console.log('Test 7: Fixed Delay (10 seconds)');
166{
167 const wait = 10000;
168 assert(wait === 10000, 'Delay is fixed at 10 seconds');
169 assert(wait >= 5000 && wait <= 15000, 'Delay is reasonable for pacing');
170 console.log();
171}
172
173// Test 8: Retry Logic Simulation
174console.log('Test 8: Retry Logic Simulation (High Quality -> Standard)');
175{
176 const runRetryTests = async () => {
177 // Test 8a: High quality succeeds
178 {
179 let highQualityFailed = false;
180 const downloadWithRetry = async (url) => {
181 const isHighQuality = url.includes(':orig');
182 if (isHighQuality && highQualityFailed) {
183 throw new Error('High quality download failed');
184 }
185 if (isHighQuality && !highQualityFailed) {
186 return { buffer: Buffer.from('high quality'), mimeType: 'image/jpeg' };
187 }
188 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' };
189 };
190
191 highQualityFailed = false;
192 const result1 = await downloadWithRetry('https://example.com/image.jpg:orig');
193 assert(result1.buffer.toString() === 'high quality', 'High quality download succeeds when available');
194 }
195
196 // Test 8b: High quality fails, falls back to standard
197 {
198 let callCount = 0;
199 const downloadWithRetry = async (url) => {
200 callCount++;
201 const isHighQuality = url.includes(':orig');
202
203 if (isHighQuality && callCount === 1) {
204 throw new Error('High quality download failed');
205 }
206
207 if (isHighQuality && callCount === 2) {
208 const fallbackUrl = url.replace(':orig?', '?');
209 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' };
210 }
211
212 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' };
213 };
214
215 const result2 = await downloadWithRetry('https://example.com/image.jpg:orig');
216 assert(result2.buffer.toString() === 'standard quality', 'Falls back to standard quality on failure');
217 }
218
219 // Test 8c: Standard URL doesn't use retry logic
220 {
221 const downloadWithRetry = async (url) => {
222 const isHighQuality = url.includes(':orig');
223 if (isHighQuality) {
224 throw new Error('Should not be high quality');
225 }
226 return { buffer: Buffer.from('standard quality'), mimeType: 'image/jpeg' };
227 };
228
229 const result3 = await downloadWithRetry('https://example.com/image.jpg');
230 assert(result3.buffer.toString() === 'standard quality', 'Standard URL downloads directly');
231 }
232 };
233
234 runRetryTests().catch((err) => {
235 console.log(` ✗ Retry test error: ${err.message}`);
236 testsFailed++;
237 });
238 console.log();
239}
240
241// Test 9: Image Compression Quality Settings
242console.log('Test 9: Image Compression Quality Settings');
243{
244 const settings = {
245 jpeg: { quality: 92, mozjpeg: true },
246 jpegFallback: { quality: 85, mozjpeg: true },
247 png: { compressionLevel: 9, adaptiveFiltering: true },
248 webp: { quality: 90, effort: 6 },
249 };
250
251 assert(settings.jpeg.quality === 92, 'JPEG quality is 92%');
252 assert(settings.jpeg.mozjpeg === true, 'JPEG uses mozjpeg');
253 assert(settings.jpegFallback.quality === 85, 'Fallback JPEG quality is 85%');
254 assert(settings.png.compressionLevel === 9, 'PNG compression level is 9 (max)');
255 assert(settings.webp.quality === 90, 'WebP quality is 90%');
256 assert(settings.webp.effort === 6, 'WebP encoding effort is 6 (high)');
257 console.log();
258}
259
260// Test 10: Bluesky Size Limits
261console.log('Test 10: Bluesky Size Limits Compliance');
262{
263 const MAX_SIZE = 950 * 1024;
264 const LARGE_IMAGE_THRESHOLD = 2000;
265 const FALLBACK_THRESHOLD = 1600;
266
267 assert(MAX_SIZE === 972800, 'Max size is 950KB (972800 bytes)');
268 assert(LARGE_IMAGE_THRESHOLD === 2000, 'Large image threshold is 2000px');
269 assert(FALLBACK_THRESHOLD === 1600, 'Fallback threshold is 1600px');
270 console.log();
271}
272
273// Summary
274console.log('─'.repeat(40));
275console.log(`Tests passed: ${testsPassed}`);
276console.log(`Tests failed: ${testsFailed}`);
277console.log('─'.repeat(40));
278
279if (testsFailed > 0) {
280 console.log('\nSome tests failed!');
281 process.exit(1);
282} else {
283 console.log('\nAll tests passed!');
284 process.exit(0);
285}