fix: ensure API consistency for /tasks and empty array responses (#1988)

- Renames the `/tasks/all` endpoint to `/tasks` for consistency with
other collection endpoints like `/projects` and `/labels`
- Returns `[]` instead of `null` for empty pagination results across all
list endpoints
- Updates the frontend service to use the new endpoint path
- Updates API token tests to use the new endpoint path

Fixes #1984
This commit is contained in:
kolaente
2025-12-15 16:34:13 +01:00
committed by GitHub
parent 4ae72740cb
commit 0b3decd869
5 changed files with 15 additions and 7 deletions

View File

@@ -21,7 +21,7 @@ export default class TaskService extends AbstractService<ITask> {
constructor() {
super({
create: '/projects/{projectId}/tasks',
getAll: '/tasks/all',
getAll: '/tasks',
get: '/tasks/{id}',
update: '/tasks/{id}',
delete: '/tasks/{id}',

View File

@@ -221,7 +221,7 @@ type taskSearchOptions struct {
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/all [get]
// @Router /tasks [get]
func (t *Task) ReadAll(_ *xorm.Session, _ web.Auth, _ string, _ int, _ int) (result interface{}, resultCount int, totalItems int64, err error) {
return nil, 0, 0, nil
}

View File

@@ -425,7 +425,7 @@ func registerAPIRoutes(a *echo.Group) {
}
a.PUT("/projects/:project/tasks", taskHandler.CreateWeb)
a.GET("/tasks/:projecttask", taskHandler.ReadOneWeb)
a.GET("/tasks/all", taskCollectionHandler.ReadAllWeb)
a.GET("/tasks", taskCollectionHandler.ReadAllWeb)
a.DELETE("/tasks/:projecttask", taskHandler.DeleteWeb)
a.POST("/tasks/:projecttask", taskHandler.UpdateWeb)

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"math"
"net/http"
"reflect"
"strconv"
vconfig "code.vikunja.io/api/pkg/config"
@@ -128,6 +129,13 @@ func (c *WebHandler) ReadAllWeb(ctx echo.Context) error {
return HandleHTTPError(err)
}
// Ensure we return an empty array instead of null when there are no results.
// We need to use reflection here because a nil slice wrapped in an interface{}
// is not equal to nil (the interface contains a nil value but is not nil itself).
if result == nil || (reflect.ValueOf(result).Kind() == reflect.Slice && reflect.ValueOf(result).IsNil()) {
result = []interface{}{}
}
err = ctx.JSON(http.StatusOK, result)
if err != nil {
return HandleHTTPError(err)

View File

@@ -35,7 +35,7 @@ func TestAPIToken(t *testing.T) {
t.Run("valid token", func(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := routes.SetupTokenMiddleware()(func(c echo.Context) error {
@@ -55,7 +55,7 @@ func TestAPIToken(t *testing.T) {
t.Run("invalid token", func(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := routes.SetupTokenMiddleware()(func(c echo.Context) error {
@@ -68,7 +68,7 @@ func TestAPIToken(t *testing.T) {
t.Run("expired token", func(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := routes.SetupTokenMiddleware()(func(c echo.Context) error {
@@ -94,7 +94,7 @@ func TestAPIToken(t *testing.T) {
t.Run("jwt", func(t *testing.T) {
e, err := setupTestEnv()
require.NoError(t, err)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/all", nil)
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks", nil)
res := httptest.NewRecorder()
c := e.NewContext(req, res)
h := routes.SetupTokenMiddleware()(func(c echo.Context) error {