/**
* Unit tests for data fetcher
*/
import { TikTokDataFetcher, createDataFetcher, fetchTikTokData } from '../data-fetcher';
// Mock fetch globally
global.fetch = jest.fn();
describe('TikTokDataFetcher', () => {
const mockConfig = {
appToken: 'test-app-token',
tableId: 'test-table-id',
apiKey: 'test-api-key'
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('constructor', () => {
test('should create instance with valid config', () => {
const fetcher = new TikTokDataFetcher(mockConfig);
expect(fetcher).toBeInstanceOf(TikTokDataFetcher);
});
test('should set default values', () => {
const fetcher = new TikTokDataFetcher(mockConfig);
expect(fetcher).toBeDefined();
});
test('should accept optional parameters', () => {
const fetcher = new TikTokDataFetcher({
...mockConfig,
mcpProxyUrl: 'http://localhost:3000',
region: 'cn',
pageSize: 100
});
expect(fetcher).toBeDefined();
});
});
describe('fetchData', () => {
test('should fetch data via API when apiKey is provided', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [
{
record_id: '1',
fields: {
'Video ID': '123',
'Title': 'Test Video',
'Views': 1000,
'Likes': 100,
'Comments': 10,
'Shares': 5,
'Watch %': 75,
'Date published': Date.now(),
'Duration': 45
}
}
],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher(mockConfig);
const data = await fetcher.fetchData();
expect(data).toBeInstanceOf(Array);
expect(data.length).toBe(1);
expect(data[0].videoId).toBe('123');
});
test('should use cache when enabled', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [
{
record_id: '1',
fields: {
'Video ID': '123',
'Title': 'Test Video',
'Views': 1000,
'Likes': 100,
'Comments': 10,
'Shares': 5,
'Watch %': 75,
'Date published': Date.now(),
'Duration': 45
}
}
],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
...mockConfig,
enableCache: true,
cacheTTL: 60000
});
// First call should fetch from API
const data1 = await fetcher.fetchData();
expect(global.fetch).toHaveBeenCalledTimes(1);
// Second call should use cache
const data2 = await fetcher.fetchData();
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(data2).toEqual(data1);
});
test('should bypass cache when forceRefresh is true', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
...mockConfig,
enableCache: true
});
await fetcher.fetchData();
await fetcher.fetchData(true);
expect(global.fetch).toHaveBeenCalledTimes(2);
});
test('should handle API errors', async () => {
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error'
});
const fetcher = new TikTokDataFetcher(mockConfig);
await expect(fetcher.fetchData()).rejects.toThrow();
});
test('should handle Bitable API error codes', async () => {
const mockResponse = {
code: 400,
msg: 'Bad Request',
data: null
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher(mockConfig);
await expect(fetcher.fetchData()).rejects.toThrow('Bad Request');
});
});
describe('cache management', () => {
test('should clear cache', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
...mockConfig,
enableCache: true
});
await fetcher.fetchData();
fetcher.clearCache();
await fetcher.fetchData();
expect(global.fetch).toHaveBeenCalledTimes(2);
});
});
describe('config updates', () => {
test('should update configuration', () => {
const fetcher = new TikTokDataFetcher(mockConfig);
fetcher.updateConfig({
pageSize: 100,
region: 'us'
});
expect(fetcher).toBeDefined();
});
});
});
describe('createDataFetcher', () => {
test('should create fetcher instance', () => {
const fetcher = createDataFetcher({
appToken: 'test',
tableId: 'test'
});
expect(fetcher).toBeInstanceOf(TikTokDataFetcher);
});
});
describe('fetchTikTokData', () => {
test('should fetch data using default configuration', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const data = await fetchTikTokData('test-app', 'test-table', {
apiKey: 'test-key'
});
expect(data).toBeInstanceOf(Array);
});
});
describe('Data transformation', () => {
test('should transform Bitable records correctly', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [
{
record_id: '1',
fields: {
'Video ID': '123',
'Title': 'Test Video',
'Views': 1000,
'Likes': 100,
'Comments': 10,
'Shares': 5,
'Watch %': 75,
'Date published': 1705276800000,
'Duration': 45
}
}
],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
appToken: 'test',
tableId: 'test',
apiKey: 'test-key'
});
const data = await fetcher.fetchData();
expect(data[0]).toMatchObject({
videoId: '123',
title: 'Test Video',
views: 1000,
likes: 100,
comments: 10,
shares: 5,
watchPercent: 75,
duration: 45
});
expect(data[0].datePublished).toBeDefined();
});
test('should skip records with missing required fields', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [
{
record_id: '1',
fields: {
'Video ID': '123',
'Title': 'Valid Video',
'Views': 1000
}
},
{
record_id: '2',
fields: {
'Views': 1000
// Missing Video ID and Title
}
}
],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
appToken: 'test',
tableId: 'test',
apiKey: 'test-key'
});
const data = await fetcher.fetchData();
expect(data.length).toBe(1);
expect(data[0].videoId).toBe('123');
});
test('should handle numeric string values', async () => {
const mockResponse = {
code: 0,
msg: 'success',
data: {
items: [
{
record_id: '1',
fields: {
'Video ID': '123',
'Title': 'Test',
'Views': '1000',
'Likes': '100',
'Comments': '10'
}
}
],
has_more: false
}
};
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => mockResponse
});
const fetcher = new TikTokDataFetcher({
appToken: 'test',
tableId: 'test',
apiKey: 'test-key'
});
const data = await fetcher.fetchData();
expect(typeof data[0].views).toBe('number');
expect(data[0].views).toBe(1000);
});
});