254 lines
9.2 KiB
Go
254 lines
9.2 KiB
Go
package rule
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
. "github.com/onsi/gomega"
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
"github.com/glidea/zenfeed/pkg/component"
|
|
"github.com/glidea/zenfeed/pkg/model"
|
|
"github.com/glidea/zenfeed/pkg/storage/feed"
|
|
"github.com/glidea/zenfeed/pkg/storage/feed/block"
|
|
"github.com/glidea/zenfeed/pkg/test"
|
|
)
|
|
|
|
func TestPeriodicExecute(t *testing.T) {
|
|
RegisterTestingT(t)
|
|
|
|
// --- Test types ---
|
|
type givenDetail struct {
|
|
config *Config
|
|
feedStorageMock func(m *mock.Mock) // Function to set expectations
|
|
}
|
|
type whenDetail struct {
|
|
now time.Time
|
|
}
|
|
type thenExpected struct {
|
|
queryCalled bool
|
|
queryOpts *block.QueryOptions // Only check relevant fields like start/end
|
|
sentToOut *Result
|
|
err error // Expected error (can be wrapped)
|
|
isErr bool
|
|
}
|
|
|
|
// --- Test cases ---
|
|
mockFeeds := []*block.FeedVO{
|
|
{Feed: &model.Feed{ID: 1, Labels: model.Labels{{Key: "content_hash", Value: "a"}}}},
|
|
{Feed: &model.Feed{ID: 2, Labels: model.Labels{{Key: "content_hash", Value: "b"}}}},
|
|
}
|
|
baseConfig := &Config{
|
|
Name: "test-periodic",
|
|
EveryDay: "09:00~18:00", // Will be parsed in Validate
|
|
Threshold: 0.7,
|
|
Query: "test query",
|
|
}
|
|
// Manually parse time for expected values
|
|
startTime, _ := time.ParseInLocation(timeFmt, "09:00", time.Local)
|
|
endTime, _ := time.ParseInLocation(timeFmt, "18:00", time.Local)
|
|
|
|
crossDayConfig := &Config{
|
|
Name: "test-crossday",
|
|
EveryDay: "-22:00~06:00", // Will be parsed in Validate
|
|
Threshold: 0.7,
|
|
Query: "test query",
|
|
}
|
|
// Manually parse time for expected values
|
|
crossStartTime, _ := time.ParseInLocation(timeFmt, "22:00", time.Local)
|
|
crossEndTime, _ := time.ParseInLocation(timeFmt, "06:00", time.Local)
|
|
|
|
tests := []test.Case[givenDetail, whenDetail, thenExpected]{
|
|
{
|
|
Scenario: "Non-crossDay, feeds found, should query and notify",
|
|
Given: "a non-crossDay config and FeedStorage returns feeds",
|
|
When: "execute is called within the configured day",
|
|
Then: "FeedStorage should be queried with the correct daily time range and result sent to Out",
|
|
GivenDetail: givenDetail{
|
|
config: baseConfig,
|
|
feedStorageMock: func(m *mock.Mock) {
|
|
m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
|
|
Return(mockFeeds, nil)
|
|
},
|
|
},
|
|
WhenDetail: whenDetail{
|
|
now: time.Date(2024, 1, 15, 10, 0, 0, 0, time.Local), // 10:00 AM
|
|
},
|
|
ThenExpected: thenExpected{
|
|
queryCalled: true,
|
|
queryOpts: &block.QueryOptions{
|
|
Start: time.Date(2024, 1, 15, startTime.Hour(), startTime.Minute(), 0, 0, time.Local),
|
|
End: time.Date(2024, 1, 15, endTime.Hour(), endTime.Minute(), 0, 0, time.Local),
|
|
Query: baseConfig.Query,
|
|
Limit: 500,
|
|
},
|
|
sentToOut: &Result{
|
|
Rule: baseConfig.Name,
|
|
Time: time.Date(2024, 1, 15, startTime.Hour(), startTime.Minute(), 0, 0, time.Local),
|
|
Feeds: mockFeeds,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Scenario: "CrossDay, feeds found, should query and notify",
|
|
Given: "a crossDay config and FeedStorage returns feeds",
|
|
When: "execute is called within the configured day",
|
|
Then: "FeedStorage should be queried with the correct cross-day time range and result sent to Out",
|
|
GivenDetail: givenDetail{
|
|
config: crossDayConfig,
|
|
feedStorageMock: func(m *mock.Mock) {
|
|
m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
|
|
Return(mockFeeds, nil)
|
|
},
|
|
},
|
|
WhenDetail: whenDetail{
|
|
now: time.Date(2024, 1, 15, 03, 0, 0, 0, time.Local), // 03:00 AM
|
|
},
|
|
ThenExpected: thenExpected{
|
|
queryCalled: true,
|
|
queryOpts: &block.QueryOptions{
|
|
Start: time.Date(2024, 1, 14, crossStartTime.Hour(), crossStartTime.Minute(), 0, 0, time.Local),
|
|
End: time.Date(2024, 1, 15, crossEndTime.Hour(), crossEndTime.Minute(), 0, 0, time.Local),
|
|
Query: crossDayConfig.Query,
|
|
Limit: 500,
|
|
},
|
|
sentToOut: &Result{
|
|
Rule: crossDayConfig.Name,
|
|
Time: time.Date(2024, 1, 14, crossStartTime.Hour(), crossStartTime.Minute(), 0, 0, time.Local),
|
|
Feeds: mockFeeds,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Scenario: "Non-crossDay, no feeds found, should query but not notify",
|
|
Given: "a non-crossDay config and FeedStorage returns no feeds",
|
|
When: "execute is called",
|
|
Then: "FeedStorage should be queried but nothing sent to Out",
|
|
GivenDetail: givenDetail{
|
|
config: baseConfig,
|
|
feedStorageMock: func(m *mock.Mock) {
|
|
m.On("Query", mock.Anything, mock.AnythingOfType("block.QueryOptions")).
|
|
Return([]*block.FeedVO{}, nil) // Empty result
|
|
},
|
|
},
|
|
WhenDetail: whenDetail{
|
|
now: time.Date(2024, 1, 15, 11, 0, 0, 0, time.Local), // 11:00 AM
|
|
},
|
|
ThenExpected: thenExpected{
|
|
queryCalled: true,
|
|
queryOpts: &block.QueryOptions{
|
|
Start: time.Date(2024, 1, 15, startTime.Hour(), startTime.Minute(), 0, 0, time.Local),
|
|
End: time.Date(2024, 1, 15, endTime.Hour(), endTime.Minute(), 0, 0, time.Local),
|
|
Query: baseConfig.Query,
|
|
Limit: 500,
|
|
},
|
|
sentToOut: nil,
|
|
},
|
|
},
|
|
}
|
|
|
|
// --- Run tests ---
|
|
for _, tt := range tests {
|
|
t.Run(tt.Scenario, func(t *testing.T) {
|
|
// --- Given ---
|
|
configCopy := *tt.GivenDetail.config
|
|
err := configCopy.Validate()
|
|
Expect(err).NotTo(HaveOccurred(), "Config validation failed in test setup")
|
|
|
|
outCh := make(chan *Result, 1)
|
|
var capturedOpts block.QueryOptions
|
|
var mockStorageInstance *mock.Mock
|
|
|
|
// Create mock factory using feed.NewFactory and capture the mock instance
|
|
mockOption := component.MockOption(func(m *mock.Mock) {
|
|
mockStorageInstance = m // Capture the mock instance
|
|
// Setup mock expectation for FeedStorage.Query, including option capture
|
|
if tt.GivenDetail.feedStorageMock != nil {
|
|
tt.GivenDetail.feedStorageMock(m)
|
|
// Enhance the mock setup to capture arguments if the mock function exists
|
|
// Find the Query expectation and add argument capture logic
|
|
for _, call := range m.ExpectedCalls {
|
|
if call.Method == "Query" {
|
|
// Replace the generic matcher for options with one that captures
|
|
for i, arg := range call.Arguments {
|
|
if _, ok := arg.(mock.AnythingOfTypeArgument); ok && i == 1 { // Assuming options is the second argument (index 1)
|
|
call.Arguments[i] = mock.MatchedBy(func(opts block.QueryOptions) bool {
|
|
capturedOpts = opts // Capture the options
|
|
return true
|
|
})
|
|
break
|
|
}
|
|
}
|
|
break // Assume only one Query expectation per test case here
|
|
}
|
|
}
|
|
}
|
|
})
|
|
mockFeedFactory := feed.NewFactory(mockOption)
|
|
mockFeedStorage, factoryErr := mockFeedFactory.New(component.Global, nil, feed.Dependencies{}) // Use factory to create mock
|
|
Expect(factoryErr).NotTo(HaveOccurred())
|
|
|
|
dependencies := Dependencies{
|
|
FeedStorage: mockFeedStorage, // Use the created mock storage
|
|
Out: outCh,
|
|
}
|
|
|
|
r := &periodic{
|
|
Base: component.New(&component.BaseConfig[Config, Dependencies]{
|
|
Name: "PeriodicRuler",
|
|
Instance: "test-instance",
|
|
Config: &configCopy,
|
|
Dependencies: dependencies,
|
|
}),
|
|
}
|
|
|
|
// --- When ---
|
|
err = r.execute(context.Background(), tt.WhenDetail.now)
|
|
|
|
// --- Then ---
|
|
if tt.ThenExpected.isErr {
|
|
Expect(err).To(HaveOccurred())
|
|
// Use MatchError for potentially wrapped errors, providing a more precise check
|
|
Expect(err).To(MatchError(tt.ThenExpected.err))
|
|
Expect(len(outCh)).To(Equal(0))
|
|
} else {
|
|
Expect(err).NotTo(HaveOccurred())
|
|
if tt.ThenExpected.sentToOut != nil {
|
|
Expect(len(outCh)).To(Equal(1))
|
|
receivedResult := <-outCh
|
|
Expect(receivedResult.Rule).To(Equal(tt.ThenExpected.sentToOut.Rule))
|
|
Expect(receivedResult.Time.Unix()).To(Equal(tt.ThenExpected.sentToOut.Time.Unix()))
|
|
Expect(receivedResult.Feeds).To(Equal(tt.ThenExpected.sentToOut.Feeds))
|
|
} else {
|
|
Expect(len(outCh)).To(Equal(0))
|
|
}
|
|
}
|
|
|
|
// Verify FeedStorage.Query call and options using the captured mock instance
|
|
if mockStorageInstance != nil { // Ensure mock instance was captured
|
|
if tt.ThenExpected.queryCalled {
|
|
// Assert the expectation set up in feedStorageMockFn was met
|
|
mockStorageInstance.AssertCalled(t, "Query", mock.Anything, mock.AnythingOfType("block.QueryOptions"))
|
|
// Assert specific fields of the captured options
|
|
Expect(capturedOpts.Start.Unix()).To(Equal(tt.ThenExpected.queryOpts.Start.Unix()), "Start time mismatch")
|
|
Expect(capturedOpts.End.Unix()).To(Equal(tt.ThenExpected.queryOpts.End.Unix()), "End time mismatch")
|
|
Expect(capturedOpts.Query).To(Equal(tt.ThenExpected.queryOpts.Query), "Query string mismatch")
|
|
Expect(capturedOpts.Threshold).To(Equal(configCopy.Threshold), "Threshold mismatch")
|
|
Expect(capturedOpts.LabelFilters).To(Equal(configCopy.LabelFilters), "LabelFilters mismatch")
|
|
Expect(capturedOpts.Limit).To(Equal(tt.ThenExpected.queryOpts.Limit), "Limit mismatch")
|
|
} else {
|
|
mockStorageInstance.AssertNotCalled(t, "Query", mock.Anything, mock.Anything)
|
|
}
|
|
// Optionally, assert all expectations are met
|
|
// mockStorageInstance.AssertExpectations(t) // Uncomment if you want strict expectation matching
|
|
} else if tt.ThenExpected.queryCalled {
|
|
// Fail if query was expected but mock instance wasn't captured (indicates setup issue)
|
|
t.Fatal("Expected query call but mock instance was not captured")
|
|
}
|
|
|
|
close(outCh)
|
|
})
|
|
}
|
|
}
|