-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpath_test.go
More file actions
199 lines (179 loc) · 6.24 KB
/
Copy pathpath_test.go
File metadata and controls
199 lines (179 loc) · 6.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package nexus
import (
"context"
"testing"
"time"
)
// TestPath_RegistersForServiceLookup verifies the round-trip:
// nexus.Path on the module → modulePublicPath registry →
// (*App).Service constructs a Service whose GraphQLPath is
// rooted at <path>/graphql. This is the contract that lets a
// constructor write app.Service("uaa") and inherit the module's
// public path without needing AtGraphQL.
func TestPath_RegistersForServiceLookup(t *testing.T) {
resetPublicPathRegistryForTest(t)
// Build a module that declares Path. The sub-options don't
// matter for this test — we're checking the side effect of
// Path on the registry, which Module() applies during the
// option walk regardless of what else is in the module.
_ = Module("uaa",
Path("/oats-uaa"),
)
if got := modulePublicPathOf("uaa"); got != "/oats-uaa" {
t.Fatalf("registry: got %q, want %q", got, "/oats-uaa")
}
// app.Service("uaa") should now produce a Service rooted at
// /oats-uaa/graphql, not the framework default /graphql.
app := New(Config{})
svc := app.Service("uaa")
if svc.GraphQLPath() != "/oats-uaa/graphql" {
t.Fatalf("Service.GraphQLPath: got %q, want %q",
svc.GraphQLPath(), "/oats-uaa/graphql")
}
// A different service name (no matching module) keeps the
// framework default — Path is scoped to the module that
// declared it, not a global REST/GraphQL switch.
other := app.Service("billing")
if other.GraphQLPath() != DefaultGraphQLPath {
t.Fatalf("unrelated service: got %q, want %q",
other.GraphQLPath(), DefaultGraphQLPath)
}
}
// TestPath_NormalizesLooseInput ensures Path() accepts the same
// loose forms RoutePrefix already does (no leading slash, trailing
// slash, "/") and stores the canonical "/seg" form so downstream
// consumers (REST mount, GraphQL path) get a consistent shape.
func TestPath_NormalizesLooseInput(t *testing.T) {
cases := []struct {
in string
want string
}{
{"/oats-uaa", "/oats-uaa"},
{"oats-uaa", "/oats-uaa"},
{"/oats-uaa/", "/oats-uaa"},
{"oats-uaa/", "/oats-uaa"},
{"", ""},
{"/", ""},
}
for _, tc := range cases {
opt := Path(tc.in)
got := opt.(pathOption).normalizedPath()
if got != tc.want {
t.Errorf("Path(%q): got %q want %q", tc.in, got, tc.want)
}
}
}
// TestPath_AtGraphQLOverrides confirms that an explicit
// (*Service).AtGraphQL(...) AFTER the auto-derivation still wins.
// This matters when one service in a module needs a different
// GraphQL path than the module's public root — Path is the
// default, AtGraphQL is the escape hatch.
func TestPath_AtGraphQLOverrides(t *testing.T) {
resetPublicPathRegistryForTest(t)
_ = Module("uaa", Path("/oats-uaa"))
app := New(Config{})
svc := app.Service("uaa").AtGraphQL("/custom/graphql")
if svc.GraphQLPath() != "/custom/graphql" {
t.Fatalf("AtGraphQL override: got %q, want %q",
svc.GraphQLPath(), "/custom/graphql")
}
}
// TestPath_MultiServiceModuleAllMountUnderPath is the regression
// test for the original bug report: when a single module declares
// nexus.Path("/oats-uaa") AND has handlers attached to multiple
// services (e.g. "uaa" + "user" inside the uaa module), every
// field should mount at /oats-uaa/graphql — not just the ones
// whose service name happens to match the module name.
//
// We exercise this through the public surface: build the module
// options, run them through fx end-to-end, and inspect the gin
// engine's registered routes to confirm /oats-uaa/graphql is the
// only GraphQL mount and /graphql is NOT registered.
func TestPath_MultiServiceModuleAllMountUnderPath(t *testing.T) {
resetPublicPathRegistryForTest(t)
type uaaArgs struct {
Token string `graphql:"token"`
}
type userArgs struct {
ID string `graphql:"id"`
}
type uaaSvc struct{ *Service }
type userSvc struct{ *Service }
newUaaSvc := func(app *App) *uaaSvc { return &uaaSvc{app.Service("uaa")} }
newUserSvc := func(app *App) *userSvc { return &userSvc{app.Service("user")} }
newCheckToken := func(_ *uaaSvc, _ Params[uaaArgs]) (string, error) { return "ok", nil }
newGetUser := func(_ *userSvc, _ Params[userArgs]) (string, error) { return "u", nil }
mod := Module("uaa",
Path("/oats-uaa"),
Provide(newUaaSvc, newUserSvc),
AsQuery(newCheckToken),
AsQuery(newGetUser),
)
app, err := newApp(Config{Server: ServerConfig{Addr: "127.0.0.1:0"}}, mod)
if err != nil {
t.Fatalf("newApp: %v", err)
}
defer app.Stop()
routes := app.Engine().Routes()
var gqlPaths []string
for _, r := range routes {
if r.Path == "/oats-uaa/graphql" || r.Path == "/graphql" {
if !contains(gqlPaths, r.Path) {
gqlPaths = append(gqlPaths, r.Path)
}
}
}
if !contains(gqlPaths, "/oats-uaa/graphql") {
t.Errorf("missing /oats-uaa/graphql mount; routes seen: %v", gqlPaths)
}
if contains(gqlPaths, "/graphql") {
t.Errorf("unexpected /graphql mount — Path should have moved every module field there; routes seen: %v", gqlPaths)
}
}
// newApp is a thin wrapper over InProcess (the public in-process boot used by
// the nexustest harness): it boots cfg+opts with nexus.Run's full early→user→
// late option ordering — so AsRest/AsQuery/AsMutation/AsWS mount for real — on an
// ephemeral port, and exposes Stop(). Used to drive the full mount path
// in-process and inspect the resulting route table / registry.
func newApp(cfg Config, opts ...Option) (*testApp, error) {
app, stop, err := InProcess(cfg, opts...)
if err != nil {
return nil, err
}
return &testApp{App: app, stop: stop}, nil
}
type testApp struct {
*App
stop func(context.Context) error
}
func (a *testApp) Stop() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = a.stop(ctx)
}
func contains(xs []string, s string) bool {
for _, x := range xs {
if x == s {
return true
}
}
return false
}
// resetPublicPathRegistryForTest wipes the package-global
// modulePublicPath map so consecutive tests don't bleed state.
// Mirrors resetDeploymentDefaultsForTest in deployment_gate_test.go.
func resetPublicPathRegistryForTest(t *testing.T) {
t.Helper()
modulePublicPathMu.Lock()
for k := range modulePublicPath {
delete(modulePublicPath, k)
}
modulePublicPathMu.Unlock()
t.Cleanup(func() {
modulePublicPathMu.Lock()
for k := range modulePublicPath {
delete(modulePublicPath, k)
}
modulePublicPathMu.Unlock()
})
}