mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 16:02:24 +00:00
feat: cover the acquisition record with live repository tests against a real schema, so the LEFT JOIN, the four columns added to the admin projection and the three channel filters are exercised rather than assumed, including that a second signup link cannot rewrite where a workspace came from and that the join does not fan out a row
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
)
|
||||
|
||||
// Acquisition is written once at signup and read back through the admin org
|
||||
// list, which means a LEFT JOIN, four extra scanned columns and three new
|
||||
// filters. None of that is exercised by a unit test, so it is checked here
|
||||
// against a real schema.
|
||||
//
|
||||
// WARMBLY_TEST_DB=postgres://warmbly:warmbly@localhost:15432/warmbly_dev?sslmode=disable \
|
||||
// go test ./internal/repository/ -run LiveOrgAcquisition -v
|
||||
|
||||
func TestLiveOrgAcquisitionRoundTrips(t *testing.T) {
|
||||
_, pool := liveContactDB(t)
|
||||
ctx := context.Background()
|
||||
f := newAdminFixture(t, pool)
|
||||
repo := NewOrganizationRepository(pool)
|
||||
|
||||
// A workspace with no row reads as no acquisition, which is the normal
|
||||
// case for a direct signup.
|
||||
got, err := repo.GetOrganizationAcquisition(ctx, f.org)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrganizationAcquisition before any write: %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("an org that carried nothing reads %+v, want nil", got)
|
||||
}
|
||||
|
||||
acq := &models.OrgAcquisition{
|
||||
OrganizationID: f.org,
|
||||
LandingPath: "https://warmbly.com/pricing?utm_source=x",
|
||||
ReferrerHost: "HTTPS://News.YCombinator.com/item?id=1",
|
||||
UTMSource: "newsletter",
|
||||
UTMMedium: "email",
|
||||
UTMCampaign: "launch",
|
||||
}
|
||||
if err := repo.RecordOrganizationAcquisition(ctx, acq); err != nil {
|
||||
t.Fatalf("RecordOrganizationAcquisition: %v", err)
|
||||
}
|
||||
|
||||
got, err = repo.GetOrganizationAcquisition(ctx, f.org)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrganizationAcquisition: %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("the recorded acquisition did not read back")
|
||||
}
|
||||
// Stored normalized: a full URL becomes a path, a referrer becomes a host.
|
||||
if got.LandingPath != "/pricing" {
|
||||
t.Errorf("landing_path = %q, want %q", got.LandingPath, "/pricing")
|
||||
}
|
||||
if got.ReferrerHost != "news.ycombinator.com" {
|
||||
t.Errorf("referrer_host = %q, want %q", got.ReferrerHost, "news.ycombinator.com")
|
||||
}
|
||||
if got.UTMSource != "newsletter" || got.UTMMedium != "email" || got.UTMCampaign != "launch" {
|
||||
t.Errorf("utm fields = %q/%q/%q", got.UTMSource, got.UTMMedium, got.UTMCampaign)
|
||||
}
|
||||
// Absent fields stay NULL rather than becoming "".
|
||||
if got.UTMTerm != "" || got.UTMContent != "" {
|
||||
t.Errorf("absent utm fields read %q/%q, want empty", got.UTMTerm, got.UTMContent)
|
||||
}
|
||||
|
||||
// The record is the account's origin, not a mutable setting: a second
|
||||
// signup link must not rewrite where the workspace actually came from.
|
||||
if err := repo.RecordOrganizationAcquisition(ctx, &models.OrgAcquisition{
|
||||
OrganizationID: f.org,
|
||||
UTMSource: "paid-ads",
|
||||
}); err != nil {
|
||||
t.Fatalf("second RecordOrganizationAcquisition: %v", err)
|
||||
}
|
||||
got, err = repo.GetOrganizationAcquisition(ctx, f.org)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrganizationAcquisition after the second write: %v", err)
|
||||
}
|
||||
if got.UTMSource != "newsletter" {
|
||||
t.Fatalf("utm_source = %q after a second write; the first signup must win", got.UTMSource)
|
||||
}
|
||||
|
||||
// A signup that carried nothing writes nothing at all.
|
||||
if err := repo.RecordOrganizationAcquisition(ctx, &models.OrgAcquisition{OrganizationID: f.org}); err != nil {
|
||||
t.Fatalf("recording an empty acquisition should be a no-op, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The admin list joins the table and filters on it. A wrong join or a column
|
||||
// out of order in the projection would break the scan for every row, acquisition
|
||||
// or not.
|
||||
func TestLiveOrgAcquisitionSurfacesInTheAdminList(t *testing.T) {
|
||||
_, pool := liveContactDB(t)
|
||||
ctx := context.Background()
|
||||
f := newAdminFixture(t, pool)
|
||||
repo := NewOrganizationRepository(pool)
|
||||
|
||||
find := func(t *testing.T, search *models.AdminOrgSearch) *models.AdminOrgListItem {
|
||||
t.Helper()
|
||||
search.Query = f.tag
|
||||
search.Limit = 10
|
||||
res, err := repo.SearchOrganizationsForAdmin(ctx, search)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchOrganizationsForAdmin: %v", err)
|
||||
}
|
||||
for i := range res.Data {
|
||||
if res.Data[i].ID == f.org {
|
||||
return &res.Data[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Before any acquisition row: the org still lists, with empty channel.
|
||||
item := find(t, &models.AdminOrgSearch{})
|
||||
if item == nil {
|
||||
t.Fatal("the fixture org did not appear in the admin list")
|
||||
}
|
||||
if item.UTMSource != nil {
|
||||
t.Errorf("utm_source = %v for an org with no acquisition row, want nil", *item.UTMSource)
|
||||
}
|
||||
// It counts as a direct signup and not as a tagged one.
|
||||
if find(t, &models.AdminOrgSearch{NoAcquisition: true}) == nil {
|
||||
t.Error("an org with no acquisition row should match no_acquisition")
|
||||
}
|
||||
if find(t, &models.AdminOrgSearch{HasAcquisition: true}) != nil {
|
||||
t.Error("an org with no acquisition row should not match has_acquisition")
|
||||
}
|
||||
|
||||
if err := repo.RecordOrganizationAcquisition(ctx, &models.OrgAcquisition{
|
||||
OrganizationID: f.org,
|
||||
UTMSource: "newsletter",
|
||||
UTMMedium: "email",
|
||||
LandingPath: "/pricing",
|
||||
}); err != nil {
|
||||
t.Fatalf("RecordOrganizationAcquisition: %v", err)
|
||||
}
|
||||
|
||||
item = find(t, &models.AdminOrgSearch{})
|
||||
if item == nil {
|
||||
t.Fatal("the fixture org disappeared from the admin list after the join had a row")
|
||||
}
|
||||
if item.UTMSource == nil || *item.UTMSource != "newsletter" {
|
||||
t.Errorf("utm_source = %v, want newsletter", item.UTMSource)
|
||||
}
|
||||
if item.UTMMedium == nil || *item.UTMMedium != "email" {
|
||||
t.Errorf("utm_medium = %v, want email", item.UTMMedium)
|
||||
}
|
||||
if item.LandingPath == nil || *item.LandingPath != "/pricing" {
|
||||
t.Errorf("landing_path = %v, want /pricing", item.LandingPath)
|
||||
}
|
||||
// The join must not duplicate the row.
|
||||
if item.MemberCount != 1 {
|
||||
t.Errorf("member_count = %d, want 1: the acquisition join should not fan out rows", item.MemberCount)
|
||||
}
|
||||
|
||||
// And the channel filters select on it.
|
||||
if find(t, &models.AdminOrgSearch{UTMSource: "newsletter"}) == nil {
|
||||
t.Error("filtering by utm_source=newsletter did not find the org")
|
||||
}
|
||||
if find(t, &models.AdminOrgSearch{UTMSource: "paid-ads"}) != nil {
|
||||
t.Error("filtering by a different utm_source still found the org")
|
||||
}
|
||||
if find(t, &models.AdminOrgSearch{UTMMedium: "email"}) == nil {
|
||||
t.Error("filtering by utm_medium=email did not find the org")
|
||||
}
|
||||
if find(t, &models.AdminOrgSearch{HasAcquisition: true}) == nil {
|
||||
t.Error("has_acquisition did not find an org that has a row")
|
||||
}
|
||||
if find(t, &models.AdminOrgSearch{NoAcquisition: true}) != nil {
|
||||
t.Error("no_acquisition found an org that has a row")
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if _, err := pool.Exec(context.Background(),
|
||||
`DELETE FROM organization_acquisition WHERE organization_id = $1`, f.org); err != nil {
|
||||
t.Errorf("cleanup acquisition: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user