ci / api / api (push) Successful in 52s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m9s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m35s
release / release (версия и заметки) (push) Successful in 5s
release / android (APK в релиз) (push) Successful in 4m6s
ci / db / db (push) Successful in 1m15s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m19s
ci / ts / ts (packages + web) (push) Successful in 2m34s
release / desktop (Linux и Windows в релиз) (push) Successful in 14m27s
release / prune (старые файлы и прогоны) (push) Successful in 6s
473 lines
22 KiB
Go
473 lines
22 KiB
Go
package auth_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"pipu/api/internal/auth"
|
|
"pipu/api/internal/platform/apperr"
|
|
)
|
|
|
|
// signIn входит под пользователем, у которого может стоять must_change_password, и сразу меняет пароль.
|
|
func (e *env) signIn(username, password string) string {
|
|
e.t.Helper()
|
|
r := e.mustLogin(username, password, "web")
|
|
if user(r)["must_change_password"] == true {
|
|
r = e.do(http.MethodPost, "/api/v1/me/password",
|
|
map[string]any{"new_password": username + "-own-password"}, bearer(access(r)))
|
|
require.Equal(e.t, http.StatusOK, r.Code, r.Body.String())
|
|
}
|
|
return access(r)
|
|
}
|
|
|
|
func (e *env) createUser(token, username, role string) resp {
|
|
e.t.Helper()
|
|
return e.do(http.MethodPost, "/api/v1/admin/users",
|
|
map[string]any{"username": username, "role": role, "display_name": " " + username + " "}, bearer(token))
|
|
}
|
|
|
|
// Админ создаёт модератора, модератор — пользователя,
|
|
// но не модератора.
|
|
func TestAdminCreatesModeratorWhoCreatesUser(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
admin := e.signIn("root", "root-password-1")
|
|
|
|
created := e.createUser(admin, "Mod.One", "moderator")
|
|
require.Equal(t, http.StatusCreated, created.Code, created.Body.String())
|
|
modUser := created.body["user"].(map[string]any)
|
|
assert.Equal(t, "mod.one", modUser["username"])
|
|
assert.Equal(t, "Mod.One", modUser["display_name"])
|
|
assert.Equal(t, true, modUser["must_change_password"])
|
|
temp := created.body["temporary_password"].(string)
|
|
assert.Len(t, temp, 12)
|
|
|
|
first := e.mustLogin("mod.one", temp, "web")
|
|
assert.Equal(t, auth.CodePasswordChangeRequired,
|
|
e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(access(first))).code())
|
|
mod := e.signIn("mod.one", temp)
|
|
|
|
listenerUser := e.createUser(mod, "listener", "user")
|
|
require.Equal(t, http.StatusCreated, listenerUser.Code)
|
|
for _, role := range []string{"moderator", "admin"} {
|
|
r := e.createUser(mod, "sneaky-"+role, role)
|
|
assert.Equal(t, http.StatusForbidden, r.Code, role)
|
|
assert.Equal(t, auth.CodeForbidden, r.code())
|
|
}
|
|
|
|
list := e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(mod))
|
|
require.Equal(t, http.StatusOK, list.Code)
|
|
var names []string
|
|
for _, it := range list.body["items"].([]any) {
|
|
names = append(names, it.(map[string]any)["username"].(string))
|
|
}
|
|
assert.Equal(t, []string{"listener", "mod.one", "root"}, names)
|
|
assert.Nil(t, list.body["next_cursor"])
|
|
|
|
listener := e.signIn("listener", listenerUser.body["temporary_password"].(string))
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(listener)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.createUser(listener, "another", "user").code())
|
|
}
|
|
|
|
func TestAdminChangesUsername(t *testing.T) {
|
|
e := newEnv(t)
|
|
root := e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
listener := e.addUser("listener", "listener-password-1", auth.RoleUser)
|
|
mod := e.addUser("mod", "mod-password-1", auth.RoleModerator)
|
|
tok := e.signIn("root", "root-password-1")
|
|
patch := func(token string, id uuid.UUID, username string) resp {
|
|
return e.do(http.MethodPatch, "/api/v1/admin/users/"+id.String(), map[string]any{"username": username}, bearer(token))
|
|
}
|
|
for _, target := range []auth.User{listener, mod, root} {
|
|
r := patch(tok, target.ID, " NEW."+target.Username+" ")
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
assert.Equal(t, "new."+target.Username, r.body["username"])
|
|
assert.Equal(t, "user.username_change", e.store.Audit[len(e.store.Audit)-1].Action)
|
|
assert.Equal(t, http.StatusOK, e.login("new."+target.Username, map[uuid.UUID]string{
|
|
root.ID: "root-password-1", listener.ID: "listener-password-1", mod.ID: "mod-password-1",
|
|
}[target.ID], "web").Code)
|
|
}
|
|
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/me", nil, bearer(tok)).Code, "own rename preserves session")
|
|
assert.Equal(t, auth.CodeUsernameTaken, patch(tok, listener.ID, "NEW.ROOT").code())
|
|
assert.Equal(t, auth.CodeUsernameInvalid, patch(tok, listener.ID, "invalid space").code())
|
|
assert.Equal(t, auth.CodeUsernameInvalid, patch(tok, listener.ID, "").code())
|
|
assert.Equal(t, "new.listener", e.store.Users[listener.ID].Username)
|
|
modTok := e.signIn("new.mod", "mod-password-1")
|
|
assert.Equal(t, auth.CodeForbidden, patch(modTok, listener.ID, "forbidden").code())
|
|
assert.Equal(t, auth.CodeForbidden, patch(modTok, mod.ID, "forbidden").code())
|
|
}
|
|
|
|
func TestCreateUserValidation(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
admin := e.signIn("root", "root-password-1")
|
|
|
|
assert.Equal(t, auth.CodeUsernameTaken, e.createUser(admin, "ROOT", "user").code())
|
|
assert.Equal(t, auth.CodeUsernameInvalid, e.createUser(admin, "no spaces", "user").code())
|
|
assert.Equal(t, auth.CodeRoleInvalid, e.createUser(admin, "valid", "root").code())
|
|
}
|
|
|
|
// Матрица «кто кого может» через API: сброс пароля, блокировка, смена роли, удаление.
|
|
func TestManageUsersMatrix(t *testing.T) {
|
|
type actorCase struct {
|
|
role auth.Role
|
|
// разрешено ли действие над целью с ролью user / moderator / admin
|
|
reset, block, role2, del [3]bool
|
|
}
|
|
cases := []actorCase{
|
|
{auth.RoleModerator, [3]bool{true, false, false}, [3]bool{true, false, false}, [3]bool{false, false, false}, [3]bool{false, false, false}},
|
|
{auth.RoleAdmin, [3]bool{true, true, true}, [3]bool{true, true, true}, [3]bool{true, true, true}, [3]bool{true, true, true}},
|
|
}
|
|
targets := [3]auth.Role{auth.RoleUser, auth.RoleModerator, auth.RoleAdmin}
|
|
for _, ac := range cases {
|
|
for ti, tr := range targets {
|
|
actions := []struct {
|
|
name string
|
|
allowed bool
|
|
call func(e *env, tok, id string) resp
|
|
}{
|
|
{"reset", ac.reset[ti], func(e *env, tok, id string) resp {
|
|
return e.do(http.MethodPost, "/api/v1/admin/users/"+id+"/reset-password", nil, bearer(tok))
|
|
}},
|
|
{"block", ac.block[ti], func(e *env, tok, id string) resp {
|
|
return e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"is_blocked": true}, bearer(tok))
|
|
}},
|
|
{"role", ac.role2[ti], func(e *env, tok, id string) resp {
|
|
next := "user"
|
|
if tr == auth.RoleUser {
|
|
next = "moderator"
|
|
}
|
|
return e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"role": next}, bearer(tok))
|
|
}},
|
|
{"delete", ac.del[ti], func(e *env, tok, id string) resp {
|
|
return e.do(http.MethodDelete, "/api/v1/admin/users/"+id, nil, bearer(tok))
|
|
}},
|
|
}
|
|
for _, a := range actions {
|
|
t.Run(fmt.Sprintf("%s %s %s", ac.role, a.name, tr), func(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("actor", "actor-password-1", ac.role)
|
|
e.addUser("spare-admin", "spare-password-1", auth.RoleAdmin) // чтобы не упереться в «последнего админа»
|
|
target := e.addUser("target", "target-password", tr)
|
|
tok := e.signIn("actor", "actor-password-1")
|
|
r := a.call(e, tok, target.ID.String())
|
|
if a.allowed {
|
|
assert.Less(t, r.Code, 300, r.Body.String())
|
|
} else {
|
|
assert.Equal(t, http.StatusForbidden, r.Code, r.Body.String())
|
|
assert.Equal(t, auth.CodeForbidden, r.code())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestModeratorCannotTouchSelfOrRoles(t *testing.T) {
|
|
e := newEnv(t)
|
|
mod := e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
|
u := e.addUser("listener", "listener-pass", auth.RoleUser)
|
|
tok := e.signIn("mod", "moderator-pass-1")
|
|
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+mod.ID.String(),
|
|
map[string]any{"display_name": "Boss"}, bearer(tok)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+u.ID.String(),
|
|
map[string]any{"display_name": "ok", "role": "moderator"}, bearer(tok)).code(), "one forbidden change rejects the patch")
|
|
assert.Nil(t, e.store.Users[u.ID].DisplayName, "rejected patch changes nothing")
|
|
|
|
ok := e.do(http.MethodPatch, "/api/v1/admin/users/"+u.ID.String(),
|
|
map[string]any{"display_name": "Listener", "role": "user"}, bearer(tok))
|
|
require.Equal(t, http.StatusOK, ok.Code, ok.Body.String())
|
|
assert.Equal(t, "Listener", ok.body["display_name"])
|
|
}
|
|
|
|
func TestAdminCannotDemoteBlockOrDeleteSelf(t *testing.T) {
|
|
e := newEnv(t)
|
|
root := e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
e.addUser("other", "other-password-1", auth.RoleAdmin)
|
|
tok := e.signIn("root", "root-password-1")
|
|
id := root.ID.String()
|
|
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"role": "user"}, bearer(tok)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"is_blocked": true}, bearer(tok)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodDelete, "/api/v1/admin/users/"+id, nil, bearer(tok)).code())
|
|
rename := e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"display_name": "Root"}, bearer(tok))
|
|
assert.Equal(t, http.StatusOK, rename.Code)
|
|
}
|
|
|
|
func TestLastAdminIsProtected(t *testing.T) {
|
|
// Через API последнего админа не затронуть: себя менять нельзя, а действующий админ — второй.
|
|
// Остаётся гонка (админ удалён, пока его запрос в пути) — её и закрывает проверка в сервисе.
|
|
e := newEnv(t)
|
|
only := e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
ghost := auth.Principal{UserID: uuid.New(), Role: auth.RoleAdmin}
|
|
ctx := context.Background()
|
|
|
|
role := auth.RoleUser
|
|
blocked := true
|
|
for name, call := range map[string]func() error{
|
|
"demote": func() error { _, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{Role: &role}); return err },
|
|
"block": func() error {
|
|
_, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{IsBlocked: &blocked})
|
|
return err
|
|
},
|
|
"delete": func() error { return e.svc.DeleteUser(ctx, ghost, only.ID) },
|
|
} {
|
|
err := call()
|
|
var ae *apperr.Error
|
|
require.ErrorAs(t, err, &ae, name)
|
|
assert.Equal(t, auth.CodeLastAdmin, ae.Code, name)
|
|
assert.Equal(t, http.StatusConflict, ae.Status, name)
|
|
}
|
|
assert.Equal(t, auth.RoleAdmin, e.store.Users[only.ID].Role)
|
|
assert.False(t, e.store.Users[only.ID].IsBlocked)
|
|
assert.Empty(t, e.store.Audit, "rolled back changes leave no audit")
|
|
|
|
// Заблокированный админ не считается: второго активного нет.
|
|
e.addUser("sleeping", "sleeping-pass-1", auth.RoleAdmin, func(u *auth.User) { u.IsBlocked = true })
|
|
_, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{Role: &role})
|
|
assert.ErrorIs(t, err, apperr.Conflict(auth.CodeLastAdmin, ""))
|
|
}
|
|
|
|
func TestBlockAndRoleChangeRevokeAccess(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
|
admin := e.signIn("root", "root-password-1")
|
|
web := e.mustLogin("listener", "listener-pass", "web")
|
|
cookie := web.cookie(auth.RefreshCookie)
|
|
|
|
r := e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"role": "moderator"}, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
assert.Equal(t, "moderator", r.body["role"])
|
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(access(web))).code())
|
|
|
|
refreshed := e.do(http.MethodPost, "/api/v1/auth/refresh", nil, withCookie(cookie))
|
|
require.Equal(t, http.StatusOK, refreshed.Code, "role change keeps sessions")
|
|
assert.Equal(t, "moderator", user(refreshed)["role"])
|
|
tok := access(refreshed)
|
|
|
|
r = e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"is_blocked": true}, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code)
|
|
assert.Equal(t, true, r.body["is_blocked"])
|
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/me", nil, bearer(tok)).Code)
|
|
assert.Equal(t, auth.CodeRefreshInvalid,
|
|
e.do(http.MethodPost, "/api/v1/auth/refresh", nil, withCookie(refreshed.cookie(auth.RefreshCookie))).code(),
|
|
"blocking ends sessions")
|
|
|
|
r = e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"is_blocked": false}, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code)
|
|
e.mustLogin("listener", "listener-pass", "web")
|
|
|
|
var actions []string
|
|
for _, a := range e.store.Audit {
|
|
actions = append(actions, a.Action)
|
|
}
|
|
assert.Equal(t, []string{"user.role_change", "user.block", "user.unblock"}, actions)
|
|
assert.Equal(t, map[string]any{"from": "user", "to": "moderator"}, e.store.Audit[0].Payload)
|
|
}
|
|
|
|
func TestNoOpPatch(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
|
admin := e.signIn("root", "root-password-1")
|
|
r := e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(),
|
|
map[string]any{"role": "user", "is_blocked": false}, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code)
|
|
assert.Empty(t, e.store.Audit)
|
|
assert.Zero(t, e.store.Users[target.ID].TokenVersion)
|
|
|
|
assert.Equal(t, auth.CodeUserNotFound, e.do(http.MethodPatch, "/api/v1/admin/users/"+uuid.NewString(),
|
|
map[string]any{"display_name": "x"}, bearer(admin)).code())
|
|
}
|
|
|
|
// Сброс пароля → обязательная смена при входе.
|
|
func TestResetPassword(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
|
target := e.addUser("listener", "listener-pass", auth.RoleUser, func(u *auth.User) { u.FailedLoginCount = 9 })
|
|
mod := e.signIn("mod", "moderator-pass-1")
|
|
phone := e.mustLogin("listener", "listener-pass", "android")
|
|
|
|
r := e.do(http.MethodPost, "/api/v1/admin/users/"+target.ID.String()+"/reset-password", nil, bearer(mod))
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
temp := r.body["temporary_password"].(string)
|
|
|
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(access(phone))).code())
|
|
assert.Equal(t, auth.CodeRefreshInvalid, e.do(http.MethodPost, "/api/v1/auth/refresh",
|
|
map[string]any{"refresh_token": phone.body["refresh_token"]}).code())
|
|
assert.Equal(t, auth.CodeInvalidCredentials, e.login("listener", "listener-pass", "web").code())
|
|
|
|
again := e.mustLogin("listener", temp, "web")
|
|
assert.Equal(t, true, user(again)["must_change_password"])
|
|
assert.Equal(t, auth.CodePasswordChangeRequired, e.do(http.MethodGet, "/api/v1/me/sessions", nil, bearer(access(again))).code())
|
|
assert.Equal(t, "user.password_reset", e.store.Audit[len(e.store.Audit)-1].Action)
|
|
}
|
|
|
|
func TestResetRecommendations(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
|
peer := e.addUser("other", "moderator-pass-2", auth.RoleModerator)
|
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
|
mod := e.signIn("mod", "moderator-pass-1")
|
|
|
|
r := e.do(http.MethodPost, "/api/v1/admin/users/"+target.ID.String()+"/reset-recommendations", nil, bearer(mod))
|
|
require.Equal(t, http.StatusNoContent, r.Code, r.Body.String())
|
|
assert.Equal(t, []uuid.UUID{target.ID}, e.store.RecResets)
|
|
assert.Equal(t, "user.recommendations_reset", e.store.Audit[len(e.store.Audit)-1].Action)
|
|
|
|
// Модератор не трогает других модераторов.
|
|
assert.Equal(t, http.StatusForbidden,
|
|
e.do(http.MethodPost, "/api/v1/admin/users/"+peer.ID.String()+"/reset-recommendations", nil, bearer(mod)).Code)
|
|
assert.Len(t, e.store.RecResets, 1)
|
|
}
|
|
|
|
func TestDeleteUser(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
|
admin := e.signIn("root", "root-password-1")
|
|
tok := access(e.mustLogin("listener", "listener-pass", "web"))
|
|
|
|
assert.Equal(t, http.StatusNoContent, e.do(http.MethodDelete, "/api/v1/admin/users/"+target.ID.String(), nil, bearer(admin)).Code)
|
|
assert.NotContains(t, e.store.Users, target.ID)
|
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(tok)).code())
|
|
assert.Equal(t, auth.CodeUserNotFound, e.do(http.MethodDelete, "/api/v1/admin/users/"+target.ID.String(), nil, bearer(admin)).code())
|
|
last := e.store.Audit[len(e.store.Audit)-1]
|
|
assert.Equal(t, "user.delete", last.Action)
|
|
assert.Equal(t, "listener", last.Payload["username"])
|
|
}
|
|
|
|
func TestListUsersFiltersAndPages(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
for _, n := range []string{"anna", "boris", "vera", "gleb", "dina"} {
|
|
e.addUser(n, n+"-password", auth.RoleUser)
|
|
}
|
|
name := "Анна Каренина"
|
|
e.addUser("kate", "kate-password", auth.RoleModerator, func(u *auth.User) { u.DisplayName = &name })
|
|
admin := e.signIn("root", "root-password-1")
|
|
list := func(q url.Values) resp {
|
|
return e.do(http.MethodGet, "/api/v1/admin/users?"+q.Encode(), nil, bearer(admin))
|
|
}
|
|
usernames := func(r resp) []string {
|
|
var out []string
|
|
for _, it := range r.body["items"].([]any) {
|
|
out = append(out, it.(map[string]any)["username"].(string))
|
|
}
|
|
return out
|
|
}
|
|
|
|
var (
|
|
all []string
|
|
cursor string
|
|
)
|
|
for page := 0; ; page++ {
|
|
q := url.Values{"limit": {"3"}}
|
|
if cursor != "" {
|
|
q.Set("cursor", cursor)
|
|
}
|
|
r := list(q)
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
all = append(all, usernames(r)...)
|
|
next, _ := r.body["next_cursor"].(string)
|
|
if next == "" {
|
|
break
|
|
}
|
|
cursor = next
|
|
require.Less(t, page, 5)
|
|
}
|
|
assert.Equal(t, []string{"anna", "boris", "dina", "gleb", "kate", "root", "vera"}, all)
|
|
|
|
assert.Equal(t, []string{"kate"}, usernames(list(url.Values{"q": {"анна"}})), "matches display name, case-insensitive")
|
|
assert.Equal(t, []string{"anna"}, usernames(list(url.Values{"q": {"AN"}})))
|
|
assert.Equal(t, []string{"kate"}, usernames(list(url.Values{"role": {"moderator"}})))
|
|
assert.Equal(t, auth.CodeCursorBad, list(url.Values{"cursor": {"%%%"}}).code())
|
|
assert.Equal(t, http.StatusBadRequest, list(url.Values{"limit": {"lots"}}).Code)
|
|
}
|
|
|
|
func TestAudit(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
|
admin := e.signIn("root", "root-password-1")
|
|
for _, n := range []string{"u1", "u2", "u3"} {
|
|
require.Equal(t, http.StatusCreated, e.createUser(admin, n+"-user", "user").Code)
|
|
}
|
|
|
|
r := e.do(http.MethodGet, "/api/v1/admin/audit?limit=2", nil, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
items := r.body["items"].([]any)
|
|
require.Len(t, items, 2)
|
|
newest := items[0].(map[string]any)
|
|
assert.Equal(t, "user.create", newest["action"])
|
|
assert.Equal(t, "u3-user", newest["payload"].(map[string]any)["username"])
|
|
assert.Equal(t, "root", newest["actor"].(map[string]any)["username"])
|
|
assert.Equal(t, "user", newest["target_type"])
|
|
|
|
next := r.body["next_cursor"].(string)
|
|
r2 := e.do(http.MethodGet, "/api/v1/admin/audit?limit=2&cursor="+next, nil, bearer(admin))
|
|
rest := r2.body["items"].([]any)
|
|
require.Len(t, rest, 1)
|
|
assert.Equal(t, "u1-user", rest[0].(map[string]any)["payload"].(map[string]any)["username"])
|
|
assert.Nil(t, r2.body["next_cursor"])
|
|
|
|
mod := e.signIn("mod", "moderator-pass-1")
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code())
|
|
}
|
|
|
|
// Аудит загрузок выдаёт только админ, и он не следует из роли.
|
|
func TestReviewUploadsGrant(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
admin := e.signIn("root", "root-password-1")
|
|
target := e.addUser("mod", "mod-password-1", auth.RoleModerator)
|
|
mod := e.signIn("mod", "mod-password-1")
|
|
path := "/api/v1/admin/users/" + target.ID.String()
|
|
grant := map[string]any{"reviews_uploads": true}
|
|
|
|
me := e.do(http.MethodGet, "/api/v1/me", nil, bearer(admin))
|
|
assert.Equal(t, false, me.body["reviews_uploads"])
|
|
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, path, grant, bearer(mod)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPost, "/api/v1/admin/users",
|
|
map[string]any{"username": "rev", "role": "user", "reviews_uploads": true}, bearer(mod)).code())
|
|
|
|
r := e.do(http.MethodPatch, path, grant, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
assert.Equal(t, true, r.body["reviews_uploads"])
|
|
assert.Equal(t, "moderator", r.body["role"])
|
|
me = e.do(http.MethodGet, "/api/v1/me", nil, bearer(mod))
|
|
assert.Equal(t, true, me.body["reviews_uploads"])
|
|
}
|
|
|
|
func TestManageUpdatesGrantAndRevoke(t *testing.T) {
|
|
e := newEnv(t)
|
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
|
admin := e.signIn("root", "root-password-1")
|
|
target := e.addUser("mod", "mod-password-1", auth.RoleModerator)
|
|
mod := e.signIn("mod", "mod-password-1")
|
|
path := "/api/v1/admin/users/" + target.ID.String()
|
|
grant := map[string]any{"manages_updates": true}
|
|
assert.Equal(t, true, e.do(http.MethodGet, "/api/v1/me", nil, bearer(admin)).body["manages_updates"])
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, path, grant, bearer(mod)).code())
|
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPost, "/api/v1/admin/users", map[string]any{"username": "delegate", "role": "user", "manages_updates": true}, bearer(mod)).code())
|
|
r := e.do(http.MethodPatch, path, grant, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
|
assert.Equal(t, true, r.body["manages_updates"])
|
|
assert.Equal(t, true, e.do(http.MethodGet, "/api/v1/me", nil, bearer(mod)).body["manages_updates"])
|
|
r = e.do(http.MethodPatch, path, map[string]any{"manages_updates": false}, bearer(admin))
|
|
require.Equal(t, http.StatusOK, r.Code)
|
|
assert.Equal(t, false, e.do(http.MethodGet, "/api/v1/me", nil, bearer(mod)).body["manages_updates"])
|
|
r = e.do(http.MethodPost, "/api/v1/admin/users", map[string]any{"username": "delegate", "role": "user", "manages_updates": true}, bearer(admin))
|
|
require.Equal(t, http.StatusCreated, r.Code, r.Body.String())
|
|
}
|