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.
221 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|