refactor(api): enforce protocol-neutral service boundaries

- refactor: move HTTP status and DTO concerns out of model and service
  layers into API and handler code.
- feat: add admin service boundary and route auth through services
  instead of direct repository access.
- test: add architecture and error tests covering package boundaries,
  redaction, and service behavior.
- docs: record the protocol-neutral boundary decision and update
  architecture and roadmap notes.
This commit is contained in:
2026-07-05 23:30:17 +08:00
parent 28e17a5b08
commit 63ede5c237
34 changed files with 1028 additions and 438 deletions
+13 -14
View File
@@ -5,7 +5,6 @@ import (
"context"
"errors"
"io"
"net/http"
"strings"
"sync"
"testing"
@@ -18,16 +17,16 @@ 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 {
// assertAppError checks that err is an *model.AppError with the expected kind.
func assertAppError(t *testing.T, err error, wantKind model.ErrorKind) 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)
if ae.Kind != wantKind {
t.Errorf("kind = %q, want %q; message = %q", ae.Kind, wantKind, ae.Message)
return false
}
return true
@@ -151,7 +150,7 @@ func TestFileService_UploadRejectsOversizedFileAndCleansStaging(t *testing.T) {
ctx := context.Background()
_, err := svc.Upload(ctx, "user1", nil, "too-large.txt", strings.NewReader("123456"))
assertAppError(t, err, http.StatusRequestEntityTooLarge)
assertAppError(t, err, model.KindPayloadTooLarge)
store.mu.RLock()
defer store.mu.RUnlock()
@@ -285,7 +284,7 @@ func TestFileService_DownloadForbidden(t *testing.T) {
}
_, _, err = svc.Download(ctx, "user2", info.ID)
assertAppError(t, err, http.StatusForbidden)
assertAppError(t, err, model.KindPermissionDenied)
}
func TestFileService_DownloadDirectory(t *testing.T) {
@@ -326,7 +325,7 @@ func TestFileService_GetNotFound(t *testing.T) {
ctx := context.Background()
_, err := svc.Get(ctx, "user1", "nonexistent")
assertAppError(t, err, http.StatusNotFound)
assertAppError(t, err, model.KindNotFound)
}
func TestFileService_GetForbidden(t *testing.T) {
@@ -339,7 +338,7 @@ func TestFileService_GetForbidden(t *testing.T) {
}
_, err = svc.Get(ctx, "user2", info.ID)
assertAppError(t, err, http.StatusForbidden)
assertAppError(t, err, model.KindPermissionDenied)
}
func TestFileService_List(t *testing.T) {
@@ -437,7 +436,7 @@ func TestFileService_Delete(t *testing.T) {
}
_, err = svc.Get(ctx, "user1", info.ID)
assertAppError(t, err, http.StatusNotFound)
assertAppError(t, err, model.KindNotFound)
}
func TestFileService_DeleteNonEmptyDirectory(t *testing.T) {
@@ -469,7 +468,7 @@ func TestFileService_DeleteForbidden(t *testing.T) {
}
err = svc.Delete(ctx, "user2", info.ID)
assertAppError(t, err, http.StatusForbidden)
assertAppError(t, err, model.KindPermissionDenied)
}
func TestFileService_CreateDir(t *testing.T) {
@@ -513,7 +512,7 @@ func TestFileService_VerifyParentForbidden(t *testing.T) {
}
_, err = svc.Upload(ctx, "user2", &dir.ID, "stolen.txt", strings.NewReader("data"))
assertAppError(t, err, http.StatusForbidden)
assertAppError(t, err, model.KindPermissionDenied)
}
func TestFileService_SoftDelete(t *testing.T) {
@@ -530,7 +529,7 @@ func TestFileService_SoftDelete(t *testing.T) {
}
_, err = svc.Get(ctx, "user1", info.ID)
assertAppError(t, err, http.StatusNotFound)
assertAppError(t, err, model.KindNotFound)
}
func TestFileService_SoftDeleteKeepsStorage(t *testing.T) {
@@ -590,5 +589,5 @@ func TestFileService_SoftDeleteForbidden(t *testing.T) {
}
err = svc.Delete(ctx, "user2", info.ID)
assertAppError(t, err, http.StatusForbidden)
assertAppError(t, err, model.KindPermissionDenied)
}