package shell import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "path/filepath" "strings" "testing" "testing/fstest" ) var clientFS = fstest.MapFS{ "index.html": {Data: []byte("Warp")}, "assets/app-1.js": {Data: []byte("console.log(1)")}, } // homeserver is a fake homeserver: /versions, and a well-known document // when delegate is set. func homeserver(t *testing.T, delegate string) *httptest.Server { t.Helper() mux := http.NewServeMux() mux.HandleFunc("GET /_matrix/client/versions", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"versions":["v1.11"]}`)) }) if delegate != "" { mux.HandleFunc("GET /.well-known/matrix/client", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"m.homeserver":{"base_url":"` + delegate + `/"}}`)) }) } srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } func newShell(t *testing.T, opts Options) (*Shell, *Store) { t.Helper() if opts.Store == nil { opts.Store = NewStore(filepath.Join(t.TempDir(), "desktop.json")) } if opts.Client == nil { opts.Client = clientFS } s, err := New(opts) if err != nil { t.Fatal(err) } return s, opts.Store } func do(h http.Handler, method, target, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, strings.NewReader(body)) if method == http.MethodPost && strings.HasPrefix(target, SetupPath) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } rec := httptest.NewRecorder() h.ServeHTTP(rec, req) return rec } func TestFirstRunAsksForTheHomeserver(t *testing.T) { s, _ := newShell(t, Options{}) for _, p := range []string{"/", "/room/!abc:warp.example", SetupPath} { rec := do(s, http.MethodGet, p, "") if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "Welcome to Warp") { t.Errorf("GET %s: %d %q", p, rec.Code, rec.Body.String()) } } if rec := do(s, http.MethodPost, "/_matrix/client/v3/login", "{}"); rec.Code != http.StatusServiceUnavailable { t.Errorf("POST before setup: %d", rec.Code) } } func TestSetupSavesTheHomeserverAndServesTheClient(t *testing.T) { hs := homeserver(t, "") s, store := newShell(t, Options{}) rec := do(s, http.MethodPost, SetupPath, url.Values{"server": {hs.URL + "/"}}.Encode()) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `http-equiv="refresh"`) { t.Fatalf("setup: %d %q", rec.Code, rec.Body.String()) } if got := s.Homeserver(); got != hs.URL { t.Errorf("Homeserver() = %q, want %q", got, hs.URL) } if st, _ := store.Load(); st.Homeserver != hs.URL { t.Errorf("saved %q", st.Homeserver) } // The client is served now, told to call the homeserver directly. if rec := do(s, http.MethodGet, "/", ""); !strings.Contains(rec.Body.String(), "Warp") { t.Errorf("GET / after setup: %q", rec.Body.String()) } rec = do(s, http.MethodGet, "/config.json", "") var cfg struct { HomeserverURL string `json:"homeserver_url"` } if err := json.Unmarshal(rec.Body.Bytes(), &cfg); err != nil || cfg.HomeserverURL != hs.URL { t.Errorf("config.json %q", rec.Body.String()) } // Nothing is proxied. if rec := do(s, http.MethodGet, "/_matrix/client/versions", ""); rec.Code != http.StatusNotFound { t.Errorf("/_matrix proxied: %d", rec.Code) } // The next run starts with it. s2, _ := newShell(t, Options{Store: store}) if s2.Homeserver() != hs.URL { t.Errorf("next run: %q", s2.Homeserver()) } if rec := do(s2, http.MethodGet, SetupPath, ""); !strings.Contains(rec.Body.String(), "Change homeserver") { t.Errorf("setup page once configured: %q", rec.Body.String()) } } func TestSetupRejectsWhatIsntAHomeserver(t *testing.T) { notMatrix := httptest.NewServer(http.NotFoundHandler()) t.Cleanup(notMatrix.Close) s, store := newShell(t, Options{}) rec := do(s, http.MethodPost, SetupPath, url.Values{"server": {notMatrix.URL}}.Encode()) if !strings.Contains(rec.Body.String(), "doesn’t look like a Matrix homeserver") { t.Errorf("body %q", rec.Body.String()) } // What was typed stays in the field. if !strings.Contains(rec.Body.String(), `value="`+notMatrix.URL+`"`) { t.Errorf("input not kept: %q", rec.Body.String()) } if s.Homeserver() != "" { t.Errorf("homeserver set to %q", s.Homeserver()) } if st, _ := store.Load(); st.Homeserver != "" { t.Errorf("saved %q", st.Homeserver) } } func TestDamagedSettingsAskAgain(t *testing.T) { store := NewStore(filepath.Join(t.TempDir(), "desktop.json")) if err := store.Save(Settings{Homeserver: "matrix.example"}); err != nil { t.Fatal(err) } s, _ := newShell(t, Options{Store: store}) if s.Homeserver() != "" { t.Errorf("Homeserver() = %q", s.Homeserver()) } if rec := do(s, http.MethodGet, "/", ""); !strings.Contains(rec.Body.String(), "Welcome to Warp") { t.Errorf("GET /: %q", rec.Body.String()) } } func TestOverrideIsNotSaved(t *testing.T) { store := NewStore(filepath.Join(t.TempDir(), "desktop.json")) if err := store.Save(Settings{Homeserver: "https://saved.example"}); err != nil { t.Fatal(err) } s, _ := newShell(t, Options{Store: store, Homeserver: "http://localhost:8008"}) if s.Homeserver() != "http://localhost:8008" { t.Errorf("Homeserver() = %q", s.Homeserver()) } if st, _ := store.Load(); st.Homeserver != "https://saved.example" { t.Errorf("saved %q", st.Homeserver) } if _, err := New(Options{Store: store, Homeserver: "matrix.example"}); err == nil { t.Error("a homeserver without a scheme was accepted") } } func TestCSPAllowsCallBackends(t *testing.T) { s, _ := newShell(t, Options{Homeserver: "http://localhost:8008"}) csp := do(s, http.MethodGet, "/", "").Header().Get("Content-Security-Policy") if !strings.Contains(csp, "connect-src 'self' http://localhost:8008 https: wss:;") { t.Errorf("CSP %q", csp) } } func TestDesktopScriptsAreAddedToTheClient(t *testing.T) { s, _ := newShell(t, Options{Homeserver: "http://localhost:8008", Client: fstest.MapFS{ "index.html": {Data: []byte("Warp")}, }}) want := `` + "\n " + `` + "\n " + `` for _, p := range []string{"/", "/room/!abc:warp.example"} { body := do(s, http.MethodGet, p, "").Body.String() if !strings.Contains(body, want) { t.Errorf("GET %s: %q", p, body) } } rec := do(s, http.MethodGet, linkScriptPath, "") if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/javascript") || !strings.Contains(rec.Body.String(), "/_desktop/open") { t.Errorf("links.js: %d %q", rec.Code, rec.Header().Get("Content-Type")) } rec = do(s, http.MethodGet, bridgeScriptPath, "") if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), "window.warpDesktop") { t.Errorf("bridge.js: %d %q", rec.Code, rec.Body.String()) } } func TestOpenURL(t *testing.T) { var opened []string s, _ := newShell(t, Options{OpenURL: func(u string) error { opened = append(opened, u); return nil }}) for _, u := range []string{"https://example.org/a?b=c", "mailto:alice@example.org"} { if rec := do(s, http.MethodPost, openPath, `{"url":"`+u+`"}`); rec.Code != http.StatusNoContent { t.Errorf("open %s: %d", u, rec.Code) } } for _, u := range []string{"file:///etc/passwd", "javascript:alert(1)", "https://", "ms-settings:"} { if rec := do(s, http.MethodPost, openPath, `{"url":"`+u+`"}`); rec.Code != http.StatusBadRequest { t.Errorf("open %s: %d", u, rec.Code) } } if rec := do(s, http.MethodGet, openPath, ""); rec.Code != http.StatusMethodNotAllowed { t.Errorf("GET open: %d", rec.Code) } if len(opened) != 2 { t.Errorf("opened %q", opened) } } func TestDiscover(t *testing.T) { target := homeserver(t, "") delegating := homeserver(t, target.URL) badDelegate := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"m.homeserver":{"base_url":"ftp://x"}}`)) })) t.Cleanup(badDelegate.Close) c := http.DefaultClient ctx := context.Background() if got, err := Discover(ctx, c, " "+target.URL+"/ "); err != nil || got != target.URL { t.Errorf("plain: %q %v", got, err) } if got, err := Discover(ctx, c, delegating.URL); err != nil || got != target.URL { t.Errorf("delegated: %q %v", got, err) } if _, err := Discover(ctx, c, badDelegate.URL); err == nil || !strings.Contains(err.Error(), "isn’t a valid homeserver address") { t.Errorf("bad delegation: %v", err) } // Delegation to an address that doesn't answer falls back to the one // typed, when that one is a homeserver itself. deadDelegation := homeserver(t, "http://127.0.0.1:1") if got, err := Discover(ctx, c, deadDelegation.URL); err != nil || got != deadDelegation.URL { t.Errorf("dead delegation: %q %v", got, err) } nowhere := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"m.homeserver":{"base_url":"http://127.0.0.1:1"}}`)) })) t.Cleanup(nowhere.Close) if _, err := Discover(ctx, c, nowhere.URL); err == nil || !strings.Contains(err.Error(), "points to http://127.0.0.1:1. Can’t reach 127.0.0.1:1.") { t.Errorf("delegation to nowhere: %v", err) } for _, in := range []string{"", "https://x.example/?a=b", "ftp://x.example"} { if _, err := Discover(ctx, c, in); err == nil { t.Errorf("%q accepted", in) } } // A bare name gets https://; nothing listens there, so it can't be reached. if _, err := Discover(ctx, c, "127.0.0.1:1"); err == nil || !strings.Contains(err.Error(), "Can’t reach 127.0.0.1:1") { t.Errorf("unreachable: %v", err) } }