Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions metal/cli/seo/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
274 changes: 274 additions & 0 deletions metal/kernel/kernel_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -110,6 +116,25 @@ 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, 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)
}
}
}()

Ignite("/nonexistent/.env", portal.GetDefaultValidator())
}

func TestAppBootNil(t *testing.T) {
defer func() {
if r := recover(); r == nil {
Expand Down Expand Up @@ -145,6 +170,48 @@ 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,
}

t.Run("Environment type checks", func(t *testing.T) {
if !app.IsLocal() {
t.Fatal("expected IsLocal to be true")
}

originalType := e.App.Type
e.App.Type = "production"
defer func() { e.App.Type = originalType }()

if !app.IsProduction() {
t.Fatal("expected IsProduction to be true")
}
})

t.Run("Accessors return correct values", func(t *testing.T) {
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{}

Expand Down Expand Up @@ -222,6 +289,99 @@ 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)
}

t.Run("RouterFields", func(t *testing.T) {
if modem == nil {
t.Fatal("NewRouter returned a nil router on success")
}

if modem.Env != e {
t.Error("router env mismatch")
}

if modem.Db != dbConn {
t.Error("router db mismatch")
}

if modem.Validator != validator {
t.Error("router validator mismatch")
}

if modem.Mux == nil {
t.Error("expected mux to be initialized")
}
})

t.Run("PipelineFields", func(t *testing.T) {
if modem.Pipeline.Env != e {
t.Error("pipeline env mismatch")
}

if modem.Pipeline.ApiKeys == nil || modem.Pipeline.ApiKeys.DB != dbConn {
t.Error("pipeline api keys not configured")
}

if modem.Pipeline.TokenHandler == nil {
t.Error("expected token handler to be configured")
}
})

t.Run("PublicMiddleware", func(t *testing.T) {
handler := modem.Pipeline.PublicMiddleware.Handle(func(http.ResponseWriter, *http.Request) *metalhttp.ApiError {
return nil
})

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) {
// Create a temporary directory with a lowercase path
tempDir := getLowerTempDir(t)
Expand Down Expand Up @@ -304,8 +464,122 @@ func TestMakeSentry(t *testing.T) {
if s == nil || s.Handler == nil || s.Options == nil {
t.Fatalf("sentry setup failed")
}

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.WaitForDelivery {
t.Fatalf("expected WaitForDelivery to be disabled in local environment")
}
})

t.Run("production environment", func(t *testing.T) {
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")
}

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)
}
}()

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
Expand Down
Loading