[object Object]

← back to Cli Printing Press

test(cli): cover templated BaseURL + GraphQLEndpointPath rendering

7e08324af40e463bdd0cdfec831e880c36bfbf98 · 2026-04-29 20:46:49 -0500 · Cathryn Lavery

Adds TestGenerateGraphQLEndpointPathRendersTemplatedURL to lock in the
PR-1 contract: a GraphQL spec with a templated host (https://{shop}) and
a templated endpoint path (/admin/api/{version}/graphql.json) generates
a client whose const graphqlEndpointPath carries the placeholders
unchanged, whose config.go preserves the templated BaseURL, and whose
runtime URL — once {shop}/{version} are substituted — equals the real
Shopify Admin endpoint.

The substitution itself ships in PR-2; this test mimics it inline so
the structural piece (BaseURL+GraphQLEndpointPath concatenation, no
hardcoded "/graphql" left behind) regresses loudly if anyone re-flattens
the URL into a single field.

Files touched

Diff

commit 7e08324af40e463bdd0cdfec831e880c36bfbf98
Author: Cathryn Lavery <50469282+cathrynlavery@users.noreply.github.com>
Date:   Wed Apr 29 20:46:49 2026 -0500

    test(cli): cover templated BaseURL + GraphQLEndpointPath rendering
    
    Adds TestGenerateGraphQLEndpointPathRendersTemplatedURL to lock in the
    PR-1 contract: a GraphQL spec with a templated host (https://{shop}) and
    a templated endpoint path (/admin/api/{version}/graphql.json) generates
    a client whose const graphqlEndpointPath carries the placeholders
    unchanged, whose config.go preserves the templated BaseURL, and whose
    runtime URL — once {shop}/{version} are substituted — equals the real
    Shopify Admin endpoint.
    
    The substitution itself ships in PR-2; this test mimics it inline so
    the structural piece (BaseURL+GraphQLEndpointPath concatenation, no
    hardcoded "/graphql" left behind) regresses loudly if anyone re-flattens
    the URL into a single field.
---
 internal/generator/generator_test.go | 115 +++++++++++++++++++++++++++++++++++
 1 file changed, 115 insertions(+)

diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index 3e46dff5..05f8d17f 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -3775,6 +3775,121 @@ func TestGenerateGraphQLCompiles(t *testing.T) {
 	runGoCommand(t, outputDir, "build", "./...")
 }
 
+// TestGenerateGraphQLEndpointPathRendersTemplatedURL guards PR-1's contract:
+// when a GraphQL spec sets BaseURL to a templated host and GraphQLEndpointPath
+// to a templated path (the Shopify shape), the generated client must build
+// the request URL by concatenating BaseURL + GraphQLEndpointPath without
+// stripping or normalizing the {var} placeholders. PR-2 will substitute the
+// placeholders against env vars at request time; this test simulates that
+// substitution to prove the URL the runtime sees would resolve correctly.
+//
+// The test deliberately builds the spec by hand (instead of going through
+// graphql.ParseSDL) because no real-world API in the parser's
+// knownGraphQLDefaults table uses a templated host today — Shopify is the
+// motivating case but ships outside this repo. Constructing the spec inline
+// keeps the test focused on the template plumbing.
+func TestGenerateGraphQLEndpointPathRendersTemplatedURL(t *testing.T) {
+	t.Parallel()
+
+	const (
+		baseURL          = "https://{shop}"
+		endpointPath     = "/admin/api/{version}/graphql.json"
+		shopValue        = "foo.myshopify.com"
+		versionValue     = "2026-04"
+		expectedFinalURL = "https://foo.myshopify.com/admin/api/2026-04/graphql.json"
+	)
+
+	apiSpec := &spec.APISpec{
+		Name:                 "shopify",
+		Description:          "Shopify Admin GraphQL (test fixture)",
+		Version:              "2026-04",
+		BaseURL:              baseURL,
+		GraphQLEndpointPath:  endpointPath,
+		EndpointTemplateVars: []string{"shop", "version"},
+		Auth: spec.AuthConfig{
+			Type:    "api_key",
+			Header:  "X-Shopify-Access-Token",
+			EnvVars: []string{"SHOPIFY_ACCESS_TOKEN"},
+		},
+		Config: spec.ConfigSpec{
+			Format: "toml",
+			Path:   "~/.config/shopify-pp-cli/config.toml",
+		},
+		Resources: map[string]spec.Resource{
+			// isGraphQLSpec gates emission of graphql.go on every list endpoint
+			// having Path == "/graphql". The path field is a sentinel for
+			// "this resource flows through the GraphQL transport"; it is
+			// distinct from GraphQLEndpointPath, which is the URL path the
+			// client posts to at runtime.
+			"orders": {
+				Description: "Orders",
+				Endpoints: map[string]spec.Endpoint{
+					"list": {
+						Method:       "GET",
+						Path:         "/graphql",
+						Description:  "List orders",
+						ResponsePath: "data.orders.nodes",
+						Pagination: &spec.Pagination{
+							Type:           "cursor",
+							LimitParam:     "first",
+							CursorParam:    "after",
+							NextCursorPath: "data.orders.pageInfo.endCursor",
+							HasMoreField:   "data.orders.pageInfo.hasNextPage",
+						},
+						Response: spec.ResponseDef{Type: "array", Item: "Order"},
+					},
+				},
+			},
+		},
+		Types: map[string]spec.TypeDef{
+			"Order": {Fields: []spec.TypeField{
+				{Name: "id", Type: "string"},
+				{Name: "name", Type: "string"},
+			}},
+		},
+	}
+
+	outputDir := filepath.Join(t.TempDir(), naming.CLI(apiSpec.Name))
+	gen := New(apiSpec, outputDir)
+	require.NoError(t, gen.Generate())
+
+	graphqlGoPath := filepath.Join(outputDir, "internal", "client", "graphql.go")
+	graphqlGoBytes, err := os.ReadFile(graphqlGoPath)
+	require.NoError(t, err, "graphql.go should be generated for a GraphQL spec")
+	graphqlGo := string(graphqlGoBytes)
+
+	// The endpoint path constant must render the templated value verbatim.
+	// Anything that strips or pre-resolves the {var} placeholders here would
+	// break PR-2's runtime substitution and quietly send requests to the
+	// wrong host once a real shop is configured.
+	assert.Contains(t, graphqlGo, `const graphqlEndpointPath = "`+endpointPath+`"`,
+		"graphql.go must render GraphQLEndpointPath into a const")
+	assert.NotContains(t, graphqlGo, `c.Post("/graphql"`,
+		"graphql.go must not retain the hardcoded /graphql path after PR-1")
+	assert.Contains(t, graphqlGo, "c.Post(graphqlEndpointPath",
+		"Query/Mutate must post against the rendered constant, not a literal")
+
+	// The config struct must carry the templated BaseURL through unchanged so
+	// PR-2 can resolve {shop} against SHOPIFY_SHOP at Load() time without
+	// re-deriving the URL shape.
+	configGoBytes, err := os.ReadFile(filepath.Join(outputDir, "internal", "config", "config.go"))
+	require.NoError(t, err)
+	assert.Contains(t, string(configGoBytes), `BaseURL: "`+baseURL+`"`,
+		"config.go must carry the templated BaseURL into Load()'s defaults")
+
+	// Simulate the runtime URL build: client.do() concatenates BaseURL+path.
+	// PR-1 must produce a URL that — once env vars are substituted — points
+	// at the real Shopify endpoint. The substitution itself ships in PR-2;
+	// here we mimic it inline to prove the structure is right.
+	rawURL := baseURL + endpointPath
+	resolved := strings.NewReplacer(
+		"{shop}", shopValue,
+		"{version}", versionValue,
+	).Replace(rawURL)
+	assert.Equal(t, expectedFinalURL, resolved,
+		"BaseURL + GraphQLEndpointPath must resolve to the Shopify Admin endpoint after env substitution")
+}
+
 // TestGenerateMCPMainStdioDefault confirms that a spec with no mcp: block
 // produces the same stdio-only MCP entry point we've always emitted. Remote
 // transport is opt-in; the default stays on the current behavior so existing

← 699e8ed7 feat(cli): split BaseURL from GraphQLEndpointPath in GraphQL  ·  back to Cli Printing Press  ·  feat(cli): emit buildURL helper + Config.TemplateVars for te 289e5988 →