Files
sm-sitemap-plugin/sitemap_smoke_test.go
Jakub Zych e02d0b173f feat: IndexNow key, key file and pings
- saving with IndexNow on generates a 32-hex key (crypto/rand);
  indexnow_new_key generates a new one, so the old key file answers 404
- GET /_sitemap/indexnow/{file} serves <key>.txt as text/plain while
  IndexNow is on; the SEO layer maps it to the site root
- after each rebuild in production the added, changed and removed
  addresses on the site host are queued as golem15.sitemap.indexnow jobs
  (at most 10 000 per request, five attempts)
- the job posts {host, key, urlList} to api.indexnow.org through a
  fetchguard client limited to that host and records the answer; 429 and
  5xx retry, 400, 403 and 422 do not
- the IndexNow tab shows the S2 status partial: key or no key, last
  notification or never, and the 403, 422 and 429 callouts
2026-11-04 12:26:47 +01:00

191 lines
5.6 KiB
Go

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
}