a18f96912d
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
88 lines
2.1 KiB
Go
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
|
|
}
|