package main import ( "context" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" ) // recordNotFound turns the record door's membership 404 into what actually // went wrong, using the app's route list as the witness (#264): a host the // list carries is a box route with no record, a host it does not carry is a // miss, and a list that will not answer either leaves the membership riddle // standing, since then the account or app really may be wrong. func TestRecordNotFoundNarrowsTheMembership404(t *testing.T) { routes := `{"routes":[{"host":"shop.example.com","kind":"instance","environment":"staging"},` + `{"host":"canary-ab12.shop.acme.oncarlos.test","kind":"instance","channel":"canary/ab12","platform_canary":true}]}` boxRoute := func(h string) string { return "BOX " + h } cases := []struct { name string host string routesOK bool in error want string // substring of the error; "" means the input error, unchanged }{ {"box route with no record", "shop.example.com", true, notFoundErr(), "BOX shop.example.com"}, {"platform canary is not sent to the box", "canary-ab12.shop.acme.oncarlos.test", true, notFoundErr(), "is a canary route the platform mints for shop"}, {"no such host", "shop.exmaple.com", true, notFoundErr(), "shop has no instance named shop.exmaple.com"}, {"route list refused too", "shop.example.com", false, notFoundErr(), ""}, {"another error passes through", "shop.example.com", true, errors.New("could not read the instance"), ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { listed := false ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasSuffix(r.URL.Path, "/routes") { t.Errorf("unexpected request %s", r.URL.Path) } listed = true if !c.routesOK { http.NotFound(w, r) return } io.WriteString(w, routes) })) defer ts.Close() err := recordNotFound(context.Background(), newTestSession(ts.URL), apiAcct{Seg: "acme"}, "shop", c.host, c.in, boxRoute) if c.want == "" { if err != c.in { t.Fatalf("err = %v, want the input error %v unchanged", err, c.in) } } else if err == nil || !strings.Contains(err.Error(), c.want) { t.Fatalf("err = %v, want it to contain %q", err, c.want) } if errors.Is(c.in, errMembershipNotFound) != listed { t.Fatalf("route list consulted = %v, want it only for the membership 404", listed) } }) } } // The membership message must survive as itself everywhere else: the // sentinel only changes how recordNotFound recognises it, not what it says. func TestNotFoundErrIsTheSentinel(t *testing.T) { if !errors.Is(apiErrorFromBody(http.StatusNotFound, []byte("404 page not found\n")), errMembershipNotFound) { t.Fatal("a text/plain 404 must come back as errMembershipNotFound") } if !strings.Contains(notFoundErr().Error(), "check --account and --app") { t.Fatalf("notFoundErr() = %q, want the membership message", notFoundErr()) } }