Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/lib/telegram/chat/__tests__/bufferStartsWith.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, it, expect } from 'vitest';
import bufferStartsWith from '../bufferStartsWith';

describe('bufferStartsWith', () => {
it('returns true when the buffer starts with the magic bytes', () => {
expect(
bufferStartsWith(
Buffer.from([0xff, 0xd8, 0xff, 0x00]),
[0xff, 0xd8, 0xff]
)
).toBe(true);
});

it('returns false when the prefix does not match', () => {
expect(
bufferStartsWith(Buffer.from([0x00, 0xd8, 0xff]), [0xff, 0xd8, 0xff])
).toBe(false);
});
});
25 changes: 25 additions & 0 deletions src/lib/telegram/chat/__tests__/detectMimeTypeFromBuffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import detectMimeTypeFromBuffer from '../detectMimeTypeFromBuffer';

const readFixture = (name: string): Buffer =>
readFileSync(join(__dirname, 'fixtures', name));

describe('detectMimeTypeFromBuffer', () => {
it('detects HEIC from magic bytes when the file path has no extension', () => {
const buffer = readFixture('apple-styled-photo.heic');
expect(detectMimeTypeFromBuffer(buffer)).toBe('image/heic');
});

it('detects HEIC for the shelf fixture', () => {
const buffer = readFixture('shelf-christmas-decoration.heic');
expect(detectMimeTypeFromBuffer(buffer)).toBe('image/heic');
});

it('returns undefined for unknown bytes', () => {
expect(
detectMimeTypeFromBuffer(Buffer.from('not-an-image'))
).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import extractExifCaptureDate from '../extractExifCaptureDate';

const readFixture = (name: string): Buffer =>
readFileSync(join(__dirname, 'fixtures', name));

describe('extractExifCaptureDate HEIC integration', () => {
it('extracts the correct UTC instant from the shelf HEIC fixture', async () => {
const result = await extractExifCaptureDate(
readFixture('shelf-christmas-decoration.heic')
);

expect(result).toBe(
Math.floor(new Date('2023-12-13T12:03:39.000Z').getTime() / 1000)
);
});

it('extracts the correct UTC instant from an Apple multi-brand HEIC fixture', async () => {
const result = await extractExifCaptureDate(
readFixture('apple-styled-photo.heic')
);

expect(result).toBe(
Math.floor(new Date('2026-07-05T16:02:17.000Z').getTime() / 1000)
);
});
});
22 changes: 18 additions & 4 deletions src/lib/telegram/chat/__tests__/extractExifCaptureDate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ beforeEach(() => {
});

describe('extractExifCaptureDate', () => {
it('requests raw (non-revived) values including OffsetTimeOriginal', async () => {
it('requests raw (non-revived) values including offset tags', async () => {
mockParse.mockResolvedValue({
DateTimeOriginal: '2026:07:09 23:32:10',
OffsetTimeOriginal: '+09:00',
Expand All @@ -23,7 +23,7 @@ describe('extractExifCaptureDate', () => {
await extractExifCaptureDate(Buffer.from('jpeg-bytes'));

expect(mockParse).toHaveBeenCalledWith(expect.any(Buffer), {
pick: ['DateTimeOriginal', 'OffsetTimeOriginal'],
pick: ['DateTimeOriginal', 'OffsetTimeOriginal', 'OffsetTime'],
reviveValues: false,
});
});
Expand All @@ -41,6 +41,19 @@ describe('extractExifCaptureDate', () => {
);
});

it('falls back to OffsetTime when OffsetTimeOriginal is absent', async () => {
mockParse.mockResolvedValue({
DateTimeOriginal: '2023:12:13 13:03:39',
OffsetTime: '+01:00',
});

const result = await extractExifCaptureDate(Buffer.from('heic-bytes'));

expect(result).toBe(
(Date.UTC(2023, 11, 13, 13, 3, 39) - 60 * 60_000) / 1000
);
});

it('subtracts a negative OffsetTimeOriginal correctly', async () => {
mockParse.mockResolvedValue({
DateTimeOriginal: '2026:07:09 23:32:10',
Expand All @@ -54,18 +67,19 @@ describe('extractExifCaptureDate', () => {
);
});

it('skips EXIF entirely when OffsetTimeOriginal is absent, even if DateTimeOriginal is present', async () => {
it('skips EXIF entirely when no offset tag is present, even if DateTimeOriginal is present', async () => {
mockParse.mockResolvedValue({ DateTimeOriginal: '2026:07:09 23:32:10' });

const result = await extractExifCaptureDate(Buffer.from('jpeg-bytes'));

expect(result).toBeUndefined();
});

it('skips EXIF when OffsetTimeOriginal is present but malformed', async () => {
it('skips EXIF when offset tags are present but malformed', async () => {
mockParse.mockResolvedValue({
DateTimeOriginal: '2026:07:09 23:32:10',
OffsetTimeOriginal: 'garbage',
OffsetTime: 'also-garbage',
});

const result = await extractExifCaptureDate(Buffer.from('jpeg-bytes'));
Expand Down
23 changes: 23 additions & 0 deletions src/lib/telegram/chat/__tests__/fetchTelegramFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import fetchTelegramFile from '../fetchTelegramFile';

vi.mock('../getTelegramFilePath', () => ({ default: vi.fn() }));
Expand Down Expand Up @@ -41,6 +43,27 @@ describe('fetchTelegramFile', () => {
expect(result.mimeType).toBe('video/mp4');
});

it('sniffs HEIC mimeType from magic bytes when the Telegram path has no extension', async () => {
const buffer = readFileSync(
join(__dirname, 'fixtures', 'apple-styled-photo.heic')
);
vi.mocked(getTelegramFilePath).mockResolvedValue('documents/111');
vi.mocked(global.fetch).mockResolvedValue({
ok: true,
arrayBuffer: () =>
Promise.resolve(
buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
)
),
} as never);

const result = await fetchTelegramFile('file-id');

expect(result.mimeType).toBe('image/heic');
});

it('throws when the Telegram file response is not ok', async () => {
vi.mocked(global.fetch).mockResolvedValue({
ok: false,
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ describe('getMimeTypeFromFilePath', () => {
['image.png', 'image/png'],
['animation.gif', 'image/gif'],
['photo.webp', 'image/webp'],
['photo.heic', 'image/heic'],
['photo.heif', 'image/heif'],
['video.mp4', 'video/mp4'],
['video.mov', 'video/quicktime'],
['video.avi', 'video/x-msvideo'],
Expand Down
20 changes: 20 additions & 0 deletions src/lib/telegram/chat/__tests__/needsMimeSniff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest';
import needsMimeSniff from '../needsMimeSniff';

describe('needsMimeSniff', () => {
it('returns true for file attachments without a useful mimeType', () => {
expect(needsMimeSniff({ type: 'file' } as never)).toBe(true);
expect(
needsMimeSniff({
type: 'file',
mimeType: 'application/octet-stream',
} as never)
).toBe(true);
});

it('returns false when the mimeType is already known', () => {
expect(
needsMimeSniff({ type: 'file', mimeType: 'image/jpeg' } as never)
).toBe(false);
});
});
8 changes: 4 additions & 4 deletions src/lib/telegram/chat/__tests__/parseExifOffsetMs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ describe('parseExifOffsetMs', () => {
expect(parseExifOffsetMs('-05:00')).toBe(-5 * 60 * 60_000);
});

it('parses a non-zero minutes offset', () => {
it('parses a half-hour offset', () => {
expect(parseExifOffsetMs('+05:30')).toBe((5 * 60 + 30) * 60_000);
});

it('returns undefined for undefined input', () => {
it('returns undefined for missing values', () => {
expect(parseExifOffsetMs(undefined)).toBeUndefined();
});

it('returns undefined for a malformed string', () => {
it('returns undefined for malformed strings', () => {
expect(parseExifOffsetMs('not-an-offset')).toBeUndefined();
});

it('returns undefined for a non-string value', () => {
it('returns undefined for non-string types', () => {
expect(parseExifOffsetMs(540)).toBeUndefined();
});
});
18 changes: 18 additions & 0 deletions src/lib/telegram/chat/__tests__/readFtypBrands.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import readFtypBrands from '../readFtypBrands';

describe('readFtypBrands', () => {
it('reads compatible brands from a HEIC fixture', () => {
const buffer = readFileSync(
join(__dirname, 'fixtures', 'apple-styled-photo.heic')
);

expect(readFtypBrands(buffer)).toContain('heic');
});

it('returns an empty array for non-ftyp bytes', () => {
expect(readFtypBrands(Buffer.from('not-ftyp'))).toEqual([]);
});
});
21 changes: 21 additions & 0 deletions src/lib/telegram/chat/__tests__/resolveExifOffsetMs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import resolveExifOffsetMs from '../resolveExifOffsetMs';

describe('resolveExifOffsetMs', () => {
it('prefers OffsetTimeOriginal over OffsetTime', () => {
expect(
resolveExifOffsetMs({
OffsetTimeOriginal: '+09:00',
OffsetTime: '+01:00',
})
).toBe(9 * 60 * 60_000);
});

it('falls back to OffsetTime when OffsetTimeOriginal is absent', () => {
expect(resolveExifOffsetMs({ OffsetTime: '+01:00' })).toBe(60 * 60_000);
});

it('returns undefined when both offset tags are absent', () => {
expect(resolveExifOffsetMs({})).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import resolveTelegramFileMimeType from '../resolveTelegramFileMimeType';

const readFixture = (name: string): Buffer =>
readFileSync(join(__dirname, 'fixtures', name));

describe('resolveTelegramFileMimeType', () => {
it('prefers the file path extension when present', () => {
const buffer = readFixture('shelf-christmas-decoration.heic');
expect(resolveTelegramFileMimeType('photos/file.jpg', buffer)).toBe(
'image/jpeg'
);
});

it('sniffs HEIC from magic bytes when the Telegram path has no extension', () => {
const buffer = readFixture('apple-styled-photo.heic');
expect(resolveTelegramFileMimeType('documents/111', buffer)).toBe(
'image/heic'
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

vi.mock('../resolveMediaAttachmentType', () => ({ default: vi.fn() }));
vi.mock('../extractTelegramFileIds', () => ({ default: vi.fn() }));
vi.mock('../fetchTelegramFile', () => ({ default: vi.fn() }));

import resolveMediaAttachmentType from '../resolveMediaAttachmentType';
import extractTelegramFileIds from '../extractTelegramFileIds';
import fetchTelegramFile from '../fetchTelegramFile';
import resolveTelegramMediaAttachment from '../resolveTelegramMediaAttachment';

beforeEach(() => {
vi.clearAllMocks();
});

describe('resolveTelegramMediaAttachment', () => {
it('returns the attachment with a resolved type when mime is already known', async () => {
vi.mocked(resolveMediaAttachmentType).mockReturnValue('image');
const attachment = { type: 'file', mimeType: 'image/jpeg' } as never;

const result = await resolveTelegramMediaAttachment(
{ attachments: [attachment] } as never,
attachment
);

expect(result).toEqual({
type: 'image',
mimeType: 'image/jpeg',
});
expect(fetchTelegramFile).not.toHaveBeenCalled();
});

it('sniffs mime from Telegram file bytes when the document has no useful mimeType', async () => {
vi.mocked(resolveMediaAttachmentType)
.mockReturnValueOnce(undefined)
.mockReturnValueOnce('image');
vi.mocked(extractTelegramFileIds).mockReturnValue({
fileId: 'file-1',
thumbFileId: undefined,
});
vi.mocked(fetchTelegramFile).mockResolvedValue({
buffer: Buffer.from('bytes'),
mimeType: 'image/heic',
});

const attachment = {
type: 'file',
mimeType: 'application/octet-stream',
} as never;

const result = await resolveTelegramMediaAttachment(
{ attachments: [attachment] } as never,
attachment
);

expect(fetchTelegramFile).toHaveBeenCalledWith('file-1');
expect(result).toEqual({
type: 'image',
mimeType: 'image/heic',
});
});

it('returns undefined when sniffing still does not resolve to media', async () => {
vi.mocked(resolveMediaAttachmentType).mockReturnValue(undefined);
vi.mocked(extractTelegramFileIds).mockReturnValue({
fileId: 'file-1',
thumbFileId: undefined,
});
vi.mocked(fetchTelegramFile).mockResolvedValue({
buffer: Buffer.from('bytes'),
mimeType: 'application/octet-stream',
});

const attachment = { type: 'file' } as never;

const result = await resolveTelegramMediaAttachment(
{ attachments: [attachment] } as never,
attachment
);

expect(result).toBeUndefined();
});
});
4 changes: 4 additions & 0 deletions src/lib/telegram/chat/bufferStartsWith.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const bufferStartsWith = (buffer: Buffer, magic: number[]): boolean =>
magic.every((byte, index) => buffer[index] === byte);

export default bufferStartsWith;
Loading
Loading