diff --git a/README.md b/README.md index 154557b..446304a 100644 --- a/README.md +++ b/README.md @@ -103,4 +103,30 @@ To make sure that the test is actually run and not cached, use `count` option: ```bash go test -v -tags=integration ./communities -run Integration -timeout 15s -count 1 -``` \ No newline at end of file +``` + +### Regenerating artifacts + +Everything you need comes included in the repo. But if you decide to change things, +you will need to regenerate some artifacts. There are two: + +- the protobuf +- the mocks + +The easiest way is to regenerate all in one go: + +```bash +go generate ./... +``` + +If you just need to regenerate the mocks: + +```bash +go generate ./communities +``` + +If you just need to regenerate the protobuf: + +```bash +go generate ./protobuf +``` diff --git a/communities/codex_archive_downloader.go b/communities/codex_archive_downloader.go index b922fd4..ecf81a1 100644 --- a/communities/codex_archive_downloader.go +++ b/communities/codex_archive_downloader.go @@ -24,7 +24,7 @@ type CodexArchiveProcessor interface { // CodexArchiveDownloader handles downloading individual archive files from Codex storage type CodexArchiveDownloader struct { - codexClient *CodexClient + codexClient CodexClientInterface index *protobuf.CodexWakuMessageArchiveIndex communityID string existingArchiveIDs []string @@ -40,15 +40,15 @@ type CodexArchiveDownloader struct { // Download control downloadComplete bool - downloadError error cancelled bool + pollingInterval time.Duration // configurable polling interval for HasCid checks // Callback for signaling archive download completion onArchiveDownloaded func(hash string, from, to uint64) } // NewCodexArchiveDownloader creates a new archive downloader -func NewCodexArchiveDownloader(codexClient *CodexClient, index *protobuf.CodexWakuMessageArchiveIndex, communityID string, existingArchiveIDs []string, cancelChan chan struct{}) *CodexArchiveDownloader { +func NewCodexArchiveDownloader(codexClient CodexClientInterface, index *protobuf.CodexWakuMessageArchiveIndex, communityID string, existingArchiveIDs []string, cancelChan chan struct{}) *CodexArchiveDownloader { return &CodexArchiveDownloader{ codexClient: codexClient, index: index, @@ -59,9 +59,17 @@ func NewCodexArchiveDownloader(codexClient *CodexClient, index *protobuf.CodexWa totalDownloadedArchivesCount: len(existingArchiveIDs), archiveDownloadProgress: make(map[string]int64), archiveDownloadCancel: make(map[string]chan struct{}), + pollingInterval: 1 * time.Second, // Default production polling interval } } +// SetPollingInterval sets the polling interval for HasCid checks (useful for testing) +func (d *CodexArchiveDownloader) SetPollingInterval(interval time.Duration) { + d.mu.Lock() + defer d.mu.Unlock() + d.pollingInterval = interval +} + // SetOnArchiveDownloaded sets a callback function to be called when an archive is successfully downloaded func (d *CodexArchiveDownloader) SetOnArchiveDownloaded(callback func(hash string, from, to uint64)) { d.onArchiveDownloaded = callback @@ -106,13 +114,6 @@ func (d *CodexArchiveDownloader) IsDownloadComplete() bool { return d.downloadComplete } -// GetDownloadError returns any error that occurred during download -func (d *CodexArchiveDownloader) GetDownloadError() error { - d.mu.RLock() - defer d.mu.RUnlock() - return d.downloadError -} - // IsCancelled returns whether the download was cancelled func (d *CodexArchiveDownloader) IsCancelled() bool { d.mu.RLock() @@ -215,10 +216,10 @@ func (d *CodexArchiveDownloader) downloadAllArchives() { } d.mu.Unlock() - // poll every second until we confirm it's downloaded + // poll at configured interval until we confirm it's downloaded // or timeout after 30 seconds timeout := time.After(30 * time.Second) - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(d.pollingInterval) defer ticker.Stop() PollLoop: for { diff --git a/communities/codex_archive_downloader_test.go b/communities/codex_archive_downloader_test.go new file mode 100644 index 0000000..c68d679 --- /dev/null +++ b/communities/codex_archive_downloader_test.go @@ -0,0 +1,138 @@ +//go:build !disable_torrent +// +build !disable_torrent + +package communities_test + +import ( + "testing" + "time" + + "go-codex-client/communities" + "go-codex-client/protobuf" + + mock_communities "go-codex-client/communities/mock" + + "go.uber.org/mock/gomock" +) + +// Helper function to create a test index with a single archive +func createTestIndex() *protobuf.CodexWakuMessageArchiveIndex { + return &protobuf.CodexWakuMessageArchiveIndex{ + Archives: map[string]*protobuf.CodexWakuMessageArchiveIndexMetadata{ + "test-archive-hash-1": { + Cid: "test-cid-1", + Metadata: &protobuf.WakuMessageArchiveMetadata{ + From: 1000, + To: 2000, + }, + }, + }, + } +} + +func TestCodexArchiveDownloader_BasicSingleArchive(t *testing.T) { + // Create gomock controller + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Create test data + index := createTestIndex() + communityID := "test-community" + existingArchiveIDs := []string{} // No existing archives + cancelChan := make(chan struct{}) + + // Create mock client using gomock + mockClient := mock_communities.NewMockCodexClientInterface(ctrl) + + // Set up expectations + 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( + mockClient.EXPECT().HasCid("test-cid-1").Return(false, nil), + mockClient.EXPECT().HasCid("test-cid-1").Return(true, nil), + ) + + // Create downloader with mock client + downloader := communities.NewCodexArchiveDownloader(mockClient, index, communityID, existingArchiveIDs, cancelChan) + + // 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 + if downloader.GetTotalArchivesCount() != 1 { + t.Errorf("Expected 1 total archive, got %d", downloader.GetTotalArchivesCount()) + } + + if downloader.GetTotalDownloadedArchivesCount() != 0 { + t.Errorf("Expected 0 downloaded archives initially, got %d", downloader.GetTotalDownloadedArchivesCount()) + } + + if downloader.IsDownloadComplete() { + t.Error("Expected download to not be complete initially") + } + + // Start the download + downloader.StartDownload() + + // Wait for download to complete (with timeout) + timeout := time.After(5 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + +waitLoop: + for { + select { + case <-timeout: + t.Fatal("Timeout waiting for download to complete") + case <-ticker.C: + if downloader.IsDownloadComplete() { + break waitLoop + } + } + } + // Verify final state + if !downloader.IsDownloadComplete() { + t.Error("Expected download to be complete") + } + + if downloader.GetTotalDownloadedArchivesCount() != 1 { + t.Errorf("Expected 1 downloaded archive, got %d", downloader.GetTotalDownloadedArchivesCount()) + } + + // Verify callback was invoked + if !callbackInvoked { + t.Error("Expected callback to be invoked") + } + + if callbackHash != "test-archive-hash-1" { + t.Errorf("Expected callback hash 'test-archive-hash-1', got '%s'", callbackHash) + } + + if callbackFrom != 1000 { + t.Errorf("Expected callback from 1000, got %d", callbackFrom) + } + + if callbackTo != 2000 { + t.Errorf("Expected callback to 2000, got %d", callbackTo) + } + + t.Logf("✅ Basic single archive download test passed") + t.Logf(" - All mock expectations satisfied") + t.Logf(" - Callback invoked: %v", callbackInvoked) +} diff --git a/communities/codex_client_interface.go b/communities/codex_client_interface.go new file mode 100644 index 0000000..6798d03 --- /dev/null +++ b/communities/codex_client_interface.go @@ -0,0 +1,20 @@ +//go:build !disable_torrent +// +build !disable_torrent + +package communities + +import ( + "context" +) + +// Mock generation instruction above will create a mock in package `mock_communities` +// (folder `mock/`) so tests can import it as e.g. `go-codex-client/communities/mock` or +// with an alias like `mocks` to avoid import-cycle issues. +// +// CodexClientInterface defines the interface for CodexClient operations needed by the downloader +// +//go:generate mockgen -package=mock_communities -source=codex_client_interface.go -destination=mock/codex_client_interface.go +type CodexClientInterface interface { + TriggerDownloadWithContext(ctx context.Context, cid string) (*CodexManifest, error) + HasCid(cid string) (bool, error) +} diff --git a/communities/mock/codex_client_interface.go b/communities/mock/codex_client_interface.go new file mode 100644 index 0000000..dec90d4 --- /dev/null +++ b/communities/mock/codex_client_interface.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: codex_client_interface.go +// +// Generated by this command: +// +// mockgen -package=mock_communities -source=codex_client_interface.go -destination=mock/codex_client_interface.go +// + +// Package mock_communities is a generated GoMock package. +package mock_communities + +import ( + context "context" + communities "go-codex-client/communities" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockCodexClientInterface is a mock of CodexClientInterface interface. +type MockCodexClientInterface struct { + ctrl *gomock.Controller + recorder *MockCodexClientInterfaceMockRecorder + isgomock struct{} +} + +// MockCodexClientInterfaceMockRecorder is the mock recorder for MockCodexClientInterface. +type MockCodexClientInterfaceMockRecorder struct { + mock *MockCodexClientInterface +} + +// NewMockCodexClientInterface creates a new mock instance. +func NewMockCodexClientInterface(ctrl *gomock.Controller) *MockCodexClientInterface { + mock := &MockCodexClientInterface{ctrl: ctrl} + mock.recorder = &MockCodexClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCodexClientInterface) EXPECT() *MockCodexClientInterfaceMockRecorder { + return m.recorder +} + +// HasCid mocks base method. +func (m *MockCodexClientInterface) HasCid(cid string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasCid", cid) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// HasCid indicates an expected call of HasCid. +func (mr *MockCodexClientInterfaceMockRecorder) HasCid(cid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasCid", reflect.TypeOf((*MockCodexClientInterface)(nil).HasCid), cid) +} + +// TriggerDownloadWithContext mocks base method. +func (m *MockCodexClientInterface) TriggerDownloadWithContext(ctx context.Context, cid string) (*communities.CodexManifest, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TriggerDownloadWithContext", ctx, cid) + ret0, _ := ret[0].(*communities.CodexManifest) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// TriggerDownloadWithContext indicates an expected call of TriggerDownloadWithContext. +func (mr *MockCodexClientInterfaceMockRecorder) TriggerDownloadWithContext(ctx, cid any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TriggerDownloadWithContext", reflect.TypeOf((*MockCodexClientInterface)(nil).TriggerDownloadWithContext), ctx, cid) +} diff --git a/go.mod b/go.mod index 0b12e00..ed40209 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module go-codex-client -go 1.21 +go 1.23.0 require google.golang.org/protobuf v1.34.1 + +require go.uber.org/mock v0.6.0 // indirect diff --git a/go.sum b/go.sum index 33bede0..b693b12 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=