package user import ( "bytes" "net/http" "net/http/httptest" "regexp" "strings" "testing" "time" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/compass" ) // TestBackendLinkShowPost pins plan 08's link code transport (C-WR-01, // C-IN-06): POST /backend-link/show answers exactly like the GET route (the // data for a live code, 404 for an unknown, a used or an expired one), both // with Cache-Control no-store, private; a code in the query string of the // show or confirm POST is ignored (RC-IN-01); My account answers the link as // /link-admin#code=<64 hex> with no query string; // golem15.user.backend_links.page_path changes the path, and a value // without a leading slash gets one. Postgres only: under -short nothing // runs. func TestBackendLinkShowPost(t *testing.T) { if testing.Short() { return } env := newAdminEnv(t) show := func(t *testing.T, code string) (get, post *struct { status int body string cache string }) { t.Helper() g := env.call(t, http.MethodGet, "/_user/api/v1/backend-link/"+code, "", nil) p := env.call(t, http.MethodPost, "/_user/api/v1/backend-link/show", "", map[string]string{"code": code}) return &struct { status int body string cache string }{g.Code, g.Body.String(), g.Header().Get("Cache-Control")}, &struct { status int body string cache string }{p.Code, p.Body.String(), p.Header().Get("Cache-Control")} } same := func(t *testing.T, code string, status int) { t.Helper() get, post := show(t, code) if get.status != status || post.status != status { t.Fatalf("show %s: GET %d, POST %d, want %d", code[:8], get.status, post.status, status) } if !bytes.Equal([]byte(get.body), []byte(post.body)) { t.Fatalf("POST show differs from GET:\nGET %s\nPOST %s", get.body, post.body) } if get.cache != "no-store, private" || post.cache != "no-store, private" { t.Fatalf("Cache-Control: GET %q, POST %q", get.cache, post.cache) } } t.Run("POST show answers like GET", func(t *testing.T) { token := env.admin(t) live := env.startLink(t, token) same(t, live, http.StatusOK) if _, post := show(t, live); !strings.Contains(post.body, `"admin"`) || !strings.Contains(post.body, `"expires_at"`) { t.Fatalf("POST show data = %s", post.body) } same(t, strings.Repeat("a", 64), http.StatusNotFound) used := env.startLink(t, env.admin(t)) if status, body := env.confirmLink(t, freshUserToken(t, env.seedUser(t, "show-used", nil)), used); status != http.StatusOK { t.Fatalf("confirm = %d %s", status, body) } same(t, used, http.StatusNotFound) expiring := env.admin(t) expired := env.startLink(t, expiring) if err := env.gdb.Model(&models.BackendLinkRequest{}).Where("backend_user_id = ?", adminID(t, expiring)). Update("expires_at", time.Now().Add(-time.Minute)).Error; err != nil { t.Fatal(err) } same(t, expired, http.StatusNotFound) }) t.Run("a code in the query string is ignored", func(t *testing.T) { live := env.startLink(t, env.admin(t)) path := "/_user/api/v1/backend-link/show?code=" + live for name, send := range map[string]func() *httptest.ResponseRecorder{ "no body": func() *httptest.ResponseRecorder { return env.send(t, http.MethodPost, path, "", nil, "", nil) }, "a JSON body without a code": func() *httptest.ResponseRecorder { return env.call(t, http.MethodPost, path, "", map[string]string{}) }, "a form body without a code": func() *httptest.ResponseRecorder { return env.send(t, http.MethodPost, path, "", []byte("other=1"), "application/x-www-form-urlencoded", nil) }, } { if got := send(); got.Code != http.StatusNotFound { t.Fatalf("show with ?code= and %s = %d %s, want 404", name, got.Code, got.Body.String()) } } user := env.seedUser(t, "show-query", nil) rec := env.send(t, http.MethodPost, "/_user/api/v1/backend-link/confirm?code="+live, freshUserToken(t, user), []byte("other=1"), "application/x-www-form-urlencoded", nil) if rec.Code != http.StatusNotFound { t.Fatalf("confirm with ?code= = %d %s, want 404", rec.Code, rec.Body.String()) } // A form body still carries the code, and the query string never did. form := env.send(t, http.MethodPost, "/_user/api/v1/backend-link/show", "", []byte("code="+live), "application/x-www-form-urlencoded", nil) if form.Code != http.StatusOK { t.Fatalf("show with the code in a form body = %d %s", form.Code, form.Body.String()) } if status, body := env.confirmLink(t, freshUserToken(t, user), live); status != http.StatusOK { t.Fatalf("confirm with the code in the body after the refusals = %d %s", status, body) } }) t.Run("the My account URL carries the code in the fragment only", func(t *testing.T) { var out struct { URL string `json:"url"` } adminInto(t, env.expect(t, http.StatusOK, http.MethodPost, adminAPI("/account/contributions/frontend_account/actions/link"), env.admin(t), map[string]any{}), &out) if !regexp.MustCompile(`^https://app\.example\.test/link-admin#code=[a-f0-9]{64}$`).MatchString(out.URL) || strings.Contains(out.URL, "?") { t.Fatalf("link URL = %q", out.URL) } }) for _, c := range []struct{ value, want string }{{"/pl/polacz", "/pl/polacz"}, {"pl/polacz", "/pl/polacz"}} { t.Run("page_path "+c.value, func(t *testing.T) { custom := newAdminEnv(t, func(cfg *compass.Config) { if err := cfg.Set("golem15.user.backend_links.page_path", c.value); err != nil { t.Fatal(err) } }) var out struct { URL string `json:"url"` } adminInto(t, custom.expect(t, http.StatusOK, http.MethodPost, adminAPI("/account/contributions/frontend_account/actions/link"), custom.admin(t), map[string]any{}), &out) if !regexp.MustCompile(`^https://app\.example\.test` + regexp.QuoteMeta(c.want) + `#code=[a-f0-9]{64}$`).MatchString(out.URL) { t.Fatalf("link URL with page_path %q = %q", c.value, out.URL) } }) } }