Files
mygo/internal/architecture_test.go
T
ld a18f96912d feat(repository): refactor query/mutation ports with capability-safe
interfaces

- refactor: split UserRepository into AuthUserRepository and
  AdminUserRepository capabilities
- refactor: split SessionRepository into AuthSessionRepository and
  repository-owned CLI/admin methods
- refactor: replace generic Repository Update/Delete with
  operation-specific params and ownership predicates
- refactor: replace CredentialRepository generic CRUD with
  passkey-specific methods
- refactor: replace FileRepository generic Create/Update/Delete with
  UploadedFileParams, DirectoryParams, and owned soft-delete
- refactor: remove repository fields from WebApp struct; repositories
  are now composition-time wiring only
- feat: add domain error kinds ErrParentNotFound, ErrParentNotDir,
  ErrDirectoryNotEmpty, ErrInvalidMove
- feat: add CredentialTypeAppPasskey constant
- feat: add testutil.SetUserAdmin for test fixture setup that bypasses
  production service ports
- test: add architecture test banning GORM Save in repository package
- test: add capability interface contract tests ensuring each service
  receives the minimal interface
- test: add blockingStorage helper for concurrent promotion tests
- test: add preserved DSN parameter test for sqliteImmediateDSN
- docs: update architecture decisions with repository write rules and
  capability separation
- docs: update roadmap to clarify atomic single-use refresh sessions
- docs: add -race test target to development docs
2026-07-16 12:24:36 +08:00

88 lines
2.1 KiB
Go

package internal_test
import (
"go/ast"
"go/parser"
"go/token"
"os"
"strconv"
"strings"
"testing"
)
func TestArchitecture_ModelAndServiceAreProtocolNeutral(t *testing.T) {
for _, dir := range []string{"model", "service"} {
imports := packageImports(t, dir)
for _, forbidden := range []string{
"net/http",
"github.com/gin-gonic/gin",
"github.com/dhao2001/mygo/internal/api",
} {
if imports[forbidden] {
t.Fatalf("internal/%s imports forbidden package %q", dir, forbidden)
}
}
}
}
func TestArchitecture_HTTPDoesNotImportRepositories(t *testing.T) {
for _, dir := range []string{"handler", "middleware"} {
imports := packageImports(t, dir)
if imports["github.com/dhao2001/mygo/internal/repository"] {
t.Fatalf("internal/%s imports internal/repository; use service boundaries instead", dir)
}
}
}
func TestArchitecture_RepositoriesDoNotUseFullEntitySave(t *testing.T) {
pkgs := parsePackage(t, "repository")
for _, pkg := range pkgs {
for filename, file := range pkg.Files {
ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if ok && selector.Sel.Name == "Save" {
t.Fatalf("%s uses GORM Save; repository writes must use operation-specific columns", filename)
}
return true
})
}
}
}
func packageImports(t *testing.T, dir string) map[string]bool {
t.Helper()
imports := map[string]bool{}
pkgs := parsePackage(t, dir)
for _, pkg := range pkgs {
for _, file := range pkg.Files {
for _, spec := range file.Imports {
path, err := strconv.Unquote(spec.Path.Value)
if err != nil {
t.Fatalf("unquote import path %s: %v", spec.Path.Value, err)
}
imports[path] = true
}
}
}
return imports
}
func parsePackage(t *testing.T, dir string) map[string]*ast.Package {
t.Helper()
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool {
name := info.Name()
return !strings.HasSuffix(name, "_test.go")
}, parser.ParseComments)
if err != nil {
t.Fatalf("parse internal/%s: %v", dir, err)
}
return pkgs
}