package cloud import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" ) func serve(t *testing.T, h http.HandlerFunc) *Client { t.Helper() srv := httptest.NewServer(h) t.Cleanup(srv.Close) c := New(srv.URL) c.SetSession(Session{Token: "test-token"}) return c } func fail(w http.ResponseWriter, status int, code, msg string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": code, "message": msg}) } // A customer with no photo is the DEFAULT configuration of this product, not a // fault. If it surfaced as an error the record sheet would show a red failure // box for every customer in every shop that has not turned images on. func TestNoPhotoIsNotAnError(t *testing.T) { for _, tc := range []struct{ code, want string }{ {"no_image", "No photo"}, {"images_disabled", "not storing"}, } { t.Run(tc.code, func(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { fail(w, http.StatusNotFound, tc.code, "server prose") }) p, err := c.VisitorImage(context.Background(), "abc") if err != nil { t.Fatalf("returned an error for a normal state: %v", err) } if p.Available { t.Error("Available should be false when there is no photo") } if p.Reason == "" { t.Error("a missing photo must come with an explanation") } }) } } func TestPhotoReturnsTheSignedLink(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer test-token" { t.Errorf("Authorization = %q", got) } json.NewEncoder(w).Encode(map[string]any{ "url": "https://example.test/signed", "expires_in": 900}) }) p, err := c.VisitorImage(context.Background(), "abc") if err != nil { t.Fatal(err) } if !p.Available || p.URL != "https://example.test/signed" || p.ExpiresIn != 900 { t.Fatalf("got %+v", p) } } // A real failure must still be a failure: silently rendering initials would // hide a broken server behind a design that looks intentional. func TestPhotoServerErrorIsAnError(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { fail(w, http.StatusInternalServerError, "server_error", "boom") }) if _, err := c.VisitorImage(context.Background(), "abc"); err == nil { t.Fatal("a 500 must not be reported as 'no photo'") } } // The server deletes stored images before it touches the database and refuses // the whole request if one fails, so an error here means NOTHING was erased. // Swallowing it would tell a shop a legal request had been honoured when it // had not. func TestForgetVisitorSurfacesFailure(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { t.Errorf("method = %s, want DELETE", r.Method) } fail(w, http.StatusBadGateway, "storage_error", "The photo could not be deleted, so nothing was erased.") }) err := c.ForgetVisitor(context.Background(), "abc") if err == nil { t.Fatal("a refused erasure must not look like success") } if err.Error() != "The photo could not be deleted, so nothing was erased." { t.Errorf("lost the server's own words: %q", err) } } func TestForgetVisitorSucceedsOn204(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) }) if err := c.ForgetVisitor(context.Background(), "abc"); err != nil { t.Fatal(err) } } // APIError carries the code without changing what anything that prints the // error sees — every existing screen relies on that text. func TestAPIErrorKeepsServerMessage(t *testing.T) { c := serve(t, func(w http.ResponseWriter, r *http.Request) { fail(w, http.StatusForbidden, "forbidden", "Your account cannot delete customer records.") }) err := c.ForgetVisitor(context.Background(), "abc") if err.Error() != "Your account cannot delete customer records." { t.Errorf("message = %q", err) } var ae *APIError if !errors.As(err, &ae) || ae.Code != "forbidden" || ae.Status != 403 { t.Errorf("code not preserved: %+v", ae) } } // An id with a slash or a space must not silently address a different route. func TestVisitorIDIsPathEscaped(t *testing.T) { var got string c := serve(t, func(w http.ResponseWriter, r *http.Request) { got = r.URL.EscapedPath() w.WriteHeader(http.StatusNoContent) }) c.ForgetVisitor(context.Background(), "a b/c") //nolint:errcheck if got != "/api/visitors/a%20b%2Fc" { t.Errorf("path = %q", got) } }