From 1abb542c2e9722c1478b9a21ca953f314edd32d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 8 May 2026 19:16:58 +0400 Subject: [PATCH] tests: fix tracker_pattern SCRIPT case, add tracker_resource e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix tracker_pattern_test.go: replace SCRIPT (removed from enum) with LOCAL_STORAGE. - Add tracker_resource_test.go with full CRUD, RBAC (viewer denied), cross-banner mismatch, and list coverage. - Add CreateTrackerResource factory helper. Signed-off-by: Émile Ré --- e2e/console/tracker_pattern_test.go | 4 +- e2e/console/tracker_resource_test.go | 444 +++++++++++++++++++++++++++ e2e/internal/factory/factory.go | 43 +++ 3 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 e2e/console/tracker_resource_test.go diff --git a/e2e/console/tracker_pattern_test.go b/e2e/console/tracker_pattern_test.go index b3721baea..ad5e1022b 100644 --- a/e2e/console/tracker_pattern_test.go +++ b/e2e/console/tracker_pattern_test.go @@ -83,7 +83,7 @@ func TestTrackerPattern_Create(t *testing.T) { "cookieCategoryId": categoryID, "pattern": "_ga", "matchType": "EXACT", - "trackerType": "SCRIPT", + "trackerType": "LOCAL_STORAGE", "displayName": "Google Analytics", "maxAgeSeconds": maxAge, "description": "Google Analytics tracking cookie", @@ -95,7 +95,7 @@ func TestTrackerPattern_Create(t *testing.T) { assert.NotEmpty(t, node.ID) assert.Equal(t, "_ga", node.Pattern) assert.Equal(t, "EXACT", node.MatchType) - assert.Equal(t, "SCRIPT", node.TrackerType) + assert.Equal(t, "LOCAL_STORAGE", node.TrackerType) assert.Equal(t, "Google Analytics", node.DisplayName) require.NotNil(t, node.MaxAgeSeconds) assert.Equal(t, maxAge, *node.MaxAgeSeconds) diff --git a/e2e/console/tracker_resource_test.go b/e2e/console/tracker_resource_test.go new file mode 100644 index 000000000..af68caf06 --- /dev/null +++ b/e2e/console/tracker_resource_test.go @@ -0,0 +1,444 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package console_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" + "go.probo.inc/probo/e2e/internal/testutil" +) + +func TestTrackerResource_Create(t *testing.T) { + t.Parallel() + + t.Run("with all fields", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + + const query = ` + mutation CreateTrackerResource($input: CreateTrackerResourceInput!) { + createTrackerResource(input: $input) { + trackerResourceEdge { + node { + id + type + origin + path + displayName + description + excluded + } + } + cookieBanner { + id + } + } + } + ` + + var result struct { + CreateTrackerResource struct { + TrackerResourceEdge struct { + Node struct { + ID string `json:"id"` + Type string `json:"type"` + Origin string `json:"origin"` + Path string `json:"path"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Excluded bool `json:"excluded"` + } `json:"node"` + } `json:"trackerResourceEdge"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"createTrackerResource"` + } + + err := owner.Execute(query, map[string]any{ + "input": map[string]any{ + "cookieCategoryId": categoryID, + "type": "SCRIPT", + "origin": "https://example.com", + "path": "/script.js", + "displayName": "Example Script", + "description": "Test", + }, + }, &result) + + require.NoError(t, err) + node := result.CreateTrackerResource.TrackerResourceEdge.Node + assert.NotEmpty(t, node.ID) + assert.Equal(t, "SCRIPT", node.Type) + assert.Equal(t, "https://example.com", node.Origin) + assert.Equal(t, "/script.js", node.Path) + assert.Equal(t, "Example Script", node.DisplayName) + assert.Equal(t, "Test", node.Description) + assert.False(t, node.Excluded) + assert.Equal(t, bannerID, result.CreateTrackerResource.CookieBanner.ID) + }) +} + +func TestTrackerResource_List(t *testing.T) { + t.Parallel() + + t.Run("via uncategorisedTrackerResources connection", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + factory.CreateTrackerResource(owner, categoryID) + factory.CreateTrackerResource(owner, categoryID) + + const query = ` + query($id: ID!) { + node(id: $id) { + ... on CookieBanner { + uncategorisedTrackerResources(first: 10) { + totalCount + edges { + node { + id + type + origin + displayName + } + } + pageInfo { + hasNextPage + hasPreviousPage + } + } + } + } + } + ` + + var result struct { + Node struct { + UncategorisedTrackerResources struct { + TotalCount int `json:"totalCount"` + Edges []struct { + Node struct { + ID string `json:"id"` + Type string `json:"type"` + Origin string `json:"origin"` + DisplayName string `json:"displayName"` + } `json:"node"` + } `json:"edges"` + PageInfo struct { + HasNextPage bool `json:"hasNextPage"` + HasPreviousPage bool `json:"hasPreviousPage"` + } `json:"pageInfo"` + } `json:"uncategorisedTrackerResources"` + } `json:"node"` + } + + err := owner.Execute(query, map[string]any{"id": bannerID}, &result) + require.NoError(t, err) + assert.GreaterOrEqual(t, result.Node.UncategorisedTrackerResources.TotalCount, 2) + assert.GreaterOrEqual(t, len(result.Node.UncategorisedTrackerResources.Edges), 2) + }) +} + +func TestTrackerResource_Update(t *testing.T) { + t.Parallel() + + t.Run("update displayName, description, and excluded", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + resourceID := factory.CreateTrackerResource(owner, categoryID, factory.Attrs{ + "displayName": "Original Name", + "description": "Original description", + }) + + const query = ` + mutation UpdateTrackerResource($input: UpdateTrackerResourceInput!) { + updateTrackerResource(input: $input) { + trackerResource { + id + displayName + description + excluded + } + cookieBanner { + id + } + } + } + ` + + var result struct { + UpdateTrackerResource struct { + TrackerResource struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + Excluded bool `json:"excluded"` + } `json:"trackerResource"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"updateTrackerResource"` + } + + err := owner.Execute(query, map[string]any{ + "input": map[string]any{ + "trackerResourceId": resourceID, + "displayName": "Updated Name", + "description": "Updated description", + "excluded": true, + }, + }, &result) + + require.NoError(t, err) + assert.Equal(t, resourceID, result.UpdateTrackerResource.TrackerResource.ID) + assert.Equal(t, "Updated Name", result.UpdateTrackerResource.TrackerResource.DisplayName) + assert.Equal(t, "Updated description", result.UpdateTrackerResource.TrackerResource.Description) + assert.True(t, result.UpdateTrackerResource.TrackerResource.Excluded) + assert.Equal(t, bannerID, result.UpdateTrackerResource.CookieBanner.ID) + }) +} + +func TestTrackerResource_MoveToCategory(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + categoryA := factory.CreateCookieCategory(owner, bannerID, factory.Attrs{"slug": "res-cat-a-move"}) + categoryB := factory.CreateCookieCategory(owner, bannerID, factory.Attrs{"slug": "res-cat-b-move"}) + resourceID := factory.CreateTrackerResource(owner, categoryA) + + const query = ` + mutation MoveTrackerResourceToCategory($input: MoveTrackerResourceToCategoryInput!) { + moveTrackerResourceToCategory(input: $input) { + trackerResource { + id + } + cookieBanner { + id + } + } + } + ` + + var result struct { + MoveTrackerResourceToCategory struct { + TrackerResource struct { + ID string `json:"id"` + } `json:"trackerResource"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"moveTrackerResourceToCategory"` + } + + err := owner.Execute(query, map[string]any{ + "input": map[string]any{ + "trackerResourceId": resourceID, + "targetCookieCategoryId": categoryB, + }, + }, &result) + + require.NoError(t, err) + assert.Equal(t, resourceID, result.MoveTrackerResourceToCategory.TrackerResource.ID) + assert.Equal(t, bannerID, result.MoveTrackerResourceToCategory.CookieBanner.ID) + }) + + t.Run("cross-banner mismatch error", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + banner1 := factory.CreateCookieBanner(owner) + banner2 := factory.CreateCookieBanner(owner) + category1 := factory.CreateCookieCategory(owner, banner1, factory.Attrs{"slug": "res-cat-x-mismatch"}) + category2 := factory.CreateCookieCategory(owner, banner2, factory.Attrs{"slug": "res-cat-y-mismatch"}) + resourceID := factory.CreateTrackerResource(owner, category1) + + _, err := owner.Do(` + mutation MoveTrackerResourceToCategory($input: MoveTrackerResourceToCategoryInput!) { + moveTrackerResourceToCategory(input: $input) { + trackerResource { id } + cookieBanner { id } + } + } + `, map[string]any{ + "input": map[string]any{ + "trackerResourceId": resourceID, + "targetCookieCategoryId": category2, + }, + }) + require.Error(t, err) + }) +} + +func TestTrackerResource_Delete(t *testing.T) { + t.Parallel() + + t.Run("success", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + resourceID := factory.CreateTrackerResource(owner, categoryID) + + const query = ` + mutation DeleteTrackerResource($input: DeleteTrackerResourceInput!) { + deleteTrackerResource(input: $input) { + deletedTrackerResourceId + cookieBanner { + id + } + } + } + ` + + var result struct { + DeleteTrackerResource struct { + DeletedTrackerResourceID string `json:"deletedTrackerResourceId"` + CookieBanner struct { + ID string `json:"id"` + } `json:"cookieBanner"` + } `json:"deleteTrackerResource"` + } + + err := owner.Execute(query, map[string]any{ + "input": map[string]any{"trackerResourceId": resourceID}, + }, &result) + + require.NoError(t, err) + assert.Equal(t, resourceID, result.DeleteTrackerResource.DeletedTrackerResourceID) + assert.Equal(t, bannerID, result.DeleteTrackerResource.CookieBanner.ID) + }) +} + +func TestTrackerResource_RBAC(t *testing.T) { + t.Parallel() + + t.Run("viewer cannot create resource", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + + _, err := viewer.Do(` + mutation CreateTrackerResource($input: CreateTrackerResourceInput!) { + createTrackerResource(input: $input) { + trackerResourceEdge { node { id } } + cookieBanner { id } + } + } + `, map[string]any{ + "input": map[string]any{ + "cookieCategoryId": categoryID, + "type": "SCRIPT", + "origin": "https://example.com", + "path": "/viewer.js", + "displayName": "Test Viewer Resource", + "description": "Should fail", + }, + }) + testutil.RequireForbiddenError(t, err, "viewer should not be able to create tracker resource") + }) + + t.Run("viewer cannot update resource", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + resourceID := factory.CreateTrackerResource(owner, categoryID) + + _, err := viewer.Do(` + mutation UpdateTrackerResource($input: UpdateTrackerResourceInput!) { + updateTrackerResource(input: $input) { + trackerResource { id } + cookieBanner { id } + } + } + `, map[string]any{ + "input": map[string]any{ + "trackerResourceId": resourceID, + "description": "Updated by Viewer", + }, + }) + testutil.RequireForbiddenError(t, err, "viewer should not be able to update tracker resource") + }) + + t.Run("viewer cannot delete resource", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + + bannerID := factory.CreateCookieBanner(owner) + categoryID := factory.CreateCookieCategory(owner, bannerID) + resourceID := factory.CreateTrackerResource(owner, categoryID) + + _, err := viewer.Do(` + mutation DeleteTrackerResource($input: DeleteTrackerResourceInput!) { + deleteTrackerResource(input: $input) { + deletedTrackerResourceId + cookieBanner { id } + } + } + `, map[string]any{ + "input": map[string]any{"trackerResourceId": resourceID}, + }) + testutil.RequireForbiddenError(t, err, "viewer should not be able to delete tracker resource") + }) + + t.Run("viewer cannot move resource", func(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) + + bannerID := factory.CreateCookieBanner(owner) + categoryA := factory.CreateCookieCategory(owner, bannerID, factory.Attrs{"slug": "rbac-res-move-a"}) + categoryB := factory.CreateCookieCategory(owner, bannerID, factory.Attrs{"slug": "rbac-res-move-b"}) + resourceID := factory.CreateTrackerResource(owner, categoryA) + + _, err := viewer.Do(` + mutation MoveTrackerResourceToCategory($input: MoveTrackerResourceToCategoryInput!) { + moveTrackerResourceToCategory(input: $input) { + trackerResource { id } + cookieBanner { id } + } + } + `, map[string]any{ + "input": map[string]any{ + "trackerResourceId": resourceID, + "targetCookieCategoryId": categoryB, + }, + }) + testutil.RequireForbiddenError(t, err, "viewer should not be able to move tracker resource") + }) +} diff --git a/e2e/internal/factory/factory.go b/e2e/internal/factory/factory.go index 0d8d3e72f..caa715502 100644 --- a/e2e/internal/factory/factory.go +++ b/e2e/internal/factory/factory.go @@ -1388,3 +1388,46 @@ func CreateTrackerPattern(c *testutil.Client, categoryID string, attrs ...Attrs) return result.CreateTrackerPattern.TrackerPatternEdge.Node.ID } + +func CreateTrackerResource(c *testutil.Client, categoryID string, attrs ...Attrs) string { + c.T.Helper() + + var a Attrs + if len(attrs) > 0 { + a = attrs[0] + } + + const query = ` + mutation($input: CreateTrackerResourceInput!) { + createTrackerResource(input: $input) { + trackerResourceEdge { + node { id } + } + } + } + ` + + input := map[string]any{ + "cookieCategoryId": categoryID, + "type": a.getString("type", "SCRIPT"), + "origin": a.getString("origin", SafeOrigin()), + "path": a.getString("path", "/"+gofakeit.LetterN(8)+".js"), + "displayName": a.getString("displayName", SafeName("Resource")), + "description": a.getString("description", "Test tracker resource"), + } + + var result struct { + CreateTrackerResource struct { + TrackerResourceEdge struct { + Node struct { + ID string `json:"id"` + } `json:"node"` + } `json:"trackerResourceEdge"` + } `json:"createTrackerResource"` + } + + err := c.Execute(query, map[string]any{"input": input}, &result) + require.NoError(c.T, err, "createTrackerResource mutation failed") + + return result.CreateTrackerResource.TrackerResourceEdge.Node.ID +}