mirror of
https://github.com/go-shiori/shiori.git
synced 2025-10-03 18:26:00 +08:00
* chore: use http.NoBody * fix: remove cookie token on logout * fix: remove token cookie on middleware and redirect * fix: frontend sets cookie token if authenticated * refactor: remove session-id, rely on token only * docs: make swagger * fix: redirect * fix: archive route handler * fix: properly unset cookie
484 lines
16 KiB
Go
484 lines
16 KiB
Go
package api_v1
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/go-shiori/shiori/internal/model"
|
|
"github.com/go-shiori/shiori/internal/testutil"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHandleListAccounts(t *testing.T) {
|
|
logger := logrus.New()
|
|
ctx := context.Background()
|
|
|
|
t.Run("requires authentication", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
HandleListAccounts(deps, c)
|
|
require.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
t.Run("requires admin access", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeUser(c)
|
|
HandleListAccounts(deps, c)
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("database error", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
|
|
// Force DB error by closing connection
|
|
deps.Database().ReaderDB().Close()
|
|
|
|
HandleListAccounts(deps, c)
|
|
require.Equal(t, http.StatusInternalServerError, w.Code)
|
|
})
|
|
|
|
t.Run("returns accounts list", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
// Create test account
|
|
_, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "gopher",
|
|
Password: "shiori",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
HandleListAccounts(deps, c)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
require.Len(t, response.Response.Message, 1) // Admin + created account
|
|
})
|
|
}
|
|
|
|
func TestHandleCreateAccount(t *testing.T) {
|
|
logger := logrus.New()
|
|
ctx := context.Background()
|
|
|
|
t.Run("requires authentication", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
HandleCreateAccount(deps, c)
|
|
require.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
t.Run("requires admin access", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeUser(c)
|
|
HandleCreateAccount(deps, c)
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("invalid json payload", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
body := `invalid json`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetFakeAdmin(c)
|
|
HandleCreateAccount(deps, c)
|
|
}, "POST", "/api/v1/accounts", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("database error", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
// Force DB error
|
|
deps.Database().WriterDB().Close()
|
|
|
|
body := `{
|
|
"username": "gopher",
|
|
"password": "shiori"
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetFakeAdmin(c)
|
|
HandleCreateAccount(deps, c)
|
|
}, "POST", "/api/v1/accounts", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusInternalServerError, w.Code)
|
|
})
|
|
|
|
t.Run("account already exists", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
// Create first account
|
|
_, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "gopher",
|
|
Password: "shiori",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Try to create duplicate account
|
|
body := `{
|
|
"username": "gopher",
|
|
"password": "shiori"
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetFakeAdmin(c)
|
|
HandleCreateAccount(deps, c)
|
|
}, "POST", "/api/v1/accounts", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusConflict, w.Code)
|
|
})
|
|
|
|
t.Run("successful creation", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
body := `{
|
|
"username": "newuser",
|
|
"password": "password",
|
|
"owner": false
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetFakeAdmin(c)
|
|
HandleCreateAccount(deps, c)
|
|
}, "POST", "/api/v1/accounts", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
response.AssertMessageContains(t, "id")
|
|
require.NotZero(t, response.Response.Message.(map[string]interface{})["id"])
|
|
})
|
|
}
|
|
|
|
func TestHandleDeleteAccount(t *testing.T) {
|
|
logger := logrus.New()
|
|
ctx := context.Background()
|
|
|
|
t.Run("requires authentication", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
HandleDeleteAccount(deps, c)
|
|
require.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
t.Run("requires admin access", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeUser(c)
|
|
HandleDeleteAccount(deps, c)
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("invalid id", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
testutil.SetRequestPathValue(c, "id", "invalid")
|
|
HandleDeleteAccount(deps, c)
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("account not found", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
testutil.SetRequestPathValue(c, "id", "999")
|
|
HandleDeleteAccount(deps, c)
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
})
|
|
|
|
t.Run("successful deletion", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
// Create account to delete
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "todelete",
|
|
Password: "password",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
HandleDeleteAccount(deps, c)
|
|
require.Equal(t, http.StatusNoContent, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestHandleUpdateAccount(t *testing.T) {
|
|
logger := logrus.New()
|
|
ctx := context.Background()
|
|
|
|
t.Run("requires authentication", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
HandleUpdateAccount(deps, c)
|
|
require.Equal(t, http.StatusUnauthorized, w.Code)
|
|
})
|
|
|
|
t.Run("requires admin access", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeUser(c)
|
|
HandleUpdateAccount(deps, c)
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("invalid id", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
c, w := testutil.NewTestWebContext()
|
|
testutil.SetFakeAdmin(c)
|
|
testutil.SetRequestPathValue(c, "id", "invalid")
|
|
HandleUpdateAccount(deps, c)
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("invalid json payload", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
body := `invalid json`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/1", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("account not found", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
body := `{"username": "newname"}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", "999")
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/999", testutil.WithBody(body))
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
})
|
|
|
|
t.Run("successful update", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
// Create account to update
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{
|
|
"username": "updated",
|
|
"owner": true
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
response.AssertMessageContains(t, "owner")
|
|
require.True(t, response.Response.Message.(map[string]any)["owner"].(bool))
|
|
})
|
|
|
|
t.Run("update with empty payload", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
Owner: model.Ptr(false),
|
|
Config: model.Ptr(model.UserConfig{
|
|
ShowId: true,
|
|
ListMode: true,
|
|
HideThumbnail: true,
|
|
}),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusBadRequest, w.Code)
|
|
|
|
// Verify no changes were made
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertNotOk(t)
|
|
})
|
|
|
|
t.Run("update username only", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{"username": "newname"}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
require.Equal(t, "newname", response.Response.Message.(map[string]any)["username"])
|
|
})
|
|
|
|
t.Run("update password only", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{"new_password": "newpass"}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
// Verify we can login with new password
|
|
loginBody := `{"username": "shiori", "password": "newpass"}`
|
|
w = testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(loginBody))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
})
|
|
|
|
t.Run("only admin can update other's passwords", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{"new_password": "newpass"}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeUser(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusForbidden, w.Code)
|
|
})
|
|
|
|
t.Run("update config only", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
Config: model.Ptr(model.UserConfig{
|
|
ShowId: false,
|
|
ListMode: false,
|
|
}),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{
|
|
"config": {
|
|
"ShowId": true,
|
|
"ListMode": true,
|
|
"HideThumbnail": true,
|
|
"HideExcerpt": true,
|
|
"Theme": "dark",
|
|
"KeepMetadata": true,
|
|
"UseArchive": true,
|
|
"CreateEbook": true,
|
|
"MakePublic": true
|
|
}
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
|
|
config := response.Response.Message.(map[string]any)["config"].(map[string]any)
|
|
require.True(t, config["ShowId"].(bool))
|
|
require.True(t, config["ListMode"].(bool))
|
|
require.True(t, config["HideThumbnail"].(bool))
|
|
require.True(t, config["HideExcerpt"].(bool))
|
|
require.Equal(t, "dark", config["Theme"])
|
|
require.True(t, config["KeepMetadata"].(bool))
|
|
require.True(t, config["UseArchive"].(bool))
|
|
require.True(t, config["CreateEbook"].(bool))
|
|
require.True(t, config["MakePublic"].(bool))
|
|
})
|
|
|
|
t.Run("update all fields", func(t *testing.T) {
|
|
_, deps := testutil.GetTestConfigurationAndDependencies(t, ctx, logger)
|
|
|
|
account, err := deps.Domains().Accounts().CreateAccount(ctx, model.AccountDTO{
|
|
Username: "shiori",
|
|
Password: "gopher",
|
|
Owner: model.Ptr(false),
|
|
Config: model.Ptr(model.UserConfig{
|
|
ShowId: false,
|
|
ListMode: false,
|
|
}),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
body := `{
|
|
"username": "updated",
|
|
"new_password": "newpass",
|
|
"owner": true,
|
|
"config": {
|
|
"ShowId": true,
|
|
"ListMode": true,
|
|
"HideThumbnail": true,
|
|
"HideExcerpt": true,
|
|
"Theme": "dark"
|
|
}
|
|
}`
|
|
w := testutil.PerformRequest(deps, func(deps model.Dependencies, c model.WebContext) {
|
|
testutil.SetRequestPathValue(c, "id", strconv.Itoa(int(account.ID)))
|
|
testutil.SetFakeAdmin(c)
|
|
HandleUpdateAccount(deps, c)
|
|
}, "PATCH", "/api/v1/accounts/"+strconv.Itoa(int(account.ID)), testutil.WithBody(body))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
response, err := testutil.NewTestResponseFromReader(w.Body)
|
|
require.NoError(t, err)
|
|
response.AssertOk(t)
|
|
|
|
msg := response.Response.Message.(map[string]any)
|
|
require.Equal(t, "updated", msg["username"])
|
|
require.True(t, msg["owner"].(bool))
|
|
|
|
config := msg["config"].(map[string]any)
|
|
require.True(t, config["ShowId"].(bool))
|
|
require.True(t, config["ListMode"].(bool))
|
|
require.True(t, config["HideThumbnail"].(bool))
|
|
require.True(t, config["HideExcerpt"].(bool))
|
|
require.Equal(t, "dark", config["Theme"])
|
|
|
|
// Verify password change
|
|
loginBody := `{"username": "updated", "password": "newpass"}`
|
|
w = testutil.PerformRequest(deps, HandleLogin, "POST", "/login", testutil.WithBody(loginBody))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
})
|
|
}
|