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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user