From 8ab168633f967ca2acda91c36d743df64f180766 Mon Sep 17 00:00:00 2001 From: Gus Date: Thu, 25 Sep 2025 16:57:17 +0800 Subject: [PATCH 1/3] Add kernel coverage for router, helpers, ignite, and sentry --- metal/kernel/kernel_test.go | 242 ++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/metal/kernel/kernel_test.go b/metal/kernel/kernel_test.go index 3601b888..ddf0f424 100644 --- a/metal/kernel/kernel_test.go +++ b/metal/kernel/kernel_test.go @@ -1,16 +1,22 @@ package kernel import ( + "context" "net/http" "net/http/httptest" "os" "strings" + "sync" "testing" + "time" + "github.com/getsentry/sentry-go" "github.com/oullin/database" "github.com/oullin/database/repository" + "github.com/oullin/metal/env" "github.com/oullin/metal/router" "github.com/oullin/pkg/auth" + metalhttp "github.com/oullin/pkg/http" "github.com/oullin/pkg/llogs" "github.com/oullin/pkg/middleware" "github.com/oullin/pkg/portal" @@ -110,6 +116,21 @@ func TestIgnite(t *testing.T) { } } +func TestIgnitePanicsOnMissingFile(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic when env file is missing") + } else { + msg, _ := r.(string) + if !strings.Contains(msg, "failed to read the .env file/values") { + t.Fatalf("unexpected panic message: %v", r) + } + } + }() + + Ignite("/nonexistent/.env", portal.GetDefaultValidator()) +} + func TestAppBootNil(t *testing.T) { defer func() { if r := recover(); r == nil { @@ -145,6 +166,41 @@ func TestAppHelpers(t *testing.T) { } } +func TestAppAccessorsReturnValues(t *testing.T) { + validEnvVars(t) + + e := MakeEnv(portal.GetDefaultValidator()) + dbConn := &database.Connection{} + sentryHub := &portal.Sentry{} + + app := &App{ + env: e, + db: dbConn, + sentry: sentryHub, + } + + if !app.IsLocal() { + t.Fatalf("expected IsLocal to be true") + } + + e.App.Type = "production" + if !app.IsProduction() { + t.Fatalf("expected IsProduction to be true") + } + + if app.GetEnv() != e { + t.Fatalf("GetEnv did not return the environment") + } + + if app.GetDB() != dbConn { + t.Fatalf("GetDB did not return the database connection") + } + + if app.GetSentry() != sentryHub { + t.Fatalf("GetSentry did not return the sentry hub") + } +} + func TestAppRecoverRepanics(t *testing.T) { app := &App{} @@ -222,6 +278,87 @@ func TestAppBootRoutes(t *testing.T) { } } +func TestAppNewRouterNilReceiver(t *testing.T) { + var app *App + + if _, err := app.NewRouter(); err == nil || !strings.Contains(err.Error(), "app is nil") { + t.Fatalf("expected error about nil app") + } +} + +func TestAppNewRouterTokenHandlerError(t *testing.T) { + app := &App{ + env: &env.Environment{ + App: env.AppEnvironment{MasterKey: "short"}, + Network: env.NetEnvironment{}, + }, + db: &database.Connection{}, + } + + if _, err := app.NewRouter(); err == nil || !strings.Contains(err.Error(), "could not create a token handler") { + t.Fatalf("expected token handler error, got %v", err) + } +} + +func TestAppNewRouterSuccess(t *testing.T) { + validEnvVars(t) + + e := MakeEnv(portal.GetDefaultValidator()) + dbConn := &database.Connection{} + validator := portal.GetDefaultValidator() + + app := &App{ + env: e, + db: dbConn, + validator: validator, + } + + modem, err := app.NewRouter() + if err != nil { + t.Fatalf("expected router, got error: %v", err) + } + + if modem.Env != e { + t.Fatalf("router env mismatch") + } + + if modem.Db != dbConn { + t.Fatalf("router db mismatch") + } + + if modem.Validator != validator { + t.Fatalf("router validator mismatch") + } + + if modem.Mux == nil { + t.Fatalf("expected mux to be initialized") + } + + if modem.Pipeline.Env != e { + t.Fatalf("pipeline env mismatch") + } + + if modem.Pipeline.ApiKeys == nil || modem.Pipeline.ApiKeys.DB != dbConn { + t.Fatalf("pipeline api keys not configured") + } + + if modem.Pipeline.TokenHandler == nil { + t.Fatalf("expected token handler to be configured") + } + + handler := modem.Pipeline.PublicMiddleware.Handle(func(http.ResponseWriter, *http.Request) *metalhttp.ApiError { + return nil + }) + + if handler == nil { + t.Fatalf("expected public middleware handler to wrap the next handler") + } + + if modem.WebsiteRoutes == nil || modem.WebsiteRoutes.SiteURL != e.App.URL { + t.Fatalf("website routes not configured") + } +} + func TestMakeLogs(t *testing.T) { // Create a temporary directory with a lowercase path tempDir := getLowerTempDir(t) @@ -304,8 +441,113 @@ func TestMakeSentry(t *testing.T) { if s == nil || s.Handler == nil || s.Options == nil { t.Fatalf("sentry setup failed") } + + if s.Options.Timeout != 2*time.Second { + t.Fatalf("unexpected timeout value: %v", s.Options.Timeout) + } + + if !s.Options.Repanic { + t.Fatalf("expected repanic to be true") + } + + if s.Options.WaitForDelivery { + t.Fatalf("expected WaitForDelivery to be disabled in local environment") + } + + t.Setenv("ENV_APP_ENV_TYPE", "production") + + prodEnv := MakeEnv(portal.GetDefaultValidator()) + prodSentry := MakeSentry(prodEnv) + + if !prodSentry.Options.WaitForDelivery { + t.Fatalf("expected WaitForDelivery to be enabled in production") + } } +func TestRecoverWithSentryCapturesEvent(t *testing.T) { + rec := &recordingTransport{} + + client, err := sentry.NewClient(sentry.ClientOptions{Transport: rec}) + if err != nil { + t.Fatalf("failed to create sentry client: %v", err) + } + + originalClient := sentry.CurrentHub().Client() + sentry.CurrentHub().BindClient(client) + defer sentry.CurrentHub().BindClient(originalClient) + + defer func() { + recovered := recover() + if recovered == nil { + t.Fatalf("expected panic to propagate") + } + + if len(rec.events) != 1 { + t.Fatalf("expected sentry event to be captured") + } + + if rec.events[0].Message != "boom" { + t.Fatalf("unexpected event message: %s", rec.events[0].Message) + } + }() + + func() { + defer RecoverWithSentry(&portal.Sentry{}) + panic("boom") + }() +} + +func TestRecoverWithSentryNilHubRepaincs(t *testing.T) { + rec := &recordingTransport{} + + client, err := sentry.NewClient(sentry.ClientOptions{Transport: rec}) + if err != nil { + t.Fatalf("failed to create sentry client: %v", err) + } + + originalClient := sentry.CurrentHub().Client() + sentry.CurrentHub().BindClient(client) + defer sentry.CurrentHub().BindClient(originalClient) + + defer func() { + if r := recover(); r == nil { + t.Fatalf("expected panic to propagate") + } + + if len(rec.events) != 0 { + t.Fatalf("expected no sentry events when hub is nil") + } + }() + + func() { + defer RecoverWithSentry(nil) + panic("boom") + }() +} + +type recordingTransport struct { + mu sync.Mutex + events []*sentry.Event +} + +func (r *recordingTransport) Configure(options sentry.ClientOptions) {} + +func (r *recordingTransport) SendEvent(event *sentry.Event) { + r.mu.Lock() + defer r.mu.Unlock() + r.events = append(r.events, event) +} + +func (r *recordingTransport) Flush(timeout time.Duration) bool { + return true +} + +func (r *recordingTransport) FlushWithContext(ctx context.Context) bool { + return true +} + +func (r *recordingTransport) Close() {} + // getLowerTempDir returns a lowercase version of t.TempDir() func getLowerTempDir(t *testing.T) string { // Create a temporary directory in /tmp which should be lowercase From 84c5d67a9b8b82c4b4f86e9a7470e540285c5d50 Mon Sep 17 00:00:00 2001 From: Gus Date: Thu, 25 Sep 2025 17:09:27 +0800 Subject: [PATCH 2/3] Refine kernel tests per review --- metal/kernel/kernel_test.go | 158 ++++++++++++++++++++++-------------- 1 file changed, 95 insertions(+), 63 deletions(-) diff --git a/metal/kernel/kernel_test.go b/metal/kernel/kernel_test.go index ddf0f424..7d069403 100644 --- a/metal/kernel/kernel_test.go +++ b/metal/kernel/kernel_test.go @@ -121,7 +121,11 @@ func TestIgnitePanicsOnMissingFile(t *testing.T) { if r := recover(); r == nil { t.Fatalf("expected panic when env file is missing") } else { - msg, _ := r.(string) + msg, ok := r.(string) + if !ok { + t.Fatalf("unexpected panic type: %T", r) + } + if !strings.Contains(msg, "failed to read the .env file/values") { t.Fatalf("unexpected panic message: %v", r) } @@ -179,26 +183,33 @@ func TestAppAccessorsReturnValues(t *testing.T) { sentry: sentryHub, } - if !app.IsLocal() { - t.Fatalf("expected IsLocal to be true") - } + t.Run("Environment type checks", func(t *testing.T) { + if !app.IsLocal() { + t.Fatal("expected IsLocal to be true") + } - e.App.Type = "production" - if !app.IsProduction() { - t.Fatalf("expected IsProduction to be true") - } + originalType := e.App.Type + e.App.Type = "production" + defer func() { e.App.Type = originalType }() - if app.GetEnv() != e { - t.Fatalf("GetEnv did not return the environment") - } + if !app.IsProduction() { + t.Fatal("expected IsProduction to be true") + } + }) - if app.GetDB() != dbConn { - t.Fatalf("GetDB did not return the database connection") - } + t.Run("Accessors return correct values", func(t *testing.T) { + if app.GetEnv() != e { + t.Fatalf("GetEnv did not return the environment") + } - if app.GetSentry() != sentryHub { - t.Fatalf("GetSentry did not return the sentry hub") - } + if app.GetDB() != dbConn { + t.Fatalf("GetDB did not return the database connection") + } + + if app.GetSentry() != sentryHub { + t.Fatalf("GetSentry did not return the sentry hub") + } + }) } func TestAppRecoverRepanics(t *testing.T) { @@ -318,45 +329,57 @@ func TestAppNewRouterSuccess(t *testing.T) { t.Fatalf("expected router, got error: %v", err) } - if modem.Env != e { - t.Fatalf("router env mismatch") - } + t.Run("RouterFields", func(t *testing.T) { + if modem == nil { + t.Fatal("NewRouter returned a nil router on success") + } - if modem.Db != dbConn { - t.Fatalf("router db mismatch") - } + if modem.Env != e { + t.Error("router env mismatch") + } - if modem.Validator != validator { - t.Fatalf("router validator mismatch") - } + if modem.Db != dbConn { + t.Error("router db mismatch") + } - if modem.Mux == nil { - t.Fatalf("expected mux to be initialized") - } + if modem.Validator != validator { + t.Error("router validator mismatch") + } - if modem.Pipeline.Env != e { - t.Fatalf("pipeline env mismatch") - } + if modem.Mux == nil { + t.Error("expected mux to be initialized") + } + }) - if modem.Pipeline.ApiKeys == nil || modem.Pipeline.ApiKeys.DB != dbConn { - t.Fatalf("pipeline api keys not configured") - } + t.Run("PipelineFields", func(t *testing.T) { + if modem.Pipeline.Env != e { + t.Error("pipeline env mismatch") + } - if modem.Pipeline.TokenHandler == nil { - t.Fatalf("expected token handler to be configured") - } + if modem.Pipeline.ApiKeys == nil || modem.Pipeline.ApiKeys.DB != dbConn { + t.Error("pipeline api keys not configured") + } - handler := modem.Pipeline.PublicMiddleware.Handle(func(http.ResponseWriter, *http.Request) *metalhttp.ApiError { - return nil + if modem.Pipeline.TokenHandler == nil { + t.Error("expected token handler to be configured") + } }) - if handler == nil { - t.Fatalf("expected public middleware handler to wrap the next handler") - } + t.Run("PublicMiddleware", func(t *testing.T) { + handler := modem.Pipeline.PublicMiddleware.Handle(func(http.ResponseWriter, *http.Request) *metalhttp.ApiError { + return nil + }) - if modem.WebsiteRoutes == nil || modem.WebsiteRoutes.SiteURL != e.App.URL { - t.Fatalf("website routes not configured") - } + if handler == nil { + t.Error("expected public middleware handler to wrap the next handler") + } + }) + + t.Run("WebsiteRoutes", func(t *testing.T) { + if modem.WebsiteRoutes == nil || modem.WebsiteRoutes.SiteURL != e.App.URL { + t.Error("website routes not configured") + } + }) } func TestMakeLogs(t *testing.T) { @@ -442,26 +465,30 @@ func TestMakeSentry(t *testing.T) { t.Fatalf("sentry setup failed") } - if s.Options.Timeout != 2*time.Second { - t.Fatalf("unexpected timeout value: %v", s.Options.Timeout) - } + t.Run("Sentry options", func(t *testing.T) { + if s.Options.Timeout != 2*time.Second { + t.Fatalf("unexpected timeout value: %v", s.Options.Timeout) + } - if !s.Options.Repanic { - t.Fatalf("expected repanic to be true") - } + if !s.Options.Repanic { + t.Fatalf("expected repanic to be true") + } - if s.Options.WaitForDelivery { - t.Fatalf("expected WaitForDelivery to be disabled in local environment") - } + if s.Options.WaitForDelivery { + t.Fatalf("expected WaitForDelivery to be disabled in local environment") + } + }) - t.Setenv("ENV_APP_ENV_TYPE", "production") + t.Run("production environment", func(t *testing.T) { + t.Setenv("ENV_APP_ENV_TYPE", "production") - prodEnv := MakeEnv(portal.GetDefaultValidator()) - prodSentry := MakeSentry(prodEnv) + prodEnv := MakeEnv(portal.GetDefaultValidator()) + prodSentry := MakeSentry(prodEnv) - if !prodSentry.Options.WaitForDelivery { - t.Fatalf("expected WaitForDelivery to be enabled in production") - } + if !prodSentry.Options.WaitForDelivery { + t.Fatalf("expected WaitForDelivery to be enabled in production") + } + }) } func TestRecoverWithSentryCapturesEvent(t *testing.T) { @@ -486,8 +513,13 @@ func TestRecoverWithSentryCapturesEvent(t *testing.T) { t.Fatalf("expected sentry event to be captured") } - if rec.events[0].Message != "boom" { - t.Fatalf("unexpected event message: %s", rec.events[0].Message) + event := rec.events[0] + if len(event.Exception) > 0 { + if event.Exception[0].Value != "boom" { + t.Fatalf("unexpected exception in sentry event: %+v", event) + } + } else if event.Message != "boom" { + t.Fatalf("unexpected event message: %s", event.Message) } }() From 963064fdff6738550aabb8543211138196743b41 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Thu, 25 Sep 2025 17:12:39 +0800 Subject: [PATCH 3/3] wip --- metal/cli/seo/generator.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/metal/cli/seo/generator.go b/metal/cli/seo/generator.go index a38805a9..245a5ef0 100644 --- a/metal/cli/seo/generator.go +++ b/metal/cli/seo/generator.go @@ -132,19 +132,25 @@ func (g *Generator) Export(origin string, data TemplateData) error { var buffer bytes.Buffer fileName := fmt.Sprintf("%s.seo.html", origin) + cli.Warningln("Executing file: " + fileName) if err = g.Page.Template.Execute(&buffer, data); err != nil { return fmt.Errorf("%s: rendering template: %w", fileName, err) } + cli.Cyanln(fmt.Sprintf("Working on directory: %s", g.Page.OutputDir)) if err = os.MkdirAll(g.Page.OutputDir, 0o755); err != nil { return fmt.Errorf("%s: creating directory for %s: %w", fileName, g.Page.OutputDir, err) } out := filepath.Join(g.Page.OutputDir, fileName) + cli.Blueln(fmt.Sprintf("Writing file on: %s", out)) if err = os.WriteFile(out, buffer.Bytes(), 0o644); err != nil { return fmt.Errorf("%s: writing %s: %w", fileName, out, err) } + cli.Grayln(fmt.Sprintf("File %s generated at: %s", fileName, out)) + cli.Grayln("------------------") + return nil }