Adds basic test for archive downloader with the mocking setup

This commit is contained in:
Marcin Czenko 2025-10-22 21:18:31 +02:00
parent 3e135f3c53
commit 3db63a3aab
No known key found for this signature in database
GPG Key ID: A0449219BDBA98AE
7 changed files with 275 additions and 14 deletions

View File

@ -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
```
```
### 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
```

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

4
go.mod
View File

@ -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

2
go.sum
View File

@ -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=