package core_test import ( "bytes" "context" "encoding/json" "errors" "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "testing" "code.nochebuena.dev/einherjar/contracts/errs" "code.nochebuena.dev/einherjar/contracts/lifecycle" "code.nochebuena.dev/einherjar/contracts/logging" "code.nochebuena.dev/einherjar/core/launcher" "code.nochebuena.dev/einherjar/core/logz" "code.nochebuena.dev/einherjar/core/valid" "code.nochebuena.dev/einherjar/core/xerrors" ) // ── Compile-time interface satisfaction ────────────────────────────────────── var _ logging.Logger = logz.New(logz.Config{}) var _ errs.CodedError = (*xerrors.Err)(nil) var _ errs.ContextualError = (*xerrors.Err)(nil) var _ valid.Validator = valid.New() var _ valid.MessageProvider = valid.DefaultMessages var _ valid.MessageProvider = valid.SpanishMessages // ── Structural: at most one exported TypeSpec per file ──────────────────────── func TestAtMostOneExportedTypePerFile(t *testing.T) { fset := token.NewFileSet() err := filepath.WalkDir(".", func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() && (d.Name() == ".git" || d.Name() == "vendor") { return filepath.SkipDir } if !strings.HasSuffix(path, ".go") { return nil } if strings.HasSuffix(path, "_test.go") { return nil } if filepath.Base(path) == "doc.go" { return nil } f, parseErr := parser.ParseFile(fset, path, nil, 0) if parseErr != nil { t.Errorf("%s: parse error: %v", path, parseErr) return nil } count := countExportedTypes(f) if count > 1 { t.Errorf("%s: has %d exported type declarations; want at most 1", path, count) } return nil }) if err != nil { t.Fatalf("walk error: %v", err) } } func countExportedTypes(f *ast.File) int { count := 0 for _, decl := range f.Decls { genDecl, ok := decl.(*ast.GenDecl) if !ok { continue } for _, spec := range genDecl.Specs { ts, ok := spec.(*ast.TypeSpec) if ok && ts.Name.IsExported() { count++ } } } return count } // ── launcher ───────────────────────────────────────────────────────────────── type mockComponent struct { initCalls *[]string startCalls *[]string stopCalls *[]string name string } func (m *mockComponent) OnInit() error { *m.initCalls = append(*m.initCalls, m.name); return nil } func (m *mockComponent) OnStart() error { *m.startCalls = append(*m.startCalls, m.name); return nil } func (m *mockComponent) OnStop() error { *m.stopCalls = append(*m.stopCalls, m.name); return nil } var _ lifecycle.Component = (*mockComponent)(nil) func TestLauncherLifecycleOrder(t *testing.T) { var inits, starts, stops []string c1 := &mockComponent{initCalls: &inits, startCalls: &starts, stopCalls: &stops, name: "c1"} c2 := &mockComponent{initCalls: &inits, startCalls: &starts, stopCalls: &stops, name: "c2"} logger := logz.New(logz.Config{Writer: &bytes.Buffer{}}) lc := launcher.New(logger) lc.Append(c1, c2) done := make(chan error, 1) go func() { done <- lc.Run() }() ctx, cancel := context.WithTimeout(context.Background(), 3e9) defer cancel() if err := lc.Shutdown(ctx); err != nil { t.Fatalf("Shutdown: %v", err) } if err := <-done; err != nil { t.Fatalf("Run: %v", err) } if len(inits) != 2 || inits[0] != "c1" || inits[1] != "c2" { t.Errorf("OnInit order: got %v, want [c1 c2]", inits) } if len(starts) != 2 || starts[0] != "c1" || starts[1] != "c2" { t.Errorf("OnStart order: got %v, want [c1 c2]", starts) } // OnStop must run in reverse order if len(stops) != 2 || stops[0] != "c2" || stops[1] != "c1" { t.Errorf("OnStop order: got %v, want [c2 c1]", stops) } } // ── logz ───────────────────────────────────────────────────────────────────── func TestLogzErrorEnrichment(t *testing.T) { var buf bytes.Buffer logger := logz.New(logz.Config{JSON: true, Writer: &buf}) err := xerrors.New(xerrors.ErrNotFound, "thing missing"). WithContext("id", "abc123") logger.Error("lookup failed", err) out := buf.String() if !strings.Contains(out, "NOT_FOUND") { t.Errorf("expected error_code=NOT_FOUND in output: %s", out) } if !strings.Contains(out, "abc123") { t.Errorf("expected context field id=abc123 in output: %s", out) } } func TestLogzContextEnrichment(t *testing.T) { var buf bytes.Buffer logger := logz.New(logz.Config{JSON: true, Writer: &buf}) ctx := logz.WithRequestID(context.Background(), "req-xyz") ctx = logz.WithField(ctx, "user_id", "u-001") logger.WithContext(ctx).Info("handling request") out := buf.String() if !strings.Contains(out, "req-xyz") { t.Errorf("expected request_id=req-xyz in output: %s", out) } if !strings.Contains(out, "u-001") { t.Errorf("expected user_id=u-001 in output: %s", out) } } // ── xerrors ─────────────────────────────────────────────────────────────────── func TestXerrorsCodeRoundtrip(t *testing.T) { err := xerrors.New(xerrors.ErrNotFound, "user not found") if err.Code() != xerrors.ErrNotFound { t.Errorf("Code(): got %s, want %s", err.Code(), xerrors.ErrNotFound) } if err.Message() != "user not found" { t.Errorf("Message(): got %s", err.Message()) } } func TestXerrorsContextRoundtrip(t *testing.T) { err := xerrors.New(xerrors.ErrInvalidInput, "bad"). WithContext("field", "email"). WithContext("rule", "required") f := err.Fields() if f["field"] != "email" { t.Errorf("Fields()[field]: got %v", f["field"]) } if f["rule"] != "required" { t.Errorf("Fields()[rule]: got %v", f["rule"]) } } func TestXerrorsWrapUnwrap(t *testing.T) { cause := errors.New("db connection refused") err := xerrors.Wrap(xerrors.ErrInternal, "query failed", cause) if !errors.Is(err, cause) { t.Error("errors.Is should find the wrapped cause") } } func TestXerrorsMarshalJSON(t *testing.T) { err := xerrors.New(xerrors.ErrNotFound, "thing missing"). WithPlatformCode("THING_NOT_FOUND") b, jsonErr := json.Marshal(err) if jsonErr != nil { t.Fatalf("MarshalJSON: %v", jsonErr) } var m map[string]any if err2 := json.Unmarshal(b, &m); err2 != nil { t.Fatalf("Unmarshal: %v", err2) } if m["code"] != "NOT_FOUND" { t.Errorf("JSON code: got %v", m["code"]) } if m["platform_code"] != "THING_NOT_FOUND" { t.Errorf("JSON platform_code: got %v", m["platform_code"]) } } func TestXerrorsConvenienceConstructors(t *testing.T) { cases := []struct { err *xerrors.Err code xerrors.Code }{ {xerrors.InvalidInput("bad"), xerrors.ErrInvalidInput}, {xerrors.OutOfRange("over"), xerrors.ErrOutOfRange}, {xerrors.Unauthorized("auth"), xerrors.ErrUnauthorized}, {xerrors.PermissionDenied("denied"), xerrors.ErrPermissionDenied}, {xerrors.NotFound("missing"), xerrors.ErrNotFound}, {xerrors.AlreadyExists("dup"), xerrors.ErrAlreadyExists}, {xerrors.Aborted("conflict"), xerrors.ErrAborted}, {xerrors.Gone("deleted"), xerrors.ErrGone}, {xerrors.PreconditionFailed("rule"), xerrors.ErrPreconditionFailed}, {xerrors.RateLimited("slow down"), xerrors.ErrRateLimited}, {xerrors.Cancelled("cancelled"), xerrors.ErrCancelled}, {xerrors.Internal("oops"), xerrors.ErrInternal}, {xerrors.DataLoss("corrupted"), xerrors.ErrDataLoss}, {xerrors.NotImplemented("todo"), xerrors.ErrNotImplemented}, {xerrors.Unavailable("down"), xerrors.ErrUnavailable}, {xerrors.DeadlineExceeded("timeout"), xerrors.ErrDeadlineExceeded}, } for _, tc := range cases { if tc.err.Code() != tc.code { t.Errorf("%s constructor: Code() = %s, want %s", tc.code, tc.err.Code(), tc.code) } } } // ── valid ───────────────────────────────────────────────────────────────────── func TestValidValidStruct(t *testing.T) { type req struct { Email string `json:"email" validate:"required,email"` } v := valid.New() if err := v.Struct(req{Email: "user@example.com"}); err != nil { t.Errorf("expected nil for valid struct, got %v", err) } } func TestValidRequiredFieldMissing(t *testing.T) { type req struct { Email string `json:"email" validate:"required"` } v := valid.New() err := v.Struct(req{}) if err == nil { t.Fatal("expected error for missing required field") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("Code(): got %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } if xe.Fields()["field"] != "email" { t.Errorf("Fields()[field]: got %v", xe.Fields()["field"]) } } func TestValidNonStructReturnsInternal(t *testing.T) { v := valid.New() err := v.Struct("not a struct") if err == nil { t.Fatal("expected error for non-struct") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInternal { t.Errorf("Code(): got %s, want %s", xe.Code(), xerrors.ErrInternal) } } func TestValidSpanishMessages(t *testing.T) { type req struct { Email string `json:"email" validate:"required"` } v := valid.New(valid.WithMessageProvider(valid.SpanishMessages)) err := v.Struct(req{}) if err == nil { t.Fatal("expected error") } var xe *xerrors.Err errors.As(err, &xe) if !strings.Contains(xe.Message(), "obligatorio") { t.Errorf("expected Spanish message, got: %s", xe.Message()) } } func TestValidCustomValidator(t *testing.T) { type req struct { Website string `json:"website" validate:"nohttp"` } v := valid.New( valid.WithCustomValidator("nohttp", func(fl valid.FieldLevel) bool { return !strings.HasPrefix(fl.Field().String(), "http://") }), ) if err := v.Struct(req{Website: "example.com"}); err != nil { t.Errorf("expected nil for valid value, got %v", err) } err := v.Struct(req{Website: "http://example.com"}) if err == nil { t.Fatal("expected error for http:// value") } var xe *xerrors.Err if !errors.As(err, &xe) { t.Fatalf("expected *xerrors.Err, got %T", err) } if xe.Code() != xerrors.ErrInvalidInput { t.Errorf("Code(): got %s, want %s", xe.Code(), xerrors.ErrInvalidInput) } } func TestValidOverrideProvider(t *testing.T) { p := valid.OverrideProvider( map[string]func(field, param string) string{ "nohttp": func(field, _ string) string { return "field '" + field + "' must not use http://" }, }, valid.DefaultMessages, ) msg := p.Message("website", "nohttp", "") if msg != "field 'website' must not use http://" { t.Errorf("custom tag message: got %q", msg) } fallback := p.Message("email", "required", "") if !strings.Contains(fallback, "is required") { t.Errorf("built-in tag should fall through to DefaultMessages, got: %q", fallback) } }