@licht-77/dmm-sdk
Version:
DMM Affiliate API v3 SDK for Node.js/TypeScript
939 lines • 61 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
// src/client.test.ts
const vitest_1 = require("vitest");
const client_1 = require("./client");
const mockFetch = vitest_1.vi.fn();
vitest_1.vi.stubGlobal('fetch', mockFetch);
(0, vitest_1.describe)('DmmApiClientOptions type', () => {
(0, vitest_1.it)('should allow baseUrl to be an optional string', () => {
const optionsWithBaseUrl = {
apiId: 'test-api-id',
affiliateId: 'test-affiliate-id',
baseUrl: 'https://example.com',
};
(0, vitest_1.expect)(optionsWithBaseUrl.baseUrl).toBe('https://example.com');
const optionsWithoutBaseUrl = {
apiId: 'test-api-id',
affiliateId: 'test-affiliate-id',
};
(0, vitest_1.expect)(optionsWithoutBaseUrl.baseUrl).toBeUndefined();
});
});
(0, vitest_1.describe)('DmmApiClient', () => {
const defaultOptions = {
apiId: 'test-api-id',
affiliateId: 'test-affiliate-id',
};
let client;
(0, vitest_1.beforeEach)(() => {
mockFetch.mockClear();
client = new client_1.DmmApiClient({
apiId: defaultOptions.apiId,
affiliateId: defaultOptions.affiliateId,
});
});
(0, vitest_1.afterEach)(() => {
// vi.clearAllTimers(); // Vitestでは通常不要か、vi.useRealTimers()などで制御
});
(0, vitest_1.describe)('baseUrl handling', () => {
const defaultApiBaseUrl = 'https://api.dmm.com/affiliate/v3';
(0, vitest_1.it)('should use the default baseUrl if none is provided', async () => {
const clientWithDefaultBaseUrl = new client_1.DmmApiClient(defaultOptions);
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await clientWithDefaultBaseUrl.getFloorList();
const urlInstance = new URL(mockFetch.mock.calls[0][0]);
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${defaultApiBaseUrl}/FloorList`);
});
(0, vitest_1.it)('should use the provided baseUrl if specified', async () => {
const customBaseUrl = 'https://custom.example.com/api';
const clientWithCustomBaseUrl = new client_1.DmmApiClient({
...defaultOptions,
baseUrl: customBaseUrl,
});
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await clientWithCustomBaseUrl.getFloorList();
const urlInstance = new URL(mockFetch.mock.calls[0][0]);
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${customBaseUrl}/FloorList`);
});
(0, vitest_1.it)('should correctly handle baseUrl with a trailing slash', async () => {
const customBaseUrlWithSlash = 'https://custom.example.com/api/';
const clientWithCustomBaseUrl = new client_1.DmmApiClient({
...defaultOptions,
baseUrl: customBaseUrlWithSlash,
});
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await clientWithCustomBaseUrl.getFloorList();
const urlInstance = new URL(mockFetch.mock.calls[0][0]);
// APIエンドポイントの前にスラッシュが2重にならないことを期待
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${customBaseUrlWithSlash.slice(0, -1)}/FloorList`);
});
(0, vitest_1.it)('should correctly handle baseUrl without a trailing slash and endpoint without leading slash', async () => {
const customBaseUrl = 'https://custom.example.com/api';
// DmmApiClientの内部でエンドポイントの先頭に `/` が付与されることを想定
const clientWithCustomBaseUrl = new client_1.DmmApiClient({
...defaultOptions,
baseUrl: customBaseUrl,
});
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
// getFloorListは内部で 'FloorList' (先頭スラッシュなし) を使うと仮定
await clientWithCustomBaseUrl.getFloorList();
const urlInstance = new URL(mockFetch.mock.calls[0][0]);
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${customBaseUrl}/FloorList`);
});
(0, vitest_1.describe)('Backward compatibility and testability', () => {
(0, vitest_1.it)('should work correctly with existing DmmApiClientOptions (without baseUrl)', async () => {
const clientWithoutBaseUrl = new client_1.DmmApiClient(defaultOptions);
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await clientWithoutBaseUrl.getFloorList();
const urlCall = mockFetch.mock.calls[0][0];
const urlInstance = new URL(urlCall);
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${defaultApiBaseUrl}/FloorList`);
(0, vitest_1.expect)(urlCall).toContain(`api_id=${defaultOptions.apiId}`);
(0, vitest_1.expect)(urlCall).toContain(`affiliate_id=${defaultOptions.affiliateId}`);
});
(0, vitest_1.it)('should use the overridden baseUrl for all API methods when testing', async () => {
const testBaseUrl = 'https://test-double.example.com/v1';
const testClient = new client_1.DmmApiClient({
...defaultOptions,
baseUrl: testBaseUrl,
});
const methodsToTest = [
{ method: 'getItemList', params: { site: 'DMM.com', service: 'digital', floor: 'videoa', hits: 1, sort: 'rank' }, endpoint: 'ItemList' },
{ method: 'getFloorList', params: undefined, endpoint: 'FloorList' },
{ method: 'searchActress', params: { initial: 'a', hits: 1 }, endpoint: 'ActressSearch' },
{ method: 'searchGenre', params: { floor_id: '123', initial: 'あ', hits: 1 }, endpoint: 'GenreSearch' },
{ method: 'searchMaker', params: { floor_id: '456', initial: 'm', hits: 1 }, endpoint: 'MakerSearch' },
{ method: 'searchSeries', params: { floor_id: '789', initial: 's', hits: 1 }, endpoint: 'SeriesSearch' },
{ method: 'searchAuthor', params: { floor_id: '101', initial: 'k', hits: 1 }, endpoint: 'AuthorSearch' },
];
for (const { method, params, endpoint } of methodsToTest) {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: { status: 200 } }) });
await testClient[method](params);
const urlCall = mockFetch.mock.calls[mockFetch.mock.calls.length - 1][0];
const urlInstance = new URL(urlCall);
(0, vitest_1.expect)(urlInstance.origin + urlInstance.pathname).toBe(`${testBaseUrl}/${endpoint}`);
(0, vitest_1.expect)(urlCall).toContain(`api_id=${defaultOptions.apiId}`);
(0, vitest_1.expect)(urlCall).toContain(`affiliate_id=${defaultOptions.affiliateId}`);
}
});
});
});
(0, vitest_1.describe)('baseUrl validation', () => {
(0, vitest_1.it)('should throw an error if baseUrl is an empty string', () => {
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ ...defaultOptions, baseUrl: '' }))
.toThrow('Invalid baseUrl: must be a valid URL string or undefined.');
});
(0, vitest_1.it)('should throw an error if baseUrl is an invalid URL', () => {
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ ...defaultOptions, baseUrl: 'invalid-url' }))
.toThrow('Invalid baseUrl: must be a valid URL string or undefined.');
});
(0, vitest_1.it)('should throw an error if baseUrl is a URL with an unsupported protocol', () => {
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ ...defaultOptions, baseUrl: 'ftp://example.com' }))
.toThrow('Invalid baseUrl: must be a valid URL string or undefined.');
});
(0, vitest_1.it)('should not throw an error for valid http or https URLs', () => {
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ ...defaultOptions, baseUrl: 'http://example.com' })).not.toThrow();
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ ...defaultOptions, baseUrl: 'https://secure.example.com/api/v2' })).not.toThrow();
});
});
(0, vitest_1.it)('should throw an error if apiId is missing', () => {
(0, vitest_1.expect)(() => new client_1.DmmApiClient({ affiliateId: 'test' }))
.toThrow('API ID and Affiliate ID are required.');
});
(0, vitest_1.it)('should default to DMM.com site for requests like getFloorList if not overridable by params', async () => {
const defaultSiteClient = new client_1.DmmApiClient(defaultOptions);
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await defaultSiteClient.getFloorList();
const expectedUrl = new URL(`${defaultSiteClient.baseUrl}/FloorList`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('getFloorList should always use DMM.com site by default', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.getFloorList();
const expectedUrl = new URL(`${client.baseUrl}/FloorList`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('should create an instance with default timeout and retries', () => {
const defaultClient = new client_1.DmmApiClient(defaultOptions);
(0, vitest_1.expect)(defaultClient.apiId).toBe(defaultOptions.apiId);
(0, vitest_1.expect)(defaultClient.affiliateId).toBe(defaultOptions.affiliateId);
});
(0, vitest_1.describe)('request method', () => {
const endpoint = '/TestEndpoint';
const params = { param1: 'value1', param2: 123 };
const expectedBaseUrl = 'https://api.dmm.com/affiliate/v3';
(0, vitest_1.it)('should call fetch with the correct URL and parameters', async () => {
const mockResponse = { result: { success: true } };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
await client.request(endpoint, { ...params, site: 'DMM.com' });
const expectedUrl = new URL(`${expectedBaseUrl}${endpoint}`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
param1: 'value1',
param2: '123',
site: 'DMM.com',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('should not include undefined parameters in the URL query', async () => {
const paramsWithUndefined = {
param1: 'value1',
param2: undefined,
param3: 'value3',
param4: undefined,
};
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ result: { success: true } }),
});
await client.request(endpoint, { ...paramsWithUndefined, site: 'DMM.com' });
const expectedUrl = new URL(`${expectedBaseUrl}${endpoint}`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
param1: 'value1',
param3: 'value3',
site: 'DMM.com',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('should return the result field on successful response', async () => {
const mockResult = { data: 'test data' };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ result: mockResult }),
});
const result = await client.request(endpoint, params);
(0, vitest_1.expect)(result).toEqual(mockResult);
});
(0, vitest_1.it)('should throw an error if response is not ok (non-retryable status)', async () => {
const errorStatus = 400;
const errorResponse = { result: { message: 'Bad Request' } };
mockFetch.mockResolvedValueOnce({
ok: false,
status: errorStatus,
statusText: 'Bad Request',
json: async () => errorResponse,
});
await (0, vitest_1.expect)(client.request(endpoint, params))
.rejects
.toThrow(`API request to ${endpoint} failed with status ${errorStatus}: ${errorResponse.result.message}`);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should not retry on 5xx errors and throw an error immediately', async () => {
const endpoint = '/Test5xxError';
const testParams = { site: 'DMM.com' };
mockFetch.mockResolvedValueOnce({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({ result: { message: 'Server Error' } }),
});
await (0, vitest_1.expect)(client.request(endpoint, testParams)).rejects.toThrow('API request to /Test5xxError failed with status 500: Server Error');
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should not retry on 429 errors and throw an error immediately', async () => {
const endpoint = '/Test429Error';
const testParams = { site: 'DMM.com' };
mockFetch.mockResolvedValueOnce({
ok: false,
status: 429,
statusText: 'Too Many Requests',
json: async () => ({ result: { message: 'Rate Limit Exceeded' } }),
});
await (0, vitest_1.expect)(client.request(endpoint, testParams)).rejects.toThrow('API request to /Test429Error failed with status 429: Rate Limit Exceeded');
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should not retry on network error (fetch throws) and throw an error immediately', async () => {
const endpoint = '/TestNetworkError';
const testParams = { site: 'DMM.com' };
const networkError = new TypeError('Network request failed');
mockFetch.mockRejectedValueOnce(networkError);
await (0, vitest_1.expect)(client.request(endpoint, testParams)).rejects.toThrow(`Error during API request to ${endpoint}: Network request failed`);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
});
// --- API メソッドのテスト ---
(0, vitest_1.it)('getItemList should call request with correct parameters', async () => {
const params = { service: 'digital', floor: 'videoa', hits: 10, keyword: 'test' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.getItemList(params);
const expectedUrl = new URL(`${client.baseUrl}/ItemList`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
service: params.service || '',
floor: params.floor || '',
hits: String(params.hits || ''),
keyword: params.keyword || '',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(expectedUrl.searchParams.getAll('site').length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('getItemList should call request with specified site', async () => {
const params = { site: 'FANZA', service: 'digital', floor: 'videoa', hits: 10 };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.getItemList(params);
const expectedUrl = new URL(`${client.baseUrl}/ItemList`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
site: 'FANZA',
service: params.service || '',
floor: params.floor || '',
hits: String(params.hits || ''),
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(expectedUrl.searchParams.getAll('site').length).toBe(1);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('getFloorList should call request with correct parameters', async () => {
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.getFloorList();
const expectedUrl = new URL(`${client.baseUrl}/FloorList`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(expectedUrl.searchParams.getAll('site').length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchActress should call request with correct parameters', async () => {
const params = { initial: 'あ', hits: 5 };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchActress(params);
const expectedUrl = new URL(`${client.baseUrl}/ActressSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
initial: params.initial || '',
hits: String(params.hits || ''),
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchActress should call request with hits and offset', async () => {
const params = { initial: 'い', hits: 10, offset: 11 };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchActress(params);
const expectedUrl = new URL(`${client.baseUrl}/ActressSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
initial: 'い',
hits: '10',
offset: '11',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchActress should call request with sort parameter', async () => {
const params = { initial: 'う', sort: '-name' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchActress(params);
const expectedUrl = new URL(`${client.baseUrl}/ActressSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
initial: 'う',
sort: '-name',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchGenre should call request with correct parameters', async () => {
const params = { floor_id: '123', initial: 'か' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchGenre(params);
const expectedUrl = new URL(`${client.baseUrl}/GenreSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
floor_id: params.floor_id || '',
initial: params.initial || '',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchMaker should call request with correct parameters', async () => {
const params = { floor_id: '456', initial: 'さ' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchMaker(params);
const expectedUrl = new URL(`${client.baseUrl}/MakerSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
floor_id: params.floor_id || '',
initial: params.initial || '',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchSeries should call request with correct parameters', async () => {
const params = { floor_id: '789', initial: 'た' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchSeries(params);
const expectedUrl = new URL(`${client.baseUrl}/SeriesSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
floor_id: params.floor_id || '',
initial: params.initial || '',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('searchAuthor should call request with correct parameters', async () => {
const params = { floor_id: '101', initial: 'な' };
mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ result: {} }) });
await client.searchAuthor(params);
const expectedUrl = new URL(`${client.baseUrl}/AuthorSearch`);
const searchParams = new URLSearchParams({
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
floor_id: params.floor_id || '',
initial: params.initial || '',
});
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
// --- Test getAllItems ---
(0, vitest_1.describe)('getAllItems', () => {
const baseParamsForGetAllItems = {
service: 'digital',
floor: 'videoa',
keyword: 'test-keyword-getallitems', // 他のテストと区別できるようなキーワード
};
const hitsPerPage = client_1.DmmApiClient.DefaultHitsPerPageForGetAllItems;
(0, vitest_1.it)('should yield all items from multiple pages', { timeout: 1000 }, async () => {
const totalItems = 250;
const page1Items = Array.from({ length: hitsPerPage }, (_, i) => ({ content_id: `item_${i + 1}` }));
const page2Items = Array.from({ length: hitsPerPage }, (_, i) => ({ content_id: `item_${i + 101}` }));
const page3Items = Array.from({ length: 50 }, (_, i) => ({ content_id: `item_${i + 201}` }));
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 1 } },
result_count: totalItems,
total_count: totalItems,
first_position: 1,
items: page1Items,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 101 } },
result_count: totalItems,
total_count: totalItems,
first_position: 101,
items: page2Items,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 201 } },
result_count: totalItems,
total_count: totalItems,
first_position: 201,
items: page3Items,
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(totalItems);
(0, vitest_1.expect)(receivedItems[0].content_id).toBe('item_1');
(0, vitest_1.expect)(receivedItems[100].content_id).toBe('item_101');
(0, vitest_1.expect)(receivedItems[249].content_id).toBe('item_250');
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(3);
const expectedUrlBase = `${client.baseUrl}/ItemList`;
const baseSearchParams = {
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
service: baseParamsForGetAllItems.service || '',
floor: baseParamsForGetAllItems.floor || '',
keyword: baseParamsForGetAllItems.keyword || '',
hits: String(hitsPerPage),
};
const url1 = new URL(expectedUrlBase);
const p1 = { ...baseSearchParams, offset: '1' };
url1.search = new URLSearchParams(p1).toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenNthCalledWith(1, url1.toString());
const url2 = new URL(expectedUrlBase);
const p2 = { ...baseSearchParams, offset: '101' };
url2.search = new URLSearchParams(p2).toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenNthCalledWith(2, url2.toString());
const url3 = new URL(expectedUrlBase);
const p3 = { ...baseSearchParams, offset: '201' };
url3.search = new URLSearchParams(p3).toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenNthCalledWith(3, url3.toString());
});
(0, vitest_1.it)('should yield no items if result_count is 0', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 1 } },
result_count: 0,
total_count: 0,
first_position: 0,
items: [],
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should yield items from a single page', async () => {
const totalItems = 50;
const pageItems = Array.from({ length: totalItems }, (_, i) => ({ content_id: `item_${i + 1}` }));
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 1 } },
result_count: totalItems,
total_count: totalItems,
first_position: 1,
items: pageItems,
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(totalItems);
(0, vitest_1.expect)(receivedItems[0].content_id).toBe('item_1');
(0, vitest_1.expect)(receivedItems[49].content_id).toBe('item_50');
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('should throw an error if getItemList fails during iteration', async () => {
const page1Items = Array.from({ length: hitsPerPage }, (_, i) => ({ content_id: `item_${i + 1}` }));
const apiError = new TypeError('Failed to fetch');
let callCount = 0;
mockFetch.mockImplementation(async () => {
callCount++;
if (callCount === 1) {
return {
ok: true,
json: async () => ({
result: {
request: { parameters: { ...baseParamsForGetAllItems, hits: hitsPerPage, offset: 1 } },
result_count: 150,
total_count: 150,
first_position: 1,
items: page1Items,
},
}),
};
}
throw apiError;
});
const generator = client.getAllItems(baseParamsForGetAllItems);
const receivedItems = [];
let caughtError = null;
try {
for await (const item of generator) {
receivedItems.push(item);
}
throw new Error('Error should have been thrown during iteration');
}
catch (error) {
caughtError = error;
}
(0, vitest_1.expect)(receivedItems.length).toBe(hitsPerPage);
(0, vitest_1.expect)(receivedItems[0].content_id).toBe('item_1');
(0, vitest_1.expect)(caughtError).toBeInstanceOf(Error);
const expectedOffsetForError = 1 + hitsPerPage;
const expectedOriginalErrorMessage = `Error during API request to /ItemList: ${apiError.message}`;
if (caughtError) {
(0, vitest_1.expect)(caughtError.message).toBe(`Error in getAllItems at offset ${expectedOffsetForError}: ${expectedOriginalErrorMessage}`);
(0, vitest_1.expect)(caughtError.cause).toBeInstanceOf(Error);
if (caughtError.cause instanceof Error) {
(0, vitest_1.expect)(caughtError.cause.message).toBe(expectedOriginalErrorMessage);
}
}
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
});
(0, vitest_1.it)('should yield items correctly and handle pagination until no more items are returned', async () => {
const params = { site: 'DMM.com', service: 'digital', floor: 'videoa', hits: 10, offset: 1 };
const mockResponse1 = {
result: {
status: 200,
result_count: 1,
total_count: 2,
first_position: 1,
items: [{ content_id: 'item1' }],
},
};
const mockResponse2 = {
result: {
status: 200,
result_count: 1,
total_count: 2,
first_position: 2,
items: [{ content_id: 'item2' }],
},
};
mockFetch
.mockResolvedValueOnce({ ok: true, json: async () => mockResponse1 })
.mockResolvedValueOnce({ ok: true, json: async () => mockResponse2 });
const items = [];
const { hits, offset, ...baseParams } = params;
for await (const item of client.getAllItems(baseParams)) {
items.push(item);
}
(0, vitest_1.expect)(items.length).toBe(2);
(0, vitest_1.expect)(items[0].content_id).toBe('item1');
(0, vitest_1.expect)(items[1].content_id).toBe('item2');
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
const calls = mockFetch.mock.calls;
const firstCallUrl = new URL(calls[0][0]);
(0, vitest_1.expect)(firstCallUrl.searchParams.get('hits')).toBe(String(client_1.DmmApiClient.DefaultHitsPerPageForGetAllItems));
(0, vitest_1.expect)(firstCallUrl.searchParams.get('offset')).toBe('1');
const secondCallUrl = new URL(calls[1][0]);
(0, vitest_1.expect)(secondCallUrl.searchParams.get('hits')).toBe(String(client_1.DmmApiClient.DefaultHitsPerPageForGetAllItems));
(0, vitest_1.expect)(secondCallUrl.searchParams.get('offset')).toBe(String(mockResponse1.result.first_position + mockResponse1.result.items.length));
});
(0, vitest_1.it)('should handle API error during pagination in getAllItems and throw enhanced error', async () => {
const params = { site: 'DMM.com', service: 'digital', floor: 'videoa', sort: 'rank' };
const mockSuccessResponse = {
result: {
status: 200,
result_count: 1,
total_count: 10,
first_position: 1,
items: [{ content_id: 'item1' }],
},
};
const mockError = new Error('Simulated API Error');
mockFetch
.mockResolvedValueOnce({ ok: true, json: async () => mockSuccessResponse })
.mockRejectedValueOnce(mockError);
const items = [];
try {
for await (const item of client.getAllItems(params)) {
items.push(item);
}
(0, vitest_1.expect)('Error was not thrown').toBe('Error should have been thrown');
}
catch (e) {
(0, vitest_1.expect)(e).toBeInstanceOf(Error);
const error = e;
(0, vitest_1.expect)(error.message).toMatch(/^Error in getAllItems at offset \d+: Error during API request to \/ItemList: Simulated API Error$/);
(0, vitest_1.expect)(error.cause).toBeInstanceOf(Error);
(0, vitest_1.expect)(error.cause?.message).toBe(`Error during API request to ${client_1.DmmApiClient.ItemListEndpoint}: ${mockError.message}`);
}
(0, vitest_1.expect)(items.length).toBe(1);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2);
});
(0, vitest_1.it)('should correctly set total_count on the first call and use it for subsequent loop condition', async () => {
const totalItemsExpected = 5; // total_countが少ないケース
const itemsPage1 = [{ content_id: 'tc_item1' }, { content_id: 'tc_item2' }];
const itemsPage2 = [{ content_id: 'tc_item3' }, { content_id: 'tc_item4' }, { content_id: 'tc_item5' }];
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: itemsPage1.length,
total_count: totalItemsExpected, // First call sets this
first_position: 1,
items: itemsPage1,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: itemsPage2.length,
total_count: totalItemsExpected, // Subsequent calls use the existing total_count
first_position: 1 + itemsPage1.length,
items: itemsPage2,
},
}),
});
// .mockResolvedValueOnce({ // This call should NOT happen as currentOffset (1+2+3=6) > totalCount (5)
// ok: true,
// json: async () => ({ result: { items: [], total_count: totalItemsExpected, first_position: 1 + itemsPage1.length + itemsPage2.length, result_count: 0 } }),
// });
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(totalItemsExpected);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2); // total_countに基づいて2回呼び出される
(0, vitest_1.expect)(receivedItems.map(i => i.content_id)).toEqual(['tc_item1', 'tc_item2', 'tc_item3', 'tc_item4', 'tc_item5']);
});
(0, vitest_1.it)('getAllItems should handle a large number of items with multiple pages', async () => {
const totalItems = 250; // DefaultHitsPerPageForGetAllItems (100) * 2 + 50
const page1Items = Array.from({ length: hitsPerPage }, (_, i) => ({ content_id: `large_item_${i + 1}` }));
const page2Items = Array.from({ length: hitsPerPage }, (_, i) => ({ content_id: `large_item_${i + 1 + hitsPerPage}` }));
const page3Items = Array.from({ length: totalItems - 2 * hitsPerPage }, (_, i) => ({ content_id: `large_item_${i + 1 + 2 * hitsPerPage}` }));
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: page1Items.length,
total_count: totalItems,
first_position: 1,
items: page1Items,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: page2Items.length,
total_count: totalItems,
first_position: 1 + hitsPerPage,
items: page2Items,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: page3Items.length,
total_count: totalItems,
first_position: 1 + 2 * hitsPerPage,
items: page3Items,
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(totalItems);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(3);
(0, vitest_1.expect)(receivedItems[0].content_id).toBe('large_item_1');
(0, vitest_1.expect)(receivedItems[totalItems - 1].content_id).toBe(`large_item_${totalItems}`);
});
(0, vitest_1.it)('getAllItems should make no API calls if total_count is 0 on the first response', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: 0,
total_count: 0, // total_count is 0
first_position: 0,
items: [],
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1); // Only the first call to get total_count
});
(0, vitest_1.it)('getAllItems should yield no items if the first response has no items and total_count is 0', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: 0,
total_count: 0,
first_position: 0,
items: [], // No items
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('getAllItems should yield no items if the first response has items but total_count is 0 (edge case, API should not do this)', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: 1, // Has an item
total_count: 0, // But total_count is 0
first_position: 1,
items: [{ content_id: 'edge_item1' }],
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
// total_countが0なので、最初のAPI呼び出しで終了し、アイテムはイールドされない
(0, vitest_1.expect)(receivedItems.length).toBe(0);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(1);
});
(0, vitest_1.it)('getAllItems should stop if items array is unexpectedly empty or missing mid-pagination', async () => {
const itemsPage1 = [{ content_id: 'stop_item1' }, { content_id: 'stop_item2' }];
mockFetch
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: itemsPage1.length,
total_count: 5, // Expects more items
first_position: 1,
items: itemsPage1,
},
}),
})
.mockResolvedValueOnce({
ok: true,
json: async () => ({
result: {
status: 200,
result_count: 0,
total_count: 5,
first_position: 1 + itemsPage1.length,
items: [], // Empty items
},
}),
});
const receivedItems = [];
for await (const item of client.getAllItems(baseParamsForGetAllItems)) {
receivedItems.push(item);
}
(0, vitest_1.expect)(receivedItems.length).toBe(itemsPage1.length); // Should only get items from the first page
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(2); // Called twice, but stopped due to empty items
(0, vitest_1.expect)(receivedItems.map(i => i.content_id)).toEqual(['stop_item1', 'stop_item2']);
});
(0, vitest_1.it)('getAllItems should correctly calculate the next offset when first_position is not 1', async () => {
// このテストケース専用のモック設定に集中する
mockFetch.mockClear(); // 念のためクリア
const firstCallResponse = {
result: {
status: 200,
result_count: 1,
total_count: 7, // Adjusted total_count
first_position: 5,
items: [{ content_id: 'fp_item1' }]
}
};
const secondCallResponse = {
result: {
status: 200,
result_count: 1,
total_count: 7, // Adjusted total_count
first_position: 6,
items: [{ content_id: 'fp_item2' }]
}
};
// このテストでは、上記レスポンス以降はアイテムがないことを示す空レスポンスを追加
const thirdCallEmptyResponse = {
result: {
status: 200,
result_count: 0,
total_count: 7, // Adjusted total_count
first_position: 7, // 6 + 1
items: []
}
};
mockFetch
.mockResolvedValueOnce({ ok: true, json: async () => firstCallResponse })
.mockResolvedValueOnce({ ok: true, json: async () => secondCallResponse })
.mockResolvedValueOnce({ ok: true, json: async () => thirdCallEmptyResponse });
const receivedItems = [];
// getAllItems に渡すパラメータは hits や offset を含まないもの
const queryParams = {
service: 'digital',
floor: 'videoa',
keyword: 'test-fp-calc', // このテスト固有のキーワード
};
for await (const item of client.getAllItems(queryParams)) {
receivedItems.push(item);
}
// 2つのアイテムが取得され、3回目のAPI呼び出しでループが終了するはず
(0, vitest_1.expect)(receivedItems.length).toBe(2);
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledTimes(3); // 2回アイテム取得 + 1回空確認
const calls = mockFetch.mock.calls;
const firstCallUrl = new URL(calls[0][0]);
(0, vitest_1.expect)(firstCallUrl.searchParams.get('offset')).toBe('1');
(0, vitest_1.expect)(firstCallUrl.searchParams.get('keyword')).toBe('test-fp-calc');
const secondCallUrl = new URL(calls[1][0]);
(0, vitest_1.expect)(secondCallUrl.searchParams.get('offset')).toBe(String(firstCallResponse.result.first_position + firstCallResponse.result.items.length) // 5 + 1 = 6
);
(0, vitest_1.expect)(secondCallUrl.searchParams.get('keyword')).toBe('test-fp-calc');
const thirdCallUrl = new URL(calls[2][0]);
(0, vitest_1.expect)(thirdCallUrl.searchParams.get('offset')).toBe(String(secondCallResponse.result.first_position + secondCallResponse.result.items.length) // 6 + 1 = 7
);
(0, vitest_1.expect)(thirdCallUrl.searchParams.get('keyword')).toBe('test-fp-calc');
(0, vitest_1.expect)(receivedItems.map(i => i.content_id)).toEqual(['fp_item1', 'fp_item2']);
});
});
(0, vitest_1.describe)('getItemList API (Endpoint Constant Test)', () => {
const itemListParams = { site: 'DMM.com', service: 'mono', floor: 'dvd', hits: 10, offset: 1, sort: 'rank' };
(0, vitest_1.it)('should call fetch with the currently hardcoded endpoint for ItemList', async () => {
const mockResponse = { result: { items: [] } };
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => mockResponse,
});
await client.getItemList(itemListParams);
const expectedUrl = new URL(`${client.baseUrl}/ItemList`);
const queryParams = {
api_id: defaultOptions.apiId,
affiliate_id: defaultOptions.affiliateId,
};
for (const key in itemListParams) {
if (Object.prototype.hasOwnProperty.call(itemListParams, key) && itemListParams[key] !== undefined) {
queryParams[key] = String(itemListParams[key]);
}
}
const searchParams = new URLSearchParams(queryParams);
expectedUrl.search = searchParams.toString();
(0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith(expectedUrl.toString());
});
(0, vitest_1.it)('should use the DmmApiClient.ItemListEndpoint constant for the endp