Skip to content

Commit 6305246

Browse files
Merge pull request #387 from rest-sh/fix/promoted-command-surface-refresh
fix(embed): refresh promoted API metadata
2 parents 0824d33 + a1037d2 commit 6305246

2 files changed

Lines changed: 349 additions & 7 deletions

File tree

internal/cli/cli_test.go

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/rest-sh/restish/v2/config"
1919
"github.com/rest-sh/restish/v2/internal/cli"
20+
internalspec "github.com/rest-sh/restish/v2/internal/spec"
2021
)
2122

2223
type roundTripperFunc func(*http.Request) (*http.Response, error)
@@ -307,6 +308,58 @@ func TestCommandSurfaceSyncedFallbackPreservesOriginalArgs(t *testing.T) {
307308
}
308309
}
309310

311+
func TestCommandSurfaceBaseURLFallbackRefreshesUnknownOperation(t *testing.T) {
312+
specIncludesNew := false
313+
var specRequests int
314+
var lastPath string
315+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
316+
switch r.URL.Path {
317+
case "/openapi.json":
318+
specRequests++
319+
w.Header().Set("Content-Type", "application/json")
320+
fmt.Fprint(w, commandSurfaceRefreshSpec(specIncludesNew))
321+
case "/new":
322+
lastPath = r.URL.Path
323+
w.Header().Set("Content-Type", "application/json")
324+
fmt.Fprint(w, `{"ok":true}`)
325+
default:
326+
http.NotFound(w, r)
327+
}
328+
}))
329+
defer srv.Close()
330+
331+
cfgFile := writeTestConfig(t, &config.Config{APIs: map[string]*config.APIConfig{
332+
"api": {BaseURL: srv.URL},
333+
}})
334+
cacheDir := t.TempDir()
335+
336+
c, out, _ := newTestCLI(t)
337+
c.Hooks().ConfigPath = cfgFile
338+
c.Hooks().SpecCachePath = cacheDir
339+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
340+
if err := c.Run([]string{"example", "--help"}); err != nil {
341+
t.Fatalf("prime help: %v", err)
342+
}
343+
if got := out.String(); !strings.Contains(got, "get-old") || strings.Contains(got, "get-new") {
344+
t.Fatalf("expected initial help to use old cached operations, got:\n%s", got)
345+
}
346+
347+
specIncludesNew = true
348+
c, _, _ = newTestCLI(t)
349+
c.Hooks().ConfigPath = cfgFile
350+
c.Hooks().SpecCachePath = cacheDir
351+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
352+
if err := c.Run([]string{"example", "get-new"}); err != nil {
353+
t.Fatalf("refreshed generated command: %v", err)
354+
}
355+
if lastPath != "/new" {
356+
t.Fatalf("request path = %q, want /new", lastPath)
357+
}
358+
if specRequests < 2 {
359+
t.Fatalf("spec requests = %d, want initial discovery and refresh", specRequests)
360+
}
361+
}
362+
310363
func commandSurfaceRefreshSpec(includeNew bool) string {
311364
paths := `"/old":{"get":{"operationId":"getOld","responses":{"200":{"description":"OK"}}}}`
312365
if includeNew {
@@ -404,6 +457,233 @@ func TestCommandSurfaceRootAuthHeader(t *testing.T) {
404457
}
405458
}
406459

460+
func TestCommandSurfaceRootAuthOperationFetchesMetadataOnFirstRun(t *testing.T) {
461+
var specRequests int
462+
var baseURL string
463+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
464+
switch r.URL.Path {
465+
case "/openapi.json":
466+
specRequests++
467+
w.Header().Set("Content-Type", "application/json")
468+
fmt.Fprint(w, commandSurfaceAuthSpec(baseURL, true))
469+
default:
470+
http.NotFound(w, r)
471+
}
472+
}))
473+
defer srv.Close()
474+
baseURL = srv.URL
475+
476+
cfgFile := writeTestConfig(t, &config.Config{APIs: map[string]*config.APIConfig{
477+
"api": {
478+
BaseURL: srv.URL,
479+
Profiles: map[string]*config.ProfileConfig{
480+
"default": profileCredentials(map[string]*config.CredentialConfig{
481+
"PartnerKey": testCredential(apiKeyAuth("header", "X-Partner-Key", "secret")),
482+
}),
483+
},
484+
},
485+
}})
486+
487+
c, out, _ := newTestCLI(t)
488+
c.Hooks().ConfigPath = cfgFile
489+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
490+
if err := c.Run([]string{"example", "auth", "header", "--operation", "partner-report"}); err != nil {
491+
t.Fatalf("auth header operation: %v", err)
492+
}
493+
if got := strings.TrimSpace(out.String()); got != "X-Partner-Key: secret" {
494+
t.Fatalf("auth header operation = %q, want X-Partner-Key: secret", got)
495+
}
496+
if specRequests != 1 {
497+
t.Fatalf("spec requests = %d, want 1", specRequests)
498+
}
499+
}
500+
501+
func TestCommandSurfaceRootAuthOperationRefreshesMissingFreshOperation(t *testing.T) {
502+
specIncludesPartner := false
503+
var specRequests int
504+
var baseURL string
505+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
506+
switch r.URL.Path {
507+
case "/openapi.json":
508+
specRequests++
509+
w.Header().Set("Content-Type", "application/json")
510+
fmt.Fprint(w, commandSurfaceAuthSpec(baseURL, specIncludesPartner))
511+
default:
512+
http.NotFound(w, r)
513+
}
514+
}))
515+
defer srv.Close()
516+
baseURL = srv.URL
517+
518+
cfgFile := writeTestConfig(t, &config.Config{APIs: map[string]*config.APIConfig{
519+
"api": {
520+
BaseURL: srv.URL,
521+
Profiles: map[string]*config.ProfileConfig{
522+
"default": profileCredentials(map[string]*config.CredentialConfig{
523+
"PartnerKey": testCredential(apiKeyAuth("header", "X-Partner-Key", "secret")),
524+
}),
525+
},
526+
},
527+
}})
528+
cacheDir := t.TempDir()
529+
530+
c, out, _ := newTestCLI(t)
531+
c.Hooks().ConfigPath = cfgFile
532+
c.Hooks().SpecCachePath = cacheDir
533+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
534+
if err := c.Run([]string{"example", "--help"}); err != nil {
535+
t.Fatalf("prime help: %v", err)
536+
}
537+
if got := out.String(); strings.Contains(got, "partner-report") {
538+
t.Fatalf("expected initial help to omit partner-report, got:\n%s", got)
539+
}
540+
541+
specIncludesPartner = true
542+
c, out, _ = newTestCLI(t)
543+
c.Hooks().ConfigPath = cfgFile
544+
c.Hooks().SpecCachePath = cacheDir
545+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
546+
if err := c.Run([]string{"example", "auth", "header", "--operation", "partner-report"}); err != nil {
547+
t.Fatalf("auth header operation: %v", err)
548+
}
549+
if got := strings.TrimSpace(out.String()); got != "X-Partner-Key: secret" {
550+
t.Fatalf("auth header operation = %q, want X-Partner-Key: secret", got)
551+
}
552+
if specRequests < 2 {
553+
t.Fatalf("spec requests = %d, want initial discovery and refresh", specRequests)
554+
}
555+
}
556+
557+
func TestCommandSurfaceRootAuthOperationUsesRawSpecCacheBeforeRefresh(t *testing.T) {
558+
var originRequests int
559+
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
560+
originRequests++
561+
http.NotFound(w, r)
562+
}))
563+
defer origin.Close()
564+
var profileRequests int
565+
profile := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
566+
profileRequests++
567+
http.NotFound(w, r)
568+
}))
569+
defer profile.Close()
570+
571+
rawSpec := openAPISpec(origin.URL, "Auth API",
572+
openAPISecuritySchemes(`"PartnerKey":{"type":"apiKey","in":"header","name":"X-Partner-Key"}`),
573+
openAPIPaths(openAPIGet("/v2/private", "", `"security":[{"PartnerKey":[]}]`)))
574+
apiSpec, err := internalspec.OpenAPILoader{}.Load([]byte(rawSpec))
575+
if err != nil {
576+
t.Fatalf("load spec: %v", err)
577+
}
578+
cacheDir := t.TempDir()
579+
if err := internalspec.StoreSpecInCache(cacheDir, "api", cli.Version, apiSpec, nil, internalspec.OperationOptions{
580+
BaseURL: origin.URL,
581+
OperationBase: "/v1",
582+
}, time.Hour); err != nil {
583+
t.Fatalf("store spec cache: %v", err)
584+
}
585+
586+
cfgFile := writeTestConfig(t, &config.Config{APIs: map[string]*config.APIConfig{
587+
"api": {
588+
BaseURL: origin.URL,
589+
OperationBase: "/v1",
590+
Profiles: map[string]*config.ProfileConfig{
591+
"staging": {
592+
BaseURL: profile.URL,
593+
OperationBase: "/v2",
594+
Credentials: map[string]*config.CredentialConfig{
595+
"PartnerKey": testCredential(apiKeyAuth("header", "X-Partner-Key", "secret")),
596+
},
597+
},
598+
},
599+
},
600+
}})
601+
602+
c, out, _ := newTestCLI(t)
603+
c.Hooks().ConfigPath = cfgFile
604+
c.Hooks().SpecCachePath = cacheDir
605+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
606+
if err := c.Run([]string{"example", "--rsh-profile", "staging", "auth", "header", "--operation", "get-private"}); err != nil {
607+
t.Fatalf("auth header operation: %v", err)
608+
}
609+
if got := strings.TrimSpace(out.String()); got != "X-Partner-Key: secret" {
610+
t.Fatalf("auth header operation = %q, want X-Partner-Key: secret", got)
611+
}
612+
if originRequests != 0 || profileRequests != 0 {
613+
t.Fatalf("discovery requests: origin=%d profile=%d, want 0", originRequests, profileRequests)
614+
}
615+
}
616+
617+
func TestCommandSurfaceRootAuthOperationConflictsDoNotRefreshMetadata(t *testing.T) {
618+
var specRequests int
619+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
620+
if r.URL.Path == "/openapi.json" {
621+
specRequests++
622+
}
623+
http.NotFound(w, r)
624+
}))
625+
defer srv.Close()
626+
627+
cfgFile := writeTestConfig(t, &config.Config{APIs: map[string]*config.APIConfig{
628+
"api": {
629+
BaseURL: srv.URL,
630+
Profiles: map[string]*config.ProfileConfig{
631+
"default": profileCredentials(map[string]*config.CredentialConfig{
632+
"PartnerKey": testCredential(apiKeyAuth("header", "X-Partner-Key", "secret")),
633+
}),
634+
},
635+
},
636+
}})
637+
638+
tests := []struct {
639+
name string
640+
args []string
641+
want string
642+
}{
643+
{
644+
name: "auth get credential and operation",
645+
args: []string{"example", "auth", "get", "PartnerKey", "--operation", "partner-report"},
646+
want: "--operation and credential ID are mutually exclusive",
647+
},
648+
{
649+
name: "auth inspect credential and operation",
650+
args: []string{"example", "auth", "inspect", "--credential", "PartnerKey", "--operation", "partner-report"},
651+
want: "--operation and --credential are mutually exclusive",
652+
},
653+
{
654+
name: "auth inspect operation and output format",
655+
args: []string{"example", "auth", "inspect", "--operation", "partner-report", "-o", "json"},
656+
want: "does not support -o/--rsh-output-format",
657+
},
658+
}
659+
for _, tt := range tests {
660+
t.Run(tt.name, func(t *testing.T) {
661+
specRequests = 0
662+
c, _, _ := newTestCLI(t)
663+
c.Hooks().ConfigPath = cfgFile
664+
c.SetCommandSurface(cli.CommandSurface{PromotedAPI: "api"})
665+
666+
err := c.Run(tt.args)
667+
if err == nil || !strings.Contains(err.Error(), tt.want) {
668+
t.Fatalf("error = %v, want %q", err, tt.want)
669+
}
670+
if specRequests != 0 {
671+
t.Fatalf("spec requests = %d, want 0", specRequests)
672+
}
673+
})
674+
}
675+
}
676+
677+
func commandSurfaceAuthSpec(baseURL string, includePartner bool) string {
678+
paths := []string{openAPIGet("/old", "oldReport", `"security":[{"PartnerKey":[]}]`)}
679+
if includePartner {
680+
paths = append(paths, openAPIGet("/partner", "partnerReport", `"security":[{"PartnerKey":[]}]`))
681+
}
682+
return openAPISpec(baseURL, "Auth API",
683+
openAPISecuritySchemes(`"PartnerKey":{"type":"apiKey","in":"header","name":"X-Partner-Key"}`),
684+
openAPIPaths(paths...))
685+
}
686+
407687
func writeCommandSurfaceSpecConfig(t *testing.T, c *cli.CLI, apiName, operationID string) {
408688
t.Helper()
409689
specPath := filepath.Join(t.TempDir(), "openapi.yaml")

0 commit comments

Comments
 (0)