feat: G15Office job, widget settings and submissions admin, embed.js and the settings importer
- SyncG15Office files each submission as a G15Office task with its screenshot through a fetchguard client limited to the configured host; a River job with 3 attempts, PHP's messages on failure, nothing sent when unconfigured - the replay of PHP's recorded G15Office exchange (classes/testdata) asserts the task JSON, headers and the screenshot part - settings screen (cabana field types only), read-only submissions list, manage_settings permission and navigation - embed.js served byte for byte at /plugins/golem15/feedback/assets/js/embed.js - feedback:import-settings copies the WinterCMS settings row once
This commit is contained in:
170
feedback_test.go
170
feedback_test.go
@@ -3,9 +3,13 @@ package feedback
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -18,12 +22,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/classes"
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/console"
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/controllers/api"
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/internal/pgtest"
|
||||
"git.golem15.com/golem15/sm-feedback-plugin/models"
|
||||
usercontrollers "git.golem15.com/golem15/sm-user-plugin/controllers"
|
||||
usermodels "git.golem15.com/golem15/sm-user-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"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/lagoon/attach"
|
||||
@@ -38,6 +44,10 @@ import (
|
||||
// testJWTSecret is the test-only golem15.user JWT secret.
|
||||
const testJWTSecret = "feedback-test-jwt-secret-0123456789abcdef"
|
||||
|
||||
// testAdminSecret is the test-only admin JWT secret (the plugin has admin
|
||||
// controllers, so the admin needs one to boot).
|
||||
const testAdminSecret = "feedback-test-admin-secret-0123456789abcdef"
|
||||
|
||||
// testWidgetKey is the widget key the tests store.
|
||||
const testWidgetKey = "wk_TestKey0123456789abcdefghijklmnop"
|
||||
|
||||
@@ -54,6 +64,7 @@ func testConfig(t testing.TB) *compass.Config {
|
||||
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{
|
||||
"SUMMER_ENV=development",
|
||||
"SUMMER_GOLEM15__USER__JWT__SECRET=" + testJWTSecret,
|
||||
"SUMMER_ADMIN__JWT__SECRET=" + testAdminSecret,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -782,3 +793,162 @@ func TestFeedbackOptionsPreflight(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// phpEmbedJSSHA256 is the sha256 of the WinterCMS plugin's
|
||||
// assets/js/embed.js, which the Go plugin serves byte for byte.
|
||||
const phpEmbedJSSHA256 = "a76d27dc5debfee5834fb533320be201ff22324a0e845c373dbef8c2dd79bb50"
|
||||
|
||||
// TestEmbedJSServed: GET /plugins/golem15/feedback/assets/js/embed.js is
|
||||
// the PHP file byte for byte, as JavaScript, with conditional requests.
|
||||
func TestEmbedJSServed(t *testing.T) {
|
||||
h, _, _ := newHandler(t)
|
||||
rec := do(t, h, httptest.NewRequest(http.MethodGet, EmbedJSPath, nil))
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d", rec.Code)
|
||||
}
|
||||
sum := sha256.Sum256(rec.Body.Bytes())
|
||||
if hex.EncodeToString(sum[:]) != phpEmbedJSSHA256 {
|
||||
t.Fatal("embed.js differs from the PHP file")
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); ct != "application/javascript; charset=utf-8" {
|
||||
t.Fatalf("Content-Type %q", ct)
|
||||
}
|
||||
etag := rec.Header().Get("ETag")
|
||||
req := httptest.NewRequest(http.MethodGet, EmbedJSPath, nil)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
if rec := do(t, h, req); etag == "" || rec.Code != http.StatusNotModified {
|
||||
t.Fatalf("conditional GET: %d (ETag %q)", rec.Code, etag)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeedbackImportSettings: feedback:import-settings copies the WinterCMS
|
||||
// row into the singleton once (widget key included), refuses changed
|
||||
// settings without --force, and reports a missing source cleanly.
|
||||
func TestFeedbackImportSettings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
gdb := pgtest.DB(t)
|
||||
if err := gdb.Exec(`DELETE FROM golem15_feedback_settings`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.Exec(`DROP TABLE IF EXISTS system_settings`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { gdb.Exec(`DROP TABLE IF EXISTS system_settings`) })
|
||||
cmds := (&Plugin{}).Commands()
|
||||
if len(cmds) != 1 || cmds[0].Name != "feedback:import-settings" || len(cmds[0].Flags) != 1 || cmds[0].Flags[0].Name != "force" {
|
||||
t.Fatalf("commands %+v", cmds)
|
||||
}
|
||||
|
||||
err := console.Import(ctx, gdb, false)
|
||||
if !console.IsNothingToImport(err) || err.Error() != "No system_settings table: nothing to import." {
|
||||
t.Fatalf("missing table: %v", err)
|
||||
}
|
||||
if err := gdb.Exec(`CREATE TABLE system_settings (id SERIAL PRIMARY KEY, item TEXT, value TEXT)`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := console.Import(ctx, gdb, false); !console.IsNothingToImport(err) || err.Error() != "No golem15_feedback_settings row in system_settings: nothing to import." {
|
||||
t.Fatalf("missing row: %v", err)
|
||||
}
|
||||
value := `{"enabled":"1","allow_hide":"0","widget_key":"wk_FromWinter0000000000000000000000","position":"middle-left",` +
|
||||
`"allowed_origins":"https://example.com\r\nexample.org","color_primary":"#111827","color_accent":null,` +
|
||||
`"label_title_pl":"Napisz do nas","label_title_en":"Write to us","g15office_task_status":"","g15office_task_priority":"high"}`
|
||||
if err := gdb.Exec(`INSERT INTO system_settings (item, value) VALUES ('golem15_feedback_settings', ?)`, value).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := console.Import(ctx, gdb, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := models.Instance(ctx, gdb)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !s.Enabled || s.AllowHide || s.WidgetKey != "wk_FromWinter0000000000000000000000" || *s.Position != "middle-left" ||
|
||||
*s.AllowedOrigins != "https://example.com\r\nexample.org" || *s.ColorPrimary != "#111827" || s.ColorAccent != nil ||
|
||||
*s.LabelTitlePL != "Napisz do nas" || *s.LabelTitleEN != "Write to us" || *s.LabelSuccessEN != "Thanks for the feedback!" ||
|
||||
*s.G15OfficeTaskStatus != "" || *s.G15OfficeTaskPriority != "high" {
|
||||
t.Fatalf("imported %+v", s)
|
||||
}
|
||||
if err := console.Import(ctx, gdb, false); !errors.Is(err, console.ErrSettingsCustomised) {
|
||||
t.Fatalf("second import: %v", err)
|
||||
}
|
||||
if err := gdb.Exec(`UPDATE system_settings SET value = '{"position":"bottom-left"}'`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := console.Import(ctx, gdb, true); err != nil {
|
||||
t.Fatalf("forced import: %v", err)
|
||||
}
|
||||
if s, _ := models.Instance(ctx, gdb); *s.Position != "bottom-left" || s.WidgetKey != "wk_FromWinter0000000000000000000000" {
|
||||
t.Fatalf("forced import kept %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeedbackAdminSchemas: the settings form (cabana field types only,
|
||||
// widget key read-only) and the read-only submissions list compile when
|
||||
// the admin boots, with the permission, the navigation and the settings
|
||||
// screen registered, and the settings save through cabana with a
|
||||
// generated widget key.
|
||||
func TestFeedbackAdminSchemas(t *testing.T) {
|
||||
h, app, gdb := newHandler(t)
|
||||
_ = h
|
||||
p := &Plugin{}
|
||||
ctls := p.AdminControllers()
|
||||
if len(ctls) != 1 || ctls[0].ID() != "golem15.feedback.submissions" {
|
||||
t.Fatalf("controllers %v", ctls)
|
||||
}
|
||||
list, err := cabana.CompileList("golem15.feedback", ctls[0], p.AdminFS())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(list.ToolbarButtons) != 0 || len(list.BulkActions) != 0 {
|
||||
t.Fatalf("the submissions list is not read-only: %+v %+v", list.ToolbarButtons, list.BulkActions)
|
||||
}
|
||||
for _, c := range list.Columns {
|
||||
if c.Type != "text" && c.Type != "datetime" && c.Type != "" {
|
||||
t.Fatalf("column %s renders as %s", c.Key, c.Type)
|
||||
}
|
||||
}
|
||||
if _, err := fs.Stat(p.AdminFS(), "controllers/submissions/config_form.yaml"); err == nil {
|
||||
t.Fatal("the submissions controller has a form")
|
||||
}
|
||||
raw, err := fs.ReadFile(p.AdminFS(), "models/settings/fields.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "colorpicker") || strings.Contains(string(raw), "readOnly") {
|
||||
t.Fatal("the settings form uses a field type cabana does not support")
|
||||
}
|
||||
if perms := p.Permissions(); len(perms) != 1 || perms[0].Code != "golem15.feedback.manage_settings" || perms[0].Roles[0] != "developer" {
|
||||
t.Fatalf("permissions %+v", perms)
|
||||
}
|
||||
if nav := p.Navigation(); len(nav) != 1 || nav[0].SideMenu[0].Controller != "golem15.feedback.submissions" {
|
||||
t.Fatalf("navigation %+v", nav)
|
||||
}
|
||||
if st := p.Settings(); len(st) != 1 || st[0].Code != SettingsCode || st[0].Form != "models/settings/fields.yaml" || st[0].Model != `Golem15\Feedback\Models\Settings` {
|
||||
t.Fatalf("settings %+v", st)
|
||||
}
|
||||
// The admin compiled every schema when the router was assembled; the
|
||||
// settings screen then creates the row with a widget key.
|
||||
plugins, err := party.Activate(backpack.New(app.Config), pgtest.PluginIDs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := cabana.Activate(app, plugins); err != nil {
|
||||
t.Fatalf("admin boot: %v", err)
|
||||
}
|
||||
if err := gdb.Exec(`DELETE FROM golem15_feedback_settings`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// cabana's settings save creates the row as id 1 from the form's
|
||||
// defaults, without a key.
|
||||
s := &models.Settings{ID: models.SettingsID}
|
||||
if err := gdb.Create(s).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(s.WidgetKey, "wk_") {
|
||||
t.Fatalf("a row created without a key got %q", s.WidgetKey)
|
||||
}
|
||||
s.WidgetKey = ""
|
||||
if err := gdb.Save(s).Error; err != nil || !strings.HasPrefix(s.WidgetKey, "wk_") {
|
||||
t.Fatalf("an emptied key was saved as %q (%v)", s.WidgetKey, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user