From 462b8e37233d8b55c7324e0fef1a7dc4e7d9b079 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 07:30:24 +1100 Subject: [PATCH 1/6] moving method for more coheasion --- internal/organisations/service.go | 19 ------------------- internal/organisations/service_test.go | 11 ----------- internal/products/interfaces.go | 1 + internal/products/service.go | 19 +++++++++++++++++++ internal/products/service_test.go | 14 ++++++++++++++ internal/watchtower/products.go | 2 +- 6 files changed, 35 insertions(+), 31 deletions(-) diff --git a/internal/organisations/service.go b/internal/organisations/service.go index 491ef80..9821f44 100644 --- a/internal/organisations/service.go +++ b/internal/organisations/service.go @@ -218,22 +218,3 @@ func (s Service) Update(ctx context.Context, params UpdateOrgParams) (Organisati return toOrganisationDTO(model), nil } - -// AssociateProductToOrg associates a product with an organisation based on the provided organisation and product IDs. -func (s Service) AssociateProductToOrg(ctx context.Context, orgID int64, productID int64) error { - logger := logging.FromContext(ctx).With("service", "organisations") - - logger.Debug("Associating product to organisation", "orgID", orgID, "productID", productID) - - err := s.store.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ - OrganisationID: sql.NullInt64{Int64: orgID, Valid: true}, - ProductID: sql.NullInt64{Int64: productID, Valid: true}, - }) - - if err != nil { - logger.Error("Error associating product to organisation", "error", err) - return err - } - - return nil -} diff --git a/internal/organisations/service_test.go b/internal/organisations/service_test.go index 84d66fb..55d7804 100644 --- a/internal/organisations/service_test.go +++ b/internal/organisations/service_test.go @@ -116,17 +116,6 @@ func TestService(t *testing.T) { odize.AssertNoError(t, err) }). - Test("AssociateProductToOrg should create link in product_organisations", func(t *testing.T) { - org, _ := s.Create(ctx, CreateOrgParams{FriendlyName: "Product Org", Namespace: "prod-ns"}) - productID := int64(123) - - err := s.AssociateProductToOrg(ctx, org.ID, productID) - odize.AssertNoError(t, err) - - fetchedOrg, err := s.GetOrgAssociatedToProduct(ctx, productID) - odize.AssertNoError(t, err) - odize.AssertEqual(t, org.ID, fetchedOrg.ID) - }). Run() odize.AssertNoError(t, err) diff --git a/internal/products/interfaces.go b/internal/products/interfaces.go index 970dfb8..4c44961 100644 --- a/internal/products/interfaces.go +++ b/internal/products/interfaces.go @@ -22,6 +22,7 @@ type ProductBaseStore interface { UpdateProduct(ctx context.Context, arg database.UpdateProductParams) (database.Product, error) UpdateProductSync(ctx context.Context, id int64) error DeleteProduct(ctx context.Context, id int64) error + AddProductToOrganisation(ctx context.Context, arg database.AddProductToOrganisationParams) error } type RepoStore interface { diff --git a/internal/products/service.go b/internal/products/service.go index 64568c1..4abf0a9 100644 --- a/internal/products/service.go +++ b/internal/products/service.go @@ -554,3 +554,22 @@ func (s *Service) BulkInsertRepoDetails(ctx context.Context, repoDetails github. return nil } + +// AssociateProductToOrg associates a product with an organisation based on the provided organisation and product IDs. +func (s *Service) AssociateProductToOrg(ctx context.Context, orgID int64, productID int64) error { + logger := logging.FromContext(ctx).With("service", "products") + + logger.Debug("Associating product to organisation", "orgID", orgID, "productID", productID) + + err := s.store.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ + OrganisationID: sql.NullInt64{Int64: orgID, Valid: true}, + ProductID: sql.NullInt64{Int64: productID, Valid: true}, + }) + + if err != nil { + logger.Error("Error associating product to organisation", "error", err) + return err + } + + return nil +} diff --git a/internal/products/service_test.go b/internal/products/service_test.go index 67954f0..10e1df5 100644 --- a/internal/products/service_test.go +++ b/internal/products/service_test.go @@ -508,6 +508,20 @@ func TestService(t *testing.T) { odize.AssertEqual(t, "MEDIUM", updated.Severity) odize.AssertEqual(t, sec.ID, updated.ID) }). + Test("AssociateProductToOrg should create link in product_organisations", func(t *testing.T) { + org, _ := _testDB.CreateOrganisation(ctx, database.CreateOrganisationParams{ + FriendlyName: "Product Org", + Namespace: "prod-ns", + }) + productID := int64(123) + + err := s.AssociateProductToOrg(ctx, org.ID, productID) + odize.AssertNoError(t, err) + + fetchedOrg, err := _testDB.GetOrganisationForProduct(ctx, sql.NullInt64{Int64: productID, Valid: true}) + odize.AssertNoError(t, err) + odize.AssertEqual(t, org.ID, fetchedOrg.ID) + }). Run() odize.AssertNoError(t, err) diff --git a/internal/watchtower/products.go b/internal/watchtower/products.go index a1566b4..65d4ed9 100644 --- a/internal/watchtower/products.go +++ b/internal/watchtower/products.go @@ -14,7 +14,7 @@ func (s *Service) CreateProduct(name string, description string, tags []string, return products.ProductDTO{}, err } - err = s.orgSvc.AssociateProductToOrg(s.ctx, organisationID, prod.ID) + err = s.productSvc.AssociateProductToOrg(s.ctx, organisationID, prod.ID) if err != nil { return products.ProductDTO{}, err } From bcb26723c1b938bedb3be5b20c12c267f4ab7a79 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 07:44:12 +1100 Subject: [PATCH 2/6] refactor and clean up --- internal/products/service.go | 29 +++++-------- internal/products/service_test.go | 70 +++++++++++++------------------ internal/products/types.go | 7 ++-- internal/watchtower/products.go | 20 +++------ 4 files changed, 48 insertions(+), 78 deletions(-) diff --git a/internal/products/service.go b/internal/products/service.go index 4abf0a9..33d362a 100644 --- a/internal/products/service.go +++ b/internal/products/service.go @@ -47,6 +47,16 @@ func (s *Service) Create(ctx context.Context, params CreateProductParams) (Produ return ProductDTO{}, err } + err = s.store.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ + OrganisationID: sql.NullInt64{Int64: params.OrganisationID, Valid: true}, + ProductID: sql.NullInt64{Int64: prod.ID, Valid: true}, + }) + if err != nil { + logger.Error("Error associating product to organisation", "error", err) + + return ProductDTO{}, err + } + return toProductDTO(prod), nil } @@ -554,22 +564,3 @@ func (s *Service) BulkInsertRepoDetails(ctx context.Context, repoDetails github. return nil } - -// AssociateProductToOrg associates a product with an organisation based on the provided organisation and product IDs. -func (s *Service) AssociateProductToOrg(ctx context.Context, orgID int64, productID int64) error { - logger := logging.FromContext(ctx).With("service", "products") - - logger.Debug("Associating product to organisation", "orgID", orgID, "productID", productID) - - err := s.store.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ - OrganisationID: sql.NullInt64{Int64: orgID, Valid: true}, - ProductID: sql.NullInt64{Int64: productID, Valid: true}, - }) - - if err != nil { - logger.Error("Error associating product to organisation", "error", err) - return err - } - - return nil -} diff --git a/internal/products/service_test.go b/internal/products/service_test.go index 10e1df5..cab04ce 100644 --- a/internal/products/service_test.go +++ b/internal/products/service_test.go @@ -24,11 +24,16 @@ func TestService(t *testing.T) { }) err := group. - Test("Create should create a product", func(t *testing.T) { + Test("Create should create a product and associate it with an organisation", func(t *testing.T) { + org, _ := _testDB.CreateOrganisation(ctx, database.CreateOrganisationParams{ + FriendlyName: "Test Org Create", + Namespace: "test-ns-create", + }) params := CreateProductParams{ - Name: "Test Product", - Desc: "Test Description", - Tags: []string{"tag1", "tag2"}, + Name: "Test Product", + Desc: "Test Description", + Tags: []string{"tag1", "tag2"}, + OrganisationID: org.ID, } prod, err := s.Create(ctx, params) @@ -38,6 +43,11 @@ func TestService(t *testing.T) { odize.AssertEqual(t, params.Desc, prod.Description) odize.AssertEqual(t, 2, len(prod.Tags)) odize.AssertTrue(t, prod.ID > 0) + + // Verify association + fetchedOrg, err := _testDB.GetOrganisationForProduct(ctx, sql.NullInt64{Int64: prod.ID, Valid: true}) + odize.AssertNoError(t, err) + odize.AssertEqual(t, org.ID, fetchedOrg.ID) }). Test("Get should return a product", func(t *testing.T) { params := CreateProductParams{ @@ -248,11 +258,12 @@ func TestService(t *testing.T) { }). Test("GetPullRequests and GetPullRequestByOrg", func(t *testing.T) { tag := fmt.Sprintf("pr-tag-%d", time.Now().UnixNano()) - prod, _ := s.Create(ctx, CreateProductParams{Name: "PR Product", Tags: []string{tag}}) orgID := int64(456) - _ = _testDB.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ - ProductID: sql.NullInt64{Int64: prod.ID, Valid: true}, - OrganisationID: sql.NullInt64{Int64: orgID, Valid: true}, + + prod, _ := s.Create(ctx, CreateProductParams{ + Name: "PR Product", + Tags: []string{tag}, + OrganisationID: orgID, }) repoName := "pr-repo" @@ -281,6 +292,7 @@ func TestService(t *testing.T) { odize.AssertTrue(t, len(orgPrs) > 0) }). Test("GetRecentPullRequests should return external IDs of recent PRs", func(t *testing.T) { + params := CreatePRParams{ ExternalID: uuid.New().String(), Title: uuid.New().String(), @@ -294,24 +306,14 @@ func TestService(t *testing.T) { err := s.CreateRepo(ctx, CreateRepoParams{Name: params.RepositoryName, Topic: "tag", Owner: "owner"}) odize.AssertNoError(t, err) - prodID, err := s.Create(ctx, CreateProductParams{ - Name: params.RepositoryName, - Desc: "", - Tags: []string{"tag"}, + _, err = s.Create(ctx, CreateProductParams{ + Name: params.RepositoryName, + Desc: "", + Tags: []string{"tag"}, + OrganisationID: 1, }) odize.AssertNoError(t, err) - _ = _testDB.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ - ProductID: sql.NullInt64{ - Int64: prodID.ID, - Valid: true, - }, - OrganisationID: sql.NullInt64{ - Int64: 0, - Valid: true, - }, - }) - err = s.CreatePullRequest(ctx, params) odize.AssertNoError(t, err) @@ -330,11 +332,11 @@ func TestService(t *testing.T) { }). Test("GetSecurity and GetSecurityByOrg", func(t *testing.T) { tag := fmt.Sprintf("sec-tag-%d", time.Now().UnixNano()) - prod, _ := s.Create(ctx, CreateProductParams{Name: "Sec Product", Tags: []string{tag}}) orgID := int64(789) - _ = _testDB.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ - ProductID: sql.NullInt64{Int64: prod.ID, Valid: true}, - OrganisationID: sql.NullInt64{Int64: orgID, Valid: true}, + prod, _ := s.Create(ctx, CreateProductParams{ + Name: "Sec Product", + Tags: []string{tag}, + OrganisationID: orgID, }) repoName := "sec-repo" @@ -508,20 +510,6 @@ func TestService(t *testing.T) { odize.AssertEqual(t, "MEDIUM", updated.Severity) odize.AssertEqual(t, sec.ID, updated.ID) }). - Test("AssociateProductToOrg should create link in product_organisations", func(t *testing.T) { - org, _ := _testDB.CreateOrganisation(ctx, database.CreateOrganisationParams{ - FriendlyName: "Product Org", - Namespace: "prod-ns", - }) - productID := int64(123) - - err := s.AssociateProductToOrg(ctx, org.ID, productID) - odize.AssertNoError(t, err) - - fetchedOrg, err := _testDB.GetOrganisationForProduct(ctx, sql.NullInt64{Int64: productID, Valid: true}) - odize.AssertNoError(t, err) - odize.AssertEqual(t, org.ID, fetchedOrg.ID) - }). Run() odize.AssertNoError(t, err) diff --git a/internal/products/types.go b/internal/products/types.go index 2dac93c..ad143b9 100644 --- a/internal/products/types.go +++ b/internal/products/types.go @@ -59,9 +59,10 @@ type SecurityDTO struct { } type CreateProductParams struct { - Name string - Desc string - Tags []string + Name string + Desc string + Tags []string + OrganisationID int64 } type UpdateProductParams struct { diff --git a/internal/watchtower/products.go b/internal/watchtower/products.go index 65d4ed9..d1e4ee0 100644 --- a/internal/watchtower/products.go +++ b/internal/watchtower/products.go @@ -4,22 +4,12 @@ import "watchtower/internal/products" // CreateProduct creates a new product and associates it with an organisation. func (s *Service) CreateProduct(name string, description string, tags []string, organisationID int64) (products.ProductDTO, error) { - - prod, err := s.productSvc.Create(s.ctx, products.CreateProductParams{ - Name: name, - Tags: tags, - Desc: description, + return s.productSvc.Create(s.ctx, products.CreateProductParams{ + Name: name, + Tags: tags, + Desc: description, + OrganisationID: organisationID, }) - if err != nil { - return products.ProductDTO{}, err - } - - err = s.productSvc.AssociateProductToOrg(s.ctx, organisationID, prod.ID) - if err != nil { - return products.ProductDTO{}, err - } - - return prod, nil } // GetProductByID fetches a product by its ID. From ce255373f01e4677161fa44dabea4dc1637fbddf Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 07:56:01 +1100 Subject: [PATCH 3/6] adding tests --- internal/organisations/service_test.go | 28 ++++++++++ internal/watchtower/organisations_test.go | 68 +++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 internal/watchtower/organisations_test.go diff --git a/internal/organisations/service_test.go b/internal/organisations/service_test.go index 55d7804..ec29eee 100644 --- a/internal/organisations/service_test.go +++ b/internal/organisations/service_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "testing" + "watchtower/internal/database" "github.com/code-gorilla-au/odize" ) @@ -116,6 +117,33 @@ func TestService(t *testing.T) { odize.AssertNoError(t, err) }). + Test("GetOrgAssociatedToProduct should return the organisation associated with a product", func(t *testing.T) { + + org, err := s.Create(ctx, CreateOrgParams{ + FriendlyName: "Associated Org", + Namespace: "assoc-ns", + }) + odize.AssertNoError(t, err) + + prod, err := _testDB.CreateProduct(ctx, database.CreateProductParams{ + Name: "Test Product", + Description: "Test Description", + }) + odize.AssertNoError(t, err) + + err = _testDB.AddProductToOrganisation(ctx, database.AddProductToOrganisationParams{ + ProductID: sql.NullInt64{Int64: prod.ID, Valid: true}, + OrganisationID: sql.NullInt64{Int64: org.ID, Valid: true}, + }) + odize.AssertNoError(t, err) + + fetchedOrg, err := s.GetOrgAssociatedToProduct(ctx, prod.ID) + odize.AssertNoError(t, err) + + odize.AssertEqual(t, org.ID, fetchedOrg.ID) + odize.AssertEqual(t, org.FriendlyName, fetchedOrg.FriendlyName) + odize.AssertEqual(t, org.Namespace, fetchedOrg.Namespace) + }). Run() odize.AssertNoError(t, err) diff --git a/internal/watchtower/organisations_test.go b/internal/watchtower/organisations_test.go new file mode 100644 index 0000000..de3ff0a --- /dev/null +++ b/internal/watchtower/organisations_test.go @@ -0,0 +1,68 @@ +package watchtower + +import ( + "context" + "testing" + + "github.com/code-gorilla-au/odize" +) + +func TestService_Organisations(t *testing.T) { + group := odize.NewGroup(t, nil) + + var s *Service + ctx := context.Background() + + group.BeforeEach(func() { + s = NewService(ctx, _testDB, _testTxnDB) + }) + + err := group. + Test("DeleteAllOrgs should delete all organisations and their products", func(t *testing.T) { + // 1. Create orgs + org1, err := s.CreateOrganisation("Org 1", "ns-1", "token-1", "desc-1") + odize.AssertNoError(t, err) + + org2, err := s.CreateOrganisation("Org 2", "ns-2", "token-2", "desc-2") + odize.AssertNoError(t, err) + + // 2. Create products for orgs + _, err = s.CreateProduct("Prod 1", "desc-p1", []string{"tag1"}, org1.ID) + odize.AssertNoError(t, err) + + _, err = s.CreateProduct("Prod 2", "desc-p2", []string{"tag2"}, org2.ID) + odize.AssertNoError(t, err) + + // 3. Verify they exist + allOrgs, err := s.GetAllOrganisations() + odize.AssertNoError(t, err) + odize.AssertTrue(t, len(allOrgs) >= 2) + + prods1, err := s.GetAllProductsForOrganisation(org1.ID) + odize.AssertNoError(t, err) + odize.AssertEqual(t, 1, len(prods1)) + + prods2, err := s.GetAllProductsForOrganisation(org2.ID) + odize.AssertNoError(t, err) + odize.AssertEqual(t, 1, len(prods2)) + + // 4. Delete all + err = s.DeleteAllOrgs() + odize.AssertNoError(t, err) + + // 5. Verify they are gone + remainingOrgs, err := s.GetAllOrganisations() + odize.AssertNoError(t, err) + odize.AssertEqual(t, 0, len(remainingOrgs)) + + // Check if products are also gone + remainingProds1, _ := s.productSvc.GetByOrg(ctx, org1.ID) + odize.AssertEqual(t, 0, len(remainingProds1)) + + remainingProds2, _ := s.productSvc.GetByOrg(ctx, org2.ID) + odize.AssertEqual(t, 0, len(remainingProds2)) + }). + Run() + + odize.AssertNoError(t, err) +} From 54c861d3326766518a089aff8555bbc54f6e2652 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 08:02:06 +1100 Subject: [PATCH 4/6] adding tests for notifications --- internal/watchtower/notifications_test.go | 81 +++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/internal/watchtower/notifications_test.go b/internal/watchtower/notifications_test.go index b38646f..d271ec4 100644 --- a/internal/watchtower/notifications_test.go +++ b/internal/watchtower/notifications_test.go @@ -2,9 +2,11 @@ package watchtower import ( "context" + "strings" "testing" "time" "watchtower/internal/notifications" + "watchtower/internal/products" "github.com/code-gorilla-au/odize" "github.com/google/uuid" @@ -82,6 +84,85 @@ func TestService_Notifications(t *testing.T) { odize.AssertEqual(t, 0, len(unread)) }). + Test("CreateUnreadPRNotification should create notifications for recent PRs", func(t *testing.T) { + // Setup: Org, Product, Repo, PR + org, err := s.CreateOrganisation("Test Org PR", "test-org-pr", "token", "desc") + odize.AssertNoError(t, err) + + _, err = s.CreateProduct("Test Product PR", "desc", []string{"tag-pr"}, org.ID) + odize.AssertNoError(t, err) + + err = s.productSvc.CreateRepo(ctx, products.CreateRepoParams{ + Name: "repo-pr", + Url: "url", + Topic: "tag-pr", + Owner: "owner", + }) + odize.AssertNoError(t, err) + + externalID := uuid.New().String() + err = s.productSvc.CreatePullRequest(ctx, products.CreatePRParams{ + ExternalID: externalID, + Title: "PR Title", + RepositoryName: "repo-pr", + Url: "url", + State: "OPEN", + Author: "author", + CreatedAt: time.Now(), + }) + odize.AssertNoError(t, err) + + // Action + err = s.CreateUnreadPRNotification() + odize.AssertNoError(t, err) + + // Verify + unreadNotification, err := s.notificationSvc.GetNotificationByExternalID(ctx, externalID) + odize.AssertNoError(t, err) + + odize.AssertEqual(t, "OPEN_PULL_REQUEST", unreadNotification.Type) + odize.AssertTrue(t, strings.Contains(unreadNotification.Content, "repo-pr")) + odize.AssertTrue(t, strings.Contains(unreadNotification.Content, "New pull request")) + }). + Test("CreateUnreadSecurityNotification should create notifications for recent security alerts", func(t *testing.T) { + // Setup: Org, Product, Repo, Security Alert + org, err := s.CreateOrganisation("Test Org Sec", "test-org-sec", "token", "desc") + odize.AssertNoError(t, err) + + _, err = s.CreateProduct("Test Product Sec", "desc", []string{"tag-sec"}, org.ID) + odize.AssertNoError(t, err) + + err = s.productSvc.CreateRepo(ctx, products.CreateRepoParams{ + Name: "repo-sec", + Url: "url", + Topic: "tag-sec", + Owner: "owner", + }) + odize.AssertNoError(t, err) + + externalID := uuid.New().String() + err = s.productSvc.UpsertSecurity(ctx, products.CreateSecurityParams{ + ExternalID: externalID, + RepositoryName: "repo-sec", + PackageName: "pkg", + State: "OPEN", + Severity: "HIGH", + CreatedAt: time.Now(), + }) + odize.AssertNoError(t, err) + + // Action + err = s.CreateUnreadSecurityNotification() + odize.AssertNoError(t, err) + + // Verify + unreadNotification, err := s.notificationSvc.GetNotificationByExternalID(ctx, externalID) + odize.AssertNoError(t, err) + + odize.AssertEqual(t, "OPEN_SECURITY_ALERT", unreadNotification.Type) + odize.AssertTrue(t, strings.Contains(unreadNotification.Content, "repo-sec")) + odize.AssertTrue(t, strings.Contains(unreadNotification.Content, "New security alert")) + }). Run() odize.AssertNoError(t, err) From f569dde54319c81f16f96851727828bce925ff95 Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 08:08:27 +1100 Subject: [PATCH 5/6] adding test for sync --- internal/watchtower/sync_test.go | 123 +++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/internal/watchtower/sync_test.go b/internal/watchtower/sync_test.go index bc93946..04b367b 100644 --- a/internal/watchtower/sync_test.go +++ b/internal/watchtower/sync_test.go @@ -1855,6 +1855,129 @@ func TestService_GetSecurityByOrganisation(t *testing.T) { odize.AssertNoError(t, err) } +func TestService_SyncProduct(t *testing.T) { + group := odize.NewGroup(t, nil) + + var s *Service + var ctx context.Context + var ghMock *ghClientMock + var orgID int64 + var product products.ProductDTO + + group.BeforeAll(func() { + ctx = context.Background() + }) + + group.BeforeEach(func() { + ghMock = &ghClientMock{ + SearchReposFunc: func(owner string, topic string, token string) (github.QuerySearch[github.Repository], error) { + return github.QuerySearch[github.Repository]{ + Data: github.QueryData[github.Repository]{ + Search: github.Search[github.Repository]{ + PageInfo: github.PageInfo{}, + Edges: []github.Node[github.Repository]{ + { + Node: github.Repository{ + Url: "http://github.com/test/repo", + Name: "test-repo", + Owner: github.Owner{ + Login: owner, + }, + VulnerabilityAlerts: github.RootNode[github.VulnerabilityAlerts]{}, + PullRequests: github.RootNode[github.PullRequest]{}, + }, + }, + }, + }, + }, + }, nil + }, + GetRepoDetailsFunc: func(owner, repo, token string) (github.QueryRepository, error) { + return github.QueryRepository{ + Data: github.RepositoryData{ + Repository: github.Repository{ + Url: "http://github.com/test/repo", + Name: repo, + Owner: github.Owner{ + Login: owner, + }, + VulnerabilityAlerts: github.RootNode[github.VulnerabilityAlerts]{ + Nodes: []github.VulnerabilityAlerts{ + { + State: "OPEN", + ID: "sec-1", + SecurityVulnerability: github.SecurityVulnerability{ + Package: github.Package{Name: "pkg-1"}, + Advisory: github.Advisory{Severity: "HIGH"}, + FirstPatchedVersion: github.FirstPatchedVersion{Identifier: "1.0.1"}, + }, + CreatedAt: time.Now(), + }, + }, + }, + PullRequests: github.RootNode[github.PullRequest]{ + Nodes: []github.PullRequest{ + { + ID: "pr-1", + Title: "PR 1", + State: github.PrOpen, + Author: github.Author{Login: "author-1"}, + CreatedAt: time.Now(), + }, + }, + }, + }, + }, + }, nil + }, + } + s = &Service{ + ctx: ctx, + ghClient: ghMock, + orgSvc: organisations.New(_testDB, _testTxnDB, func(tx *sql.Tx) organisations.OrgStore { + return _testDB.WithTx(tx) + }), + productSvc: products.New(_testDB), + } + + timestamp := time.Now().UnixNano() + org, err := s.CreateOrganisation(fmt.Sprintf("prod_sync_org_%d", timestamp), fmt.Sprintf("prod_sync_ns_%d", timestamp), "token", "desc") + odize.AssertNoError(t, err) + orgID = org.ID + + product, err = s.CreateProduct(fmt.Sprintf("Sync Product %d", timestamp), "desc", []string{"tag1", "tag2"}, orgID) + odize.AssertNoError(t, err) + }) + + err := group. + Test("should successfully sync a product", func(t *testing.T) { + err := s.SyncProduct(product.ID) + odize.AssertNoError(t, err) + + prs, err := s.productSvc.GetPullRequests(ctx, product.ID) + odize.AssertNoError(t, err) + odize.AssertTrue(t, len(prs) > 0) + + sec, err := s.productSvc.GetSecurity(ctx, product.ID) + odize.AssertNoError(t, err) + odize.AssertTrue(t, len(sec) > 0) + }). + Test("should return error if product not found", func(t *testing.T) { + err := s.SyncProduct(-1) + odize.AssertError(t, err) + }). + Test("should return error if SearchRepos fails", func(t *testing.T) { + ghMock.SearchReposFunc = func(owner, topic, token string) (github.QuerySearch[github.Repository], error) { + return github.QuerySearch[github.Repository]{}, fmt.Errorf("api error") + } + + err := s.SyncProduct(product.ID) + odize.AssertError(t, err) + }). + Run() + odize.AssertNoError(t, err) +} + func TestService_SyncOrgs(t *testing.T) { group := odize.NewGroup(t, nil) From cf09652a7423d061311270e00765cbe6609d36ae Mon Sep 17 00:00:00 2001 From: frag223 Date: Fri, 9 Jan 2026 08:12:07 +1100 Subject: [PATCH 6/6] missing import --- internal/watchtower/sync_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/watchtower/sync_test.go b/internal/watchtower/sync_test.go index 04b367b..7a036a6 100644 --- a/internal/watchtower/sync_test.go +++ b/internal/watchtower/sync_test.go @@ -3,6 +3,7 @@ package watchtower import ( "context" "database/sql" + "errors" "fmt" "testing" "time" @@ -1968,7 +1969,7 @@ func TestService_SyncProduct(t *testing.T) { }). Test("should return error if SearchRepos fails", func(t *testing.T) { ghMock.SearchReposFunc = func(owner, topic, token string) (github.QuerySearch[github.Repository], error) { - return github.QuerySearch[github.Repository]{}, fmt.Errorf("api error") + return github.QuerySearch[github.Repository]{}, errors.New("api error") } err := s.SyncProduct(product.ID)