Files
sm-bm-plugin/plugin_test.go
Jakub Zych ebd9117f8c feat: lecturer on class sessions and in the session course ref
The schedule showed a lecturer only where the session notes named one
(Globalizacja), so the student plan had nobody under the other courses.
The session JSON now carries the course lecturer in its course ref and an
optional per-session lecturer (new nullable column, admin field, seed key)
for courses shared between two people. The student app shows the
session's own lecturer or, failing that, the course's.
2026-10-17 11:20:11 +02:00

221 lines
7.0 KiB
Go

package bm_test
import (
"os"
"path/filepath"
"slices"
"strings"
"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 != 13 {
t.Errorf("Migrations = %d, want 13", 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",
"GET /api/v1/bm/me",
"GET /api/v1/bm/courses",
"GET /api/v1/bm/materials",
"GET /api/v1/bm/materials/{id}",
"POST /api/v1/bm/materials",
"PUT /api/v1/bm/materials/{id}",
"DELETE /api/v1/bm/materials/{id}",
"POST /api/v1/bm/seed",
}
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 strings.HasPrefix(rt.Pattern, "/api/v1/bm/") {
if !slices.Contains(rt.Middleware, "user.api_token") || !slices.ContainsFunc(rt.Middleware, func(name string) bool { return strings.HasPrefix(name, "user.scope:") }) {
t.Errorf("%s %s is not behind the shared token and a scope: %v", rt.Method, rt.Pattern, rt.Middleware)
}
if rt.Pattern != "/api/v1/bm/me" && !slices.ContainsFunc(rt.Middleware, func(name string) bool { return strings.HasPrefix(name, "user.permission:") }) {
t.Errorf("%s %s has no user permission check: %v", rt.Method, rt.Pattern, rt.Middleware)
}
} else 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)
}
for _, rt := range router.Routes() {
if rt.Pattern == "/prezentacje/{slugHash}" {
t.Errorf("GET /prezentacje/{slugHash} is registered without golem15.presentations")
}
}
}
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)
}
}