refactor: remove pocketbase (#1138)

This commit is contained in:
Jonas Plum
2025-09-02 21:58:08 +02:00
committed by GitHub
parent f28c238135
commit eba2615ec0
435 changed files with 42677 additions and 4730 deletions

222
testing/api_comment_test.go Normal file
View File

@@ -0,0 +1,222 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestCommentsCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListComments",
Method: http.MethodGet,
URL: "/api/comments?ticket=test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateComment",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/comments",
Body: s(map[string]any{
"author": "u_bob_analyst",
"message": "new",
"ticket": "test-ticket",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetComment",
Method: http.MethodGet,
URL: "/api/comments/c_test_comment",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"c_test_comment"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"c_test_comment"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateComment",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/comments/c_test_comment",
Body: s(map[string]any{"message": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"c_test_comment"`,
`"message":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"c_test_comment"`,
`"message":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteComment",
Method: http.MethodDelete,
URL: "/api/comments/c_test_comment",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

View File

@@ -0,0 +1,61 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestFeaturesConfig(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "Config",
Method: http.MethodGet,
URL: "/api/config",
},
userTests: []userTest{
{
Name: "NoAuth",
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":`,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":`,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

151
testing/api_file_test.go Normal file
View File

@@ -0,0 +1,151 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestFilesCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListFiles",
Method: http.MethodGet,
URL: "/api/files?ticket=test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{"X-Total-Count": "1"},
ExpectedEvents: map[string]int{},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{"X-Total-Count": "1"},
ExpectedEvents: map[string]int{},
},
},
},
{
baseTest: baseTest{
Name: "CreateFile",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/files",
Body: s(map[string]any{
"ticket": "test-ticket",
"name": "new.txt",
"size": 3,
"blob": "data:text/plain;base64,bmV3",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"ticket":"test-ticket"`},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetFile",
Method: http.MethodGet,
URL: "/api/files/b_test_file",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"id":"b_test_file"`},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"id":"b_test_file"`},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "DeleteFile",
Method: http.MethodDelete,
URL: "/api/files/b_test_file",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

222
testing/api_link_test.go Normal file
View File

@@ -0,0 +1,222 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestLinksCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListLinks",
Method: http.MethodGet,
URL: "/api/links?ticket=test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateLink",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/links",
Body: s(map[string]any{
"ticket": "test-ticket",
"name": "new",
"url": "https://example.com/new",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetLink",
Method: http.MethodGet,
URL: "/api/links/l_test_link",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"l_test_link"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"l_test_link"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateLink",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/links/l_test_link",
Body: s(map[string]any{"name": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"l_test_link"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"l_test_link"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteLink",
Method: http.MethodDelete,
URL: "/api/links/l_test_link",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

View File

@@ -3,6 +3,8 @@ package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestReactionsCollection(t *testing.T) {
@@ -10,41 +12,37 @@ func TestReactionsCollection(t *testing.T) {
testSets := []catalystTest{
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "ListReactions",
Method: http.MethodGet,
URL: "/api/collections/reactions/records",
URL: "/api/reactions",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"totalItems":0`,
`"items":[]`,
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"totalItems":3`,
`"id":"r_reaction"`,
`"missing required scopes"`,
},
NotExpectedContent: []string{
`"items":[]`,
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"totalItems":3`,
`"id":"r_reaction"`,
`"id":"r-test-webhook"`,
},
ExpectedHeaders: map[string]string{
"X-Total-Count": "3",
},
NotExpectedContent: []string{
`"items":[]`,
@@ -54,11 +52,11 @@ func TestReactionsCollection(t *testing.T) {
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "CreateReaction",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/reactions/records",
URL: "/api/reactions",
Body: s(map[string]any{
"name": "test",
"trigger": "webhook",
@@ -67,34 +65,25 @@ func TestReactionsCollection(t *testing.T) {
"actiondata": map[string]any{"script": "print('Hello, World!')"},
}),
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusBadRequest,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"message":"Failed to create record."`,
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"name":"test"`,
},
NotExpectedContent: []string{
`"items":[]`,
},
ExpectedEvents: map[string]int{
"OnModelAfterCreate": 1,
"OnModelBeforeCreate": 1,
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"name":"test"`,
@@ -103,8 +92,8 @@ func TestReactionsCollection(t *testing.T) {
`"items":[]`,
},
ExpectedEvents: map[string]int{
"OnModelAfterCreate": 1,
"OnModelBeforeCreate": 1,
// "OnModelAfterCreate": 1,
// "OnModelBeforeCreate": 1,
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
@@ -112,82 +101,74 @@ func TestReactionsCollection(t *testing.T) {
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "GetReaction",
Method: http.MethodGet,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/reactions/records/r_reaction",
URL: "/api/reactions/r-test-webhook",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"message":"The requested resource wasn't found."`,
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"id":"r_reaction"`,
`"missing required scopes"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
`"id":"r-test-webhook"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "UpdateReaction",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/reactions/records/r_reaction",
URL: "/api/reactions/r-test-webhook",
Body: s(map[string]any{"name": "update"}),
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"message":"The requested resource wasn't found."`,
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
ExpectedStatus: http.StatusOK,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"id":"r_reaction"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"r_reaction"`,
`"id":"r-test-webhook"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnModelAfterUpdate": 1,
"OnModelBeforeUpdate": 1,
// "OnModelAfterUpdate": 1,
// "OnModelBeforeUpdate": 1,
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
@@ -195,37 +176,34 @@ func TestReactionsCollection(t *testing.T) {
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "DeleteReaction",
Method: http.MethodDelete,
URL: "/api/collections/reactions/records/r_reaction",
URL: "/api/reactions/r-test-webhook",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusNotFound,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"message":"The requested resource wasn't found."`,
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
"OnModelBeforeDelete": 1,
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnModelAfterDelete": 1,
"OnModelBeforeDelete": 1,
// "OnModelAfterDelete": 1,
// "OnModelBeforeDelete": 1,
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
@@ -233,6 +211,7 @@ func TestReactionsCollection(t *testing.T) {
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()

View File

@@ -3,6 +3,8 @@ package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func Test_Routes(t *testing.T) {
@@ -10,56 +12,56 @@ func Test_Routes(t *testing.T) {
testSets := []catalystTest{
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "Root",
Method: http.MethodGet,
URL: "/",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusFound,
},
{
Name: "Analyst",
AuthRecord: analystEmail,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusFound,
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusFound,
},
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "Config",
Method: http.MethodGet,
URL: "/api/config",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
Name: "NoAuth",
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":[]`,
`"flags":`,
},
},
{
Name: "Analyst",
AuthRecord: analystEmail,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":[]`,
`"flags":`,
},
},
{
Name: "Admin",
Admin: adminEmail,
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"flags":[]`,
`"flags":`,
},
},
},

223
testing/api_task_test.go Normal file
View File

@@ -0,0 +1,223 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestTasksCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListTasks",
Method: http.MethodGet,
URL: "/api/tasks?ticket=test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateTask",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/tasks",
Body: s(map[string]any{
"ticket": "test-ticket",
"name": "new",
"open": true,
"owner": "u_bob_analyst",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetTask",
Method: http.MethodGet,
URL: "/api/tasks/k_test_task",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"k_test_task"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"k_test_task"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateTask",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/tasks/k_test_task",
Body: s(map[string]any{"name": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"k_test_task"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"k_test_task"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteTask",
Method: http.MethodDelete,
URL: "/api/tasks/k_test_task",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

134
testing/api_test.go Normal file
View File

@@ -0,0 +1,134 @@
package testing
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
type request struct {
Method string
URL string
Body []byte
}
func TestAPI(t *testing.T) { //nolint:cyclop
t.Parallel()
requests := [][]request{
{
{"GET", "/ui/login", nil},
{"GET", "/api/config", nil},
},
{
{"POST", "/auth/local/login", []byte(`{"email":"admin@catalyst-soar.com","password":"password123"}`)},
},
{
{"GET", "/auth/user", nil},
{"GET", "/api/sidebar", nil},
{"GET", "/ui/groups", nil},
{"GET", "/api/tickets", nil},
{"GET", "/api/tickets", nil},
{"GET", "/api/tasks", nil},
{"GET", "/auth/user", nil},
{"GET", "/auth/user", nil},
{"GET", "/api/sidebar", nil},
{"GET", "/api/groups", nil},
{"GET", "/api/config", nil},
},
{
{"POST", "/api/groups", []byte(`{"name":"playwright-59537c9d-772f-45f0-a8f0-bb99656fa7ed","permissions":["ticket:read"]}`)},
},
{
{"GET", "/api/groups/ID", nil},
{"GET", "/api/groups", nil},
{"GET", "/api/config", nil},
},
{
{"GET", "/api/groups/ID/parents", nil},
{"GET", "/api/groups/ID/permissions", nil},
{"GET", "/api/groups/ID/children", nil},
{"GET", "/api/groups/ID/users", nil},
{"GET", "/api/users", nil},
},
{
{"GET", "/ui/login", nil},
{"GET", "/api/config", nil},
},
{
{"POST", "/auth/local/login", []byte(`{"email":"admin@catalyst-soar.com","password":"password123"}`)},
},
{
{"GET", "/auth/user", nil},
{"GET", "/api/sidebar", nil},
{"GET", "/ui/groups", nil},
{"GET", "/api/tickets", nil},
{"GET", "/api/tickets", nil},
{"GET", "/api/tasks", nil},
{"GET", "/auth/user", nil},
{"GET", "/auth/user", nil},
{"GET", "/api/sidebar", nil},
{"GET", "/api/groups", nil},
{"GET", "/api/config", nil},
},
{
{"POST", "/api/groups", []byte(`{"name":"playwright-c9bdcbf8-6aba-4974-9c12-f77fa7f15b34","permissions":["ticket:read"]}`)},
},
}
app, cleanup, _ := App(t)
t.Cleanup(cleanup)
id, token := "", ""
for _, batch := range requests {
var wg sync.WaitGroup
wg.Add(len(batch))
for _, req := range batch {
go func() {
defer wg.Done()
url := req.URL
if strings.Contains(url, "ID") {
url = strings.ReplaceAll(url, "ID", id)
}
ctx, cancel := context.WithTimeout(t.Context(), time.Second*10)
defer cancel()
r, err := http.NewRequestWithContext(ctx, req.Method, url, bytes.NewReader(req.Body))
assert.NoError(t, err)
if token != "" {
r.Header.Set("Authorization", "Bearer "+token)
}
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
assert.Equal(t, 200, w.Code, "expected status code 200: %s for %s %s", w.Body.String(), req.Method, url)
if w.Code == 200 && req.Method == http.MethodPost && gjson.Get(w.Body.String(), "id").Exists() {
id = gjson.Get(w.Body.String(), "id").String()
}
if w.Code == 200 && req.Method == http.MethodPost && gjson.Get(w.Body.String(), "token").Exists() {
token = gjson.Get(w.Body.String(), "token").String()
}
}()
}
wg.Wait()
}
}

226
testing/api_ticket_test.go Normal file
View File

@@ -0,0 +1,226 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestTicketsCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListTickets",
Method: http.MethodGet,
URL: "/api/tickets",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateTicket",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/tickets",
Body: s(map[string]any{
"name": "new",
"type": "incident",
"description": "test",
"open": true,
"owner": "u_bob_analyst",
"resolution": "",
"schema": map[string]any{},
"state": map[string]any{},
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"name":"new"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"name":"new"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetTicket",
Method: http.MethodGet,
URL: "/api/tickets/test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-ticket"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-ticket"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateTicket",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/tickets/test-ticket",
Body: s(map[string]any{"name": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-ticket"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-ticket"`,
`"name":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteTicket",
Method: http.MethodDelete,
URL: "/api/tickets/test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

View File

@@ -0,0 +1,222 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestTimelineCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListTimeline",
Method: http.MethodGet,
URL: "/api/timeline?ticket=test-ticket",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "1",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateTimeline",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/timeline",
Body: s(map[string]any{
"ticket": "test-ticket",
"message": "new",
"time": "2023-01-01T00:00:00Z",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"ticket":"test-ticket"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetTimeline",
Method: http.MethodGet,
URL: "/api/timeline/h_test_timeline",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"h_test_timeline"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"h_test_timeline"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateTimeline",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/timeline/h_test_timeline",
Body: s(map[string]any{"message": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"h_test_timeline"`,
`"message":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"h_test_timeline"`,
`"message":"update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteTimeline",
Method: http.MethodDelete,
URL: "/api/timeline/h_test_timeline",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

213
testing/api_type_test.go Normal file
View File

@@ -0,0 +1,213 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestTypesCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListTypes",
Method: http.MethodGet,
URL: "/api/types",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "4",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{
"X-Total-Count": "4",
},
ExpectedEvents: map[string]int{"OnRecordsListRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "CreateType",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/types",
Body: s(map[string]any{
"singular": "Example",
"plural": "Examples",
"icon": "Bug",
"schema": map[string]any{},
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"singular":"Example"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetType",
Method: http.MethodGet,
URL: "/api/types/test-type",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-type"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-type"`,
},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateType",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/types/test-type",
Body: s(map[string]any{"singular": "Update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"id":"test-type"`,
`"singular":"Update"`,
},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteType",
Method: http.MethodDelete,
URL: "/api/types/test-type",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"invalid bearer token"`,
},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{
`"missing required scopes"`,
},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

View File

@@ -6,6 +6,8 @@ import (
"time"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestWebhookReactions(t *testing.T) {
@@ -21,13 +23,13 @@ func TestWebhookReactions(t *testing.T) {
testSets := []catalystTest{
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "TriggerWebhookReaction",
Method: http.MethodGet,
RequestHeaders: map[string]string{"Authorization": "Bearer 1234567890"},
URL: "/reaction/test",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
@@ -36,12 +38,12 @@ func TestWebhookReactions(t *testing.T) {
},
},
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "TriggerWebhookReaction2",
Method: http.MethodGet,
URL: "/reaction/test2",
},
userTests: []UserTest{
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusOK,
@@ -78,16 +80,17 @@ func TestHookReactions(t *testing.T) {
testSets := []catalystTest{
{
baseTest: BaseTest{
baseTest: baseTest{
Name: "TriggerHookReaction",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/collections/tickets/records",
URL: "/api/tickets",
Body: s(map[string]any{
"type": "alert",
"name": "test",
}),
},
userTests: []UserTest{
userTests: []userTest{
// {
// Name: "Unauthorized",
// ExpectedStatus: http.StatusOK,
@@ -95,15 +98,14 @@ func TestHookReactions(t *testing.T) {
// },
{
Name: "Analyst",
AuthRecord: analystEmail,
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{
`"collectionName":"tickets"`,
`"name":"test"`,
},
ExpectedEvents: map[string]int{
"OnModelAfterCreate": 1,
"OnModelBeforeCreate": 1,
// "OnModelAfterCreate": 1,
// "OnModelBeforeCreate": 1,
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},

182
testing/api_webhook_test.go Normal file
View File

@@ -0,0 +1,182 @@
package testing
import (
"net/http"
"testing"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestWebhooksCollection(t *testing.T) {
t.Parallel()
testSets := []catalystTest{
{
baseTest: baseTest{
Name: "ListWebhooks",
Method: http.MethodGet,
URL: "/api/webhooks",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
ExpectedEvents: map[string]int{},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedHeaders: map[string]string{"X-Total-Count": "1"},
ExpectedContent: []string{`"id":"w_test_webhook"`},
ExpectedEvents: map[string]int{},
},
},
},
{
baseTest: baseTest{
Name: "CreateWebhook",
Method: http.MethodPost,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/webhooks",
Body: s(map[string]any{
"name": "new",
"collection": "tickets",
"destination": "https://example.com/new",
}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"name":"new"`},
ExpectedEvents: map[string]int{
"OnRecordAfterCreateRequest": 1,
"OnRecordBeforeCreateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "GetWebhook",
Method: http.MethodGet,
URL: "/api/webhooks/w_test_webhook",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"id":"w_test_webhook"`},
ExpectedEvents: map[string]int{"OnRecordViewRequest": 1},
},
},
},
{
baseTest: baseTest{
Name: "UpdateWebhook",
Method: http.MethodPatch,
RequestHeaders: map[string]string{"Content-Type": "application/json"},
URL: "/api/webhooks/w_test_webhook",
Body: s(map[string]any{"name": "update"}),
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusOK,
ExpectedContent: []string{`"id":"w_test_webhook"`, `"name":"update"`},
ExpectedEvents: map[string]int{
"OnRecordAfterUpdateRequest": 1,
"OnRecordBeforeUpdateRequest": 1,
},
},
},
},
{
baseTest: baseTest{
Name: "DeleteWebhook",
Method: http.MethodDelete,
URL: "/api/webhooks/w_test_webhook",
},
userTests: []userTest{
{
Name: "Unauthorized",
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"invalid bearer token"`},
},
{
Name: "Analyst",
AuthRecord: data.AnalystEmail,
ExpectedStatus: http.StatusUnauthorized,
ExpectedContent: []string{`"missing required scopes"`},
},
{
Name: "Admin",
Admin: data.AdminEmail,
ExpectedStatus: http.StatusNoContent,
ExpectedEvents: map[string]int{
"OnRecordAfterDeleteRequest": 1,
"OnRecordBeforeDeleteRequest": 1,
},
},
},
},
}
for _, testSet := range testSets {
t.Run(testSet.baseTest.Name, func(t *testing.T) {
t.Parallel()
for _, userTest := range testSet.userTests {
t.Run(userTest.Name, func(t *testing.T) {
t.Parallel()
runMatrixTest(t, testSet.baseTest, userTest)
})
}
})
}
}

View File

@@ -1,36 +0,0 @@
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]
}

View File

@@ -1,41 +0,0 @@
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))
})
}
}

Binary file not shown.

View File

@@ -1,29 +1,46 @@
package testing
import (
"encoding/json"
"net/http"
"github.com/labstack/echo/v5"
"github.com/go-chi/chi/v5"
)
type RecordingServer struct {
server *echo.Echo
server chi.Router
Entries []string
}
func NewRecordingServer() *RecordingServer {
e := echo.New()
e := chi.NewRouter()
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
e.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
b, err := json.Marshal(map[string]any{
"status": "ok",
})
if err != nil {
http.Error(w, "failed to marshal response", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
_, _ = w.Write(b)
})
e.Any("/*", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]any{
e.HandleFunc("/*", func(w http.ResponseWriter, _ *http.Request) {
b, err := json.Marshal(map[string]any{
"test": true,
})
if err != nil {
http.Error(w, "failed to marshal response", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
_, _ = w.Write(b)
})
return &RecordingServer{

25
testing/test_all.sh Executable file
View File

@@ -0,0 +1,25 @@
reset_catalyst_data() {
if [ -d "catalyst_data" ]; then
rm -rf catalyst_data
fi
mkdir -p catalyst_data
}
# function to run playwright tests
run_playwright_tests() {
cd ui
bun test:e2e
cd ..
}
run_upgradetest() {
# iterate over all folders in ./testing/data
for dir in ./testing/data/*/; do
echo "Running tests with data from $dir"
reset_catalyst_data
cp "$dir"/data.db catalyst_data/data.db
run_playwright_tests
done
}
run_upgradetest

View File

@@ -1,164 +1,47 @@
package testing
import (
"fmt"
"os"
"context"
"testing"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tokens"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app"
"github.com/SecurityBrewery/catalyst/migrations"
"github.com/SecurityBrewery/catalyst/app/counter"
"github.com/SecurityBrewery/catalyst/app/data"
"github.com/SecurityBrewery/catalyst/app/hook"
)
func App(t *testing.T) (*pocketbase.PocketBase, *Counter, func()) {
func App(t *testing.T) (*app.App, func(), *counter.Counter) {
t.Helper()
temp, err := os.MkdirTemp("", "catalyst_test_data")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
baseApp, err := app.App(temp, true)
if err != nil {
t.Fatal(err)
}
catalyst, cleanup, err := app.New(t.Context(), dir)
require.NoError(t, err)
if err := baseApp.Bootstrap(); err != nil {
t.Fatal(fmt.Errorf("failed to bootstrap: %w", err))
}
data.DefaultTestData(t, dir, catalyst.Queries)
baseApp.Settings().Logs.MaxDays = 0
defaultTestData(t, baseApp)
counter := countEvents(baseApp)
return baseApp, counter, func() { _ = os.RemoveAll(temp) }
return catalyst, cleanup, countEvents(catalyst.Hooks)
}
func generateAdminToken(t *testing.T, baseApp core.App, email string) (string, error) {
t.Helper()
func countEvents(hooks *hook.Hooks) *counter.Counter {
c := counter.NewCounter()
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"))
hooks.OnRecordsListRequest.Subscribe(count(c, "OnRecordsListRequest"))
hooks.OnRecordViewRequest.Subscribe(count(c, "OnRecordViewRequest"))
hooks.OnRecordBeforeCreateRequest.Subscribe(count(c, "OnRecordBeforeCreateRequest"))
hooks.OnRecordAfterCreateRequest.Subscribe(count(c, "OnRecordAfterCreateRequest"))
hooks.OnRecordBeforeUpdateRequest.Subscribe(count(c, "OnRecordBeforeUpdateRequest"))
hooks.OnRecordAfterUpdateRequest.Subscribe(count(c, "OnRecordAfterUpdateRequest"))
hooks.OnRecordBeforeDeleteRequest.Subscribe(count(c, "OnRecordBeforeDeleteRequest"))
hooks.OnRecordAfterDeleteRequest.Subscribe(count(c, "OnRecordAfterDeleteRequest"))
return c
}
func count[T any](c *Counter, name string) func(_ T) error {
return func(_ T) error {
func count(c *counter.Counter, name string) func(ctx context.Context, table string, record any) {
return func(_ context.Context, _ string, _ any) {
c.Increment(name)
return nil
}
}

View File

@@ -1,128 +0,0 @@
package testing
import (
"testing"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/models"
"github.com/SecurityBrewery/catalyst/migrations"
)
const (
adminEmail = "admin@catalyst-soar.com"
analystEmail = "analyst@catalyst-soar.com"
)
func defaultTestData(t *testing.T, app core.App) {
t.Helper()
adminTestData(t, app)
userTestData(t, app)
ticketTestData(t, app)
reactionTestData(t, app)
}
func adminTestData(t *testing.T, app core.App) {
t.Helper()
admin := &models.Admin{Email: adminEmail}
if err := admin.SetPassword("password"); err != nil {
t.Fatal(err)
}
if err := app.Dao().SaveAdmin(admin); err != nil {
t.Fatal(err)
}
}
func userTestData(t *testing.T, app core.App) {
t.Helper()
collection, err := app.Dao().FindCollectionByNameOrId(migrations.UserCollectionName)
if err != nil {
t.Fatal(err)
}
record := models.NewRecord(collection)
record.SetId("u_bob_analyst")
_ = record.SetUsername("u_bob_analyst")
_ = record.SetPassword("password")
record.Set("name", "Bob Analyst")
record.Set("email", analystEmail)
_ = record.SetVerified(true)
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)
}
}
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()
collection, err := app.Dao().FindCollectionByNameOrId(migrations.ReactionCollectionName)
if err != nil {
t.Fatal(err)
}
record := models.NewRecord(collection)
record.SetId("r_reaction")
record.Set("name", "Reaction")
record.Set("trigger", "webhook")
record.Set("triggerdata", `{"token":"1234567890","path":"test"}`)
record.Set("action", "python")
record.Set("actiondata", `{"requirements":"requests","script":"print('Hello, World!')"}`)
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)
}
record = models.NewRecord(collection)
record.SetId("r_reaction_webhook")
record.Set("name", "Reaction")
record.Set("trigger", "webhook")
record.Set("triggerdata", `{"path":"test2"}`)
record.Set("action", "webhook")
record.Set("actiondata", `{"headers":{"Content-Type":"application/json"},"url":"http://127.0.0.1:12345/webhook"}`)
if err := app.Dao().SaveRecord(record); err != nil {
t.Fatal(err)
}
record = models.NewRecord(collection)
record.SetId("r_reaction_hook")
record.Set("name", "Hook")
record.Set("trigger", "hook")
record.Set("triggerdata", `{"collections":["tickets"],"events":["create"]}`)
record.Set("action", "python")
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,17 +3,17 @@ package testing
import (
"bytes"
"encoding/json"
"fmt"
"net/http/httptest"
"testing"
"time"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/auth"
)
type BaseTest struct {
type baseTest struct {
Name string
Method string
RequestHeaders map[string]string
@@ -21,36 +21,28 @@ type BaseTest struct {
Body string
}
type UserTest struct {
type userTest struct {
Name string
AuthRecord string
Admin string
ExpectedStatus int
ExpectedHeaders map[string]string
ExpectedContent []string
NotExpectedContent []string
ExpectedEvents map[string]int
}
type catalystTest struct {
baseTest BaseTest
userTests []UserTest
baseTest baseTest
userTests []userTest
}
func runMatrixTest(t *testing.T, baseTest BaseTest, userTest UserTest) {
func runMatrixTest(t *testing.T, baseTest baseTest, userTest userTest) {
t.Helper()
baseApp, counter, baseAppCleanup := App(t)
defer baseAppCleanup()
baseApp, cleanup, counter := App(t)
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))
}
t.Cleanup(cleanup)
recorder := httptest.NewRecorder()
body := bytes.NewBufferString(baseTest.Body)
@@ -61,26 +53,42 @@ func runMatrixTest(t *testing.T, baseTest BaseTest, userTest UserTest) {
}
if userTest.AuthRecord != "" {
token, err := generateRecordToken(t, baseApp, userTest.AuthRecord)
user, err := baseApp.Queries.UserByEmail(t.Context(), &userTest.AuthRecord)
require.NoError(t, err)
req.Header.Set("Authorization", token)
permissions, err := baseApp.Queries.ListUserPermissions(t.Context(), user.ID)
require.NoError(t, err)
loginToken, err := auth.CreateAccessToken(t.Context(), &user, permissions, time.Hour, baseApp.Queries)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+loginToken)
}
if userTest.Admin != "" {
token, err := generateAdminToken(t, baseApp, userTest.Admin)
user, err := baseApp.Queries.UserByEmail(t.Context(), &userTest.Admin)
require.NoError(t, err)
req.Header.Set("Authorization", token)
permissions, err := baseApp.Queries.ListUserPermissions(t.Context(), user.ID)
require.NoError(t, err)
loginToken, err := auth.CreateAccessToken(t.Context(), &user, permissions, time.Hour, baseApp.Queries)
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+loginToken)
}
server.ServeHTTP(recorder, req)
baseApp.ServeHTTP(recorder, req)
res := recorder.Result()
defer res.Body.Close()
assert.Equal(t, userTest.ExpectedStatus, res.StatusCode)
for k, v := range userTest.ExpectedHeaders {
assert.Equal(t, v, res.Header.Get(k))
}
for _, expectedContent := range userTest.ExpectedContent {
assert.Contains(t, recorder.Body.String(), expectedContent)
}
@@ -90,7 +98,7 @@ func runMatrixTest(t *testing.T, baseTest BaseTest, userTest UserTest) {
}
for event, count := range userTest.ExpectedEvents {
assert.Equal(t, count, counter.Count(event))
assert.Equalf(t, count, counter.Count(event), "expected %d events for %s, got %d", count, event, counter.Count(event))
}
}

67
testing/upgrade_test.go Normal file
View File

@@ -0,0 +1,67 @@
package testing
import (
"log"
"os"
"path/filepath"
"testing"
"github.com/SecurityBrewery/catalyst/app"
)
func TestUpgrades(t *testing.T) {
t.Parallel()
dirEntries, err := os.ReadDir("data")
if err != nil {
t.Fatal(err)
}
for _, entry := range dirEntries {
if !entry.IsDir() {
continue
}
t.Run(entry.Name(), func(t *testing.T) {
t.Parallel()
db, err := copyDatabase(t, entry.Name())
if err != nil {
log.Fatal(err)
}
pb, cleanup, err := app.New(t.Context(), db)
if err != nil {
log.Fatal(err)
}
t.Cleanup(cleanup)
ValidateUpgradeTestData(t, pb.Queries)
})
}
}
func copyDatabase(t *testing.T, directory string) (string, error) {
t.Helper()
dest := t.TempDir()
dstDB, err := os.Create(filepath.Join(dest, "data.db"))
if err != nil {
return "", err
}
defer dstDB.Close()
srcDB, err := os.Open(filepath.Join("data", directory, "data.db"))
if err != nil {
return "", err
}
defer srcDB.Close()
if _, err := dstDB.ReadFrom(srcDB); err != nil {
return "", err
}
return dest, nil
}

View File

@@ -0,0 +1,21 @@
package testing
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/SecurityBrewery/catalyst/app/data"
)
func TestUpgradeTestData(t *testing.T) {
t.Parallel()
app, cleanup, _ := App(t)
t.Cleanup(cleanup)
require.NoError(t, data.GenerateUpgradeTestData(t.Context(), app.Queries))
ValidateUpgradeTestData(t, app.Queries)
}

99
testing/validate.go Normal file
View File

@@ -0,0 +1,99 @@
package testing
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"
"golang.org/x/crypto/bcrypt"
"github.com/SecurityBrewery/catalyst/app/data"
"github.com/SecurityBrewery/catalyst/app/database/sqlc"
)
func ValidateUpgradeTestData(t *testing.T, queries *sqlc.Queries) {
t.Helper()
// users
userRecord, err := queries.UserByUserName(t.Context(), "u_test")
require.NoError(t, err, "failed to find user by username")
assert.Equal(t, "u_test", userRecord.ID, "user ID does not match expected value")
assert.Equal(t, "u_test", userRecord.Username, "username does not match expected value")
assert.Equal(t, "Test User", *userRecord.Name, "name does not match expected value")
assert.Equal(t, "user@catalyst-soar.com", *userRecord.Email, "email does not match expected value")
assert.True(t, userRecord.Active, "user should be verified")
require.NoError(t, bcrypt.CompareHashAndPassword([]byte(userRecord.Passwordhash), []byte("1234567890")), "password hash does not match expected value")
for id := range data.CreateUpgradeTestDataTickets() {
ticket, err := queries.Ticket(t.Context(), id)
require.NoError(t, err, "failed to find ticket")
assert.Equal(t, "phishing-123", ticket.Name)
assert.Equal(t, "Phishing email reported by several employees.", ticket.Description)
assert.True(t, ticket.Open)
assert.Equal(t, "alert", ticket.Type)
assert.Equal(t, "u_test", *ticket.Owner)
assert.JSONEq(t, `{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`, string(ticket.Schema))
assert.JSONEq(t, `{"severity":"Medium"}`, string(ticket.State))
}
for id := range data.CreateUpgradeTestDataComments() {
comment, err := queries.GetComment(t.Context(), id)
require.NoError(t, err, "failed to find comment")
assert.Equal(t, "This is a test comment.", comment.Message)
}
for id := range data.CreateUpgradeTestDataTimeline() {
timeline, err := queries.GetTimeline(t.Context(), id)
require.NoError(t, err, "failed to find timeline")
assert.Equal(t, "This is a test timeline message.", timeline.Message)
}
for id := range data.CreateUpgradeTestDataTasks() {
task, err := queries.GetTask(t.Context(), id)
require.NoError(t, err, "failed to find task")
assert.Equal(t, "This is a test task.", task.Name)
}
for id := range data.CreateUpgradeTestDataLinks() {
link, err := queries.GetLink(t.Context(), id)
require.NoError(t, err, "failed to find link")
assert.Equal(t, "https://www.example.com", link.Url)
assert.Equal(t, "This is a test link.", link.Name)
}
for id := range data.CreateUpgradeTestDataReaction() {
reaction, err := queries.GetReaction(t.Context(), id)
require.NoError(t, err, "failed to find reaction")
assert.Equal(t, "Create New Ticket", reaction.Name)
assert.Equal(t, "schedule", reaction.Trigger)
assert.JSONEq(t, "{\"expression\":\"12 * * * *\"}", string(reaction.Triggerdata))
assert.Equal(t, "python", reaction.Action)
assert.Equal(t, "pocketbase", gjson.GetBytes(reaction.Actiondata, "requirements").String())
equalWithoutSpace(t, data.Script, gjson.GetBytes(reaction.Actiondata, "script").String())
}
}
func equalWithoutSpace(t *testing.T, expected, actual string) {
t.Helper()
assert.Equal(t, removeAllWhitespace(expected), removeAllWhitespace(actual))
}
func removeAllWhitespace(s string) string {
return strings.Map(func(r rune) rune {
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
return -1 // remove whitespace characters
}
return r // keep other characters
}, s)
}