feat: improve python actions (#1083)

This commit is contained in:
Jonas Plum
2024-07-21 02:56:43 +02:00
committed by GitHub
parent 81bfbb2072
commit 91429effe2
55 changed files with 1143 additions and 585 deletions

View File

@@ -6,18 +6,16 @@ import (
)
func TestReactionsCollection(t *testing.T) {
baseApp, adminToken, analystToken, baseAppCleanup := BaseApp(t)
defer baseAppCleanup()
t.Parallel()
testSets := []authMatrixText{
testSets := []catalystTest{
{
baseTest: BaseTest{
Name: "ListReactions",
Method: http.MethodGet,
URL: "/api/collections/reactions/records",
TestAppFactory: AppFactory(baseApp),
Name: "ListReactions",
Method: http.MethodGet,
URL: "/api/collections/reactions/records",
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
@@ -29,7 +27,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"totalItems":3`,
@@ -42,7 +40,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"totalItems":3`,
@@ -68,9 +66,8 @@ func TestReactionsCollection(t *testing.T) {
"action": "python",
"actiondata": map[string]any{"script": "print('Hello, World!')"},
}),
TestAppFactory: AppFactory(baseApp),
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusBadRequest,
@@ -80,7 +77,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"name":"test"`,
@@ -97,7 +94,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"name":"test"`,
@@ -120,9 +117,8 @@ func TestReactionsCollection(t *testing.T) {
Method: http.MethodGet,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/reactions/records/r_reaction",
TestAppFactory: AppFactory(baseApp),
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
@@ -132,7 +128,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
@@ -141,7 +137,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
@@ -157,9 +153,8 @@ func TestReactionsCollection(t *testing.T) {
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/reactions/records/r_reaction",
Body: s(map[string]any{"name": "update"}),
TestAppFactory: AppFactory(baseApp),
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
@@ -169,7 +164,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
@@ -184,7 +179,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
@@ -201,12 +196,11 @@ func TestReactionsCollection(t *testing.T) {
},
{
baseTest: BaseTest{
Name: "DeleteReaction",
Method: http.MethodDelete,
URL: "/api/collections/reactions/records/r_reaction",
TestAppFactory: AppFactory(baseApp),
Name: "DeleteReaction",
Method: http.MethodDelete,
URL: "/api/collections/reactions/records/r_reaction",
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
@@ -216,7 +210,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
@@ -227,7 +221,7 @@ func TestReactionsCollection(t *testing.T) {
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
@@ -241,9 +235,14 @@ func TestReactionsCollection(t *testing.T) {
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
for _, authBasedExpectation := range testSet.authBasedExpectations {
scenario := mergeScenario(testSet.baseTest, authBasedExpectation)
scenario.Test(t)
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}

36
testing/counter.go Normal file
View File

@@ -0,0 +1,36 @@
package testing
import "sync"
type Counter struct {
mux sync.Mutex
counts map[string]int
}
func NewCounter() *Counter {
return &Counter{
counts: make(map[string]int),
}
}
func (c *Counter) Increment(name string) {
c.mux.Lock()
defer c.mux.Unlock()
if _, ok := c.counts[name]; !ok {
c.counts[name] = 0
}
c.counts[name]++
}
func (c *Counter) Count(name string) int {
c.mux.Lock()
defer c.mux.Unlock()
if _, ok := c.counts[name]; !ok {
return 0
}
return c.counts[name]
}

41
testing/counter_test.go Normal file
View File

@@ -0,0 +1,41 @@
package testing
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCounter(t *testing.T) {
t.Parallel()
type args struct {
name string
repeat int
}
tests := []struct {
name string
args args
want int
}{
{
name: "Test Counter",
args: args{name: "test", repeat: 5},
want: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := NewCounter()
for range tt.args.repeat {
c.Increment(tt.args.name)
}
assert.Equal(t, tt.want, c.Count(tt.args.name))
})
}
}

37
testing/http.go Normal file
View File

@@ -0,0 +1,37 @@
package testing
import (
"context"
"errors"
"net/http"
"time"
)
func WaitForStatus(url string, status int, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
start := time.Now()
for {
if time.Since(start) > timeout {
return errors.New("timeout")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err == nil && resp.StatusCode == status {
resp.Body.Close()
break
}
time.Sleep(100 * time.Millisecond)
}
return nil
}

View File

@@ -3,93 +3,80 @@ package testing
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestWebhookReactions(t *testing.T) {
baseApp, adminToken, analystToken, baseAppCleanup := BaseApp(t)
defer baseAppCleanup()
t.Parallel()
server := NewRecordingServer()
go http.ListenAndServe("127.0.0.1:12345", server) //nolint:gosec,errcheck
testSets := []authMatrixText{
if err := WaitForStatus("http://127.0.0.1:12345/health", http.StatusOK, 5*time.Second); err != nil {
t.Fatal(err)
}
testSets := []catalystTest{
{
baseTest: BaseTest{
Name: "TriggerWebhookReaction",
Method: http.MethodGet,
RequestHeaders: map[string]string{"Authorization": "Bearer 1234567890"},
URL: "/reaction/test",
TestAppFactory: AppFactory(baseApp),
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`Hello, World!`},
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`Hello, World!`},
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`Hello, World!`},
},
},
},
{
baseTest: BaseTest{
Name: "TriggerWebhookReaction2",
Method: http.MethodGet,
URL: "/reaction/test2",
TestAppFactory: AppFactory(baseApp),
Name: "TriggerWebhookReaction2",
Method: http.MethodGet,
URL: "/reaction/test2",
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"test":true`},
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"test":true`},
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"test":true`},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
for _, authBasedExpectation := range testSet.authBasedExpectations {
scenario := mergeScenario(testSet.baseTest, authBasedExpectation)
scenario.Test(t)
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}
func TestHookReactions(t *testing.T) {
baseApp, _, analystToken, baseAppCleanup := BaseApp(t)
defer baseAppCleanup()
t.Parallel()
server := NewRecordingServer()
go http.ListenAndServe("127.0.0.1:12346", server) //nolint:gosec,errcheck
testSets := []authMatrixText{
if err := WaitForStatus("http://127.0.0.1:12346/health", http.StatusOK, 5*time.Second); err != nil {
t.Fatal(err)
}
testSets := []catalystTest{
{
baseTest: BaseTest{
Name: "TriggerHookReaction",
@@ -99,9 +86,8 @@ func TestHookReactions(t *testing.T) {
Body: s(map[string]any{
"name": "test",
}),
TestAppFactory: AppFactory(baseApp),
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
// {
// Name: "Unauthorized",
// ExpectedStatus: http.StatusOK,
@@ -109,7 +95,7 @@ func TestHookReactions(t *testing.T) {
// },
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"collectionName":"tickets"`,
@@ -133,12 +119,17 @@ func TestHookReactions(t *testing.T) {
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
for _, authBasedExpectation := range testSet.authBasedExpectations {
scenario := mergeScenario(testSet.baseTest, authBasedExpectation)
scenario.Test(t)
}
t.Parallel()
require.NotEmpty(t, server.Entries)
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
require.NotEmpty(t, server.Entries)
})
}
})
}
}

View File

@@ -1,19 +1,38 @@
package testing
import "net/http"
import (
"net/http"
"github.com/labstack/echo/v5"
)
type RecordingServer struct {
server *echo.Echo
Entries []string
}
func NewRecordingServer() *RecordingServer {
return &RecordingServer{}
e := echo.New()
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
"status": "ok",
})
})
e.Any("/*", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
"test": true,
})
})
return &RecordingServer{
server: e,
}
}
func (s *RecordingServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.Entries = append(s.Entries, r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"test":true}`)) //nolint:errcheck
s.server.ServeHTTP(w, r)
}

View File

@@ -6,63 +6,60 @@ import (
)
func Test_Routes(t *testing.T) {
baseApp, adminToken, analystToken, baseAppCleanup := BaseApp(t)
defer baseAppCleanup()
t.Parallel()
testSets := []authMatrixText{
testSets := []catalystTest{
{
baseTest: BaseTest{
Name: "Root",
Method: http.MethodGet,
URL: "/",
TestAppFactory: AppFactory(baseApp),
Name: "Root",
Method: http.MethodGet,
URL: "/",
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusFound,
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusFound,
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusFound,
},
},
},
{
baseTest: BaseTest{
Name: "Config",
Method: http.MethodGet,
URL: "/api/config",
TestAppFactory: AppFactory(baseApp),
Name: "Config",
Method: http.MethodGet,
URL: "/api/config",
},
authBasedExpectations: []AuthBasedExpectation{
userTests: []UserTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":null`,
`"flags":[]`,
},
},
{
Name: "Analyst",
RequestHeaders: map[string]string{"Authorization": analystToken},
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":null`,
`"flags":[]`,
},
},
{
Name: "Admin",
RequestHeaders: map[string]string{"Authorization": adminToken},
Admin: adminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":null`,
`"flags":[]`,
},
},
},
@@ -70,9 +67,14 @@ func Test_Routes(t *testing.T) {
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
for _, authBasedExpectation := range testSet.authBasedExpectations {
scenario := mergeScenario(testSet.baseTest, authBasedExpectation)
scenario.Test(t)
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}

160
testing/testapp.go Normal file
View File

@@ -0,0 +1,160 @@
package testing
import (
"fmt"
"os"
"testing"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tokens"
"github.com/SecurityBrewery/catalyst/app"
"github.com/SecurityBrewery/catalyst/migrations"
)
func App(t *testing.T) (*pocketbase.PocketBase, *Counter, func()) {
t.Helper()
temp, err := os.MkdirTemp("", "catalyst_test_data")
if err != nil {
t.Fatal(err)
}
baseApp, err := app.App(temp, true)
if err != nil {
t.Fatal(err)
}
baseApp.Settings().Logs.MaxDays = 0
defaultTestData(t, baseApp)
counter := countEvents(baseApp)
return baseApp, counter, func() { _ = os.RemoveAll(temp) }
}
func generateAdminToken(t *testing.T, baseApp core.App, email string) (string, error) {
t.Helper()
admin, err := baseApp.Dao().FindAdminByEmail(email)
if err != nil {
return "", fmt.Errorf("failed to find admin: %w", err)
}
return tokens.NewAdminAuthToken(baseApp, admin)
}
func generateRecordToken(t *testing.T, baseApp core.App, email string) (string, error) {
t.Helper()
record, err := baseApp.Dao().FindAuthRecordByEmail(migrations.UserCollectionName, email)
if err != nil {
return "", fmt.Errorf("failed to find record: %w", err)
}
return tokens.NewRecordAuthToken(baseApp, record)
}
func countEvents(t *pocketbase.PocketBase) *Counter {
c := NewCounter()
t.OnBeforeApiError().Add(count[*core.ApiErrorEvent](c, "OnBeforeApiError"))
t.OnBeforeApiError().Add(count[*core.ApiErrorEvent](c, "OnBeforeApiError"))
t.OnAfterApiError().Add(count[*core.ApiErrorEvent](c, "OnAfterApiError"))
t.OnModelBeforeCreate().Add(count[*core.ModelEvent](c, "OnModelBeforeCreate"))
t.OnModelAfterCreate().Add(count[*core.ModelEvent](c, "OnModelAfterCreate"))
t.OnModelBeforeUpdate().Add(count[*core.ModelEvent](c, "OnModelBeforeUpdate"))
t.OnModelAfterUpdate().Add(count[*core.ModelEvent](c, "OnModelAfterUpdate"))
t.OnModelBeforeDelete().Add(count[*core.ModelEvent](c, "OnModelBeforeDelete"))
t.OnModelAfterDelete().Add(count[*core.ModelEvent](c, "OnModelAfterDelete"))
t.OnRecordsListRequest().Add(count[*core.RecordsListEvent](c, "OnRecordsListRequest"))
t.OnRecordViewRequest().Add(count[*core.RecordViewEvent](c, "OnRecordViewRequest"))
t.OnRecordBeforeCreateRequest().Add(count[*core.RecordCreateEvent](c, "OnRecordBeforeCreateRequest"))
t.OnRecordAfterCreateRequest().Add(count[*core.RecordCreateEvent](c, "OnRecordAfterCreateRequest"))
t.OnRecordBeforeUpdateRequest().Add(count[*core.RecordUpdateEvent](c, "OnRecordBeforeUpdateRequest"))
t.OnRecordAfterUpdateRequest().Add(count[*core.RecordUpdateEvent](c, "OnRecordAfterUpdateRequest"))
t.OnRecordBeforeDeleteRequest().Add(count[*core.RecordDeleteEvent](c, "OnRecordBeforeDeleteRequest"))
t.OnRecordAfterDeleteRequest().Add(count[*core.RecordDeleteEvent](c, "OnRecordAfterDeleteRequest"))
t.OnRecordAuthRequest().Add(count[*core.RecordAuthEvent](c, "OnRecordAuthRequest"))
t.OnRecordBeforeAuthWithPasswordRequest().Add(count[*core.RecordAuthWithPasswordEvent](c, "OnRecordBeforeAuthWithPasswordRequest"))
t.OnRecordAfterAuthWithPasswordRequest().Add(count[*core.RecordAuthWithPasswordEvent](c, "OnRecordAfterAuthWithPasswordRequest"))
t.OnRecordBeforeAuthWithOAuth2Request().Add(count[*core.RecordAuthWithOAuth2Event](c, "OnRecordBeforeAuthWithOAuth2Request"))
t.OnRecordAfterAuthWithOAuth2Request().Add(count[*core.RecordAuthWithOAuth2Event](c, "OnRecordAfterAuthWithOAuth2Request"))
t.OnRecordBeforeAuthRefreshRequest().Add(count[*core.RecordAuthRefreshEvent](c, "OnRecordBeforeAuthRefreshRequest"))
t.OnRecordAfterAuthRefreshRequest().Add(count[*core.RecordAuthRefreshEvent](c, "OnRecordAfterAuthRefreshRequest"))
t.OnRecordBeforeRequestPasswordResetRequest().Add(count[*core.RecordRequestPasswordResetEvent](c, "OnRecordBeforeRequestPasswordResetRequest"))
t.OnRecordAfterRequestPasswordResetRequest().Add(count[*core.RecordRequestPasswordResetEvent](c, "OnRecordAfterRequestPasswordResetRequest"))
t.OnRecordBeforeConfirmPasswordResetRequest().Add(count[*core.RecordConfirmPasswordResetEvent](c, "OnRecordBeforeConfirmPasswordResetRequest"))
t.OnRecordAfterConfirmPasswordResetRequest().Add(count[*core.RecordConfirmPasswordResetEvent](c, "OnRecordAfterConfirmPasswordResetRequest"))
t.OnRecordBeforeRequestVerificationRequest().Add(count[*core.RecordRequestVerificationEvent](c, "OnRecordBeforeRequestVerificationRequest"))
t.OnRecordAfterRequestVerificationRequest().Add(count[*core.RecordRequestVerificationEvent](c, "OnRecordAfterRequestVerificationRequest"))
t.OnRecordBeforeConfirmVerificationRequest().Add(count[*core.RecordConfirmVerificationEvent](c, "OnRecordBeforeConfirmVerificationRequest"))
t.OnRecordAfterConfirmVerificationRequest().Add(count[*core.RecordConfirmVerificationEvent](c, "OnRecordAfterConfirmVerificationRequest"))
t.OnRecordBeforeRequestEmailChangeRequest().Add(count[*core.RecordRequestEmailChangeEvent](c, "OnRecordBeforeRequestEmailChangeRequest"))
t.OnRecordAfterRequestEmailChangeRequest().Add(count[*core.RecordRequestEmailChangeEvent](c, "OnRecordAfterRequestEmailChangeRequest"))
t.OnRecordBeforeConfirmEmailChangeRequest().Add(count[*core.RecordConfirmEmailChangeEvent](c, "OnRecordBeforeConfirmEmailChangeRequest"))
t.OnRecordAfterConfirmEmailChangeRequest().Add(count[*core.RecordConfirmEmailChangeEvent](c, "OnRecordAfterConfirmEmailChangeRequest"))
t.OnRecordListExternalAuthsRequest().Add(count[*core.RecordListExternalAuthsEvent](c, "OnRecordListExternalAuthsRequest"))
t.OnRecordBeforeUnlinkExternalAuthRequest().Add(count[*core.RecordUnlinkExternalAuthEvent](c, "OnRecordBeforeUnlinkExternalAuthRequest"))
t.OnRecordAfterUnlinkExternalAuthRequest().Add(count[*core.RecordUnlinkExternalAuthEvent](c, "OnRecordAfterUnlinkExternalAuthRequest"))
t.OnMailerBeforeAdminResetPasswordSend().Add(count[*core.MailerAdminEvent](c, "OnMailerBeforeAdminResetPasswordSend"))
t.OnMailerAfterAdminResetPasswordSend().Add(count[*core.MailerAdminEvent](c, "OnMailerAfterAdminResetPasswordSend"))
t.OnMailerBeforeRecordResetPasswordSend().Add(count[*core.MailerRecordEvent](c, "OnMailerBeforeRecordResetPasswordSend"))
t.OnMailerAfterRecordResetPasswordSend().Add(count[*core.MailerRecordEvent](c, "OnMailerAfterRecordResetPasswordSend"))
t.OnMailerBeforeRecordVerificationSend().Add(count[*core.MailerRecordEvent](c, "OnMailerBeforeRecordVerificationSend"))
t.OnMailerAfterRecordVerificationSend().Add(count[*core.MailerRecordEvent](c, "OnMailerAfterRecordVerificationSend"))
t.OnMailerBeforeRecordChangeEmailSend().Add(count[*core.MailerRecordEvent](c, "OnMailerBeforeRecordChangeEmailSend"))
t.OnMailerAfterRecordChangeEmailSend().Add(count[*core.MailerRecordEvent](c, "OnMailerAfterRecordChangeEmailSend"))
t.OnRealtimeConnectRequest().Add(count[*core.RealtimeConnectEvent](c, "OnRealtimeConnectRequest"))
t.OnRealtimeDisconnectRequest().Add(count[*core.RealtimeDisconnectEvent](c, "OnRealtimeDisconnectRequest"))
t.OnRealtimeBeforeMessageSend().Add(count[*core.RealtimeMessageEvent](c, "OnRealtimeBeforeMessageSend"))
t.OnRealtimeAfterMessageSend().Add(count[*core.RealtimeMessageEvent](c, "OnRealtimeAfterMessageSend"))
t.OnRealtimeBeforeSubscribeRequest().Add(count[*core.RealtimeSubscribeEvent](c, "OnRealtimeBeforeSubscribeRequest"))
t.OnRealtimeAfterSubscribeRequest().Add(count[*core.RealtimeSubscribeEvent](c, "OnRealtimeAfterSubscribeRequest"))
t.OnSettingsListRequest().Add(count[*core.SettingsListEvent](c, "OnSettingsListRequest"))
t.OnSettingsBeforeUpdateRequest().Add(count[*core.SettingsUpdateEvent](c, "OnSettingsBeforeUpdateRequest"))
t.OnSettingsAfterUpdateRequest().Add(count[*core.SettingsUpdateEvent](c, "OnSettingsAfterUpdateRequest"))
t.OnCollectionsListRequest().Add(count[*core.CollectionsListEvent](c, "OnCollectionsListRequest"))
t.OnCollectionViewRequest().Add(count[*core.CollectionViewEvent](c, "OnCollectionViewRequest"))
t.OnCollectionBeforeCreateRequest().Add(count[*core.CollectionCreateEvent](c, "OnCollectionBeforeCreateRequest"))
t.OnCollectionAfterCreateRequest().Add(count[*core.CollectionCreateEvent](c, "OnCollectionAfterCreateRequest"))
t.OnCollectionBeforeUpdateRequest().Add(count[*core.CollectionUpdateEvent](c, "OnCollectionBeforeUpdateRequest"))
t.OnCollectionAfterUpdateRequest().Add(count[*core.CollectionUpdateEvent](c, "OnCollectionAfterUpdateRequest"))
t.OnCollectionBeforeDeleteRequest().Add(count[*core.CollectionDeleteEvent](c, "OnCollectionBeforeDeleteRequest"))
t.OnCollectionAfterDeleteRequest().Add(count[*core.CollectionDeleteEvent](c, "OnCollectionAfterDeleteRequest"))
t.OnCollectionsBeforeImportRequest().Add(count[*core.CollectionsImportEvent](c, "OnCollectionsBeforeImportRequest"))
t.OnCollectionsAfterImportRequest().Add(count[*core.CollectionsImportEvent](c, "OnCollectionsAfterImportRequest"))
t.OnAdminsListRequest().Add(count[*core.AdminsListEvent](c, "OnAdminsListRequest"))
t.OnAdminViewRequest().Add(count[*core.AdminViewEvent](c, "OnAdminViewRequest"))
t.OnAdminBeforeCreateRequest().Add(count[*core.AdminCreateEvent](c, "OnAdminBeforeCreateRequest"))
t.OnAdminAfterCreateRequest().Add(count[*core.AdminCreateEvent](c, "OnAdminAfterCreateRequest"))
t.OnAdminBeforeUpdateRequest().Add(count[*core.AdminUpdateEvent](c, "OnAdminBeforeUpdateRequest"))
t.OnAdminAfterUpdateRequest().Add(count[*core.AdminUpdateEvent](c, "OnAdminAfterUpdateRequest"))
t.OnAdminBeforeDeleteRequest().Add(count[*core.AdminDeleteEvent](c, "OnAdminBeforeDeleteRequest"))
t.OnAdminAfterDeleteRequest().Add(count[*core.AdminDeleteEvent](c, "OnAdminAfterDeleteRequest"))
t.OnAdminAuthRequest().Add(count[*core.AdminAuthEvent](c, "OnAdminAuthRequest"))
t.OnAdminBeforeAuthWithPasswordRequest().Add(count[*core.AdminAuthWithPasswordEvent](c, "OnAdminBeforeAuthWithPasswordRequest"))
t.OnAdminAfterAuthWithPasswordRequest().Add(count[*core.AdminAuthWithPasswordEvent](c, "OnAdminAfterAuthWithPasswordRequest"))
t.OnAdminBeforeAuthRefreshRequest().Add(count[*core.AdminAuthRefreshEvent](c, "OnAdminBeforeAuthRefreshRequest"))
t.OnAdminAfterAuthRefreshRequest().Add(count[*core.AdminAuthRefreshEvent](c, "OnAdminAfterAuthRefreshRequest"))
t.OnAdminBeforeRequestPasswordResetRequest().Add(count[*core.AdminRequestPasswordResetEvent](c, "OnAdminBeforeRequestPasswordResetRequest"))
t.OnAdminAfterRequestPasswordResetRequest().Add(count[*core.AdminRequestPasswordResetEvent](c, "OnAdminAfterRequestPasswordResetRequest"))
t.OnAdminBeforeConfirmPasswordResetRequest().Add(count[*core.AdminConfirmPasswordResetEvent](c, "OnAdminBeforeConfirmPasswordResetRequest"))
t.OnAdminAfterConfirmPasswordResetRequest().Add(count[*core.AdminConfirmPasswordResetEvent](c, "OnAdminAfterConfirmPasswordResetRequest"))
t.OnFileDownloadRequest().Add(count[*core.FileDownloadEvent](c, "OnFileDownloadRequest"))
t.OnFileBeforeTokenRequest().Add(count[*core.FileTokenEvent](c, "OnFileBeforeTokenRequest"))
t.OnFileAfterTokenRequest().Add(count[*core.FileTokenEvent](c, "OnFileAfterTokenRequest"))
t.OnFileAfterTokenRequest().Add(count[*core.FileTokenEvent](c, "OnFileAfterTokenRequest"))
return c
}
func count[T any](c *Counter, name string) func(_ T) error {
return func(_ T) error {
c.Increment(name)
return nil
}
}

View File

@@ -19,6 +19,7 @@ func defaultTestData(t *testing.T, app core.App) {
adminTestData(t, app)
userTestData(t, app)
ticketTestData(t, app)
reactionTestData(t, app)
}
@@ -57,6 +58,30 @@ func userTestData(t *testing.T, app core.App) {
}
}
func ticketTestData(t *testing.T, app core.App) {
t.Helper()
collection, err := app.Dao().FindCollectionByNameOrId(migrations.TicketCollectionName)
if err != nil {
t.Fatal(err)
}
record := models.NewRecord(collection)
record.SetId("t_test")
record.Set("name", "Test Ticket")
record.Set("type", "incident")
record.Set("description", "This is a test ticket.")
record.Set("open", true)
record.Set("schema", `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`)
record.Set("state", `{"tlp":"AMBER"}`)
record.Set("owner", "u_bob_analyst")
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)
}
}
func reactionTestData(t *testing.T, app core.App) {
t.Helper()
@@ -69,9 +94,9 @@ func reactionTestData(t *testing.T, app core.App) {
record.SetId("r_reaction")
record.Set("name", "Reaction")
record.Set("trigger", "webhook")
record.Set("triggerdata", `{"path":"test"}`)
record.Set("triggerdata", `{"token":"1234567890","path":"test"}`)
record.Set("action", "python")
record.Set("actiondata", `{"bootstrap":"requests","script":"print('Hello, World!')"}`)
record.Set("actiondata", `{"requirements":"requests","script":"print('Hello, World!')"}`)
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)
@@ -95,7 +120,7 @@ func reactionTestData(t *testing.T, app core.App) {
record.Set("trigger", "hook")
record.Set("triggerdata", `{"collections":["tickets"],"events":["create"]}`)
record.Set("action", "python")
record.Set("actiondata", `{"bootstrap":"requests","script":"import requests\nrequests.post('http://127.0.0.1:12346/test', json={'test':True})"}`)
record.Set("actiondata", `{"requirements":"requests","script":"import requests\nrequests.post('http://127.0.0.1:12346/test', json={'test':True})"}`)
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)

View File

@@ -3,162 +3,95 @@ package testing
import (
"bytes"
"encoding/json"
"os"
"fmt"
"net/http/httptest"
"testing"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tests"
"github.com/pocketbase/pocketbase/tokens"
"github.com/SecurityBrewery/catalyst/app"
"github.com/SecurityBrewery/catalyst/migrations"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func BaseApp(t *testing.T) (core.App, string, string, func()) {
t.Helper()
temp, err := os.MkdirTemp("", "catalyst_test_data")
if err != nil {
t.Fatal(err)
}
baseApp := app.App(temp)
if err := app.Bootstrap(baseApp); err != nil {
t.Fatal(err)
}
defaultTestData(t, baseApp)
adminToken, err := generateAdminToken(t, baseApp, adminEmail)
if err != nil {
t.Fatal(err)
}
analystToken, err := generateRecordToken(t, baseApp, analystEmail)
if err != nil {
t.Fatal(err)
}
return baseApp, adminToken, analystToken, func() { _ = os.RemoveAll(temp) }
}
func AppFactory(baseApp core.App) func(t *testing.T) *tests.TestApp {
return func(t *testing.T) *tests.TestApp {
t.Helper()
testApp, err := tests.NewTestApp(baseApp.DataDir())
if err != nil {
t.Fatal(err)
}
app.BindHooks(testApp)
if err := app.Bootstrap(testApp); err != nil {
t.Fatal(err)
}
return testApp
}
}
func App(t *testing.T) (*tests.TestApp, func()) {
t.Helper()
baseApp, _, _, cleanup := BaseApp(t)
testApp := AppFactory(baseApp)(t)
return testApp, cleanup
}
func generateAdminToken(t *testing.T, baseApp core.App, email string) (string, error) {
t.Helper()
app, err := tests.NewTestApp(baseApp.DataDir())
if err != nil {
return "", err
}
defer app.Cleanup()
admin, err := app.Dao().FindAdminByEmail(email)
if err != nil {
return "", err
}
return tokens.NewAdminAuthToken(app, admin)
}
func generateRecordToken(t *testing.T, baseApp core.App, email string) (string, error) {
t.Helper()
app, err := tests.NewTestApp(baseApp.DataDir())
if err != nil {
t.Fatal(err)
}
defer app.Cleanup()
record, err := app.Dao().FindAuthRecordByEmail(migrations.UserCollectionName, email)
if err != nil {
return "", err
}
return tokens.NewRecordAuthToken(app, record)
}
type BaseTest struct {
Name string
Method string
RequestHeaders map[string]string
URL string
Body string
TestAppFactory func(t *testing.T) *tests.TestApp
}
type AuthBasedExpectation struct {
type UserTest struct {
Name string
RequestHeaders map[string]string
AuthRecord string
Admin string
ExpectedStatus int
ExpectedContent []string
NotExpectedContent []string
ExpectedEvents map[string]int
}
type authMatrixText struct {
baseTest BaseTest
authBasedExpectations []AuthBasedExpectation
type catalystTest struct {
baseTest BaseTest
userTests []UserTest
}
func mergeScenario(base BaseTest, expectation AuthBasedExpectation) tests.ApiScenario {
return tests.ApiScenario{
Name: expectation.Name,
Method: base.Method,
Url: base.URL,
Body: bytes.NewBufferString(base.Body),
TestAppFactory: base.TestAppFactory,
func runMatrixTest(t *testing.T, baseTest BaseTest, userTest UserTest) {
t.Helper()
RequestHeaders: mergeMaps(base.RequestHeaders, expectation.RequestHeaders),
ExpectedStatus: expectation.ExpectedStatus,
ExpectedContent: expectation.ExpectedContent,
NotExpectedContent: expectation.NotExpectedContent,
ExpectedEvents: expectation.ExpectedEvents,
}
}
baseApp, counter, baseAppCleanup := App(t)
defer baseAppCleanup()
func mergeMaps(a, b map[string]string) map[string]string {
if a == nil {
return b
server, err := apis.InitApi(baseApp)
require.NoError(t, err)
if err := baseApp.OnBeforeServe().Trigger(&core.ServeEvent{
App: baseApp,
Router: server,
}); err != nil {
t.Fatal(fmt.Errorf("failed to trigger OnBeforeServe: %w", err))
}
if b == nil {
return a
recorder := httptest.NewRecorder()
body := bytes.NewBufferString(baseTest.Body)
req := httptest.NewRequest(baseTest.Method, baseTest.URL, body)
for k, v := range baseTest.RequestHeaders {
req.Header.Set(k, v)
}
for k, v := range b {
a[k] = v
if userTest.AuthRecord != "" {
token, err := generateRecordToken(t, baseApp, userTest.AuthRecord)
require.NoError(t, err)
req.Header.Set("Authorization", token)
}
return a
if userTest.Admin != "" {
token, err := generateAdminToken(t, baseApp, userTest.Admin)
require.NoError(t, err)
req.Header.Set("Authorization", token)
}
server.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
assert.Equal(t, userTest.ExpectedStatus, res.StatusCode)
for _, expectedContent := range userTest.ExpectedContent {
assert.Contains(t, recorder.Body.String(), expectedContent)
}
for _, notExpectedContent := range userTest.NotExpectedContent {
assert.NotContains(t, recorder.Body.String(), notExpectedContent)
}
for event, count := range userTest.ExpectedEvents {
assert.Equal(t, count, counter.Count(event))
}
}
func b(data map[string]any) []byte {