Files
sm-quizzes-plugin/harness_test.go

201 lines
5.3 KiB
Go

package quizzes_test
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "git.golem15.com/golem15/sm-quizzes-plugin"
_ "git.golem15.com/golem15/sm-user-plugin"
usermodels "git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/party"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"gorm.io/gorm"
)
const testSecret = "quizzes-test-only-hs256-secret-with-32-bytes"
// The Docker tests run against a throwaway testcontainers Postgres, never a
// developer or shared database. Under -short the container is not started
// and the tests skip.
var (
pgContainer *postgres.PostgresContainer
pgAdmin *sql.DB
pgDSN string
)
func TestMain(m *testing.M) {
if !isShort() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
err := startPostgres(ctx)
cancel()
if err != nil {
fmt.Fprintf(os.Stderr, "quizzes: testcontainers postgres: %v\n", err)
stopPostgres()
os.Exit(1)
}
}
code := m.Run()
stopPostgres()
os.Exit(code)
}
func isShort() bool {
for _, a := range os.Args {
if a == "-test.short" || a == "-test.short=true" {
return true
}
}
return false
}
func startPostgres(ctx context.Context) error {
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("quizzes"),
postgres.WithUsername("quizzes"),
postgres.WithPassword("quizzes"),
postgres.BasicWaitStrategies(),
)
if err != nil {
return err
}
pgContainer = ctr
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return err
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return err
}
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return err
}
pgAdmin, pgDSN = db, dsn
return nil
}
func stopPostgres() {
if pgAdmin != nil {
_ = pgAdmin.Close()
}
if pgContainer != nil {
_ = testcontainers.TerminateContainer(pgContainer)
}
}
// testDatabase creates a database for one test and drops it when the test
// ends.
func testDatabase(t *testing.T) *sql.DB {
t.Helper()
if testing.Short() {
t.Skip("requires testcontainers postgres")
}
if pgAdmin == nil {
t.Fatal("postgres unavailable: the container was not started")
}
name := "quizzes_" + strings.ToLower(strings.NewReplacer("/", "_", "-", "_", " ", "_").Replace(t.Name()))
if len(name) > 60 {
name = name[:60]
}
quoted := `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
if _, err := pgAdmin.ExecContext(t.Context(), `CREATE DATABASE `+quoted+` TEMPLATE template0 ENCODING 'UTF8'`); err != nil {
t.Fatalf("create %s: %v", name, err)
}
u, err := url.Parse(pgDSN)
if err != nil {
t.Fatal(err)
}
u.Path = "/" + name
db, err := sql.Open("pgx", u.String())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = db.Close()
_, _ = pgAdmin.ExecContext(context.Background(), `DROP DATABASE IF EXISTS `+quoted+` WITH (FORCE)`)
})
return db
}
// testConfig is an application config with the HTTP limits, the user JWT
// secret and the admin secret the activated plugins need.
func testConfig(t *testing.T) *compass.Config {
t.Helper()
dir := t.TempDir()
httpYAML := "body_limits:\n default_bytes: 1048576\n upload_bytes: 10485760\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, Env: "testing", Environ: []string{
"SUMMER_GOLEM15__USER__JWT__SECRET=" + testSecret,
}})
if err != nil {
t.Fatal(err)
}
if err := cfg.Set("admin.jwt.secret", "test-only-secret-with-at-least-32-bytes"); err != nil {
t.Fatal(err)
}
return cfg
}
// activate boots golem15.user and golem15.quizzes the way the generated
// main does. With gdb it publishes the database first, as tests and serve do.
func activate(t *testing.T, sqlDB *sql.DB, gdb *gorm.DB) (*backpack.App, []party.Plugin) {
t.Helper()
app := backpack.New(testConfig(t))
if gdb != nil {
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
t.Fatalf("lagoon.Publish: %v", err)
}
}
plugins, err := party.Activate(app, []string{"golem15.user", "golem15.quizzes"})
if err != nil {
t.Fatalf("Activate: %v", err)
}
return app, plugins
}
// migrated activates both plugins on a fresh, migrated database.
func migrated(t *testing.T) (*backpack.App, []party.Plugin, *gorm.DB) {
t.Helper()
sqlDB := testDatabase(t)
gdb, err := lagoon.Use(t.Context(), sqlDB)
if err != nil {
t.Fatalf("lagoon.Use: %v", err)
}
app, plugins := activate(t, sqlDB, gdb)
if err := lagoon.Migrate(gdb, plugins); err != nil {
t.Fatalf("lagoon.Migrate: %v", err)
}
return app, plugins, gdb
}
// newUser inserts an active user and returns it with a bearer token.
func newUser(t *testing.T, gdb *gorm.DB, email string) (*usermodels.User, string) {
t.Helper()
u := &usermodels.User{Email: email, Password: "x", IsActivated: true}
if err := gdb.Create(u).Error; err != nil {
t.Fatalf("create user %s: %v", email, err)
}
token, _, err := bouncer.Mint(testSecret, fmt.Sprint(u.ID), "http://127.0.0.1/_user/api/v1/login", time.Hour)
if err != nil {
t.Fatal(err)
}
return u, token
}