package sitemap_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "sync/atomic" "testing" "time" _ "git.golem15.com/golem15/sm-sitemap-plugin" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bonfire" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/compass" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/party" "git.golem15.com/golem15/summercms/modules/surf" "gorm.io/gorm" ) // sitemapEnv is a booted application with the sitemap and the fake // content plugin. type sitemapEnv struct { h http.Handler app *backpack.App gdb *gorm.DB plugins []party.Plugin } // envOverride is the override key that sets the app environment // (SUMMER_ENV) instead of a config key; development by default. const envOverride = "__env" // bootPlugins boots the application with the plugins ids on a fresh // database; overrides set further config keys (envOverride the // environment). func bootPlugins(t *testing.T, ids []string, overrides ...map[string]any) *sitemapEnv { t.Helper() sqlDB := freshDB(t) gdb, err := lagoon.Use(t.Context(), sqlDB) if err != nil { t.Fatal(err) } dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: sitemap-smoke\nlocale: en\nfallback_locale: en\nurl: https://api.example.test\n"), 0o644); err != nil { t.Fatal(err) } env := "development" for _, o := range overrides { if v, ok := o[envOverride].(string); ok { env = v } } cfg, err := compass.Open(compass.Options{ Dir: dir, Environ: []string{"SUMMER_ENV=" + env, "SUMMER_ADMIN__JWT__SECRET=sitemap-smoke-test-only-hs256-secret-key"}, }) if err != nil { t.Fatal(err) } settings := map[string]any{ "http.body_limits.default_bytes": 1048576, "http.body_limits.upload_bytes": 8388608, "mail.driver": "log", "mail.from": "noreply@example.test", } for _, o := range overrides { for key, value := range o { if key != envOverride { settings[key] = value } } } for key, value := range settings { if err := cfg.Set(key, value); err != nil { t.Fatal(err) } } app := backpack.New(cfg) if err := lagoon.Publish(app, sqlDB, gdb); err != nil { t.Fatal(err) } plugins, err := party.Activate(app, ids) if err != nil { t.Fatal(err) } if err := lagoon.Migrate(gdb, plugins); err != nil { t.Fatal(err) } h, err := surf.Assemble(app, plugins) if err != nil { t.Fatal(err) } return &sitemapEnv{h: h, app: app, gdb: gdb, plugins: plugins} } func (e *sitemapEnv) get(t *testing.T, path string) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodGet, path, nil) req.Host = "api.example.test" rec := httptest.NewRecorder() e.h.ServeHTTP(rec, req) return rec } func writeFile(t *testing.T, dir, name, body string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } func testOutput() (bonfire.Output, *bytes.Buffer) { var buf bytes.Buffer return bonfire.NewOutput(strings.NewReader(""), &buf, &buf), &buf } var adminSeq atomic.Uint64 // call sends an admin API request with an optional bearer token and JSON // body. func (e *sitemapEnv) call(t *testing.T, method, path, token string, body any) *httptest.ResponseRecorder { t.Helper() var reader io.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { t.Fatal(err) } reader = bytes.NewReader(raw) } req := httptest.NewRequest(method, cabana.DefaultAdminPrefix+"/api/v1"+path, reader) req.Host = "api.example.test" req.RemoteAddr = fmt.Sprintf("198.51.100.%d:4000", adminSeq.Add(1)%250+1) req.Header.Set("Accept-Language", "en") if body != nil { req.Header.Set("Content-Type", "application/json") } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rec := httptest.NewRecorder() e.h.ServeHTTP(rec, req) return rec } // admin signs in a new administrator whose role grants permissions. func (e *sitemapEnv) admin(t *testing.T, permissions ...string) string { t.Helper() stamp := fmt.Sprintf("%d%d", time.Now().UnixNano()%1_000_000_000, adminSeq.Add(1)) login, password := "admin-"+stamp, "admin-secret-"+stamp hash, err := bouncer.HashPassword(10, password) if err != nil { t.Fatal(err) } grants := map[string]int{} for _, code := range permissions { grants[code] = 1 } raw, _ := json.Marshal(grants) var roleID uint if err := e.gdb.Raw(`INSERT INTO backend_user_roles (name, code, permissions, is_system, created_at, updated_at) VALUES (?, ?, ?, FALSE, NOW(), NOW()) RETURNING id`, "Role "+stamp, "role-"+stamp, string(raw)).Scan(&roleID).Error; err != nil || roleID == 0 { t.Fatalf("backend role: id=%d err=%v", roleID, err) } if err := e.gdb.Exec(`INSERT INTO backend_users (login, email, password, is_activated, is_superuser, role_id, created_at, updated_at) VALUES (?, ?, ?, TRUE, FALSE, ?, NOW(), NOW())`, login, login+"@example.test", hash, roleID).Error; err != nil { t.Fatal(err) } rec := e.call(t, http.MethodPost, "/auth/login", "", map[string]string{"login": login, "password": password}) var body struct { Data struct { AccessToken string `json:"access_token"` } `json:"data"` } if rec.Code != http.StatusOK || json.Unmarshal(rec.Body.Bytes(), &body) != nil || body.Data.AccessToken == "" { t.Fatalf("admin login: %d %s", rec.Code, rec.Body.String()) } return body.Data.AccessToken }