The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
1954 lines
65 KiB
Go
1954 lines
65 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package cookiebanner
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.gearno.de/kit/log"
|
|
"go.gearno.de/kit/pg"
|
|
"go.probo.inc/probo/internal/test"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
)
|
|
|
|
// promotionFixture extends workerFixture with a CommonThirdParty and a
|
|
// CommonTrackerPattern linking the catalog to the test pattern. It is
|
|
// the minimum scaffolding resolveOrgThirdParty needs to run end-to-end.
|
|
type promotionFixture struct {
|
|
workerFixture
|
|
commonThirdParty coredata.CommonThirdParty
|
|
commonPatternID gid.GID
|
|
trackerPattern coredata.TrackerPattern
|
|
commonThirdPartyID gid.GID
|
|
}
|
|
|
|
func seedPromotionFixture(t *testing.T, ctx context.Context, client *pg.Client) promotionFixture {
|
|
t.Helper()
|
|
|
|
fx := seedWorkerFixture(t, ctx, client)
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
// common_third_parties (name/slug) and common_tracker_patterns
|
|
// (tracker_type, pattern, max_age_seconds) are global, NOT
|
|
// tenant-scoped, and both carry unique indexes. Tests run in
|
|
// parallel, so the catalog rows must be unique per fixture or
|
|
// concurrent runs collide. The tenant id is unique per fixture and
|
|
// makes a stable, collision-free suffix.
|
|
suffix := fx.scope.GetTenantID().String()
|
|
patternName := "_ga_" + suffix
|
|
|
|
commonThirdPartyID := gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType)
|
|
commonThirdParty := coredata.CommonThirdParty{
|
|
ID: commonThirdPartyID,
|
|
Name: "Google " + suffix,
|
|
Slug: "google-" + suffix,
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
WebsiteURL: new("https://google.com"),
|
|
Certifications: []string{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
commonPattern := coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
CommonThirdPartyID: &commonThirdPartyID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
Description: "",
|
|
Confidence: 0.9,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
pattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &commonPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: patternName,
|
|
Description: "",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := commonThirdParty.Insert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := commonPattern.Upsert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
return pattern.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
if _, err := tx.Exec(ctx, `DELETE FROM common_third_party_domains WHERE common_third_party_id = $1`, commonThirdPartyID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, commonPattern.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `DELETE FROM common_third_parties WHERE id = $1`, commonThirdPartyID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `DELETE FROM third_parties WHERE organization_id = $1`, fx.organizationID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
return promotionFixture{
|
|
workerFixture: fx,
|
|
commonThirdParty: commonThirdParty,
|
|
commonPatternID: commonPattern.ID,
|
|
commonThirdPartyID: commonThirdPartyID,
|
|
trackerPattern: pattern,
|
|
}
|
|
}
|
|
|
|
func newMappingHandler(client *pg.Client) *trackerMappingHandler {
|
|
return &trackerMappingHandler{
|
|
pg: client,
|
|
logger: log.NewLogger(log.WithOutput(io.Discard)),
|
|
}
|
|
}
|
|
|
|
// promote runs resolveOrgThirdParty, which manages its own short
|
|
// transactions internally and only links to an existing org ThirdParty
|
|
// (it never creates one).
|
|
func promote(
|
|
t *testing.T,
|
|
ctx context.Context,
|
|
h *trackerMappingHandler,
|
|
tp coredata.TrackerPattern,
|
|
commonThirdPartyID gid.GID,
|
|
) *gid.GID {
|
|
t.Helper()
|
|
|
|
got, err := h.resolveOrgThirdParty(ctx, tp, commonThirdPartyID)
|
|
require.NoError(t, err)
|
|
|
|
return got
|
|
}
|
|
|
|
func TestPromoteThirdParty_ExactCommonLink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
existing := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
return existing.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
got := promote(t, ctx, newMappingHandler(client), fx.trackerPattern, fx.commonThirdPartyID)
|
|
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, existing.ID, *got, "should return the existing org ThirdParty linked by common id")
|
|
}
|
|
|
|
func TestPromoteThirdParty_HeuristicMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
// Append a corporate suffix to the catalog name so the heuristic
|
|
// matches on the suffix-stripped name (score 0.9) rather than an
|
|
// exact link.
|
|
manualEntry := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
Name: fx.commonThirdParty.Name + " LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
Level: 1,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
return manualEntry.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
got := promote(t, ctx, newMappingHandler(client), fx.trackerPattern, fx.commonThirdPartyID)
|
|
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, manualEntry.ID, *got, "heuristic match should return the manually-entered ThirdParty")
|
|
|
|
var reloaded coredata.ThirdParty
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, manualEntry.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.CommonThirdPartyID, "matched row must be tagged with common_third_party_id")
|
|
assert.Equal(t, fx.commonThirdPartyID, *reloaded.CommonThirdPartyID)
|
|
}
|
|
|
|
// TestPromoteThirdParty_NoCreateWithoutMatch asserts that when no
|
|
// existing org ThirdParty matches the catalog third party, resolution
|
|
// returns nothing: the worker never creates a brand new org ThirdParty
|
|
// (that is done only through the explicit ImportFromCommon action).
|
|
func TestPromoteThirdParty_NoCreateWithoutMatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
got := promote(t, ctx, newMappingHandler(client), fx.trackerPattern, fx.commonThirdPartyID)
|
|
|
|
assert.Nil(t, got, "resolution must not create a new org ThirdParty")
|
|
}
|
|
|
|
// TestProcess_PreservesCatalogMappingOnReTrigger asserts that when
|
|
// Process is called for a pattern that already carries a
|
|
// common_tracker_pattern_id, the catalog pipeline is skipped and the
|
|
// existing catalog link is preserved verbatim.
|
|
func TestProcess_PreservesCatalogMappingOnReTrigger(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
return fx.trackerPattern.SetMappingRequested(ctx, tx)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, fx.trackerPattern))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.CommonTrackerPatternID, "common tracker pattern link must be preserved")
|
|
assert.Equal(t, fx.commonPatternID, *reloaded.CommonTrackerPatternID)
|
|
assert.Nil(t, reloaded.ThirdPartyID, "no org ThirdParty exists to link, so third_party_id stays unset")
|
|
}
|
|
|
|
// TestProcess_UncategorisedPatternIsNotPromoted asserts that a pattern
|
|
// still in the uncategorised category gets its catalog mapping
|
|
// resolved but is NOT promoted to an org ThirdParty.
|
|
func TestProcess_UncategorisedPatternIsNotPromoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`UPDATE tracker_patterns
|
|
SET cookie_category_id = $1,
|
|
mapping_requested_at = $2
|
|
WHERE id = $3`,
|
|
fx.uncategorisedID,
|
|
time.Now().UTC().Truncate(time.Microsecond),
|
|
fx.trackerPattern.ID,
|
|
)
|
|
|
|
return err
|
|
}))
|
|
|
|
var reloadedBefore coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedBefore.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, reloadedBefore))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.CommonTrackerPatternID, "catalog mapping must still be resolved")
|
|
assert.Equal(t, fx.commonPatternID, *reloaded.CommonTrackerPatternID)
|
|
assert.Nil(t, reloaded.ThirdPartyID, "uncategorised pattern must not be promoted to org ThirdParty")
|
|
}
|
|
|
|
// TestProcess_ExtensionPatternIsNotPromoted asserts that even when a
|
|
// pattern has a catalog link, a Source=EXTENSION pattern stays
|
|
// un-promoted.
|
|
func TestProcess_ExtensionPatternIsNotPromoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
source := coredata.CookieSourceExtension
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`UPDATE tracker_patterns
|
|
SET source = $1,
|
|
mapping_requested_at = $2
|
|
WHERE id = $3`,
|
|
source,
|
|
now,
|
|
fx.trackerPattern.ID,
|
|
)
|
|
|
|
return err
|
|
}))
|
|
|
|
var reloadedBefore coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedBefore.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, reloadedBefore))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
assert.Nil(t, reloaded.ThirdPartyID, "EXTENSION-sourced pattern must not be promoted")
|
|
}
|
|
|
|
// TestProcess_NoOpWhenAlreadyPromoted asserts that re-running the
|
|
// worker on a pattern that already has a third_party_id leaves the
|
|
// row alone (the guard in Process).
|
|
func TestProcess_NoOpWhenAlreadyPromoted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
preExisting := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := preExisting.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
fx.trackerPattern.ThirdPartyID = &preExisting.ID
|
|
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`UPDATE tracker_patterns
|
|
SET third_party_id = $1,
|
|
mapping_requested_at = $2
|
|
WHERE id = $3`,
|
|
preExisting.ID,
|
|
now,
|
|
fx.trackerPattern.ID,
|
|
)
|
|
|
|
return err
|
|
}))
|
|
|
|
var reloadedBefore coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedBefore.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, reloadedBefore))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.ThirdPartyID)
|
|
assert.Equal(t, preExisting.ID, *reloaded.ThirdPartyID, "third_party_id must not be overwritten")
|
|
}
|
|
|
|
func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &fx.commonPatternID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_gid",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_gid",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unmappedPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ga_unknown",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_ga_unknown",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// Detected trackers store the eTLD+1 (uri.ExtractDomain), so the
|
|
// sibling lookup matches on that exact value. Use a vendor-specific
|
|
// domain rather than shared infrastructure (e.g. googletagmanager.com),
|
|
// which resolveDeterministic strips before sibling grouping.
|
|
initiatorDomain := "google-analytics.com"
|
|
siblingDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "_gid",
|
|
InitiatorDomain: &initiatorDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unmappedDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &unmappedPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "_ga_unknown",
|
|
InitiatorDomain: &initiatorDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := unmappedPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := siblingDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := unmappedDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
|
|
var got *catalogMatch
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
var err error
|
|
|
|
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"google-analytics.com"})
|
|
|
|
return err
|
|
}))
|
|
|
|
require.NotNil(t, got, "sibling origin match should return a catalog match")
|
|
require.NotNil(t, got.commonPatternID, "sibling origin match should return a common tracker pattern ID")
|
|
require.NotNil(t, got.thirdPartyID, "sibling origin match should surface the sibling's org third party directly")
|
|
assert.Equal(t, orgThirdParty.ID, *got.thirdPartyID)
|
|
|
|
var commonPattern coredata.CommonTrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return commonPattern.LoadByID(ctx, conn, *got.commonPatternID)
|
|
}))
|
|
|
|
require.NotNil(t, commonPattern.CommonThirdPartyID)
|
|
assert.Equal(t, fx.commonThirdPartyID, *commonPattern.CommonThirdPartyID)
|
|
assert.Equal(t, float32(0.7), commonPattern.Confidence)
|
|
}
|
|
|
|
func TestMatchBySiblingOrigin_AmbiguousThirdParties(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
otherSuffix := fx.scope.GetTenantID().String()
|
|
otherCommonThirdPartyID := gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType)
|
|
otherCommonThirdParty := coredata.CommonThirdParty{
|
|
ID: otherCommonThirdPartyID,
|
|
Name: "Facebook " + otherSuffix,
|
|
Slug: "facebook-" + otherSuffix,
|
|
Category: coredata.ThirdPartyCategoryMarketing,
|
|
Certifications: []string{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
orgThirdPartyA := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
orgThirdPartyB := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &otherCommonThirdPartyID,
|
|
Name: "Facebook",
|
|
Category: coredata.ThirdPartyCategoryMarketing,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingA := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
ThirdPartyID: &orgThirdPartyA.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "sibling_a",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "sibling_a",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingB := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
ThirdPartyID: &orgThirdPartyB.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "sibling_b",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "sibling_b",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unmappedPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "ambiguous_test",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "ambiguous_test",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
sharedDomain := "shared-tracker.com"
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := otherCommonThirdParty.Insert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := orgThirdPartyA.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := orgThirdPartyB.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingA.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingB.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := unmappedPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detA := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingA.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "sibling_a",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detA.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detB := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingB.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "sibling_b",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detB.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detUnmapped := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &unmappedPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "ambiguous_test",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detUnmapped.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_third_parties WHERE id = $1`, otherCommonThirdPartyID)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
h := newMappingHandler(client)
|
|
|
|
var got *catalogMatch
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
var err error
|
|
|
|
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"shared-tracker.com"})
|
|
|
|
return err
|
|
}))
|
|
|
|
assert.Nil(t, got, "ambiguous siblings mapping to different third parties should return nil")
|
|
}
|
|
|
|
func TestMatchBySiblingOrigin_NoSiblings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedWorkerFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
unmappedPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "lonely_cookie",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "lonely_cookie",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
return unmappedPattern.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
|
|
var got *catalogMatch
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
var err error
|
|
|
|
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"unique-domain.com"})
|
|
|
|
return err
|
|
}))
|
|
|
|
assert.Nil(t, got, "no siblings sharing the domain should return nil")
|
|
}
|
|
|
|
func TestMatchBySiblingOrigin_EmptyDomains(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedWorkerFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
pattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "no_domains",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "no_domains",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
return pattern.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
|
|
var got *catalogMatch
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
var err error
|
|
|
|
got, err = h.matchBySiblingOrigin(ctx, tx, pattern, nil)
|
|
|
|
return err
|
|
}))
|
|
|
|
assert.Nil(t, got, "nil domains should immediately return nil")
|
|
}
|
|
|
|
func TestMatchBySiblingOrigin_ConvergentSiblings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingA := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "converge_a",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "converge_a",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingB := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "converge_b",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "converge_b",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unmappedPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "converge_target",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "converge_target",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
sharedDomain := "google.com"
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingA.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingB.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := unmappedPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detA := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingA.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "converge_a",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detA.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detB := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingB.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "converge_b",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detB.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
detUnmapped := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &unmappedPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "converge_target",
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
if _, err := detUnmapped.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
|
|
var got *catalogMatch
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
var err error
|
|
|
|
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"google.com"})
|
|
|
|
return err
|
|
}))
|
|
|
|
require.NotNil(t, got, "multiple siblings converging to same third party should succeed")
|
|
require.NotNil(t, got.commonPatternID)
|
|
|
|
var commonPattern coredata.CommonTrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return commonPattern.LoadByID(ctx, conn, *got.commonPatternID)
|
|
}))
|
|
|
|
require.NotNil(t, commonPattern.CommonThirdPartyID)
|
|
assert.Equal(t, fx.commonThirdPartyID, *commonPattern.CommonThirdPartyID)
|
|
}
|
|
|
|
func TestPromoteThirdParty_ExactCommonLinkIgnoresSimilarUnlinked(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
manualEntry := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
linked := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := manualEntry.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return linked.Insert(ctx, tx, fx.scope)
|
|
}))
|
|
|
|
got := promote(t, ctx, newMappingHandler(client), fx.trackerPattern, fx.commonThirdPartyID)
|
|
|
|
require.NotNil(t, got)
|
|
assert.Equal(t, linked.ID, *got, "exact-link path must short-circuit before the heuristic fires")
|
|
}
|
|
|
|
// TestProcess_BackfillsCommonThirdPartyFromSibling asserts that a pattern
|
|
// linked to an unlinked catalog row (no common_third_party_id) gets its
|
|
// catalog row backfilled from a sibling signal, and is promoted directly
|
|
// to the sibling's existing org ThirdParty.
|
|
func TestProcess_BackfillsCommonThirdPartyFromSibling(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &fx.commonPatternID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_gid_backfill",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_gid_backfill",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unlinkedCommon := coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ga_backfill",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
Confidence: 0.5,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &unlinkedCommon.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ga_backfill",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_ga_backfill",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// A vendor-specific initiator domain: resolveDeterministic strips
|
|
// shared infrastructure (e.g. googletagmanager.com) before sibling
|
|
// grouping, so the backfill must be driven by a real vendor domain.
|
|
initiatorDomain := "google-analytics.com"
|
|
|
|
siblingDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "_gid_backfill",
|
|
InitiatorDomain: &initiatorDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
targetDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &target.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "_ga_backfill",
|
|
InitiatorDomain: &initiatorDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if _, err := unlinkedCommon.Upsert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := target.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := siblingDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := targetDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, unlinkedCommon.ID)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
var reloadedCommon coredata.CommonTrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedCommon.LoadByID(ctx, conn, unlinkedCommon.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedCommon.CommonThirdPartyID, "the unlinked catalog row must be backfilled")
|
|
assert.Equal(t, fx.commonThirdPartyID, *reloadedCommon.CommonThirdPartyID)
|
|
|
|
var reloadedTarget coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedTarget.LoadByID(ctx, conn, fx.scope, target.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedTarget.ThirdPartyID, "target must be promoted to the sibling's org third party")
|
|
assert.Equal(t, orgThirdParty.ID, *reloadedTarget.ThirdPartyID)
|
|
require.NotNil(t, reloadedTarget.CommonTrackerPatternID)
|
|
assert.Equal(t, unlinkedCommon.ID, *reloadedTarget.CommonTrackerPatternID, "the existing catalog link must be preserved")
|
|
}
|
|
|
|
// TestProcess_UncategorisedLinksExistingThirdParty asserts that an
|
|
// uncategorised pattern is still linked to an already-existing matching
|
|
// org ThirdParty (linking to an existing party is ungated); only the
|
|
// creation of a new party stays gated, as covered by
|
|
// TestProcess_UncategorisedPatternIsNotPromoted.
|
|
func TestProcess_UncategorisedLinksExistingThirdParty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
existing := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := existing.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`UPDATE tracker_patterns
|
|
SET cookie_category_id = $1,
|
|
mapping_requested_at = $2
|
|
WHERE id = $3`,
|
|
fx.uncategorisedID,
|
|
now,
|
|
fx.trackerPattern.ID,
|
|
)
|
|
|
|
return err
|
|
}))
|
|
|
|
var reloadedBefore coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedBefore.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, reloadedBefore))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.ThirdPartyID, "uncategorised pattern must still link to an existing org third party")
|
|
assert.Equal(t, existing.ID, *reloaded.ThirdPartyID)
|
|
}
|
|
|
|
// TestProcess_SiblingPromotionOnFirstPartyOrigin asserts that a pattern
|
|
// detected on the banner's own (first-party) origin is still grouped with
|
|
// its siblings sharing that origin. Sibling matching is an org-local
|
|
// co-occurrence signal and must not be defeated by the first-party domain
|
|
// filter that only protects the global catalog (domain) match.
|
|
func TestProcess_SiblingPromotionOnFirstPartyOrigin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
siblingPattern := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &fx.commonPatternID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_sibling_fp",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_sibling_fp",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unlinkedCommon := coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "__support__",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
Confidence: 0.5,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &unlinkedCommon.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "__support__",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "__support__",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// The banner origin in seedWorkerFixture is an *.example.com host, so
|
|
// its eTLD+1 (the first-party domain) is "example.com". Detecting both
|
|
// patterns on that domain means uri.FilterFirstPartyDomains would strip
|
|
// it — the regression this test guards against.
|
|
firstPartyDomain := "example.com"
|
|
|
|
siblingDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &siblingPattern.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "_sibling_fp",
|
|
InitiatorDomain: &firstPartyDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
targetDetected := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &target.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: "__support__",
|
|
InitiatorDomain: &firstPartyDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if _, err := unlinkedCommon.Upsert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := siblingPattern.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := target.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := siblingDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := targetDetected.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, unlinkedCommon.ID)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
var reloadedCommon coredata.CommonTrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedCommon.LoadByID(ctx, conn, unlinkedCommon.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedCommon.CommonThirdPartyID, "catalog row must be backfilled from the first-party sibling")
|
|
assert.Equal(t, fx.commonThirdPartyID, *reloadedCommon.CommonThirdPartyID)
|
|
|
|
var reloadedTarget coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedTarget.LoadByID(ctx, conn, fx.scope, target.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedTarget.ThirdPartyID, "target sharing a first-party origin must be promoted via its sibling")
|
|
assert.Equal(t, orgThirdParty.ID, *reloadedTarget.ThirdPartyID)
|
|
}
|
|
|
|
// TestProcess_ReenqueuesUnmappedSiblingOnResolve asserts that when a
|
|
// pattern newly resolves a catalog third party, same-banner siblings
|
|
// that share an initiator domain but are still unpromoted get their
|
|
// mapping re-armed (backward propagation), while the already-promoted
|
|
// sibling that supplied the resolution is left untouched.
|
|
func TestProcess_ReenqueuesUnmappedSiblingOnResolve(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
mappedSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &fx.commonPatternID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_gid_reenq",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_gid_reenq",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ga_reenq_target",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_ga_reenq_target",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
unmappedSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_unmapped_reenq",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_unmapped_reenq",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
sharedDomain := "reenq-tracker.com"
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, p := range []coredata.TrackerPattern{mappedSibling, target, unmappedSibling} {
|
|
if err := p.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for id, identifier := range map[gid.GID]string{
|
|
mappedSibling.ID: "_gid_reenq",
|
|
target.ID: "_ga_reenq_target",
|
|
unmappedSibling.ID: "_unmapped_reenq",
|
|
} {
|
|
patternID := id
|
|
det := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &patternID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: identifier,
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
var reloadedTarget coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedTarget.LoadByID(ctx, conn, fx.scope, target.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedTarget.ThirdPartyID, "target must resolve via its promoted sibling")
|
|
|
|
var reloadedUnmapped coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedUnmapped.LoadByID(ctx, conn, fx.scope, unmappedSibling.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedUnmapped.MappingRequestedAt, "unmapped sibling sharing the origin must be re-enqueued")
|
|
|
|
var reloadedMapped coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedMapped.LoadByID(ctx, conn, fx.scope, mappedSibling.ID)
|
|
}))
|
|
|
|
assert.Nil(t, reloadedMapped.MappingRequestedAt, "already-promoted sibling must not be re-enqueued")
|
|
}
|
|
|
|
// TestProcess_DoesNotReenqueuePromotedOrExtensionSiblings asserts that
|
|
// the re-enqueue skips siblings that are already promoted or
|
|
// EXTENSION-sourced, while still re-arming a plain unmapped sibling.
|
|
func TestProcess_DoesNotReenqueuePromotedOrExtensionSiblings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &fx.commonThirdPartyID,
|
|
Name: "Google LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
mappedSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
CommonTrackerPatternID: &fx.commonPatternID,
|
|
ThirdPartyID: &orgThirdParty.ID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_gid_guard",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_gid_guard",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ga_guard_target",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_ga_guard_target",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
plainSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_plain_guard",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_plain_guard",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
extensionSource := coredata.CookieSourceExtension
|
|
extensionSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_ext_guard",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_ext_guard",
|
|
Source: &extensionSource,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
sharedDomain := "guard-tracker.com"
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
patterns := map[gid.GID]string{
|
|
mappedSibling.ID: "_gid_guard",
|
|
target.ID: "_ga_guard_target",
|
|
plainSibling.ID: "_plain_guard",
|
|
extensionSibling.ID: "_ext_guard",
|
|
}
|
|
|
|
for _, p := range []coredata.TrackerPattern{mappedSibling, target, plainSibling, extensionSibling} {
|
|
if err := p.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for id, identifier := range patterns {
|
|
patternID := id
|
|
det := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &patternID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: identifier,
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
reload := func(id gid.GID) coredata.TrackerPattern {
|
|
var p coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return p.LoadByID(ctx, conn, fx.scope, id)
|
|
}))
|
|
|
|
return p
|
|
}
|
|
|
|
require.NotNil(t, reload(target.ID).ThirdPartyID, "target must resolve via its promoted sibling")
|
|
require.NotNil(t, reload(plainSibling.ID).MappingRequestedAt, "plain unmapped sibling must be re-enqueued")
|
|
assert.Nil(t, reload(mappedSibling.ID).MappingRequestedAt, "promoted sibling must not be re-enqueued")
|
|
assert.Nil(t, reload(extensionSibling.ID).MappingRequestedAt, "EXTENSION-sourced sibling must not be re-enqueued")
|
|
}
|
|
|
|
// TestProcess_NoReenqueueWhenCommonThirdPartyPreexisted asserts that the
|
|
// re-trigger path, where the pattern's linked catalog row already carries
|
|
// a common third party, adds no new signal and therefore leaves unmapped
|
|
// siblings untouched (the cascade terminator).
|
|
func TestProcess_NoReenqueueWhenCommonThirdPartyPreexisted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedPromotionFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
|
|
unmappedSibling := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: "_unmapped_preexist",
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: "_unmapped_preexist",
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
sharedDomain := "preexist-tracker.com"
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := unmappedSibling.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
for id, identifier := range map[gid.GID]string{
|
|
fx.trackerPattern.ID: fx.trackerPattern.Pattern,
|
|
unmappedSibling.ID: "_unmapped_preexist",
|
|
} {
|
|
patternID := id
|
|
det := coredata.DetectedTracker{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
|
CookieBannerID: fx.banner.ID,
|
|
TrackerPatternID: &patternID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Identifier: identifier,
|
|
InitiatorDomain: &sharedDomain,
|
|
LastDetectedAt: now,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return fx.trackerPattern.SetMappingRequested(ctx, tx)
|
|
}))
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, fx.trackerPattern))
|
|
|
|
var reloadedUnmapped coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedUnmapped.LoadByID(ctx, conn, fx.scope, unmappedSibling.ID)
|
|
}))
|
|
|
|
assert.Nil(t, reloadedUnmapped.MappingRequestedAt, "re-trigger with a pre-existing common third party must not re-enqueue siblings")
|
|
}
|
|
|
|
// TestProcess_FirstPartyVerdictIsTerminal asserts that a pattern whose
|
|
// matching catalog row carries the FIRST_PARTY verdict is linked to that
|
|
// row but never attributed a third party: the heuristic signals and the
|
|
// agent are short-circuited, leaving third_party_id unset.
|
|
func TestProcess_FirstPartyVerdictIsTerminal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedWorkerFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
patternName := "loglevel_" + fx.scope.GetTenantID().String()
|
|
|
|
firstPartyCommon := coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
TrackerType: coredata.TrackerTypeLocalStorage,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
Confidence: 0.8,
|
|
Attribution: coredata.CommonTrackerPatternAttributionFirstParty,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// An org pattern with no catalog link yet, so matchByPattern resolves
|
|
// the FIRST_PARTY row by (tracker_type, pattern, max_age).
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeLocalStorage,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: patternName,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if _, err := firstPartyCommon.Upsert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := target.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return target.SetMappingRequested(ctx, tx)
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, firstPartyCommon.ID)
|
|
return nil
|
|
})
|
|
})
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, target.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.CommonTrackerPatternID, "the pattern must be linked to the first-party catalog row for coverage")
|
|
assert.Equal(t, firstPartyCommon.ID, *reloaded.CommonTrackerPatternID)
|
|
assert.Nil(t, reloaded.ThirdPartyID, "a first-party verdict must never attribute a third party")
|
|
|
|
reloadedCommon := coredata.CommonTrackerPattern{}
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedCommon.LoadByID(ctx, conn, firstPartyCommon.ID)
|
|
}))
|
|
|
|
assert.Equal(t, coredata.CommonTrackerPatternAttributionFirstParty, reloadedCommon.Attribution, "verdict must remain first-party")
|
|
assert.Nil(t, reloadedCommon.CommonThirdPartyID, "first-party row must stay vendor-free")
|
|
}
|
|
|
|
// TestProcess_LowConfidenceCatalogVendorNotAdopted asserts that a catalog
|
|
// row whose vendor was attributed below trustedAttributionConfidence is
|
|
// not adopted deterministically: the pattern links to the row but is not
|
|
// promoted to the vendor's org third party, so a single low-confidence
|
|
// guess never auto-propagates across organizations.
|
|
func TestProcess_LowConfidenceCatalogVendorNotAdopted(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := test.PGClient(t)
|
|
ctx := context.Background()
|
|
fx := seedWorkerFixture(t, ctx, client)
|
|
|
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
|
suffix := fx.scope.GetTenantID().String()
|
|
patternName := "lowconf_" + suffix
|
|
|
|
commonThirdPartyID := gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType)
|
|
commonThirdParty := coredata.CommonThirdParty{
|
|
ID: commonThirdPartyID,
|
|
Name: "Acme " + suffix,
|
|
Slug: "acme-lowconf-" + suffix,
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// Agent-tier (0.8) attribution: below the 0.9 trusted bar.
|
|
lowConfCommon := coredata.CommonTrackerPattern{
|
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
|
CommonThirdPartyID: &commonThirdPartyID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
Confidence: agentSourceConfidence,
|
|
Attribution: coredata.CommonTrackerPatternAttributionThirdParty,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
// An existing org party for the vendor, so promotion WOULD link if the
|
|
// vendor were adopted.
|
|
orgThirdParty := coredata.ThirdParty{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CommonThirdPartyID: &commonThirdPartyID,
|
|
Name: "Acme LLC",
|
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
|
Certifications: []string{},
|
|
Countries: coredata.CountryCodes{},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
target := coredata.TrackerPattern{
|
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
|
OrganizationID: fx.organizationID,
|
|
CookieBannerID: fx.banner.ID,
|
|
CookieCategoryID: fx.normalCategoryID,
|
|
TrackerType: coredata.TrackerTypeCookie,
|
|
Pattern: patternName,
|
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
|
DisplayName: patternName,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
|
if err := commonThirdParty.Insert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := lowConfCommon.Upsert(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := target.Insert(ctx, tx, fx.scope); err != nil {
|
|
return err
|
|
}
|
|
|
|
return target.SetMappingRequested(ctx, tx)
|
|
}))
|
|
|
|
t.Cleanup(func() {
|
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, lowConfCommon.ID)
|
|
_, _ = tx.Exec(ctx, `DELETE FROM common_third_parties WHERE id = $1`, commonThirdPartyID)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
h := newMappingHandler(client)
|
|
require.NoError(t, h.Process(ctx, target))
|
|
|
|
var reloaded coredata.TrackerPattern
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloaded.LoadByID(ctx, conn, fx.scope, target.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloaded.CommonTrackerPatternID, "the pattern must still be linked to the catalog row")
|
|
assert.Equal(t, lowConfCommon.ID, *reloaded.CommonTrackerPatternID)
|
|
assert.Nil(t, reloaded.ThirdPartyID, "a below-trust catalog vendor must not be adopted/promoted")
|
|
|
|
// The catalog row is untouched: its low-confidence vendor remains for
|
|
// a later evidence-backed corroboration.
|
|
reloadedCommon := coredata.CommonTrackerPattern{}
|
|
|
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
|
return reloadedCommon.LoadByID(ctx, conn, lowConfCommon.ID)
|
|
}))
|
|
|
|
require.NotNil(t, reloadedCommon.CommonThirdPartyID, "the catalog vendor must be left in place")
|
|
assert.Equal(t, commonThirdPartyID, *reloadedCommon.CommonThirdPartyID)
|
|
}
|