Fixes after first integration test #19

Merged
robin merged 3 commits from dev into main 2026-08-21 16:22:14 +02:00
9 changed files with 39 additions and 24 deletions
+2
View File
@@ -5,6 +5,8 @@ services:
restart: unless-stopped restart: unless-stopped
env_file: env_file:
- .env - .env
ports:
- "15432:5432"
volumes: volumes:
- /srv/dttmr-api/postgresql:/var/lib/postgresql/data - /srv/dttmr-api/postgresql:/var/lib/postgresql/data
healthcheck: healthcheck:
+4 -4
View File
@@ -28,7 +28,7 @@ func NewAuthHandler(authService *domain.AuthService) *AuthHandler {
// @Success 200 {object} domain.TokenPair // @Success 200 {object} domain.TokenPair
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to login" // @Error 500 {object} response.ErrorResponse "failed to login"
// @Router /api/v1/login [post] // @Router /login [post]
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -60,7 +60,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
// @Success 200 {object} domain.TokenPair // @Success 200 {object} domain.TokenPair
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to refresh token" // @Error 500 {object} response.ErrorResponse "failed to refresh token"
// @Router /api/v1/login/refresh [post] // @Router /login/refresh [post]
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -92,7 +92,7 @@ func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
// @Success 200 {object} nil // @Success 200 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to logout" // @Error 500 {object} response.ErrorResponse "failed to logout"
// @Router /api/v1/logout [post] // @Router /logout [post]
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -123,7 +123,7 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
// @Success 200 {object} nil // @Success 200 {object} nil
// @Error 401 {object} response.ErrorResponse "failed to get auth context" // @Error 401 {object} response.ErrorResponse "failed to get auth context"
// @Error 500 {object} response.ErrorResponse "failed to logout" // @Error 500 {object} response.ErrorResponse "failed to logout"
// @Router /api/v1/logout/all [post] // @Router /logout/all [post]
func (h *AuthHandler) LogoutAllDevices(w http.ResponseWriter, r *http.Request) { func (h *AuthHandler) LogoutAllDevices(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
+1 -1
View File
@@ -25,7 +25,7 @@ type apiResponse struct {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} apiResponse // @Success 200 {object} apiResponse
// @Router /api/v1/ [get] // @Router / [get]
func DefaultHandler(w http.ResponseWriter, r *http.Request) { func DefaultHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
+15 -15
View File
@@ -28,7 +28,7 @@ func NewListHandler(listService *domain.ListService) *ListHandler {
// @Success 201 {object} domain.List // @Success 201 {object} domain.List
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to create list" // @Error 500 {object} response.ErrorResponse "failed to create list"
// @Router /api/v1/lists [post] // @Router /lists [post]
func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -68,7 +68,7 @@ func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
// @Error 400 {object} response.ErrorResponse "failed to decode request url" // @Error 400 {object} response.ErrorResponse "failed to decode request url"
// @Error 401 {object} response.ErrorResponse "not authorized" // @Error 401 {object} response.ErrorResponse "not authorized"
// @Error 500 {object} response.ErrorResponse "failed to read lists" // @Error 500 {object} response.ErrorResponse "failed to read lists"
// @Router /api/v1/lists [get] // @Router /lists [get]
func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -100,7 +100,7 @@ func (h *ListHandler) GetLists(w http.ResponseWriter, r *http.Request) {
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to add user to list" // @Error 500 {object} response.ErrorResponse "failed to add user to list"
// @Router /api/v1/lists/user [post] // @Router /lists/user [post]
func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -126,7 +126,7 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
} }
slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) slog.InfoContext(ctx, "added user to list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID))
response.JSON(ctx, w, http.StatusNoContent, nil) response.Status(ctx, w, http.StatusNoContent)
} }
// RemoveUserFromList handles the removal of a user association to a list // RemoveUserFromList handles the removal of a user association to a list
@@ -140,7 +140,7 @@ func (h *ListHandler) AddUserToList(w http.ResponseWriter, r *http.Request) {
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to remove user from list" // @Error 500 {object} response.ErrorResponse "failed to remove user from list"
// @Router /api/v1/lists/user [delete] // @Router /lists/user [delete]
func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -166,7 +166,7 @@ func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request)
} }
slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID)) slog.InfoContext(ctx, "removed user from list successfully", slog.Any("list_id", payload.ListID), slog.Any("user_id", payload.UserID))
response.JSON(ctx, w, http.StatusNoContent, nil) response.Status(ctx, w, http.StatusNoContent)
} }
// CreateListItem handles creation of a new list item on a given list // CreateListItem handles creation of a new list item on a given list
@@ -180,7 +180,7 @@ func (h *ListHandler) RemoveUserFromList(w http.ResponseWriter, r *http.Request)
// @Success 204 {object} nil // @Success 204 {object} nil
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to create list item" // @Error 500 {object} response.ErrorResponse "failed to create list item"
// @Router /api/v1/lists/item [post] // @Router /lists/item [post]
func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -220,7 +220,7 @@ func (h *ListHandler) CreateListItem(w http.ResponseWriter, r *http.Request) {
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 401 {object} response.ErrorResponse "not authorized" // @Error 401 {object} response.ErrorResponse "not authorized"
// @Error 500 {object} response.ErrorResponse "failed to update list item" // @Error 500 {object} response.ErrorResponse "failed to update list item"
// @Router /api/v1/lists/item [put] // @Router /lists/item [put]
func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -238,14 +238,14 @@ func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) {
return return
} }
err = h.ListService.UpdateListItem(ctx, payload.ListItemID, authContext.UserID, payload.Title, payload.IsCompleted) err = h.ListService.UpdateListItem(ctx, authContext.UserID, payload.ListItemID, payload.Title, payload.IsCompleted)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to update list item", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to update list item", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to update list item") response.Error(ctx, w, http.StatusInternalServerError, "failed to update list item")
return return
} }
response.JSON(ctx, w, http.StatusNoContent, nil) response.Status(ctx, w, http.StatusNoContent)
} }
// SetListItemCompleted handles updating "is_completed" of a list item // SetListItemCompleted handles updating "is_completed" of a list item
@@ -261,7 +261,7 @@ func (h *ListHandler) UpdateListItem(w http.ResponseWriter, r *http.Request) {
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 401 {object} response.ErrorResponse "not authorized" // @Error 401 {object} response.ErrorResponse "not authorized"
// @Error 500 {object} response.ErrorResponse "failed to update list item" // @Error 500 {object} response.ErrorResponse "failed to update list item"
// @Router /api/v1/lists/items/{id} [post] // @Router /lists/items/{id} [post]
func (h *ListHandler) SetListItemCompleted(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) SetListItemCompleted(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -286,14 +286,14 @@ func (h *ListHandler) SetListItemCompleted(w http.ResponseWriter, r *http.Reques
return return
} }
err = h.ListService.SetListItemCompleted(ctx, listItemID, authContext.UserID, payload.IsCompleted) err = h.ListService.SetListItemCompleted(ctx, authContext.UserID, listItemID, payload.IsCompleted)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to set list item completed", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to set list item completed", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to set list item completed") response.Error(ctx, w, http.StatusInternalServerError, "failed to set list item completed")
return return
} }
response.JSON(ctx, w, http.StatusNoContent, nil) response.Status(ctx, w, http.StatusNoContent)
} }
// GetListItems handles return all list items of a list // GetListItems handles return all list items of a list
@@ -307,7 +307,7 @@ func (h *ListHandler) SetListItemCompleted(w http.ResponseWriter, r *http.Reques
// @Error 400 {object} response.ErrorResponse "failed to decode request url" // @Error 400 {object} response.ErrorResponse "failed to decode request url"
// @Error 401 {object} response.ErrorResponse "not authorized" // @Error 401 {object} response.ErrorResponse "not authorized"
// @Error 500 {object} response.ErrorResponse "failed to read list items" // @Error 500 {object} response.ErrorResponse "failed to read list items"
// @Router /api/v1/lists/{id} [get] // @Router /lists/{id} [get]
func (h *ListHandler) GetListItems(w http.ResponseWriter, r *http.Request) { func (h *ListHandler) GetListItems(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@@ -327,7 +327,7 @@ func (h *ListHandler) GetListItems(w http.ResponseWriter, r *http.Request) {
items, err := h.ListService.GetListItems(ctx, authContext.UserID, listID) items, err := h.ListService.GetListItems(ctx, authContext.UserID, listID)
if err != nil { if err != nil {
slog.ErrorContext(ctx, "failed to set list item completed", slog.Any("error", err)) slog.ErrorContext(ctx, "failed to read list items", slog.Any("error", err))
response.Error(ctx, w, http.StatusInternalServerError, "failed to read list items") response.Error(ctx, w, http.StatusInternalServerError, "failed to read list items")
return return
} }
+1 -1
View File
@@ -28,7 +28,7 @@ func NewUserHandler(userService *domain.UserService) *UserHandler {
// @Success 201 {object} domain.User // @Success 201 {object} domain.User
// @Error 400 {object} response.ErrorResponse "failed to decode request body" // @Error 400 {object} response.ErrorResponse "failed to decode request body"
// @Error 500 {object} response.ErrorResponse "failed to create user" // @Error 500 {object} response.ErrorResponse "failed to create user"
// @Router /api/v1/users [post] // @Router /users [post]
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) { func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
+5
View File
@@ -7,6 +7,11 @@ import (
"net/http" "net/http"
) )
// TODO: Move to own file
func Status(ctx context.Context, w http.ResponseWriter, status int) {
w.WriteHeader(status)
}
func JSON(ctx context.Context, w http.ResponseWriter, status int, data any) { func JSON(ctx context.Context, w http.ResponseWriter, status int, data any) {
payload, err := json.Marshal(data) payload, err := json.Marshal(data)
if err != nil { if err != nil {
@@ -12,6 +12,7 @@ CREATE TABLE IF NOT EXISTS list_users (
PRIMARY KEY (list_id, user_id) PRIMARY KEY (list_id, user_id)
); );
CREATE UNIQUE INDEX idx_list_users_list_id_user_id ON list_users(list_id, user_id);
CREATE INDEX idx_list_users_user_id ON list_users(user_id); CREATE INDEX idx_list_users_user_id ON list_users(user_id);
CREATE TABLE IF NOT EXISTS list_items ( CREATE TABLE IF NOT EXISTS list_items (
+1 -1
View File
@@ -127,7 +127,7 @@ func (s *ListService) UpdateListItem(ctx context.Context, authUserID string, lis
return ErrListItemTitleEmpty return ErrListItemTitleEmpty
} }
if err := s.userAllowedToAccessList(ctx, authUserID, listItemID); err != nil { if err := s.userAllowedToAccessListItem(ctx, authUserID, listItemID); err != nil {
return err return err
} }
+9 -2
View File
@@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -60,11 +61,14 @@ func (r *ListRepo) CreateList(ctx context.Context, name string, userIDs []string
func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) { func (r *ListRepo) GetLists(ctx context.Context, userID string) ([]domain.List, error) {
rows, err := r.db.QueryContext(ctx, rows, err := r.db.QueryContext(ctx,
"SELECT id, name, created_at, modified_at FROM lists JOIN list_users ON list.id=list_users.list_id WHERE list_users.user_id = $1", "SELECT id, name, lists.created_at, modified_at FROM lists INNER JOIN list_users ON lists.id=list_users.list_id WHERE list_users.user_id = $1",
userID, userID,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get list items: %w", err) if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("failed to get lists: %w", err)
} }
defer rows.Close() defer rows.Close()
@@ -177,6 +181,9 @@ func (r *ListRepo) GetListItems(ctx context.Context, listID string) ([]domain.Li
listID, listID,
) )
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("failed to get list items: %w", err) return nil, fmt.Errorf("failed to get list items: %w", err)
} }
defer rows.Close() defer rows.Close()