Files
sm-bm-plugin/plugin_test.go
Jakub Zych 71b848a376 Adopt SummerCMS v0.1.1 admin features
Datepickers on native time.Time / lagoon.Date fields, file uploads on
materials, readings and class sessions managed from the course form
(course_id made nullable).
2026-10-03 06:03:34 +02:00

200 lines
6.0 KiB
Go

package bm_test
import (
"os"
"path/filepath"
"slices"
"testing"
_ "git.golem15.com/golem15/sm-user-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/pact"
"git.golem15.com/golem15/summercms/modules/party"
"git.golem15.com/golem15/summercms/modules/surf"
bm "git.golem15.com/jakub/sm-bm-plugin"
)
const (
testUserSecret = "jz-bm-test-only-user-jwt-secret-32bytes"
testAdminSecret = "jz-bm-test-only-admin-jwt-secret-32bytes"
)
// testConfig is the application config the tests boot with: the HTTP
// limits, the golem15.user JWT secret and the admin secret.
func testConfig(t *testing.T) *compass.Config {
t.Helper()
dir := t.TempDir()
httpYAML := "body_limits:\n default_bytes: 1048576\n upload_bytes: 67108864\n"
if err := os.WriteFile(filepath.Join(dir, "http.yaml"), []byte(httpYAML), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Environ: []string{
"SUMMER_ENV=testing",
"SUMMER_GOLEM15__USER__JWT__SECRET=" + testUserSecret,
"SUMMER_ADMIN__JWT__SECRET=" + testAdminSecret,
},
})
if err != nil {
t.Fatal(err)
}
return cfg
}
// activate boots golem15.user and jz.bm the way the generated main does.
func activate(t *testing.T, app *backpack.App) []party.Plugin {
t.Helper()
plugins, err := party.Activate(app, []string{"golem15.user", "jz.bm"})
if err != nil {
t.Fatalf("Activate: %v", err)
}
return plugins
}
func bmPlugin(t *testing.T, plugins []party.Plugin) *bm.Plugin {
t.Helper()
for _, p := range plugins {
if b, ok := p.(*bm.Plugin); ok {
return b
}
}
t.Fatal("jz.bm was not activated")
return nil
}
func TestPluginActivates(t *testing.T) {
app := backpack.New(testConfig(t))
p := bmPlugin(t, activate(t, app))
if p.ID() != "jz.bm" || !slices.Equal(p.Requires(), []string{"golem15.user"}) {
t.Errorf("ID %q Requires %v", p.ID(), p.Requires())
}
if n := len(p.Models()); n != 10 {
t.Errorf("Models = %d, want 10", n)
}
if n := len(p.Migrations()); n != 12 {
t.Errorf("Migrations = %d, want 12", n)
}
if !slices.ContainsFunc(p.Commands(), func(c bonfire.Command) bool { return c.Name == "bm:seed" }) {
t.Error("bm:seed is not registered")
}
if got := app.Config.Int("jz.bm.materials_per_page"); got != 20 {
t.Errorf("jz.bm.materials_per_page = %d, want the default 20", got)
}
if got := app.Config.Int("jz.bm.dashboard_limit"); got != 5 {
t.Errorf("jz.bm.dashboard_limit = %d, want the default 5", got)
}
if got := app.Config.String("jz.bm.public_url"); got != "" {
t.Errorf("jz.bm.public_url = %q, want the default empty", got)
}
}
func TestRoutes(t *testing.T) {
app := backpack.New(testConfig(t))
plugins := activate(t, app)
router, err := surf.BuildRouter(app, plugins)
if err != nil {
t.Fatalf("BuildRouter: %v", err)
}
want := []string{
"GET /_bm/api/v1/dashboard",
"GET /_bm/api/v1/courses",
"GET /_bm/api/v1/courses/{id}",
"GET /_bm/api/v1/schedule",
"GET /_bm/api/v1/meetings",
"GET /_bm/api/v1/deadlines",
"GET /_bm/api/v1/readings",
"PUT /_bm/api/v1/readings/{id}/progress",
"DELETE /_bm/api/v1/readings/{id}/progress",
"GET /_bm/api/v1/materials",
"GET /_bm/api/v1/materials/{id}",
"GET /_bm/api/v1/calendar-feed",
"PUT /_bm/api/v1/calendar-feed",
"POST /_bm/api/v1/calendar-feed/rotate",
"DELETE /_bm/api/v1/calendar-feed",
}
var got []string
sawFeed := false
for _, rt := range router.Routes() {
if rt.PluginID != "jz.bm" {
continue
}
if rt.Method+" "+rt.Pattern == "GET /{path...}" {
// The student app, public by design; see static_test.go.
if len(rt.Middleware) != 0 {
t.Errorf("the student app has middleware %v", rt.Middleware)
}
continue
}
if rt.Method+" "+rt.Pattern == "GET /_bm/calendar/{token}" {
// The calendar feed: public (the token is the credential) but
// throttled per IP and per token.
want := []string{"throttle:jz.bm.calendar-ip", "throttle:jz.bm.calendar-token"}
if !slices.Equal(rt.Middleware, want) {
t.Errorf("the calendar feed has middleware %v, want %v", rt.Middleware, want)
}
sawFeed = true
continue
}
got = append(got, rt.Method+" "+rt.Pattern)
if !slices.Contains(rt.Middleware, "jwt.auth") {
t.Errorf("%s %s is not behind jwt.auth: %v", rt.Method, rt.Pattern, rt.Middleware)
}
}
if !sawFeed {
t.Error("GET /_bm/calendar/{token} is not registered")
}
if !slices.Equal(got, want) {
t.Errorf("routes =\n%v\nwant\n%v", got, want)
}
}
func TestAdminCompiles(t *testing.T) {
app := backpack.New(testConfig(t))
plugins := activate(t, app)
p := bmPlugin(t, plugins)
routes, err := cabana.Activate(app, plugins)
if err != nil {
t.Fatalf("cabana.Activate: %v", err)
}
if routes == nil {
t.Fatal("no admin routes")
}
declared := map[string]bool{}
for _, perm := range p.Permissions() {
declared[perm.Code] = true
}
for _, ctl := range p.AdminControllers() {
perms := ctl.(pact.AdminPermissioned).RequiredPermissions()
for _, code := range perms {
if !declared[code] {
t.Errorf("%s requires undeclared %s", ctl.ID(), code)
}
}
if rec := ctl.(pact.AdminRecordSource).NewRecord(); rec == nil {
t.Errorf("%s has no record model", ctl.ID())
}
form, err := cabana.CompileForm("jz.bm", ctl, p.AdminFS())
if err != nil {
t.Errorf("%s form: %v", ctl.ID(), err)
} else if len(form.Fields) == 0 {
t.Errorf("%s form has no fields", ctl.ID())
}
if _, err := cabana.CompileList("jz.bm", ctl, p.AdminFS()); err != nil {
t.Errorf("%s list: %v", ctl.ID(), err)
}
editor := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: map[string]bool{"jz.bm.*": true}}
if !cabana.Allows(editor, perms) {
t.Errorf("jz.bm.* does not open %s", ctl.ID())
}
}
nav := p.Navigation()
if len(nav) != 1 || len(nav[0].SideMenu) != 7 {
t.Errorf("navigation = %+v, want one group with 7 side items", nav)
}
}