From 7de89a752c00fe8b75df0a0da0ea98952ed5b994 Mon Sep 17 00:00:00 2001 From: Jonas Plum Date: Sun, 2 Feb 2025 13:40:33 +0100 Subject: [PATCH] test: add upgrade tests (#1126) --- .gitignore | 3 + Makefile | 5 + app/app.go | 1 + app/fakedata.go | 11 ++ fakedata/default.go | 235 +++++++++++++++++++++++++++++++ fakedata/default_test.go | 21 +++ fakedata/records.go | 135 ++++++++++++------ upgradetest/data/v0.14.1/data.db | Bin 0 -> 208896 bytes upgradetest/upgrade_test.go | 44 ++++++ 9 files changed, 411 insertions(+), 44 deletions(-) create mode 100644 fakedata/default.go create mode 100644 fakedata/default_test.go create mode 100644 upgradetest/data/v0.14.1/data.db create mode 100644 upgradetest/upgrade_test.go diff --git a/.gitignore b/.gitignore index deb0cd8..7c8bff3 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,7 @@ pb_data catalyst catalyst_data +# ignore changes, needs to be disabled when adding new upgrade tests +upgradetest + coverage.out diff --git a/Makefile b/Makefile index 739c095..36247b7 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,11 @@ dev-10000: go run . fake-data --users 100 --tickets 10000 go run . serve --app-url http://localhost:8090 --flags dev +.PHONY: default-data +default-data: + rm -rf catalyst_data + go run . default-data + .PHONY: serve-ui serve-ui: cd ui && bun dev --port 3000 diff --git a/app/app.go b/app/app.go index cb75b82..76a04ce 100644 --- a/app/app.go +++ b/app/app.go @@ -33,6 +33,7 @@ func App(dir string, test bool) (*pocketbase.PocketBase, error) { _ = app.RootCmd.ParseFlags(os.Args[1:]) app.RootCmd.AddCommand(fakeDataCmd(app)) + app.RootCmd.AddCommand(defaultDataCmd(app)) webhook.BindHooks(app) reaction.BindHooks(app, test) diff --git a/app/fakedata.go b/app/fakedata.go index e26540c..c689789 100644 --- a/app/fakedata.go +++ b/app/fakedata.go @@ -23,3 +23,14 @@ func fakeDataCmd(app core.App) *cobra.Command { return cmd } + +func defaultDataCmd(app core.App) *cobra.Command { + cmd := &cobra.Command{ + Use: "default-data", + RunE: func(_ *cobra.Command, _ []string) error { + return fakedata.GenerateDefaultData(app) + }, + } + + return cmd +} diff --git a/fakedata/default.go b/fakedata/default.go new file mode 100644 index 0000000..679aa31 --- /dev/null +++ b/fakedata/default.go @@ -0,0 +1,235 @@ +package fakedata + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/models" + "github.com/pocketbase/pocketbase/tools/types" + + "github.com/SecurityBrewery/catalyst/migrations" +) + +func defaultData() map[string]map[string]map[string]any { + var ( + ticketCreated = time.Date(2025, 2, 1, 11, 29, 35, 0, time.UTC) + ticketUpdated = ticketCreated.Add(time.Minute * 5) + commentCreated = ticketCreated.Add(time.Minute * 10) + commentUpdated = commentCreated.Add(time.Minute * 5) + timelineCreated = ticketCreated.Add(time.Minute * 15) + timelineUpdated = timelineCreated.Add(time.Minute * 5) + taskCreated = ticketCreated.Add(time.Minute * 20) + taskUpdated = taskCreated.Add(time.Minute * 5) + linkCreated = ticketCreated.Add(time.Minute * 25) + linkUpdated = linkCreated.Add(time.Minute * 5) + reactionCreated = time.Date(2025, 2, 1, 11, 30, 0, 0, time.UTC) + reactionUpdated = reactionCreated.Add(time.Minute * 5) + ) + + createTicketActionData := `{"requirements":"pocketbase","script":"import sys\nimport json\nimport random\nimport os\n\nfrom pocketbase import PocketBase\n\n# Connect to the PocketBase server\nclient = PocketBase(os.environ[\"CATALYST_APP_URL\"])\nclient.auth_store.save(token=os.environ[\"CATALYST_TOKEN\"])\n\nnewtickets = client.collection(\"tickets\").get_list(1, 200, {\"filter\": 'name = \"New Ticket\"'})\nfor ticket in newtickets.items:\n\tclient.collection(\"tickets\").delete(ticket.id)\n\n# Create a new ticket\nclient.collection(\"tickets\").create({\n\t\"name\": \"New Ticket\",\n\t\"type\": \"alert\",\n\t\"open\": True,\n})"}` + + return map[string]map[string]map[string]any{ + migrations.TicketCollectionName: { + "t_0": { + "created": dateTime(ticketCreated), + "updated": dateTime(ticketUpdated), + "name": "phishing-123", + "type": "alert", + "description": "Phishing email reported by several employees.", + "open": true, + "schema": types.JsonRaw(`{"type":"object","properties":{"tlp":{"title":"TLP","type":"string"}}}`), + "state": types.JsonRaw(`{"severity":"Medium"}`), + "owner": "u_test", + }, + }, + migrations.CommentCollectionName: { + "c_0": { + "created": dateTime(commentCreated), + "updated": dateTime(commentUpdated), + "ticket": "t_0", + "author": "u_test", + "message": "This is a test comment.", + }, + }, + migrations.TimelineCollectionName: { + "tl_0": { + "created": dateTime(timelineCreated), + "updated": dateTime(timelineUpdated), + "ticket": "t_0", + "time": dateTime(timelineCreated), + "message": "This is a test timeline message.", + }, + }, + migrations.TaskCollectionName: { + "ts_0": { + "created": dateTime(taskCreated), + "updated": dateTime(taskUpdated), + "ticket": "t_0", + "name": "This is a test task.", + "open": true, + "owner": "u_test", + }, + }, + migrations.LinkCollectionName: { + "l_0": { + "created": dateTime(linkCreated), + "updated": dateTime(linkUpdated), + "ticket": "t_0", + "url": "https://www.example.com", + "name": "This is a test link.", + }, + }, + migrations.ReactionCollectionName: { + "w_0": { + "created": dateTime(reactionCreated), + "updated": dateTime(reactionUpdated), + "name": "Create New Ticket", + "trigger": "schedule", + "triggerdata": types.JsonRaw(triggerSchedule), + "action": "python", + "actiondata": types.JsonRaw(createTicketActionData), + }, + }, + } +} + +func GenerateDefaultData(app core.App) error { + var records []*models.Record + + // users + userRecord, err := testUser(app.Dao()) + if err != nil { + return err + } + + records = append(records, userRecord) + + // records + for collectionName, collectionRecords := range defaultData() { + collection, err := app.Dao().FindCollectionByNameOrId(collectionName) + if err != nil { + return err + } + + for id, fields := range collectionRecords { + record := models.NewRecord(collection) + record.SetId(id) + + for key, value := range fields { + record.Set(key, value) + } + + records = append(records, record) + } + } + + for _, record := range records { + if err := app.Dao().SaveRecord(record); err != nil { + return err + } + } + + return nil +} + +func ValidateDefaultData(app core.App) error { //nolint:cyclop,gocognit + // users + userRecord, err := app.Dao().FindRecordById(migrations.UserCollectionName, "u_test") + if err != nil { + return fmt.Errorf("failed to find user record: %w", err) + } + + if userRecord == nil { + return errors.New("user not found") + } + + if userRecord.Username() != "u_test" { + return fmt.Errorf(`username does not match: got %q, want "u_test"`, userRecord.Username()) + } + + if !userRecord.ValidatePassword("1234567890") { + return errors.New("password does not match") + } + + if userRecord.Get("name") != "Test User" { + return fmt.Errorf(`name does not match: got %q, want "Test User"`, userRecord.Get("name")) + } + + if userRecord.Get("email") != "user@catalyst-soar.com" { + return fmt.Errorf(`email does not match: got %q, want "user@catalyst-soar.com"`, userRecord.Get("email")) + } + + if !userRecord.Verified() { + return errors.New("user is not verified") + } + + // records + for collectionName, collectionRecords := range defaultData() { + for id, fields := range collectionRecords { + record, err := app.Dao().FindRecordById(collectionName, id) + if err != nil { + return fmt.Errorf("failed to find record %s: %w", id, err) + } + + if record == nil { + return errors.New("record not found") + } + + for key, value := range fields { + got := record.Get(key) + + if wantJSON, ok := value.(types.JsonRaw); ok { + if err := compareJSON(got, wantJSON); err != nil { + return fmt.Errorf("record field %q does not match: %w", key, err) + } + + continue + } + + if got != value { + return fmt.Errorf("record field %s does not match: got %v (%T), want %v (%T)", key, got, got, value, value) + } + } + } + } + + return nil +} + +func compareJSON(got any, wantJSON types.JsonRaw) error { + gotJSON, ok := got.(types.JsonRaw) + if !ok { + return fmt.Errorf("got %T, want %T", got, wantJSON) + } + + if !jsonEqual(gotJSON.String(), wantJSON.String()) { + return fmt.Errorf("got %v, want %v", gotJSON, wantJSON) + } + + return nil +} + +func jsonEqual(a, b string) bool { + var objA, objB interface{} + + if err := json.Unmarshal([]byte(a), &objA); err != nil { + return false + } + + if err := json.Unmarshal([]byte(b), &objB); err != nil { + return false + } + + return reflect.DeepEqual(objA, objB) +} + +func dateTime(t time.Time) types.DateTime { + dt := types.DateTime{} + _ = dt.Scan(t) + + return dt +} diff --git a/fakedata/default_test.go b/fakedata/default_test.go new file mode 100644 index 0000000..c8bd718 --- /dev/null +++ b/fakedata/default_test.go @@ -0,0 +1,21 @@ +package fakedata_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/SecurityBrewery/catalyst/fakedata" + catalystTesting "github.com/SecurityBrewery/catalyst/testing" +) + +func TestDefaultData(t *testing.T) { + t.Parallel() + + app, _, cleanup := catalystTesting.App(t) + defer cleanup() + + require.NoError(t, fakedata.GenerateDefaultData(app)) + + require.NoError(t, fakedata.ValidateDefaultData(app)) +} diff --git a/fakedata/records.go b/fakedata/records.go index f0381e2..7562994 100644 --- a/fakedata/records.go +++ b/fakedata/records.go @@ -49,38 +49,45 @@ func Records(app core.App, userCount int, ticketCount int) ([]*models.Record, er return nil, err } - users := userRecords(app.Dao(), userCount) - tickets := ticketRecords(app.Dao(), users, types, ticketCount) - reactions := reactionRecords(app.Dao()) + users, err := userRecords(app.Dao(), userCount) + if err != nil { + return nil, err + } + + tickets, err := ticketRecords(app.Dao(), users, types, ticketCount) + if err != nil { + return nil, err + } + + reactions, err := reactionRecords(app.Dao()) + if err != nil { + return nil, err + } var records []*models.Record records = append(records, users...) - records = append(records, types...) records = append(records, tickets...) records = append(records, reactions...) return records, nil } -func userRecords(dao *daos.Dao, count int) []*models.Record { - collection, err := dao.FindCollectionByNameOrId(migrations.UserCollectionName) - if err != nil { - panic(err) - } - +func userRecords(dao *daos.Dao, count int) ([]*models.Record, error) { records := make([]*models.Record, 0, count) // create the test user if _, err := dao.FindRecordById(migrations.UserCollectionName, "u_test"); err != nil { - record := models.NewRecord(collection) - record.SetId("u_test") - _ = record.SetUsername("u_test") - _ = record.SetPassword("1234567890") - record.Set("name", gofakeit.Name()) - record.Set("email", "user@catalyst-soar.com") - _ = record.SetVerified(true) + testUser, err := testUser(dao) + if err != nil { + return nil, err + } - records = append(records, record) + records = append(records, testUser) + } + + collection, err := dao.FindCollectionByNameOrId(migrations.UserCollectionName) + if err != nil { + return nil, err } for range count - 1 { @@ -95,13 +102,30 @@ func userRecords(dao *daos.Dao, count int) []*models.Record { records = append(records, record) } - return records + return records, nil } -func ticketRecords(dao *daos.Dao, users, types []*models.Record, count int) []*models.Record { +func testUser(dao *daos.Dao) (*models.Record, error) { + collection, err := dao.FindCollectionByNameOrId(migrations.UserCollectionName) + if err != nil { + return nil, err + } + + record := models.NewRecord(collection) + record.SetId("u_test") + _ = record.SetUsername("u_test") + _ = record.SetPassword("1234567890") + record.Set("name", "Test User") + record.Set("email", "user@catalyst-soar.com") + _ = record.SetVerified(true) + + return record, nil +} + +func ticketRecords(dao *daos.Dao, users, types []*models.Record, count int) ([]*models.Record, error) { collection, err := dao.FindCollectionByNameOrId(migrations.TicketCollectionName) if err != nil { - panic(err) + return nil, err } records := make([]*models.Record, 0, count) @@ -134,19 +158,42 @@ func ticketRecords(dao *daos.Dao, users, types []*models.Record, count int) []*m records = append(records, record) // Add comments - records = append(records, commentRecords(dao, users, created, record)...) - records = append(records, timelineRecords(dao, created, record)...) - records = append(records, taskRecords(dao, users, created, record)...) - records = append(records, linkRecords(dao, created, record)...) + comments, err := commentRecords(dao, users, created, record) + if err != nil { + return nil, err + } + + records = append(records, comments...) + + timelines, err := timelineRecords(dao, created, record) + if err != nil { + return nil, err + } + + records = append(records, timelines...) + + tasks, err := taskRecords(dao, users, created, record) + if err != nil { + return nil, err + } + + records = append(records, tasks...) + + links, err := linkRecords(dao, created, record) + if err != nil { + return nil, err + } + + records = append(records, links...) } - return records + return records, nil } -func commentRecords(dao *daos.Dao, users []*models.Record, created time.Time, record *models.Record) []*models.Record { +func commentRecords(dao *daos.Dao, users []*models.Record, created time.Time, record *models.Record) ([]*models.Record, error) { commentCollection, err := dao.FindCollectionByNameOrId(migrations.CommentCollectionName) if err != nil { - panic(err) + return nil, err } records := make([]*models.Record, 0, 5) @@ -166,13 +213,13 @@ func commentRecords(dao *daos.Dao, users []*models.Record, created time.Time, re records = append(records, commentRecord) } - return records + return records, nil } -func timelineRecords(dao *daos.Dao, created time.Time, record *models.Record) []*models.Record { +func timelineRecords(dao *daos.Dao, created time.Time, record *models.Record) ([]*models.Record, error) { timelineCollection, err := dao.FindCollectionByNameOrId(migrations.TimelineCollectionName) if err != nil { - panic(err) + return nil, err } records := make([]*models.Record, 0, 5) @@ -192,13 +239,13 @@ func timelineRecords(dao *daos.Dao, created time.Time, record *models.Record) [] records = append(records, timelineRecord) } - return records + return records, nil } -func taskRecords(dao *daos.Dao, users []*models.Record, created time.Time, record *models.Record) []*models.Record { +func taskRecords(dao *daos.Dao, users []*models.Record, created time.Time, record *models.Record) ([]*models.Record, error) { taskCollection, err := dao.FindCollectionByNameOrId(migrations.TaskCollectionName) if err != nil { - panic(err) + return nil, err } records := make([]*models.Record, 0, 5) @@ -219,13 +266,13 @@ func taskRecords(dao *daos.Dao, users []*models.Record, created time.Time, recor records = append(records, taskRecord) } - return records + return records, nil } -func linkRecords(dao *daos.Dao, created time.Time, record *models.Record) []*models.Record { +func linkRecords(dao *daos.Dao, created time.Time, record *models.Record) ([]*models.Record, error) { linkCollection, err := dao.FindCollectionByNameOrId(migrations.LinkCollectionName) if err != nil { - panic(err) + return nil, err } records := make([]*models.Record, 0, 5) @@ -245,7 +292,7 @@ func linkRecords(dao *daos.Dao, created time.Time, record *models.Record) []*mod records = append(records, linkRecord) } - return records + return records, nil } const createTicketPy = `import sys @@ -321,12 +368,12 @@ const ( triggerHook = `{"collections":["tickets"],"events":["create"]}` ) -func reactionRecords(dao *daos.Dao) []*models.Record { +func reactionRecords(dao *daos.Dao) ([]*models.Record, error) { var records []*models.Record collection, err := dao.FindCollectionByNameOrId(migrations.ReactionCollectionName) if err != nil { - panic(err) + return nil, err } createTicketActionData, err := json.Marshal(map[string]interface{}{ @@ -334,7 +381,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record { "script": createTicketPy, }) if err != nil { - panic(err) + return nil, err } record := models.NewRecord(collection) @@ -352,7 +399,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record { "script": alertIngestPy, }) if err != nil { - panic(err) + return nil, err } record = models.NewRecord(collection) @@ -370,7 +417,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record { "script": assignTicketsPy, }) if err != nil { - panic(err) + return nil, err } record = models.NewRecord(collection) @@ -383,5 +430,5 @@ func reactionRecords(dao *daos.Dao) []*models.Record { records = append(records, record) - return records + return records, nil } diff --git a/upgradetest/data/v0.14.1/data.db b/upgradetest/data/v0.14.1/data.db new file mode 100644 index 0000000000000000000000000000000000000000..97c7c78959a397be780a4206052ca6141b3b6bc9 GIT binary patch literal 208896 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 F{C^-&*dG7@ literal 0 HcmV?d00001 diff --git a/upgradetest/upgrade_test.go b/upgradetest/upgrade_test.go new file mode 100644 index 0000000..9f5ac23 --- /dev/null +++ b/upgradetest/upgrade_test.go @@ -0,0 +1,44 @@ +package upgradetest + +import ( + "fmt" + "log" + "os" + "path/filepath" + "testing" + + "github.com/SecurityBrewery/catalyst/app" + "github.com/SecurityBrewery/catalyst/fakedata" +) + +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() + + pb, err := app.App(filepath.Join("data", entry.Name()), true) + if err != nil { + log.Fatal(err) + } + + if err := pb.Bootstrap(); err != nil { + t.Fatal(fmt.Errorf("failed to bootstrap: %w", err)) + } + + if err := fakedata.ValidateDefaultData(pb); err != nil { + log.Fatal(err) + } + }) + } +}