Exclude uncategorised category from consent contract
The uncategorised category is an admin-side inbox for detected cookies and should not be part of the visitor-facing consent contract. Filter it out of snapshots so changes to uncategorised patterns no longer trigger version bumps. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
package console_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -526,6 +530,66 @@ func TestCookieBannerVersioning_TranslationChangesNeverBump(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func activateBanner(t *testing.T, c *testutil.Client, bannerID string) {
|
||||
t.Helper()
|
||||
|
||||
const query = `
|
||||
mutation ActivateCookieBanner($input: ActivateCookieBannerInput!) {
|
||||
activateCookieBanner(input: $input) {
|
||||
cookieBanner { id state }
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
var result struct{}
|
||||
require.NoError(t, c.Execute(query, map[string]any{
|
||||
"input": map[string]any{"cookieBannerId": bannerID},
|
||||
}, &result), "activateCookieBanner mutation failed")
|
||||
}
|
||||
|
||||
func reportDetectedCookies(t *testing.T, c *testutil.Client, bannerID string, names ...string) {
|
||||
t.Helper()
|
||||
|
||||
type entry struct {
|
||||
Name string `json:"name"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
cookies := make([]entry, len(names))
|
||||
for i, n := range names {
|
||||
cookies[i] = entry{Name: n, Source: "script"}
|
||||
}
|
||||
|
||||
body, err := json.Marshal(map[string]any{"cookies": cookies})
|
||||
require.NoError(t, err)
|
||||
|
||||
url := fmt.Sprintf("%s/cookie-banner/v1/%s/detected-cookies", c.BaseURL(), bannerID)
|
||||
resp, err := c.HTTPClient().Post(url, "application/json", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode,
|
||||
"detected-cookies endpoint should return 204")
|
||||
}
|
||||
|
||||
func TestCookieBannerVersioning_DetectedCookiesNeverBump(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("reporting detected cookies does not bump version", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||
|
||||
bannerID := factory.CreateCookieBanner(owner)
|
||||
published := publishBanner(t, owner, bannerID)
|
||||
activateBanner(t, owner, bannerID)
|
||||
baseline := published.Version
|
||||
|
||||
reportDetectedCookies(t, owner, bannerID, "_unknown_cookie", "_another")
|
||||
|
||||
got := latestVersion(t, owner, bannerID)
|
||||
assert.Equal(t, baseline, got.Version, "detected cookies should not bump the version")
|
||||
assert.Equal(t, "PUBLISHED", got.State)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -2281,10 +2281,6 @@ func (s *Service) ReportDetectedCookies(
|
||||
}
|
||||
|
||||
if inserted > 0 {
|
||||
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, banner.ID); err != nil {
|
||||
return fmt.Errorf("cannot ensure draft version: %w", err)
|
||||
}
|
||||
|
||||
if err := banner.SetPatternAnalysisRequested(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot request pattern analysis: %w", err)
|
||||
}
|
||||
|
||||
@@ -211,13 +211,15 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) {
|
||||
assert.True(t, snapshotsEqual(a, b))
|
||||
})
|
||||
|
||||
t.Run("Necessary comes first and Uncategorised comes last", func(t *testing.T) {
|
||||
t.Run("Necessary comes first and uncategorised is excluded", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
snap := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil)
|
||||
|
||||
require.Len(t, snap.Categories, 4)
|
||||
require.Len(t, snap.Categories, 3)
|
||||
assert.Equal(t, coredata.CookieCategoryKindNecessary, snap.Categories[0].Kind)
|
||||
assert.Equal(t, coredata.CookieCategoryKindUncategorised, snap.Categories[3].Kind)
|
||||
for _, c := range snap.Categories {
|
||||
assert.NotEqual(t, coredata.CookieCategoryKindUncategorised, c.Kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ import (
|
||||
)
|
||||
|
||||
// resolveTranslations converts raw DB translations into the resolved map
|
||||
// used by buildBannerConfig at serve time. Categories must be sorted in
|
||||
// snapshot order so the positional category translations align.
|
||||
// used by buildBannerConfig at serve time. Categories are filtered and sorted
|
||||
// in snapshot order so the positional category translations align.
|
||||
func resolveTranslations(
|
||||
translations coredata.CookieBannerTranslations,
|
||||
categories coredata.CookieCategories,
|
||||
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
|
||||
return buildSnapshotTranslations(translations, sortCategoriesForSnapshot(categories))
|
||||
return buildSnapshotTranslations(translations, consentCategories(categories))
|
||||
}
|
||||
|
||||
// snapshotsEqual reports whether two version snapshots are visitor-identical.
|
||||
@@ -43,34 +43,36 @@ func snapshotsEqual(a, b coredata.CookieBannerVersionSnapshot) bool {
|
||||
}
|
||||
|
||||
// snapshotCategoryKindOrder returns a stable weight per Kind so the snapshot
|
||||
// keeps the visitor-facing layout invariants (NECESSARY first, UNCATEGORISED
|
||||
// last) without depending on the admin-controlled rank.
|
||||
// keeps the visitor-facing layout invariants (NECESSARY first, then NORMAL
|
||||
// sorted by ID) without depending on the admin-controlled rank.
|
||||
func snapshotCategoryKindOrder(k coredata.CookieCategoryKind) int {
|
||||
switch k {
|
||||
case coredata.CookieCategoryKindNecessary:
|
||||
return 0
|
||||
case coredata.CookieCategoryKindNormal:
|
||||
return 1
|
||||
case coredata.CookieCategoryKindUncategorised:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
// sortCategoriesForSnapshot returns the categories ordered for snapshot
|
||||
// rendering. The order is (Kind weight, ID byte order); rank is intentionally
|
||||
// ignored so reordering is admin-only metadata and does not bump the version.
|
||||
func sortCategoriesForSnapshot(categories coredata.CookieCategories) coredata.CookieCategories {
|
||||
sorted := make(coredata.CookieCategories, len(categories))
|
||||
copy(sorted, categories)
|
||||
slices.SortStableFunc(sorted, func(a, b *coredata.CookieCategory) int {
|
||||
// consentCategories returns the categories that are part of the consent
|
||||
// contract (everything except UNCATEGORISED), sorted in snapshot order.
|
||||
// UNCATEGORISED is an admin-side inbox and never shown to visitors.
|
||||
func consentCategories(categories coredata.CookieCategories) coredata.CookieCategories {
|
||||
filtered := make(coredata.CookieCategories, 0, len(categories))
|
||||
for _, c := range categories {
|
||||
if c.Kind != coredata.CookieCategoryKindUncategorised {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(filtered, func(a, b *coredata.CookieCategory) int {
|
||||
if d := snapshotCategoryKindOrder(a.Kind) - snapshotCategoryKindOrder(b.Kind); d != 0 {
|
||||
return d
|
||||
}
|
||||
return bytes.Compare(a.ID[:], b.ID[:])
|
||||
})
|
||||
return sorted
|
||||
return filtered
|
||||
}
|
||||
|
||||
func buildSnapshot(
|
||||
@@ -78,7 +80,7 @@ func buildSnapshot(
|
||||
categories coredata.CookieCategories,
|
||||
allPatterns coredata.CookiePatterns,
|
||||
) coredata.CookieBannerVersionSnapshot {
|
||||
categories = sortCategoriesForSnapshot(categories)
|
||||
categories = consentCategories(categories)
|
||||
|
||||
cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
|
||||
for _, p := range allPatterns {
|
||||
|
||||
Reference in New Issue
Block a user