Add structured logging and centralized error handling

- Initialize slog in the serve command with terminal/file support
- Introduce `AppError` with HTTP status for unified service-layer errors
- Replace ad-hoc `api.Error` calls with `api.RespondError`
- Wrap internal errors with `model.NewInternalError` and add reference
  IDs
- Add `RequestID` middleware and switch router from `gin.Default` to
  `gin.New`
This commit is contained in:
2026-07-04 16:24:22 +08:00
parent a78d43b166
commit 1dfccf513a
16 changed files with 281 additions and 134 deletions
+24 -19
View File
@@ -3,7 +3,9 @@ package service
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"strings"
"sync"
"testing"
@@ -16,6 +18,21 @@ import (
"github.com/dhao2001/mygo/internal/storage"
)
// assertAppError checks that err is an *model.AppError with the expected status.
func assertAppError(t *testing.T, err error, wantStatus int) bool {
t.Helper()
var ae *model.AppError
if !errors.As(err, &ae) {
t.Errorf("expected *model.AppError, got %T: %v", err, err)
return false
}
if ae.Status != wantStatus {
t.Errorf("status = %d, want %d; message = %q", ae.Status, wantStatus, ae.Message)
return false
}
return true
}
// memStorage is an in-memory implementation of storage.StorageBackend for tests.
type memStorage struct {
mu sync.RWMutex
@@ -70,7 +87,7 @@ func setupFileService(t *testing.T) *FileService {
fileRepo := repository.NewFileRepository(db)
store := newMemStorage()
return NewFileService(fileRepo, store, 0) // 0 = unlimited
return NewFileService(fileRepo, store, 0, nil) // 0 = unlimited, nil logger defaults to slog.Default()
}
func TestFileService_Upload(t *testing.T) {
@@ -214,9 +231,7 @@ func TestFileService_DownloadForbidden(t *testing.T) {
}
_, _, err = svc.Download(ctx, "user2", info.ID)
if err != model.ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
assertAppError(t, err, http.StatusForbidden)
}
func TestFileService_DownloadDirectory(t *testing.T) {
@@ -257,9 +272,7 @@ func TestFileService_GetNotFound(t *testing.T) {
ctx := context.Background()
_, err := svc.Get(ctx, "user1", "nonexistent")
if err != model.ErrNotFound {
t.Errorf("expected ErrNotFound, got %v", err)
}
assertAppError(t, err, http.StatusNotFound)
}
func TestFileService_GetForbidden(t *testing.T) {
@@ -272,9 +285,7 @@ func TestFileService_GetForbidden(t *testing.T) {
}
_, err = svc.Get(ctx, "user2", info.ID)
if err != model.ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
assertAppError(t, err, http.StatusForbidden)
}
func TestFileService_List(t *testing.T) {
@@ -372,9 +383,7 @@ func TestFileService_Delete(t *testing.T) {
}
_, err = svc.Get(ctx, "user1", info.ID)
if err != model.ErrNotFound {
t.Errorf("expected ErrNotFound after delete, got %v", err)
}
assertAppError(t, err, http.StatusNotFound)
}
func TestFileService_DeleteNonEmptyDirectory(t *testing.T) {
@@ -406,9 +415,7 @@ func TestFileService_DeleteForbidden(t *testing.T) {
}
err = svc.Delete(ctx, "user2", info.ID)
if err != model.ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
assertAppError(t, err, http.StatusForbidden)
}
func TestFileService_CreateDir(t *testing.T) {
@@ -452,7 +459,5 @@ func TestFileService_VerifyParentForbidden(t *testing.T) {
}
_, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data"))
if err != model.ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
assertAppError(t, err, http.StatusForbidden)
}