package user import ( "net/http" "net/http/httptest" "strconv" "strings" "testing" "git.golem15.com/golem15/sm-user-plugin/controllers" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/bouncer" ) // TestAPITokenManagementRefusals covers the refusals of the personal API // token routes: no application, no principal, invalid bodies, and a token // that is not the caller's. func TestAPITokenManagementRefusals(t *testing.T) { app, db := sessionApp(t, "0s") if err := app.Config.Set("golem15.user.api_tokens.prefix", "usr_"); err != nil { t.Fatal(err) } owner := insertUser(t, db, "token-owner@example.test", "correct-horse") stranger := insertUser(t, db, "token-stranger@example.test", "correct-horse") call := func(handler http.Handler, method, body, id string, userID uint) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(method, "/_user/api/v1/tokens", strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") if id != "" { req.SetPathValue("id", id) } if userID != 0 { req = req.WithContext(bouncer.WithUser(req.Context(), &bouncer.Principal{ID: userID})) } rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) return rec } // Without an application or a principal nothing is served. for name, handler := range map[string]http.Handler{ "store": controllers.APITokenStore(nil), "index": controllers.APITokenIndex(nil), "destroy": controllers.APITokenDestroy(nil), } { if rec := call(handler, http.MethodPost, `{"name":"x"}`, "1", owner.ID); rec.Code != http.StatusInternalServerError { t.Fatalf("%s without an application = %d", name, rec.Code) } } for name, handler := range map[string]http.Handler{ "store": controllers.APITokenStore(app), "index": controllers.APITokenIndex(app), "destroy": controllers.APITokenDestroy(app), } { if rec := call(handler, http.MethodPost, `{"name":"x"}`, "1", 0); rec.Code != http.StatusUnauthorized { t.Fatalf("%s without a principal = %d %s", name, rec.Code, rec.Body.String()) } } // Invalid bodies are 422 on the field and mint nothing. for body, field := range map[string]string{ `{}`: "name", `{"name":" "}`: "name", `{"name":"` + strings.Repeat("n", 256) + `"}`: "name", `{"name":"a","scopes":"read"}`: "scopes", `{"name":"a","scopes":["nope"]}`: "scopes", `{"name":"a","scopes":[1]}`: "scopes", `{"name":"a","expires_at":"tomorrow"}`: "expires_at", } { rec := call(controllers.APITokenStore(app), http.MethodPost, body, "", owner.ID) if rec.Code != http.StatusUnprocessableEntity || !strings.Contains(rec.Body.String(), `"`+field+`"`) { t.Fatalf("store %s = %d %s, want 422 on %s", body, rec.Code, rec.Body.String(), field) } } var minted int64 if err := db.Model(&models.APIToken{}).Where("user_id = ?", owner.ID).Count(&minted).Error; err != nil || minted != 0 { t.Fatalf("tokens after refused creates = %d err = %v", minted, err) } // A token with an expiry is minted; only its owner can revoke it. created := call(controllers.APITokenStore(app), http.MethodPost, `{"name":"expiring","expires_at":"2099-01-02T03:04:05Z"}`, "", owner.ID) if created.Code != http.StatusCreated { t.Fatalf("create = %d %s", created.Code, created.Body.String()) } var token models.APIToken if err := db.Where("user_id = ?", owner.ID).First(&token).Error; err != nil || token.ExpiresAt == nil { t.Fatalf("stored token = %+v err = %v", token, err) } id := strconv.FormatUint(uint64(token.ID), 10) for _, bad := range []string{"", "0", "abc", "-1", "987654321"} { if rec := call(controllers.APITokenDestroy(app), http.MethodDelete, "", bad, owner.ID); rec.Code != http.StatusNotFound { t.Fatalf("destroy %q = %d %s", bad, rec.Code, rec.Body.String()) } } if rec := call(controllers.APITokenDestroy(app), http.MethodDelete, "", id, stranger.ID); rec.Code != http.StatusNotFound { t.Fatalf("destroy by another user = %d %s", rec.Code, rec.Body.String()) } if rec := call(controllers.APITokenIndex(app), http.MethodGet, "", "", stranger.ID); rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), "expiring") { t.Fatalf("another user's list = %d %s", rec.Code, rec.Body.String()) } if rec := call(controllers.APITokenDestroy(app), http.MethodDelete, "", id, owner.ID); rec.Code != http.StatusOK { t.Fatalf("destroy = %d %s", rec.Code, rec.Body.String()) } if rec := call(controllers.APITokenIndex(app), http.MethodGet, "", "", owner.ID); rec.Code != http.StatusOK || strings.Contains(rec.Body.String(), "expiring") { t.Fatalf("list after the revoke = %d %s", rec.Code, rec.Body.String()) } }