//go:build !disable_torrent // +build !disable_torrent package communities_test import ( "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "go-codex-client/communities" mock_communities "go-codex-client/communities/mock" "go-codex-client/protobuf" "go.uber.org/mock/gomock" "go.uber.org/zap" ) // CodexArchiveDownloaderTestifySuite demonstrates testify's suite functionality type CodexArchiveDownloaderTestifySuite struct { suite.Suite ctrl *gomock.Controller mockClient *mock_communities.MockCodexClientInterface index *protobuf.CodexWakuMessageArchiveIndex } // SetupTest runs before each test method func (suite *CodexArchiveDownloaderTestifySuite) SetupTest() { suite.ctrl = gomock.NewController(suite.T()) suite.mockClient = mock_communities.NewMockCodexClientInterface(suite.ctrl) suite.index = &protobuf.CodexWakuMessageArchiveIndex{ Archives: map[string]*protobuf.CodexWakuMessageArchiveIndexMetadata{ "test-archive-hash-1": { Cid: "test-cid-1", Metadata: &protobuf.WakuMessageArchiveMetadata{ From: 1000, To: 2000, }, }, }, } } // TearDownTest runs after each test method func (suite *CodexArchiveDownloaderTestifySuite) TearDownTest() { suite.ctrl.Finish() } func (suite *CodexArchiveDownloaderTestifySuite) TestBasicSingleArchive() { // Test data communityID := "test-community" existingArchiveIDs := []string{} // No existing archives cancelChan := make(chan struct{}) // Set up mock expectations - same as before suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), "test-cid-1"). Return(&communities.CodexManifest{CID: "test-cid-1"}, nil). Times(1) // First HasCid call returns false, second returns true (simulating polling) gomock.InOrder( suite.mockClient.EXPECT().HasCid("test-cid-1").Return(false, nil), suite.mockClient.EXPECT().HasCid("test-cid-1").Return(true, nil), ) // Create downloader with mock client logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, suite.index, communityID, existingArchiveIDs, cancelChan, logger) // Set fast polling interval for tests (10ms instead of default 1s) downloader.SetPollingInterval(10 * time.Millisecond) // Set up callback to track completion var callbackInvoked bool var callbackHash string var callbackFrom, callbackTo uint64 downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { callbackInvoked = true callbackHash = hash callbackFrom = from callbackTo = to }) // Verify initial state - compare testify vs standard assertions // Testify version is more readable and provides better error messages assert.Equal(suite.T(), 1, downloader.GetTotalArchivesCount(), "Total archives count should be 1") assert.Equal(suite.T(), 0, downloader.GetTotalDownloadedArchivesCount(), "Downloaded archives should be 0 initially") assert.False(suite.T(), downloader.IsDownloadComplete(), "Download should not be complete initially") // Start the download downloader.StartDownload() // Wait for download to complete (with timeout) require.Eventually(suite.T(), func() bool { return downloader.IsDownloadComplete() }, 5*time.Second, 100*time.Millisecond, "Download should complete within 5 seconds") // Verify final state - testify makes these assertions more expressive assert.True(suite.T(), downloader.IsDownloadComplete(), "Download should be complete") assert.Equal(suite.T(), 1, downloader.GetTotalDownloadedArchivesCount(), "Should have 1 downloaded archive") // Verify callback was invoked - multiple related assertions grouped logically assert.True(suite.T(), callbackInvoked, "Callback should be invoked") assert.Equal(suite.T(), "test-archive-hash-1", callbackHash, "Callback hash should match expected") assert.Equal(suite.T(), uint64(1000), callbackFrom, "Callback from should be 1000") assert.Equal(suite.T(), uint64(2000), callbackTo, "Callback to should be 2000") suite.T().Log("✅ Basic single archive download test passed") suite.T().Log(" - All mock expectations satisfied") suite.T().Logf(" - Callback invoked: %v", callbackInvoked) } func (suite *CodexArchiveDownloaderTestifySuite) TestMultipleArchives() { // Create test data with multiple archives index := &protobuf.CodexWakuMessageArchiveIndex{ Archives: map[string]*protobuf.CodexWakuMessageArchiveIndexMetadata{ "archive-1": { Cid: "cid-1", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 1000, To: 2000}, }, "archive-2": { Cid: "cid-2", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 2000, To: 3000}, }, "archive-3": { Cid: "cid-3", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 3000, To: 4000}, }, }, } communityID := "test-community" existingArchiveIDs := []string{} // No existing archives cancelChan := make(chan struct{}) // Set up expectations for all 3 archives - testify makes verification cleaner expectedCids := []string{"cid-1", "cid-2", "cid-3"} for _, cid := range expectedCids { suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), cid). Return(&communities.CodexManifest{CID: cid}, nil). Times(1) // Each archive becomes available after one poll gomock.InOrder( suite.mockClient.EXPECT().HasCid(cid).Return(false, nil), suite.mockClient.EXPECT().HasCid(cid).Return(true, nil), ) } // Create downloader logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, index, communityID, existingArchiveIDs, cancelChan, logger) downloader.SetPollingInterval(10 * time.Millisecond) // Track the order in which archives are started (deterministic) var startOrder []string downloader.SetOnStartingArchiveDownload(func(hash string, from, to uint64) { startOrder = append(startOrder, hash) }) // Track completed archives (non-deterministic due to concurrency) completedArchives := make(map[string]bool) downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { completedArchives[hash] = true }) // Initial state verification assert.Equal(suite.T(), 3, downloader.GetTotalArchivesCount(), "Should have 3 total archives") assert.Equal(suite.T(), 0, downloader.GetTotalDownloadedArchivesCount(), "Should start with 0 downloaded") assert.False(suite.T(), downloader.IsDownloadComplete(), "Should not be complete initially") // Start download downloader.StartDownload() // Wait for all downloads to complete require.Eventually(suite.T(), func() bool { return downloader.IsDownloadComplete() }, 10*time.Second, 100*time.Millisecond, "All downloads should complete within 10 seconds") // Final state verification - testify makes these checks very readable assert.True(suite.T(), downloader.IsDownloadComplete(), "Download should be complete") assert.Equal(suite.T(), 3, downloader.GetTotalDownloadedArchivesCount(), "Should have downloaded all 3 archives") // Verify all archives were processed assert.Len(suite.T(), completedArchives, 3, "Should have completed exactly 3 archives") assert.Contains(suite.T(), completedArchives, "archive-1", "Should have completed archive-1") assert.Contains(suite.T(), completedArchives, "archive-2", "Should have completed archive-2") assert.Contains(suite.T(), completedArchives, "archive-3", "Should have completed archive-3") // Verify sorting: archives should be started in most-recent-first order (deterministic) // This tests the internal sorting logic before concurrency begins expectedStartOrder := []string{"archive-3", "archive-2", "archive-1"} assert.Equal(suite.T(), expectedStartOrder, startOrder, "Archives should be started in most-recent-first order") suite.T().Log("✅ Multiple archives test passed") suite.T().Logf(" - Completed %d out of %d archives", len(completedArchives), 3) suite.T().Logf(" - Start order (sorted): %v", startOrder) } func (suite *CodexArchiveDownloaderTestifySuite) TestCancellationDuringTriggerDownload() { // Test that cancellation during TriggerDownloadWithContext is handled properly communityID := "test-community" existingArchiveIDs := []string{} // No existing archives cancelChan := make(chan struct{}) // Mock TriggerDownloadWithContext to simulate a cancellation error suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), "test-cid-1"). Return(nil, assert.AnError). // Return a generic error to simulate failure Times(1) // No HasCid calls should be made since TriggerDownload fails // (this is the key test - we shouldn't proceed to polling) // Create downloader with mock client logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, suite.index, communityID, existingArchiveIDs, cancelChan, logger) downloader.SetPollingInterval(10 * time.Millisecond) // Track callbacks - onArchiveDownloaded should NOT be called on failure var callbackInvoked bool var startCallbackInvoked bool downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { callbackInvoked = true }) downloader.SetOnStartingArchiveDownload(func(hash string, from, to uint64) { startCallbackInvoked = true }) // Start the download downloader.StartDownload() // Wait a bit to ensure the goroutine has time to complete time.Sleep(200 * time.Millisecond) // Verify the state - download should be marked complete (no pending downloads) // but no archives should be successfully downloaded assert.True(suite.T(), startCallbackInvoked, "Start callback should be invoked") assert.False(suite.T(), callbackInvoked, "Success callback should NOT be invoked on failure") assert.Equal(suite.T(), 0, downloader.GetTotalDownloadedArchivesCount(), "No archives should be downloaded on failure") assert.True(suite.T(), downloader.IsDownloadComplete(), "Download should be complete (no pending downloads)") assert.Equal(suite.T(), 0, downloader.GetPendingArchivesCount(), "No archives should be pending") suite.T().Log("✅ Cancellation during trigger download test passed") suite.T().Log(" - TriggerDownload failed as expected") suite.T().Log(" - No polling occurred (as intended)") suite.T().Log(" - Success callback was NOT invoked") } func (suite *CodexArchiveDownloaderTestifySuite) TestCancellationDuringPolling() { // Test that cancellation during the polling phase is handled properly communityID := "test-community" existingArchiveIDs := []string{} // No existing archives cancelChan := make(chan struct{}) // Mock successful TriggerDownload suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), "test-cid-1"). Return(&communities.CodexManifest{CID: "test-cid-1"}, nil). Times(1) // Mock polling - allow multiple calls, but we'll cancel before completion suite.mockClient.EXPECT(). HasCid("test-cid-1"). Return(false, nil). AnyTimes() // Allow multiple calls since timing is unpredictable // Create downloader with mock client logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, suite.index, communityID, existingArchiveIDs, cancelChan, logger) downloader.SetPollingInterval(50 * time.Millisecond) // Longer interval to allow cancellation downloader.SetPollingTimeout(1 * time.Second) // Short timeout for test (instead of 30s) // Track callbacks var successCallbackInvoked bool var startCallbackInvoked bool downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { successCallbackInvoked = true }) downloader.SetOnStartingArchiveDownload(func(hash string, from, to uint64) { startCallbackInvoked = true }) // Start the download downloader.StartDownload() // Wait for the download to start and first poll to occur time.Sleep(100 * time.Millisecond) // Verify initial state assert.True(suite.T(), startCallbackInvoked, "Start callback should be invoked") assert.Equal(suite.T(), 1, downloader.GetPendingArchivesCount(), "Should have 1 pending download") assert.False(suite.T(), downloader.IsDownloadComplete(), "Download should not be complete yet") // Cancel the entire operation close(cancelChan) // Wait for cancellation to propagate require.Eventually(suite.T(), func() bool { return downloader.IsDownloadComplete() }, 2*time.Second, 50*time.Millisecond, "Download should complete after cancellation") // Verify final state assert.False(suite.T(), successCallbackInvoked, "Success callback should NOT be invoked on cancellation") assert.Equal(suite.T(), 0, downloader.GetPendingArchivesCount(), "No archives should be pending after cancellation") assert.True(suite.T(), downloader.IsCancelled(), "Downloader should be marked as cancelled") suite.T().Log("✅ Cancellation during polling test passed") suite.T().Log(" - TriggerDownload succeeded") suite.T().Log(" - Polling started but was cancelled") suite.T().Log(" - Success callback was NOT invoked") suite.T().Log(" - Download marked as cancelled") } func (suite *CodexArchiveDownloaderTestifySuite) TestPollingTimeout() { // Test that polling timeout is handled properly (no success callback) communityID := "test-community" existingArchiveIDs := []string{} // No existing archives cancelChan := make(chan struct{}) // Mock successful TriggerDownload suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), "test-cid-1"). Return(&communities.CodexManifest{CID: "test-cid-1"}, nil). Times(1) // Mock polling to always return false (simulating timeout) suite.mockClient.EXPECT(). HasCid("test-cid-1"). Return(false, nil). AnyTimes() // Will be called multiple times until timeout // Create downloader with mock client logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, suite.index, communityID, existingArchiveIDs, cancelChan, logger) downloader.SetPollingInterval(10 * time.Millisecond) // Fast polling for test downloader.SetPollingTimeout(100 * time.Millisecond) // Short timeout for test (instead of 30s) // Track callbacks var successCallbackInvoked bool var startCallbackInvoked bool downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { successCallbackInvoked = true }) downloader.SetOnStartingArchiveDownload(func(hash string, from, to uint64) { startCallbackInvoked = true }) // Start the download downloader.StartDownload() // Wait for timeout (100ms configured timeout) // We'll wait a bit longer to ensure timeout occurs require.Eventually(suite.T(), func() bool { return downloader.IsDownloadComplete() }, 500*time.Millisecond, 50*time.Millisecond, "Download should complete after timeout") // Verify state after timeout assert.True(suite.T(), startCallbackInvoked, "Start callback should be invoked") assert.False(suite.T(), successCallbackInvoked, "Success callback should NOT be invoked on timeout") assert.Equal(suite.T(), 0, downloader.GetTotalDownloadedArchivesCount(), "No archives should be downloaded on timeout") assert.Equal(suite.T(), 0, downloader.GetPendingArchivesCount(), "No archives should be pending after timeout") assert.True(suite.T(), downloader.IsDownloadComplete(), "Download should be complete") suite.T().Log("✅ Polling timeout test passed") suite.T().Log(" - TriggerDownload succeeded") suite.T().Log(" - Polling timed out after 100ms (fast test)") suite.T().Log(" - Success callback was NOT invoked") } func (suite *CodexArchiveDownloaderTestifySuite) TestWithExistingArchives() { // Test with some archives already downloaded (existing archive IDs) index := &protobuf.CodexWakuMessageArchiveIndex{ Archives: map[string]*protobuf.CodexWakuMessageArchiveIndexMetadata{ "archive-1": { Cid: "cid-1", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 1000, To: 2000}, }, "archive-2": { Cid: "cid-2", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 2000, To: 3000}, }, "archive-3": { Cid: "cid-3", Metadata: &protobuf.WakuMessageArchiveMetadata{From: 3000, To: 4000}, }, }, } communityID := "test-community" // Simulate that we already have archive-1 and archive-3 existingArchiveIDs := []string{"archive-1", "archive-3"} cancelChan := make(chan struct{}) // Only archive-2 should be downloaded (not in existingArchiveIDs) suite.mockClient.EXPECT(). TriggerDownloadWithContext(gomock.Any(), "cid-2"). Return(&communities.CodexManifest{CID: "cid-2"}, nil). Times(1) // Only one call expected // Only archive-2 should be polled gomock.InOrder( suite.mockClient.EXPECT().HasCid("cid-2").Return(false, nil), suite.mockClient.EXPECT().HasCid("cid-2").Return(true, nil), ) // Create downloader with existing archives logger := zap.NewNop() // No-op logger for tests downloader := communities.NewCodexArchiveDownloader(suite.mockClient, index, communityID, existingArchiveIDs, cancelChan, logger) downloader.SetPollingInterval(10 * time.Millisecond) // Track which archives are started and completed var startedArchives []string var completedArchives []string downloader.SetOnStartingArchiveDownload(func(hash string, from, to uint64) { startedArchives = append(startedArchives, hash) }) downloader.SetOnArchiveDownloaded(func(hash string, from, to uint64) { completedArchives = append(completedArchives, hash) }) // Verify initial state - should start with 2 existing archives counted assert.Equal(suite.T(), 3, downloader.GetTotalArchivesCount(), "Should have 3 total archives") assert.Equal(suite.T(), 2, downloader.GetTotalDownloadedArchivesCount(), "Should start with 2 existing archives") assert.False(suite.T(), downloader.IsDownloadComplete(), "Should not be complete initially") // Start download downloader.StartDownload() // Wait for download to complete require.Eventually(suite.T(), func() bool { return downloader.IsDownloadComplete() }, 5*time.Second, 100*time.Millisecond, "Download should complete within 5 seconds") // Verify final state assert.True(suite.T(), downloader.IsDownloadComplete(), "Download should be complete") assert.Equal(suite.T(), 3, downloader.GetTotalDownloadedArchivesCount(), "Should have 3 total downloaded (2 existing + 1 new)") // Verify only missing archive was processed assert.Len(suite.T(), startedArchives, 1, "Should have started exactly 1 archive download") assert.Contains(suite.T(), startedArchives, "archive-2", "Should have started archive-2") assert.NotContains(suite.T(), startedArchives, "archive-1", "Should NOT have started archive-1 (existing)") assert.NotContains(suite.T(), startedArchives, "archive-3", "Should NOT have started archive-3 (existing)") assert.Len(suite.T(), completedArchives, 1, "Should have completed exactly 1 archive download") assert.Contains(suite.T(), completedArchives, "archive-2", "Should have completed archive-2") suite.T().Log("✅ Existing archives test passed") suite.T().Logf(" - Started with %d existing archives", len(existingArchiveIDs)) suite.T().Logf(" - Downloaded %d missing archives", len(completedArchives)) suite.T().Logf(" - Final count: %d total", downloader.GetTotalDownloadedArchivesCount()) } // Run the test suite func TestCodexArchiveDownloaderSuite(t *testing.T) { suite.Run(t, new(CodexArchiveDownloaderTestifySuite)) }