201 lines
5.3 KiB
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
|
|
}
|