package main import ( "bytes" "context" "crypto/sha256" "net/http" "net/http/httptest" "os" "os/exec" "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 err := run([]string{"greeter:hello"}, &buf); err != nil { t.Fatalf("greeter:hello: %v", err) } 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 err := run([]string{"--help"}, &buf); err != nil { t.Fatal(err) } help := buf.String() for _, name := range []string{"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 err := run([]string{"migrate"}, &buf) if err == nil || !strings.Contains(err.Error(), "database.dsn") { t.Fatalf("want empty DSN error, got %v\n%s", err, buf.String()) } } func TestBuiltBinaryUnknownCommandExitsNonZero(t *testing.T) { runSummerBuild(t) cmd := exec.Command("./bin/hello", "does:not-exist") out, err := cmd.CombinedOutput() if err == nil { t.Fatalf("expected non-zero exit, output:\n%s", 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()) }