|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "os" |
| 5 | + "path/filepath" |
| 6 | + "regexp" |
| 7 | + "testing" |
| 8 | +) |
| 9 | + |
| 10 | +/* |
| 11 | + * The brand identity in this package is hand-mirrored from the TypeScript |
| 12 | + * brand registry (see the "mirrors" comments on brand.go and |
| 13 | + * src/brand/resolve.web.ts). Nothing in either build enforces that, and the |
| 14 | + * two have already drifted once: the TS default was changed from 'bluesky' |
| 15 | + * to 'coseeker' in 61a2d7f54 without touching brand.go, so every unlisted |
| 16 | + * host served Bluesky OG/Twitter metadata and Bluesky splash colors and then |
| 17 | + * hydrated into CoSeeker. |
| 18 | + * |
| 19 | + * These tests parse the TS sources directly so the mirror cannot drift again |
| 20 | + * without a build failure. They are a stopgap: the durable fix is to generate |
| 21 | + * both sides from one manifest (brands/REVIEW_FOLLOWUPS.md item 6). |
| 22 | + */ |
| 23 | + |
| 24 | +const ( |
| 25 | + tsRegistryPath = "../../../src/brand/registry.ts" |
| 26 | + tsResolvePath = "../../../src/brand/resolve.web.ts" |
| 27 | +) |
| 28 | + |
| 29 | +var ( |
| 30 | + tsDefaultBrandRe = regexp.MustCompile(`export const DEFAULT_BRAND_ID\s*=\s*['"]([^'"]+)['"]`) |
| 31 | + tsBrandsBlockRe = regexp.MustCompile(`export const brands:\s*Record<string,\s*Brand>\s*=\s*\{([^}]*)\}`) |
| 32 | + tsBrandKeyRe = regexp.MustCompile(`(?m)^\s*(\w+)\s*,\s*$`) |
| 33 | + tsHostBlockRe = regexp.MustCompile(`const HOSTNAME_TO_BRAND_ID:\s*Record<string,\s*string>\s*=\s*\{([^}]*)\}`) |
| 34 | + tsHostEntryRe = regexp.MustCompile(`['"]([^'"]+)['"]\s*:\s*['"]([^'"]+)['"]`) |
| 35 | +) |
| 36 | + |
| 37 | +// readTS reads one of the TypeScript brand sources relative to this package. |
| 38 | +func readTS(t *testing.T, path string) string { |
| 39 | + t.Helper() |
| 40 | + b, err := os.ReadFile(filepath.Clean(path)) |
| 41 | + if err != nil { |
| 42 | + t.Fatalf("read %s: %v (these tests parse the TS brand registry; run them from the package dir)", path, err) |
| 43 | + } |
| 44 | + return string(b) |
| 45 | +} |
| 46 | + |
| 47 | +// submatch applies re to src and returns capture group 1, failing the test |
| 48 | +// with a pointer at the source file if the shape no longer matches. |
| 49 | +func submatch(t *testing.T, re *regexp.Regexp, src, path, what string) string { |
| 50 | + t.Helper() |
| 51 | + m := re.FindStringSubmatch(src) |
| 52 | + if m == nil { |
| 53 | + t.Fatalf("could not find %s in %s - the declaration was probably reshaped; update the regex in brand_test.go", what, path) |
| 54 | + } |
| 55 | + return m[1] |
| 56 | +} |
| 57 | + |
| 58 | +func TestDefaultBrandMatchesTypeScript(t *testing.T) { |
| 59 | + want := submatch(t, tsDefaultBrandRe, readTS(t, tsRegistryPath), tsRegistryPath, "DEFAULT_BRAND_ID") |
| 60 | + |
| 61 | + if defaultBrandID != want { |
| 62 | + t.Errorf("defaultBrandID = %q, but DEFAULT_BRAND_ID in %s is %q.\n"+ |
| 63 | + "An unlisted host would be server-rendered as %q and hydrate as %q: "+ |
| 64 | + "wrong link-preview cards and a flash of the wrong splash background.", |
| 65 | + defaultBrandID, tsRegistryPath, want, defaultBrandID, want) |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +func TestDefaultBrandIsRegistered(t *testing.T) { |
| 70 | + if _, ok := brands[defaultBrandID]; !ok { |
| 71 | + t.Fatalf("defaultBrandID %q is not a key in brands; ResolveBrand would return a zero Brand "+ |
| 72 | + "and the templates would render empty metadata", defaultBrandID) |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +func TestBrandIDsMatchTypeScript(t *testing.T) { |
| 77 | + src := readTS(t, tsRegistryPath) |
| 78 | + block := submatch(t, tsBrandsBlockRe, src, tsRegistryPath, "the brands record") |
| 79 | + |
| 80 | + ts := map[string]bool{} |
| 81 | + for _, m := range tsBrandKeyRe.FindAllStringSubmatch(block, -1) { |
| 82 | + ts[m[1]] = true |
| 83 | + } |
| 84 | + if len(ts) == 0 { |
| 85 | + t.Fatalf("parsed zero brand ids out of %s; update the regex in brand_test.go", tsRegistryPath) |
| 86 | + } |
| 87 | + |
| 88 | + for id := range ts { |
| 89 | + if _, ok := brands[id]; !ok { |
| 90 | + t.Errorf("brand %q is registered in %s but missing from brands in brand.go; "+ |
| 91 | + "SSR metadata and splash colors for it would fall back to %q", |
| 92 | + id, tsRegistryPath, defaultBrandID) |
| 93 | + } |
| 94 | + } |
| 95 | + for id := range brands { |
| 96 | + if !ts[id] { |
| 97 | + t.Errorf("brand %q exists in brand.go but not in %s; it was probably renamed or removed on the TS side", |
| 98 | + id, tsRegistryPath) |
| 99 | + } |
| 100 | + } |
| 101 | +} |
| 102 | + |
| 103 | +func TestHostnameMapMatchesTypeScript(t *testing.T) { |
| 104 | + src := readTS(t, tsResolvePath) |
| 105 | + block := submatch(t, tsHostBlockRe, src, tsResolvePath, "HOSTNAME_TO_BRAND_ID") |
| 106 | + |
| 107 | + ts := map[string]string{} |
| 108 | + for _, m := range tsHostEntryRe.FindAllStringSubmatch(block, -1) { |
| 109 | + ts[m[1]] = m[2] |
| 110 | + } |
| 111 | + if len(ts) == 0 { |
| 112 | + t.Fatalf("parsed zero hostnames out of %s; update the regex in brand_test.go", tsResolvePath) |
| 113 | + } |
| 114 | + |
| 115 | + for host, id := range ts { |
| 116 | + got, ok := hostnameToBrandID[host] |
| 117 | + if !ok { |
| 118 | + t.Errorf("hostname %q maps to %q in %s but is missing from hostnameToBrandID; "+ |
| 119 | + "SSR would serve the %q brand for it", host, id, tsResolvePath, defaultBrandID) |
| 120 | + continue |
| 121 | + } |
| 122 | + if got != id { |
| 123 | + t.Errorf("hostname %q maps to %q in brand.go but %q in %s", host, got, id, tsResolvePath) |
| 124 | + } |
| 125 | + } |
| 126 | + for host, id := range hostnameToBrandID { |
| 127 | + if _, ok := ts[host]; !ok { |
| 128 | + t.Errorf("hostname %q maps to %q in brand.go but is missing from %s; "+ |
| 129 | + "SSR and the hydrated bundle would disagree on it", host, id, tsResolvePath) |
| 130 | + } |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +func TestResolveBrand(t *testing.T) { |
| 135 | + tests := []struct { |
| 136 | + name string |
| 137 | + host string |
| 138 | + want string |
| 139 | + }{ |
| 140 | + { |
| 141 | + name: "listed host", |
| 142 | + host: "coseeker.com", |
| 143 | + want: "coseeker", |
| 144 | + }, |
| 145 | + { |
| 146 | + name: "www variant", |
| 147 | + host: "www.k4m2a.app", |
| 148 | + want: "k4m2a", |
| 149 | + }, |
| 150 | + { |
| 151 | + name: "port is stripped", |
| 152 | + host: "mdparivaar.com:8100", |
| 153 | + want: "mdparivaar", |
| 154 | + }, |
| 155 | + { |
| 156 | + name: "host is lowercased", |
| 157 | + host: "WWW.CoSeeker.com", |
| 158 | + want: "coseeker", |
| 159 | + }, |
| 160 | + { |
| 161 | + name: "bluesky is reachable only when listed, not as a fallback", |
| 162 | + host: "bsky.app", |
| 163 | + want: defaultBrandID, |
| 164 | + }, |
| 165 | + { |
| 166 | + name: "unlisted host falls back to the default brand", |
| 167 | + host: "staging.example.com", |
| 168 | + want: defaultBrandID, |
| 169 | + }, |
| 170 | + { |
| 171 | + name: "empty host falls back to the default brand", |
| 172 | + host: "", |
| 173 | + want: defaultBrandID, |
| 174 | + }, |
| 175 | + } |
| 176 | + |
| 177 | + for _, tt := range tests { |
| 178 | + t.Run(tt.name, func(t *testing.T) { |
| 179 | + if got := ResolveBrand(tt.host).ID; got != tt.want { |
| 180 | + t.Errorf("ResolveBrand(%q).ID = %q, want %q", tt.host, got, tt.want) |
| 181 | + } |
| 182 | + }) |
| 183 | + } |
| 184 | +} |
0 commit comments