@@ -3,10 +3,18 @@ package main
33import (
44 "os"
55 "path/filepath"
6+ "regexp"
67 "strings"
78 "testing"
89)
910
11+ // fieldSet reports whether the generated index sets field to value. It
12+ // ignores the column alignment gofmt applies, which shifts whenever a new
13+ // field lengthens the struct literal.
14+ func fieldSet (s , field , value string ) bool {
15+ return regexp .MustCompile (regexp .QuoteMeta (field ) + `:\s+` + regexp .QuoteMeta (value ) + `,` ).MatchString (s )
16+ }
17+
1018func TestNormalizeURLMatchesRuntimeRules (t * testing.T ) {
1119 cases := map [string ]string {
1220 "https://mcp.box.com" : "https://mcp.box.com" ,
@@ -125,8 +133,10 @@ remotes:
125133 transport: streamable-http
126134`
127135 for name , body := range map [string ]string {
128- "meta.yaml" : meta ,
129- "external.md" : "# external\n " ,
136+ "meta.yaml" : meta ,
137+ // The canonical callback key here takes run() through the accept
138+ // path of the template scan, not only the rejections below.
139+ "external.md" : "# external\n \n Enter {{ gram.oauth.callback_url }} in the field.\n " ,
130140 "speakeasy.md" : "# speakeasy\n " ,
131141 } {
132142 if err := os .WriteFile (filepath .Join (guidesDir , name ), []byte (body ), 0o644 ); err != nil {
@@ -215,17 +225,22 @@ remotes:
215225 }
216226 for _ , want := range []string {
217227 "Summary:" , "SpeakeasyAddServer:" , "https://mcp.example.com/demo" , "com.example/demo" ,
218- "SetupRequired: true," ,
219228 `{ID: "oauth-app", Kind: "oauth", ClientRegistration: "manual", UpstreamSetup: "provider-steps", SpeakeasySetup: "manual-oauth"},` ,
220229 } {
221230 if ! strings .Contains (string (idx ), want ) {
222231 t .Errorf ("index_gen.go missing %q" , want )
223232 }
224233 }
225234
235+ _ , afterDemo , _ := strings .Cut (string (idx ), `"demo": {` )
236+ demoEntry , _ , _ := strings .Cut (afterDemo , "\n \t }," )
237+ if ! fieldSet (demoEntry , "SetupRequired" , "true" ) {
238+ t .Errorf ("demo should need setup, got:\n %s" , demoEntry )
239+ }
240+
226241 _ , afterTenant , _ := strings .Cut (string (idx ), `"tenant": {` )
227242 tenantEntry , _ , _ := strings .Cut (afterTenant , "\n \t }," )
228- if ! strings . Contains (tenantEntry , "SetupRequired: true, " ) {
243+ if ! fieldSet (tenantEntry , "SetupRequired" , " true" ) {
229244 t .Errorf ("tenanted remote must force SetupRequired=true, got:\n %s" , tenantEntry )
230245 }
231246 // Guards the fixture itself: if its option ever needs setup, the
@@ -235,6 +250,70 @@ remotes:
235250 }
236251}
237252
253+ // The generator is the only place that enforces the one-key rule, so the
254+ // rejections matter as much as the happy path: Render substitutes with a
255+ // literal byte replacement on the strength of this check.
256+ func TestScanTemplateKeys (t * testing.T ) {
257+ for _ , tc := range []struct {
258+ name string
259+ external string
260+ speakeasy string
261+ meta string
262+ errText string
263+ }{
264+ {name : "no keys" , external : "# external\n " , speakeasy : "# speakeasy\n " },
265+ {
266+ name : "canonical key in external" ,
267+ external : "Enter " + canonicalCallbackKey + " here.\n " ,
268+ },
269+ {
270+ name : "canonical key in speakeasy" ,
271+ speakeasy : "Confirm " + canonicalCallbackKey + " matches.\n " ,
272+ },
273+ {
274+ name : "non-canonical spacing" ,
275+ external : "Enter {{gram.oauth.callback_url}} here.\n " ,
276+ errText : "only supported key" ,
277+ },
278+ {
279+ name : "unknown key" ,
280+ external : "Enter {{ gram.server.redirect_uri }} here.\n " ,
281+ errText : "only supported key" ,
282+ },
283+ {
284+ name : "key in meta" ,
285+ meta : "callback: \" " + canonicalCallbackKey + "\" \n " ,
286+ errText : "meta.yaml carries template key" ,
287+ },
288+ } {
289+ t .Run (tc .name , func (t * testing.T ) {
290+ dir := t .TempDir ()
291+ files := map [string ]string {
292+ "external.md" : "# external\n " + tc .external ,
293+ "speakeasy.md" : "# speakeasy\n " + tc .speakeasy ,
294+ }
295+ for name , body := range files {
296+ if err := os .WriteFile (filepath .Join (dir , name ), []byte (body ), 0o644 ); err != nil {
297+ t .Fatal (err )
298+ }
299+ }
300+ err := scanTemplateKeys ("demo" , dir , []byte ("slug: demo\n " + tc .meta ))
301+ if tc .errText != "" {
302+ if err == nil {
303+ t .Fatalf ("expected an error mentioning %q, got nil" , tc .errText )
304+ }
305+ if ! strings .Contains (err .Error (), tc .errText ) {
306+ t .Fatalf ("error %v should mention %q" , err , tc .errText )
307+ }
308+ return
309+ }
310+ if err != nil {
311+ t .Fatalf ("unexpected error: %v" , err )
312+ }
313+ })
314+ }
315+ }
316+
238317func TestDeriveSpeakeasySetup (t * testing.T ) {
239318 for _ , tc := range []struct {
240319 name string
0 commit comments