Files
FileHost/server/utils/storage/telegram.test.ts
O96u 10baf9d489 fix: run Telegram storage tests on an isolated database
CI workers were sharing one SQLite file, so schema setup locked and storage_object_refs was missing.
2026-10-09 15:47:09 +08:00

323 lines
8.2 KiB
TypeScript

import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi, beforeAll, beforeEach, afterAll, afterEach } from 'vitest'
import { closeDb } from '../db'
import {
parseTelegramConfig,
parseTelegramSecrets,
validateApiProxyUrl,
validateTelegramConfig,
TelegramStorageBackend,
TELEGRAM_MAX_FILE_BYTES
} from './telegram'
import {
deleteStorageObjectRef,
getStorageObjectRef
} from './storage-object-refs'
import {
ensureDefaultBackends,
ensureStorageSchema,
getStorageBackendRow,
insertTelegramBackend
} from '../storage-backends'
describe('telegram config', () => {
it('parses chat id and optional proxy', () => {
const config = parseTelegramConfig(JSON.stringify({
chatId: '-1001',
apiProxyUrl: 'http://127.0.0.1:7890'
}))
expect(config).toEqual({
chatId: '-1001',
apiProxyUrl: 'http://127.0.0.1:7890'
})
})
it('rejects invalid proxy protocol', () => {
expect(validateApiProxyUrl('socks5://127.0.0.1:1080')).toMatch(/http/)
})
it('accepts http proxy', () => {
expect(validateApiProxyUrl('http://127.0.0.1:7890')).toBeNull()
})
it('validates required chat id', () => {
expect(validateTelegramConfig({ chatId: '' })).toBeTruthy()
expect(validateTelegramConfig({ chatId: '-100' })).toBeNull()
})
it('parses bot token secret', () => {
expect(parseTelegramSecrets(JSON.stringify({ botToken: '123:abc' }))).toEqual({
botToken: '123:abc'
})
})
it('exposes 20MB limit constant', () => {
expect(TELEGRAM_MAX_FILE_BYTES).toBe(20 * 1024 * 1024)
})
})
describe('TelegramStorageBackend', () => {
const backendId = 'tg-test'
const key = 'images/2026/01/test.webp'
const tempDir = mkdtempSync(join(tmpdir(), 'pichost-tg-'))
const previousDataDir = process.env.DATA_DIR
beforeAll(() => {
process.env.DATA_DIR = tempDir
closeDb()
})
afterAll(() => {
closeDb()
if (previousDataDir === undefined) {
delete process.env.DATA_DIR
} else {
process.env.DATA_DIR = previousDataDir
}
rmSync(tempDir, { recursive: true, force: true })
})
beforeEach(() => {
ensureStorageSchema()
ensureDefaultBackends()
if (!getStorageBackendRow(backendId)) {
insertTelegramBackend({
id: backendId,
name: 'TG test',
config: { chatId: '-100' },
secrets: { botToken: 'token' },
quotaBytes: 1024 * 1024 * 1024,
enabled: false
})
}
vi.stubGlobal('fetch', vi.fn())
})
afterEach(() => {
try {
deleteStorageObjectRef(backendId, key)
} catch {
// schema setup may have failed before the ref table existed
}
vi.unstubAllGlobals()
})
it('retries sendDocument after supergroup chat id migration', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: false,
description: 'Bad Request: group chat was upgraded to a supergroup chat',
parameters: { migrate_to_chat_id: -100999 }
})
} as Response)
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: {
message_id: 99,
document: { file_id: 'MIG', file_unique_id: 'UQ2' }
}
})
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-123' },
{ botToken: 'token' }
)
await backend.put(key, new Uint8Array([4, 5]), {
originalName: 'test.webp',
uploadedAt: new Date().toISOString(),
contentType: 'image/webp',
size: 2
})
expect(fetchMock).toHaveBeenCalledTimes(2)
const ref = getStorageObjectRef(backendId, key)
expect(ref?.fileId).toBe('MIG')
})
it('put accepts a single photo object and sticker file id', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: {
message_id: 8,
sticker: { file_id: 'stk', file_unique_id: 'us' }
}
})
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'token' }
)
await backend.put(key, new Uint8Array([1]), {
originalName: 'test.webp',
uploadedAt: new Date().toISOString(),
contentType: 'image/webp',
size: 1
})
expect(getStorageObjectRef(backendId, key)?.fileId).toBe('stk')
})
it('put accepts photo array when document is absent', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: {
message_id: 7,
photo: [
{ file_id: 'small', file_unique_id: 'u1', width: 90, height: 90 },
{ file_id: 'large', file_unique_id: 'u2', width: 800, height: 600 }
]
}
})
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'token' }
)
await backend.put(key, new Uint8Array([1]), {
originalName: 'test.png',
uploadedAt: new Date().toISOString(),
contentType: 'image/png',
size: 1
})
const ref = getStorageObjectRef(backendId, key)
expect(ref?.fileId).toBe('large')
})
it('put stores file ref after sendDocument', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: {
message_id: 42,
document: {
file_id: 'AAA',
file_unique_id: 'UQ'
}
}
})
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'token' }
)
await backend.put(key, new Uint8Array([1, 2, 3]), {
originalName: 'test.webp',
uploadedAt: new Date().toISOString(),
contentType: 'image/webp',
size: 3
})
const ref = getStorageObjectRef(backendId, key)
expect(ref?.fileId).toBe('AAA')
expect(ref?.messageId).toBe(42)
})
it('testConnection reports telegram errors', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ ok: false, description: 'Unauthorized' })
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'bad' }
)
const result = await backend.testConnection()
expect(result.ok).toBe(false)
expect(result.message).toContain('Unauthorized')
})
it('createStream downloads file bytes', async () => {
const fetchMock = vi.mocked(fetch)
fetchMock.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: {
message_id: 1,
document: { file_id: 'FILE1', file_unique_id: 'U1' }
}
})
} as Response)
const putBackend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'token' }
)
await putBackend.put(key, new Uint8Array([1]), {
originalName: 'test.webp',
uploadedAt: new Date().toISOString(),
contentType: 'image/webp',
size: 1
})
fetchMock.mockReset()
fetchMock
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
ok: true,
result: { file_id: 'FILE1', file_path: 'photos/file.jpg' }
})
} as Response)
.mockResolvedValueOnce({
ok: true,
status: 200,
arrayBuffer: async () => new Uint8Array([9, 8, 7]).buffer
} as Response)
const backend = new TelegramStorageBackend(
backendId,
{ chatId: '-100' },
{ botToken: 'token' }
)
const stream = await backend.createStream(key)
const chunks: Buffer[] = []
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
expect(Buffer.concat(chunks)).toEqual(Buffer.from([9, 8, 7]))
})
})