filecoin-pin
Version:
Bridge IPFS content to Filecoin Onchain Cloud using familiar tools
359 lines • 19.6 kB
JavaScript
import { CID } from 'multiformats/cid';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { waitForIpniProviderResults } from '../../core/utils/validate-ipni-advertisement.js';
describe('waitForIpniProviderResults', () => {
const testCid = CID.parse('bafkreia5fn4rmshmb7cl7fufkpcw733b5anhuhydtqstnglpkzosqln5kq');
const defaultIndexerUrl = 'https://filecoinpin.contact';
const mockFetch = vi.fn();
const createProviderInfo = (serviceURL) => ({
id: 1234,
serviceProvider: 'f01234',
name: 'Test Provider',
products: {
PDP: {
data: {
serviceURL,
},
},
},
});
const successResponse = (multiaddrs = ['/dns/example.com/tcp/443/https']) => ({
ok: true,
json: vi.fn(async () => ({
MultihashResults: [
{
ProviderResults: multiaddrs.map((addr, index) => ({
Provider: {
ID: `12D3KooWProvider${index}`,
Addrs: [addr],
},
})),
},
],
})),
});
const emptyProviderResponse = () => ({
ok: true,
json: vi.fn(async () => ({
MultihashResults: [],
})),
});
beforeEach(() => {
vi.stubGlobal('fetch', mockFetch);
vi.useFakeTimers();
});
afterEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
vi.useRealTimers();
});
describe('successful announcement', () => {
it('should resolve true and emit a final complete event on first attempt', async () => {
mockFetch.mockResolvedValueOnce(successResponse());
const onProgress = vi.fn();
const promise = waitForIpniProviderResults(testCid, { onProgress });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith(`${defaultIndexerUrl}/cid/${testCid}`, {
headers: { Accept: 'application/json' },
});
// Should emit retryUpdate for attempt 0 and a final complete(true)
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 0 } });
expect(onProgress).toHaveBeenCalledWith({
type: 'ipniProviderResults.complete',
data: { result: true, retryCount: 0 },
});
});
it('should retry multiple times before succeeding and emit a final complete(true)', async () => {
mockFetch
.mockResolvedValueOnce({ ok: false })
.mockResolvedValueOnce({ ok: false })
.mockResolvedValueOnce({ ok: false })
.mockResolvedValueOnce(successResponse());
const onProgress = vi.fn();
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 5, onProgress });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledTimes(4);
// Expect retryUpdate with counts 0,1,2,3 and final complete with retryCount 3
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 0 } });
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 1 } });
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 2 } });
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 3 } });
expect(onProgress).toHaveBeenCalledWith({
type: 'ipniProviderResults.complete',
data: { result: true, retryCount: 3 },
});
});
it('should succeed when the expected provider advertises the derived multiaddr', async () => {
const provider = createProviderInfo('https://example.com');
const expectedMultiaddr = '/dns/example.com/tcp/443/https';
mockFetch.mockResolvedValueOnce(successResponse([expectedMultiaddr]));
const promise = waitForIpniProviderResults(testCid, { expectedProviders: [provider] });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(`${defaultIndexerUrl}/cid/${testCid}`, {
headers: { Accept: 'application/json' },
});
});
it('should succeed when all expected providers are in the IPNI ProviderResults', async () => {
const providerA = createProviderInfo('https://a.example.com');
const providerB = createProviderInfo('https://b.example.com:8443');
const expectedMultiaddrs = ['/dns/a.example.com/tcp/443/https', '/dns/b.example.com/tcp/8443/https'];
mockFetch.mockResolvedValueOnce(successResponse(expectedMultiaddrs));
const promise = waitForIpniProviderResults(testCid, { expectedProviders: [providerA, providerB] });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
});
});
describe('failed announcement', () => {
it('should reject after custom maxAttempts and emit a failed event', async () => {
mockFetch.mockResolvedValue({ ok: false });
const onProgress = vi.fn();
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 3, onProgress });
// Attach rejection handler immediately
const expectPromise = expect(promise).rejects.toThrow(`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 3 attempts`);
await vi.runAllTimersAsync();
await expectPromise;
expect(mockFetch).toHaveBeenCalledTimes(3);
// Expect retryUpdate with counts 0,1,2 and final failed event (no complete event on failure)
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 0 } });
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 1 } });
expect(onProgress).toHaveBeenCalledWith({ type: 'ipniProviderResults.retryUpdate', data: { retryCount: 2 } });
// Should emit failed event, not complete(false)
expect(onProgress).toHaveBeenCalledWith({
type: 'ipniProviderResults.failed',
data: { error: expect.any(Error) },
});
// Should NOT emit complete event
expect(onProgress).not.toHaveBeenCalledWith({
type: 'ipniProviderResults.complete',
data: { result: false, retryCount: expect.any(Number) },
});
});
it('should reject immediately when maxAttempts is 1', async () => {
mockFetch.mockResolvedValue({ ok: false });
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 1 });
// Attach rejection handler immediately
const expectPromise = expect(promise).rejects.toThrow(`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 1 attempt`);
await vi.runAllTimersAsync();
await expectPromise;
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('should reject when an expected provider is missing from the IPNI ProviderResults', async () => {
const provider = createProviderInfo('https://expected.example.com');
mockFetch.mockResolvedValueOnce(successResponse(['/dns/other.example.com/tcp/443/https']));
const promise = waitForIpniProviderResults(testCid, {
maxAttempts: 1,
expectedProviders: [provider],
});
const expectPromise = expect(promise).rejects.toThrow(`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 1 attempt. Last observation: Missing provider records with expected multiaddr(s): /dns/expected.example.com/tcp/443/https`);
await vi.runAllTimersAsync();
await expectPromise;
});
it('should reject when not all expected providers are in the IPNI ProviderResults', async () => {
const providerA = createProviderInfo('https://a.example.com');
const providerB = createProviderInfo('https://b.example.com');
mockFetch.mockResolvedValueOnce(successResponse(['/dns/a.example.com/tcp/443/https']));
const promise = waitForIpniProviderResults(testCid, {
maxAttempts: 1,
expectedProviders: [providerA, providerB],
});
const expectPromise = expect(promise).rejects.toThrow(`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 1 attempt. Last observation: Missing provider records with expected multiaddr(s): /dns/b.example.com/tcp/443/https`);
await vi.runAllTimersAsync();
await expectPromise;
});
it('should retry until the expected provider appears in subsequent attempts', async () => {
const provider = createProviderInfo('https://expected.example.com');
const expectedMultiaddr = '/dns/expected.example.com/tcp/443/https';
mockFetch
.mockResolvedValueOnce(successResponse(['/dns/other.example.com/tcp/443/https']))
.mockResolvedValueOnce(successResponse([expectedMultiaddr]));
const promise = waitForIpniProviderResults(testCid, {
maxAttempts: 3,
expectedProviders: [provider],
delayMs: 1,
});
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it('should retry when the IPNI response is empty', async () => {
const provider = createProviderInfo('https://expected.example.com');
const expectedMultiaddr = '/dns/expected.example.com/tcp/443/https';
mockFetch
.mockResolvedValueOnce(emptyProviderResponse())
.mockResolvedValueOnce(successResponse([expectedMultiaddr]));
const promise = waitForIpniProviderResults(testCid, {
maxAttempts: 3,
expectedProviders: [provider],
delayMs: 1,
});
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
});
describe('abort signal', () => {
it('should abort when signal is triggered before first check', async () => {
const abortController = new AbortController();
abortController.abort();
const promise = waitForIpniProviderResults(testCid, { signal: abortController.signal });
// Attach rejection handler immediately
const expectPromise = expect(promise).rejects.toThrow('Check IPNI announce aborted');
await vi.runAllTimersAsync();
await expectPromise;
expect(mockFetch).not.toHaveBeenCalled();
});
it('should abort when signal is triggered during retry', async () => {
const abortController = new AbortController();
mockFetch.mockResolvedValue({ ok: false });
const promise = waitForIpniProviderResults(testCid, { signal: abortController.signal, maxAttempts: 5 });
// Let first check complete
await vi.advanceTimersByTimeAsync(0);
expect(mockFetch).toHaveBeenCalledTimes(1);
// Abort before retry
abortController.abort();
// Attach rejection handler before running remaining timers
const expectPromise = expect(promise).rejects.toThrow('Check IPNI announce aborted');
await vi.runAllTimersAsync();
await expectPromise;
// Should not make additional calls after abort
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('should pass abort signal to fetch when provided', async () => {
const abortController = new AbortController();
mockFetch.mockResolvedValueOnce(successResponse());
const promise = waitForIpniProviderResults(testCid, { signal: abortController.signal });
await vi.runAllTimersAsync();
await promise;
expect(mockFetch).toHaveBeenCalledWith(`${defaultIndexerUrl}/cid/${testCid}`, {
headers: { Accept: 'application/json' },
signal: abortController.signal,
});
});
});
describe('edge cases', () => {
it('should retry when fetch throws before succeeding within maxAttempts', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error')).mockResolvedValueOnce(successResponse());
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 2, delayMs: 1 });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledTimes(2);
});
it('should handle different CID formats', async () => {
const v0Cid = CID.parse('QmNT6isqrhH6LZWg8NeXQYTD9wPjJo2BHHzyezpf9BdHbD');
mockFetch.mockResolvedValueOnce(successResponse());
const promise = waitForIpniProviderResults(v0Cid, {});
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(`${defaultIndexerUrl}/cid/${v0Cid}`, {
headers: { Accept: 'application/json' },
});
});
it('should handle empty or missing provider data gracefully', async () => {
// Test that validation handles various malformed provider responses
mockFetch.mockResolvedValueOnce({
ok: true,
json: vi.fn(async () => ({
MultihashResults: [
{
ProviderResults: [
{ Provider: null }, // null provider
{ Provider: { ID: '12D3Koo1', Addrs: [] } }, // empty addrs
{ Provider: { ID: '12D3Koo2', Addrs: ['/dns/valid.com/tcp/443/https'] } }, // valid
],
},
],
})),
});
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 1 });
await vi.runAllTimersAsync();
const result = await promise;
// Should succeed because at least one valid provider exists
expect(result).toBe(true);
});
it('should handle provider without serviceURL by falling back to generic validation', async () => {
const providerWithoutURL = {
id: 1234,
serviceProvider: 'f01234',
name: 'Test Provider',
products: { PDP: { data: {} } },
};
mockFetch.mockResolvedValueOnce(successResponse());
const promise = waitForIpniProviderResults(testCid, { expectedProviders: [providerWithoutURL] });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
});
it('should preserve parse error message instead of overwriting with generic message', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: vi.fn(async () => {
throw new Error('Invalid JSON');
}),
});
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 1 });
// Should preserve the specific "Failed to parse" message, not overwrite with generic message
const expectPromise = expect(promise).rejects.toThrow('Failed to parse IPNI response body');
await vi.runAllTimersAsync();
await expectPromise;
});
it('should clear stale multiaddrs when parse error occurs after successful response', async () => {
// Attempt 1: successful response with multiaddrs but doesn't match expectations
// Attempt 2: parse error - should clear the multiaddrs from attempt 1
const provider = createProviderInfo('https://expected.example.com');
mockFetch.mockResolvedValueOnce(successResponse(['/dns/other.example.com/tcp/443/https'])).mockResolvedValueOnce({
ok: true,
json: vi.fn(async () => {
throw new Error('Invalid JSON');
}),
});
const promise = waitForIpniProviderResults(testCid, {
maxAttempts: 2,
expectedProviders: [provider],
});
const expectPromise = expect(promise).rejects.toThrow(`IPFS root CID "${testCid.toString()}" does not have expected IPNI ProviderResults after 2 attempts. Last observation: Failed to parse IPNI response body: Invalid JSON. Expected multiaddrs: [/dns/expected.example.com/tcp/443/https]. Actual multiaddrs in response: []`);
await vi.runAllTimersAsync();
await expectPromise;
});
it('should update failure reason on each attempt instead of preserving first error', async () => {
// Attempt 1: parse error
// Attempt 2: successful parse but empty results
// Final error should report empty results as last observation, not parse error
mockFetch
.mockResolvedValueOnce({
ok: true,
json: vi.fn(async () => {
throw new Error('Invalid JSON');
}),
})
.mockResolvedValueOnce(emptyProviderResponse());
const promise = waitForIpniProviderResults(testCid, { maxAttempts: 2 });
const expectPromise = expect(promise).rejects.toThrow('Last observation: IPNI response did not include any provider results');
await vi.runAllTimersAsync();
await expectPromise;
});
it('should use custom IPNI indexer URL when provided', async () => {
const customIndexerUrl = 'https://custom-indexer.example.com';
mockFetch.mockResolvedValueOnce(successResponse());
const promise = waitForIpniProviderResults(testCid, { ipniIndexerUrl: customIndexerUrl });
await vi.runAllTimersAsync();
const result = await promise;
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(`${customIndexerUrl}/cid/${testCid}`, {
headers: { Accept: 'application/json' },
});
});
});
});
//# sourceMappingURL=validate-ipni-advertisement.test.js.map