package main import ( "bytes" "context" "crypto/sha256" "net/http" "net/http/httptest" "os" "os/exec" "slices" "strings" "testing" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/phrasebook" "git.golem15.com/golem15/summercms/modules/postcard" "git.golem15.com/golem15/summercms/modules/surf" "git.golem15.com/golem15/summercms/modules/towel" ) func TestGreeterHelloPrintsLayeredConfig(t *testing.T) { got := runHello(t, nil) want := "name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true" assertGreeting(t, got, want) } func TestGreeterHelloSeesDevelopmentOverlay(t *testing.T) { got := runHello(t, map[string]string{"SUMMER_ENV": "development"}) want := "name=hello-dev posts_per_page=10 debug=true extra=hello-from-optional events=ok collected=greeter handled=true" assertGreeting(t, got, want) } func TestGreeterHelloSeesEnvVarOverrideOfPluginDefault(t *testing.T) { got := runHello(t, map[string]string{ "SUMMER_GOLEM15__HELLO__POSTS_PER_PAGE": "25", }) want := "name=hello-app posts_per_page=25 debug=false extra=hello-from-optional events=ok collected=greeter handled=true" assertGreeting(t, got, want) } func runHello(t *testing.T, env map[string]string) string { t.Helper() for k, v := range env { t.Setenv(k, v) } var buf bytes.Buffer if code := run([]string{"greeter:hello"}, &buf); code != 0 { t.Fatalf("greeter:hello: exit %d\n%s", code, buf.String()) } return strings.TrimSpace(buf.String()) } func TestGreeterRunsWithoutOptionalPlugin(t *testing.T) { got := runHelloIDs(t, []string{"golem15.hello", "golem15.greeter"}, nil) want := "name=hello-app posts_per_page=10 debug=false extra= events=ok collected=greeter handled=true" assertGreeting(t, got, want) } func runHelloIDs(t *testing.T, ids []string, env map[string]string) string { t.Helper() for k, v := range env { t.Setenv(k, v) } cfg, err := compass.Load("config") if err != nil { t.Fatalf("Load: %v", err) } app := backpack.New(cfg) plugins, err := party.Activate(app, ids) if err != nil { t.Fatalf("Activate: %v", err) } var commands []bonfire.Command for _, plugin := range plugins { if hasCommands, ok := plugin.(pact.HasCommands); ok { commands = append(commands, hasCommands.Commands()...) } } var buf bytes.Buffer root, err := bonfire.NewRoot("hello", commands, &buf) if err != nil { t.Fatalf("NewRoot: %v", err) } root.SetArgs([]string{"greeter:hello"}) if err := root.Execute(); err != nil { t.Fatalf("execute: %v", err) } return strings.TrimSpace(buf.String()) } func TestBuiltBinaryGreeterHello(t *testing.T) { first := runSummerBuild(t) h1 := generatedHash(t) second := runSummerBuild(t) h2 := generatedHash(t) if h1 != h2 { t.Fatalf("generated files changed on second build\nfirst:\n%s\nsecond:\n%s", first, second) } cmd := exec.Command("./bin/hello", "greeter:hello") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("bin/hello greeter:hello: %v\n%s", err, out) } got := strings.TrimSpace(string(out)) want := "name=hello-app posts_per_page=10 debug=false extra=hello-from-optional events=ok collected=greeter handled=true" assertGreeting(t, got, want) } func TestHelloPluginResolvesLang(t *testing.T) { cfg, err := compass.Load("config") if err != nil { t.Fatal(err) } app := backpack.New(cfg) if _, err := party.Activate(app, []string{"golem15.hello"}); err != nil { t.Fatal(err) } tr, ok := app.Lookup[*phrasebook.Translator]() if !ok { t.Fatal("translator not published") } ctx := towel.WithLocale(context.Background(), "pl") if got := tr.Get(ctx, "golem15.hello::lang.greeting", nil); got != "Cześć" { t.Fatalf("towel locale = %q", got) } if got := tr.Get(context.Background(), "golem15.hello::lang.greeting", nil); got != "Hello" { t.Fatalf("app locale default = %q", got) } if got := tr.GetIn("en", "golem15.hello::lang.labels.pluginName", nil); got != "Hello plugin" { t.Fatalf("en = %q", got) } if got := tr.GetIn("pl", "golem15.hello::lang.labels.pluginName", nil); got != "Wtyczka Hello" { t.Fatalf("pl = %q", got) } if got := tr.GetIn("en", "golem15.hello::lang.greeting", nil); got != "Hello" { t.Fatalf("greeting = %q", got) } if got := tr.GetIn("pl-PL", "golem15.hello::lang.greeting", nil); got != "Cześć" { t.Fatalf("pl-PL = %q", got) } if got := tr.GetIn("de", "golem15.hello::lang.greeting", nil); got != "Hello" { t.Fatalf("fallback en = %q", got) } const missing = "golem15.hello::lang.does.not.exist" if got := tr.GetIn("pl-PL", missing, nil); got != missing { t.Fatalf("raw key = %q", got) } } func TestHelloPluginSendsMail(t *testing.T) { cfg, err := compass.Load("config") if err != nil { t.Fatal(err) } app := backpack.New(cfg) if _, err := party.Activate(app, []string{"golem15.hello"}); err != nil { t.Fatal(err) } mail, ok := app.Lookup[postcard.Mailer]() if !ok { t.Fatal("mailer not published") } if err := mail.Send(t.Context(), postcard.Message{ Template: "golem15.hello::mail.hello", To: []string{"ada@example.test"}, Vars: map[string]any{"Name": "Ada", "URL": "https://example.test"}, }); err != nil { t.Fatal(err) } if err := mail.Send(t.Context(), postcard.Message{ Template: "golem15.hello::mail.hello-en", To: []string{"ada@example.test"}, Vars: map[string]any{"Name": "Ada", "URL": "https://example.test"}, }); err != nil { t.Fatal(err) } err = mail.Send(t.Context(), postcard.Message{ Template: "golem15.hello::mail.does-not-exist", To: []string{"ada@example.test"}, }) if err == nil || !strings.Contains(err.Error(), "golem15.hello::mail.does-not-exist") { t.Fatalf("unknown template error = %v", err) } } func TestTypedItemRoute(t *testing.T) { cfg, err := compass.Load("config") if err != nil { t.Fatal(err) } application := backpack.New(cfg) plugins, err := party.Activate(application, PluginIDs) if err != nil { t.Fatal(err) } h, err := surf.Assemble(application, plugins) if err != nil { t.Fatal(err) } rec := httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/nope", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("malformed id status = %d", rec.Code) } rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/99", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("unknown id status = %d", rec.Code) } rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/items/1", nil)) if rec.Code != http.StatusOK || rec.Body.String() != `{"id":1}` { t.Fatalf("got %d %s", rec.Code, rec.Body.String()) } rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/kinds/other", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("enum reject status = %d", rec.Code) } rec = httptest.NewRecorder() h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/kinds/widget", nil)) if rec.Code != http.StatusOK || rec.Body.String() != `{"kind":"widget"}` { t.Fatalf("got %d %s", rec.Code, rec.Body.String()) } } func TestHelpListsServe(t *testing.T) { var buf bytes.Buffer if code := run([]string{"--help"}, &buf); code != 0 { t.Fatalf("--help: exit %d\n%s", code, buf.String()) } help := buf.String() for _, name := range []string{"Available commands:", "serve", "migrate", "migrate:status", "migrate:rollback"} { if !strings.Contains(help, name) { t.Fatalf("help missing %s:\n%s", name, help) } } } func TestMigrateWithoutDSNFailsLoudly(t *testing.T) { var buf bytes.Buffer if code := run([]string{"migrate"}, &buf); code != 1 { t.Fatalf("migrate without a DSN: exit %d, want 1\n%s", code, buf.String()) } runSummerBuild(t) cmd := exec.Command("./bin/hello", "migrate") var stderr bytes.Buffer cmd.Stderr = &stderr err := cmd.Run() if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 1 { t.Fatalf("bin/hello migrate: %v, want exit 1\n%s", err, stderr.String()) } if !strings.Contains(stderr.String(), "database.dsn") { t.Fatalf("stderr does not name database.dsn:\n%s", stderr.String()) } } func TestBuiltBinaryUnknownCommandExitsNonZero(t *testing.T) { runSummerBuild(t) cmd := exec.Command("./bin/hello", "does:not-exist") out, err := cmd.CombinedOutput() if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 2 { t.Fatalf("unknown command: %v, want exit 2, output:\n%s", err, out) } } // TestBuiltBinaryListsCommands: the bare binary prints the Artisan list with // the version header and the plugin namespace, and global options are // accepted before a command. func TestBuiltBinaryListsCommands(t *testing.T) { runSummerBuild(t) cmd := exec.Command("./bin/hello") var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr if err := cmd.Run(); err != nil { t.Fatalf("bin/hello: %v\n%s%s", err, stdout.String(), stderr.String()) } got := stdout.String() for _, want := range []string{"SummerCMS hello", "Available commands:", "greeter:hello"} { if !strings.Contains(got, want) { t.Fatalf("list missing %q:\n%s", want, got) } } if !slices.Contains(strings.Split(got, "\n"), " greeter") { t.Fatalf("list has no greeter namespace line:\n%s", got) } verbose := exec.Command("./bin/hello", "-vv", "list") if out, err := verbose.CombinedOutput(); err != nil { t.Fatalf("bin/hello -vv list: %v\n%s", err, out) } } func assertGreeting(t *testing.T, got, want string) { t.Helper() if !strings.Contains(got, "plugin\tstatus") || !strings.Contains(got, "golem15.greeter\tactive") { t.Fatalf("missing non-tty table:\n%s", got) } if lastLine(got) != want { t.Fatalf("greeting = %q, want %q\nfull:\n%s", lastLine(got), want, got) } } func lastLine(s string) string { s = strings.TrimSpace(s) if i := strings.LastIndex(s, "\n"); i >= 0 { return s[i+1:] } return s } func runSummerBuild(t *testing.T) string { t.Helper() cmd := exec.Command("go", "run", "../../cmd/summer", "build") cmd.Dir = "." out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("summer build: %v\n%s", err, out) } return string(out) } func generatedHash(t *testing.T) [32]byte { t.Helper() var buf bytes.Buffer for _, name := range []string{"main.go", "plugins.gen.go"} { body, err := os.ReadFile(name) if err != nil { t.Fatal(err) } buf.Write(body) } return sha256.Sum256(buf.Bytes()) } // TestGreeterDocumented: the greeter plugin's commands carry a description, // long help, examples and flag descriptions (bonfire.CheckDocs). func TestGreeterDocumented(t *testing.T) { cfg, err := compass.Load("config") if err != nil { t.Fatalf("Load: %v", err) } plugins, err := party.Activate(backpack.New(cfg), []string{"golem15.hello", "golem15.greeter"}) if err != nil { t.Fatalf("Activate: %v", err) } var commands []bonfire.Command for _, p := range plugins { if hc, ok := p.(pact.HasCommands); ok && p.ID() == "golem15.greeter" { commands = append(commands, hc.Commands()...) } } if len(commands) == 0 { t.Fatal("the greeter plugin has no commands") } for _, problem := range bonfire.CheckDocs(commands) { t.Error(problem) } }