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:
@@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/dhao2001/mygo/internal/model"
|
||||
"github.com/dhao2001/mygo/internal/repository"
|
||||
)
|
||||
|
||||
// AdminService handles administrator user-management operations.
|
||||
type AdminService struct {
|
||||
userRepo repository.UserRepository
|
||||
}
|
||||
|
||||
// NewAdminService creates an AdminService.
|
||||
func NewAdminService(userRepo repository.UserRepository) *AdminService {
|
||||
return &AdminService{userRepo: userRepo}
|
||||
}
|
||||
|
||||
// GetUser returns a user by ID, including deleted users.
|
||||
func (s *AdminService) GetUser(ctx context.Context, id string) (*model.User, error) {
|
||||
user, err := s.userRepo.FindByIDIncludeDeleted(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, model.NewNotFoundError("user not found", model.ErrNotFound)
|
||||
}
|
||||
return nil, model.NewInternalError("find admin user", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ListUsers returns all users, including deleted users, with pagination.
|
||||
func (s *AdminService) ListUsers(ctx context.Context, offset, limit int) ([]model.User, int64, error) {
|
||||
users, total, err := s.userRepo.ListIncludeDeleted(ctx, offset, limit)
|
||||
if err != nil {
|
||||
return nil, 0, model.NewInternalError("list admin users", err)
|
||||
}
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// DeleteUser soft-deletes a user.
|
||||
func (s *AdminService) DeleteUser(ctx context.Context, id string) error {
|
||||
if _, err := s.GetUser(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.userRepo.Delete(ctx, id); err != nil {
|
||||
return model.NewInternalError("delete admin user", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/dhao2001/mygo/internal/model"
|
||||
"github.com/dhao2001/mygo/internal/repository"
|
||||
)
|
||||
|
||||
func setupAdminService(t *testing.T) (*AdminService, repository.UserRepository) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.User{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
return NewAdminService(userRepo), userRepo
|
||||
}
|
||||
|
||||
func TestAdminServiceGetUserMissingReturnsNotFound(t *testing.T) {
|
||||
svc, _ := setupAdminService(t)
|
||||
|
||||
_, err := svc.GetUser(context.Background(), "missing")
|
||||
var ae *model.AppError
|
||||
if !errors.As(err, &ae) {
|
||||
t.Fatalf("expected AppError, got %T: %v", err, err)
|
||||
}
|
||||
if ae.Kind != model.KindNotFound {
|
||||
t.Fatalf("kind = %q, want %q", ae.Kind, model.KindNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminServiceListIncludesDeletedUsers(t *testing.T) {
|
||||
svc, repo := setupAdminService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
active := &model.User{ID: "active-1", Username: "active", Email: "active@example.com", Status: model.StatusActive}
|
||||
deleted := &model.User{ID: "deleted-1", Username: "deleted", Email: "deleted@example.com", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, active); err != nil {
|
||||
t.Fatalf("create active: %v", err)
|
||||
}
|
||||
if err := repo.Create(ctx, deleted); err != nil {
|
||||
t.Fatalf("create deleted: %v", err)
|
||||
}
|
||||
if err := repo.Delete(ctx, deleted.ID); err != nil {
|
||||
t.Fatalf("delete user: %v", err)
|
||||
}
|
||||
|
||||
users, total, err := svc.ListUsers(ctx, 0, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ListUsers = %v", err)
|
||||
}
|
||||
if total != 2 {
|
||||
t.Fatalf("total = %d, want 2", total)
|
||||
}
|
||||
if len(users) != 2 {
|
||||
t.Fatalf("len(users) = %d, want 2", len(users))
|
||||
}
|
||||
}
|
||||
+56
-23
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -16,15 +15,21 @@ import (
|
||||
|
||||
// TokenPair contains the access and refresh tokens returned after authentication.
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// CreatedPasskey contains the raw token for a newly created app passkey.
|
||||
type CreatedPasskey struct {
|
||||
ID string `json:"id"`
|
||||
Raw string `json:"raw"`
|
||||
Label string `json:"label"`
|
||||
ID string
|
||||
Raw string
|
||||
Label string
|
||||
}
|
||||
|
||||
// Principal is the authenticated user identity used by transport layers.
|
||||
type Principal struct {
|
||||
UserID string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// AuthService handles user authentication and session management.
|
||||
@@ -59,7 +64,7 @@ func NewAuthService(
|
||||
// Register creates a new user account.
|
||||
func (s *AuthService) Register(ctx context.Context, username, email, password string) (*model.User, error) {
|
||||
if username == "" || email == "" || password == "" {
|
||||
return nil, &model.AppError{Status: http.StatusBadRequest, Message: "username, email, and password are required"}
|
||||
return nil, model.NewInvalidArgumentError("username, email, and password are required")
|
||||
}
|
||||
|
||||
passwordHash, err := auth.HashPassword(password)
|
||||
@@ -77,7 +82,7 @@ func (s *AuthService) Register(ctx context.Context, username, email, password st
|
||||
|
||||
if err := s.userRepo.Create(ctx, user); err != nil {
|
||||
if errors.Is(err, model.ErrDuplicate) {
|
||||
return nil, &model.AppError{Status: http.StatusConflict, Message: "username or email already exists"}
|
||||
return nil, model.NewConflictError("username or email already exists")
|
||||
}
|
||||
return nil, model.NewInternalError("create user", err)
|
||||
}
|
||||
@@ -90,17 +95,17 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (*Token
|
||||
user, err := s.userRepo.FindByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
return nil, model.NewUnauthenticatedError("invalid email or password")
|
||||
}
|
||||
return nil, model.NewInternalError("find user", err)
|
||||
}
|
||||
|
||||
if user.Status != model.StatusActive {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
return nil, model.NewUnauthenticatedError("invalid email or password")
|
||||
}
|
||||
|
||||
if err := auth.VerifyPassword(user.PasswordHash, password); err != nil {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid email or password"}
|
||||
return nil, model.NewUnauthenticatedError("invalid email or password")
|
||||
}
|
||||
|
||||
return s.issueTokens(ctx, user.ID)
|
||||
@@ -111,32 +116,32 @@ func (s *AuthService) Login(ctx context.Context, email, password string) (*Token
|
||||
func (s *AuthService) Refresh(ctx context.Context, refreshTokenStr string) (*TokenPair, error) {
|
||||
claims, err := auth.ParseToken(refreshTokenStr, s.jwtSecret)
|
||||
if err != nil {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
|
||||
if claims.Type != auth.TokenRefresh {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
|
||||
tokenHash := auth.HashToken(refreshTokenStr)
|
||||
session, err := s.sessionRepo.FindByTokenHash(ctx, tokenHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
return nil, model.NewInternalError("find session", err)
|
||||
}
|
||||
|
||||
if session.UserID != claims.UserID {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.FindByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
if user.Status != model.StatusActive {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid token"}
|
||||
return nil, model.NewUnauthenticatedError("invalid token")
|
||||
}
|
||||
|
||||
if err := s.sessionRepo.Delete(ctx, session.ID); err != nil {
|
||||
@@ -189,14 +194,14 @@ func (s *AuthService) CreatePasskey(ctx context.Context, userID, label string) (
|
||||
// LoginWithPasskey authenticates a user using an app passkey token.
|
||||
func (s *AuthService) LoginWithPasskey(ctx context.Context, tokenStr string) (*TokenPair, error) {
|
||||
if !strings.HasPrefix(tokenStr, "mygo_") {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid passkey format"}
|
||||
return nil, model.NewUnauthenticatedError("invalid passkey format")
|
||||
}
|
||||
|
||||
tokenHash := auth.HashToken(tokenStr)
|
||||
cred, err := s.credentialRepo.FindByHash(ctx, tokenHash)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid passkey"}
|
||||
return nil, model.NewUnauthenticatedError("invalid passkey")
|
||||
}
|
||||
return nil, model.NewInternalError("find credential", err)
|
||||
}
|
||||
@@ -206,11 +211,11 @@ func (s *AuthService) LoginWithPasskey(ctx context.Context, tokenStr string) (*T
|
||||
return nil, model.NewInternalError("find user", err)
|
||||
}
|
||||
if user.Status != model.StatusActive {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid passkey"}
|
||||
return nil, model.NewUnauthenticatedError("invalid passkey")
|
||||
}
|
||||
|
||||
if cred.Type != "app_passkey" {
|
||||
return nil, &model.AppError{Status: http.StatusUnauthorized, Message: "invalid credential type"}
|
||||
return nil, model.NewUnauthenticatedError("invalid credential type")
|
||||
}
|
||||
|
||||
if err := s.credentialRepo.UpdateLastUsed(ctx, cred.ID); err != nil {
|
||||
@@ -230,18 +235,46 @@ func (s *AuthService) RevokePasskey(ctx context.Context, userID, credID string)
|
||||
cred, err := s.credentialRepo.FindByID(ctx, credID)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return &model.AppError{Status: http.StatusNotFound, Message: "passkey not found", Err: model.ErrNotFound}
|
||||
return model.NewNotFoundError("passkey not found", model.ErrNotFound)
|
||||
}
|
||||
return model.NewInternalError("find credential", err)
|
||||
}
|
||||
|
||||
if cred.UserID != userID {
|
||||
return &model.AppError{Status: http.StatusForbidden, Message: "access denied", Err: model.ErrForbidden}
|
||||
return model.NewPermissionDeniedError("access denied", model.ErrForbidden)
|
||||
}
|
||||
|
||||
return s.credentialRepo.Delete(ctx, credID)
|
||||
}
|
||||
|
||||
// AuthenticateAccessToken validates an access token and returns the active user principal.
|
||||
func (s *AuthService) AuthenticateAccessToken(ctx context.Context, accessTokenStr string) (*Principal, error) {
|
||||
claims, err := auth.ParseToken(accessTokenStr, s.jwtSecret)
|
||||
if err != nil {
|
||||
return nil, model.NewUnauthenticatedError("invalid or expired token")
|
||||
}
|
||||
|
||||
if claims.Type != auth.TokenAccess {
|
||||
return nil, model.NewUnauthenticatedError("invalid token type")
|
||||
}
|
||||
|
||||
user, err := s.userRepo.FindByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
return nil, model.NewUnauthenticatedError("user not found")
|
||||
}
|
||||
return nil, model.NewInternalError("find access token user", err)
|
||||
}
|
||||
if user.Status != model.StatusActive {
|
||||
return nil, model.NewUnauthenticatedError("user not found")
|
||||
}
|
||||
|
||||
return &Principal{
|
||||
UserID: user.ID,
|
||||
IsAdmin: user.IsAdmin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) issueTokens(ctx context.Context, userID string) (*TokenPair, error) {
|
||||
accessToken, err := auth.GenerateAccessToken(userID, s.jwtSecret, s.accessTTL)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,7 +3,6 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -347,8 +346,8 @@ func TestAuthService_RevokePasskeyNotOwner(t *testing.T) {
|
||||
|
||||
err = svc.RevokePasskey(ctx, claimsBob.UserID, pk.ID)
|
||||
var ae *model.AppError
|
||||
if !errors.As(err, &ae) || ae.Status != http.StatusForbidden {
|
||||
t.Fatalf("expected AppError 403 Forbidden, got %v", err)
|
||||
if !errors.As(err, &ae) || ae.Kind != model.KindPermissionDenied {
|
||||
t.Fatalf("expected permission denied AppError, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,8 +433,8 @@ func TestAuthService_LoginDisabledUser(t *testing.T) {
|
||||
if !errors.As(err, &ae) {
|
||||
t.Fatalf("expected AppError, got %T: %v", err, err)
|
||||
}
|
||||
if ae.Status != http.StatusUnauthorized {
|
||||
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
|
||||
if ae.Kind != model.KindUnauthenticated {
|
||||
t.Errorf("kind = %q, want %q", ae.Kind, model.KindUnauthenticated)
|
||||
}
|
||||
if ae.Message != "invalid email or password" {
|
||||
t.Errorf("message = %q, want %q", ae.Message, "invalid email or password")
|
||||
@@ -519,8 +518,8 @@ func TestAuthService_LoginWithPasskeyDisabledUser(t *testing.T) {
|
||||
if !errors.As(err, &ae) {
|
||||
t.Fatalf("expected AppError, got %T: %v", err, err)
|
||||
}
|
||||
if ae.Status != http.StatusUnauthorized {
|
||||
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
|
||||
if ae.Kind != model.KindUnauthenticated {
|
||||
t.Errorf("kind = %q, want %q", ae.Kind, model.KindUnauthenticated)
|
||||
}
|
||||
if ae.Message != "invalid passkey" {
|
||||
t.Errorf("message = %q, want %q", ae.Message, "invalid passkey")
|
||||
@@ -554,10 +553,68 @@ func TestAuthService_RefreshDisabledUser(t *testing.T) {
|
||||
if !errors.As(err, &ae) {
|
||||
t.Fatalf("expected AppError, got %T: %v", err, err)
|
||||
}
|
||||
if ae.Status != http.StatusUnauthorized {
|
||||
t.Errorf("status = %d, want %d", ae.Status, http.StatusUnauthorized)
|
||||
if ae.Kind != model.KindUnauthenticated {
|
||||
t.Errorf("kind = %q, want %q", ae.Kind, model.KindUnauthenticated)
|
||||
}
|
||||
if ae.Message != "invalid token" {
|
||||
t.Errorf("message = %q, want %q", ae.Message, "invalid token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthService_AuthenticateAccessToken(t *testing.T) {
|
||||
svc, userRepo := setupAuthServiceWithRepos(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Register = %v", err)
|
||||
}
|
||||
user.IsAdmin = true
|
||||
if err := userRepo.Update(ctx, user); err != nil {
|
||||
t.Fatalf("promote user: %v", err)
|
||||
}
|
||||
|
||||
pair, err := svc.Login(ctx, "alice@example.com", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login = %v", err)
|
||||
}
|
||||
|
||||
principal, err := svc.AuthenticateAccessToken(ctx, pair.AccessToken)
|
||||
if err != nil {
|
||||
t.Fatalf("AuthenticateAccessToken = %v", err)
|
||||
}
|
||||
if principal.UserID != user.ID {
|
||||
t.Fatalf("UserID = %q, want %q", principal.UserID, user.ID)
|
||||
}
|
||||
if !principal.IsAdmin {
|
||||
t.Fatal("IsAdmin = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthService_AuthenticateAccessTokenRejectsDeletedUser(t *testing.T) {
|
||||
svc, userRepo := setupAuthServiceWithRepos(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user, err := svc.Register(ctx, "alice", "alice@example.com", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Register = %v", err)
|
||||
}
|
||||
|
||||
pair, err := svc.Login(ctx, "alice@example.com", "password123")
|
||||
if err != nil {
|
||||
t.Fatalf("Login = %v", err)
|
||||
}
|
||||
|
||||
if err := userRepo.Delete(ctx, user.ID); err != nil {
|
||||
t.Fatalf("Delete = %v", err)
|
||||
}
|
||||
|
||||
_, err = svc.AuthenticateAccessToken(ctx, pair.AccessToken)
|
||||
var ae *model.AppError
|
||||
if !errors.As(err, &ae) {
|
||||
t.Fatalf("expected AppError, got %T: %v", err, err)
|
||||
}
|
||||
if ae.Kind != model.KindUnauthenticated {
|
||||
t.Fatalf("kind = %q, want %q", ae.Kind, model.KindUnauthenticated)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-12
@@ -22,22 +22,22 @@ import (
|
||||
|
||||
// FileInfo is the public representation of a file or directory.
|
||||
type FileInfo struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string
|
||||
UserID string
|
||||
ParentID *string
|
||||
Name string
|
||||
Size int64
|
||||
MimeType string
|
||||
IsDir bool
|
||||
Hash string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// FileList is a paginated list of files.
|
||||
type FileList struct {
|
||||
Files []FileInfo `json:"files"`
|
||||
Total int64 `json:"total"`
|
||||
Files []FileInfo
|
||||
Total int64
|
||||
}
|
||||
|
||||
// FileService handles file management business logic.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user