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
|
package console_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestCookieBannerVersioning_RealChangesStillBumpVersion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2281,10 +2281,6 @@ func (s *Service) ReportDetectedCookies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if inserted > 0 {
|
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 {
|
if err := banner.SetPatternAnalysisRequested(ctx, tx); err != nil {
|
||||||
return fmt.Errorf("cannot request pattern analysis: %w", err)
|
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))
|
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()
|
t.Parallel()
|
||||||
|
|
||||||
snap := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil)
|
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.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
|
// resolveTranslations converts raw DB translations into the resolved map
|
||||||
// used by buildBannerConfig at serve time. Categories must be sorted in
|
// used by buildBannerConfig at serve time. Categories are filtered and sorted
|
||||||
// snapshot order so the positional category translations align.
|
// in snapshot order so the positional category translations align.
|
||||||
func resolveTranslations(
|
func resolveTranslations(
|
||||||
translations coredata.CookieBannerTranslations,
|
translations coredata.CookieBannerTranslations,
|
||||||
categories coredata.CookieCategories,
|
categories coredata.CookieCategories,
|
||||||
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
|
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
|
||||||
return buildSnapshotTranslations(translations, sortCategoriesForSnapshot(categories))
|
return buildSnapshotTranslations(translations, consentCategories(categories))
|
||||||
}
|
}
|
||||||
|
|
||||||
// snapshotsEqual reports whether two version snapshots are visitor-identical.
|
// 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
|
// snapshotCategoryKindOrder returns a stable weight per Kind so the snapshot
|
||||||
// keeps the visitor-facing layout invariants (NECESSARY first, UNCATEGORISED
|
// keeps the visitor-facing layout invariants (NECESSARY first, then NORMAL
|
||||||
// last) without depending on the admin-controlled rank.
|
// sorted by ID) without depending on the admin-controlled rank.
|
||||||
func snapshotCategoryKindOrder(k coredata.CookieCategoryKind) int {
|
func snapshotCategoryKindOrder(k coredata.CookieCategoryKind) int {
|
||||||
switch k {
|
switch k {
|
||||||
case coredata.CookieCategoryKindNecessary:
|
case coredata.CookieCategoryKindNecessary:
|
||||||
return 0
|
return 0
|
||||||
case coredata.CookieCategoryKindNormal:
|
case coredata.CookieCategoryKindNormal:
|
||||||
return 1
|
return 1
|
||||||
case coredata.CookieCategoryKindUncategorised:
|
|
||||||
return 2
|
|
||||||
default:
|
default:
|
||||||
return 3
|
return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sortCategoriesForSnapshot returns the categories ordered for snapshot
|
// consentCategories returns the categories that are part of the consent
|
||||||
// rendering. The order is (Kind weight, ID byte order); rank is intentionally
|
// contract (everything except UNCATEGORISED), sorted in snapshot order.
|
||||||
// ignored so reordering is admin-only metadata and does not bump the version.
|
// UNCATEGORISED is an admin-side inbox and never shown to visitors.
|
||||||
func sortCategoriesForSnapshot(categories coredata.CookieCategories) coredata.CookieCategories {
|
func consentCategories(categories coredata.CookieCategories) coredata.CookieCategories {
|
||||||
sorted := make(coredata.CookieCategories, len(categories))
|
filtered := make(coredata.CookieCategories, 0, len(categories))
|
||||||
copy(sorted, categories)
|
for _, c := range categories {
|
||||||
slices.SortStableFunc(sorted, func(a, b *coredata.CookieCategory) int {
|
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 {
|
if d := snapshotCategoryKindOrder(a.Kind) - snapshotCategoryKindOrder(b.Kind); d != 0 {
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
return bytes.Compare(a.ID[:], b.ID[:])
|
return bytes.Compare(a.ID[:], b.ID[:])
|
||||||
})
|
})
|
||||||
return sorted
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildSnapshot(
|
func buildSnapshot(
|
||||||
@@ -78,7 +80,7 @@ func buildSnapshot(
|
|||||||
categories coredata.CookieCategories,
|
categories coredata.CookieCategories,
|
||||||
allPatterns coredata.CookiePatterns,
|
allPatterns coredata.CookiePatterns,
|
||||||
) coredata.CookieBannerVersionSnapshot {
|
) coredata.CookieBannerVersionSnapshot {
|
||||||
categories = sortCategoriesForSnapshot(categories)
|
categories = consentCategories(categories)
|
||||||
|
|
||||||
cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
|
cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
|
||||||
for _, p := range allPatterns {
|
for _, p := range allPatterns {
|
||||||
|
|||||||
Reference in New Issue
Block a user