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 != 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", "GET /api/v1/bm/me", "GET /api/v1/bm/courses", "GET /api/v1/bm/materials", "POST /api/v1/bm/materials", "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) } }