Prefix replacement of the old application-nested module path in go.mod and every import. No identifier, string, table, migration ID, config key or comment changes.
575 lines
18 KiB
Go
575 lines
18 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-user-plugin/models"
|
|
"git.golem15.com/golem15/sm-user-plugin/updates"
|
|
"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/lagoon/attach"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"git.golem15.com/golem15/summercms/modules/postcard"
|
|
"git.golem15.com/golem15/summercms/modules/surf"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
sessionSecret = "summercms-test-only-hs256-secret"
|
|
invalidLoginBody = `{"error":true,"message":"Invalid email or password"}`
|
|
prvHash = "a867434cbc213adfbe78a02bed7082a6bd99c883"
|
|
)
|
|
|
|
var (
|
|
sessionOnce sync.Once
|
|
sessionGDB *gorm.DB
|
|
sessionSQL *sql.DB
|
|
sessionErr error
|
|
)
|
|
|
|
func TestLogin(t *testing.T) {
|
|
app, gdb := sessionApp(t, "0s")
|
|
hash, err := bouncer.HashPassword(10, "correct-horse")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
user := &models.User{Email: "ada-login@example.test", Password: hash}
|
|
if err := gdb.Create(user).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"ada-login@example.test","password":"correct-horse"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token, _ := body["token"].(string)
|
|
claims := parseToken(t, token)
|
|
if claims["sub"] != strconv.FormatUint(uint64(user.ID), 10) {
|
|
t.Fatalf("sub = %v", claims["sub"])
|
|
}
|
|
if claims["prv"] != prvHash {
|
|
t.Fatalf("prv = %v", claims["prv"])
|
|
}
|
|
if claims["iss"] != "http://example.com/_user/api/v1/login" {
|
|
t.Fatalf("iss = %v", claims["iss"])
|
|
}
|
|
userBody, _ := body["user"].(map[string]any)
|
|
if userBody["email"] != user.Email || userBody["feedback_widget_hidden"] != false {
|
|
t.Fatalf("user payload = %#v", userBody)
|
|
}
|
|
if userBody["permissions"] != nil {
|
|
t.Fatalf("permissions = %#v", userBody["permissions"])
|
|
}
|
|
|
|
wrong := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"ada-login@example.test","password":"nope"}`)
|
|
unknown := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"nobody@example.test","password":"nope"}`)
|
|
if wrong.Code != http.StatusUnauthorized || wrong.Body.String() != invalidLoginBody {
|
|
t.Fatalf("wrong = %d %s", wrong.Code, wrong.Body.String())
|
|
}
|
|
if unknown.Body.String() != wrong.Body.String() {
|
|
t.Fatalf("unknown body %s", unknown.Body.String())
|
|
}
|
|
|
|
var before int64
|
|
if err := gdb.Model(&models.Throttle{}).Count(&before).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"ghost@example.test","password":"nope"}`)
|
|
if again.Body.String() != invalidLoginBody {
|
|
t.Fatal(again.Body.String())
|
|
}
|
|
var after int64
|
|
if err := gdb.Model(&models.Throttle{}).Count(&after).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after != before {
|
|
t.Fatalf("unknown email created a throttle row (%d -> %d)", before, after)
|
|
}
|
|
|
|
now := time.Now()
|
|
ip := "192.0.2.1"
|
|
throttled := &models.User{Email: "throttled@example.test", Password: hash}
|
|
if err := gdb.Create(throttled).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
row := models.Throttle{UserID: throttled.ID, IPAddress: &ip, Attempts: 5, IsSuspended: true, SuspendedAt: &now}
|
|
if err := gdb.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
blocked := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"throttled@example.test","password":"correct-horse"}`)
|
|
if blocked.Code != http.StatusUnauthorized || blocked.Body.String() != invalidLoginBody {
|
|
t.Fatalf("throttled = %d %s", blocked.Code, blocked.Body.String())
|
|
}
|
|
|
|
loc := "en"
|
|
soft := &models.User{Email: "soft@example.test", Password: hash, PreferredLocale: &loc}
|
|
if err := gdb.Create(soft).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := gdb.Model(soft).Update("deleted_at", time.Now()).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mail := &captureMail{}
|
|
if err := app.Publish[postcard.Mailer](mail); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restored := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"soft@example.test","password":"correct-horse"}`)
|
|
if restored.Code != http.StatusOK {
|
|
t.Fatalf("restore %d %s", restored.Code, restored.Body.String())
|
|
}
|
|
var againUser models.User
|
|
if err := gdb.Unscoped().First(&againUser, soft.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if againUser.DeletedAt.Valid {
|
|
t.Fatal("soft-deleted user was not restored")
|
|
}
|
|
if len(mail.got) != 1 || mail.got[0] != "golem15.user::mail.reactivate-en" {
|
|
t.Fatalf("reactivate template = %#v", mail.got)
|
|
}
|
|
}
|
|
|
|
func TestLoginSixthAttempt(t *testing.T) {
|
|
app, gdb := sessionApp(t, "0s")
|
|
hash, err := bouncer.HashPassword(10, "correct-horse")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
user := &models.User{Email: "sixth@example.test", Password: hash}
|
|
if err := gdb.Create(user).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 1; i <= 5; i++ {
|
|
rec := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"sixth@example.test","password":"wrong"}`)
|
|
if rec.Code != http.StatusUnauthorized || rec.Body.String() != invalidLoginBody {
|
|
t.Fatalf("attempt %d: %d %s", i, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
var row models.Throttle
|
|
if err := gdb.Where("user_id = ?", user.ID).Take(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.Attempts != 5 || !row.IsSuspended {
|
|
t.Fatalf("after 5 logins: %+v", row)
|
|
}
|
|
blocked := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"sixth@example.test","password":"correct-horse"}`)
|
|
if blocked.Code != http.StatusUnauthorized || blocked.Body.String() != invalidLoginBody {
|
|
t.Fatalf("6th = %d %s", blocked.Code, blocked.Body.String())
|
|
}
|
|
if err := gdb.Where("user_id = ?", user.ID).Take(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.Attempts != 5 {
|
|
t.Fatalf("6th login recorded another attempt: %+v", row)
|
|
}
|
|
}
|
|
|
|
func TestLogout(t *testing.T) {
|
|
app, gdb := sessionApp(t, "0s")
|
|
token := loginToken(t, app, gdb, "logout@example.test")
|
|
rec := bearer(t, controllers.Logout(app), http.MethodPost, "/_user/api/v1/logout", token)
|
|
if rec.Code != http.StatusOK || rec.Body.String() != `{"message":"Logged out"}` {
|
|
t.Fatalf("logout %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
fetch := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", token)
|
|
if fetch.Code != http.StatusUnauthorized || fetch.Body.String() != `{"error":true,"message":"Unauthorized"}` {
|
|
t.Fatalf("fetch after logout %d %s", fetch.Code, fetch.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFetch(t *testing.T) {
|
|
app, gdb := sessionApp(t, "0s")
|
|
token := loginToken(t, app, gdb, "fetch@example.test")
|
|
ok := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", token)
|
|
if ok.Code != http.StatusOK || !strings.Contains(ok.Body.String(), `"email":"fetch@example.test"`) {
|
|
t.Fatalf("fetch %d %s", ok.Code, ok.Body.String())
|
|
}
|
|
missing := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", "")
|
|
if missing.Code != http.StatusUnauthorized || missing.Body.String() != `{"error":true,"message":"Unauthorized"}` {
|
|
t.Fatalf("missing %d %s", missing.Code, missing.Body.String())
|
|
}
|
|
bad := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", "not-a-token")
|
|
if bad.Body.String() != missing.Body.String() {
|
|
t.Fatalf("invalid %s", bad.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestRefresh(t *testing.T) {
|
|
app, gdb := sessionApp(t, "0s")
|
|
user := insertUser(t, gdb, "refresh@example.test", "correct-horse")
|
|
sub := strconv.FormatUint(uint64(user.ID), 10)
|
|
|
|
missing := bearer(t, controllers.Refresh(app), http.MethodPost, "/_user/api/v1/refresh", "")
|
|
if missing.Code != http.StatusUnauthorized || missing.Body.String() != `{"error":"Token not found"}` {
|
|
t.Fatalf("missing %d %s", missing.Code, missing.Body.String())
|
|
}
|
|
|
|
iat := time.Now().Add(-2 * time.Hour)
|
|
old := craftToken(t, sub, "refresh-old", iat, iat.Add(time.Minute))
|
|
rec := bearer(t, controllers.Refresh(app), http.MethodPost, "/_user/api/v1/refresh", old)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("refresh %d %s", rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
next, _ := body["token"].(string)
|
|
if next == "" || next == old {
|
|
t.Fatalf("new token = %q", next)
|
|
}
|
|
if parseToken(t, next)["jti"] == "refresh-old" {
|
|
t.Fatal("refresh reused jti")
|
|
}
|
|
bl, ok := app.Lookup[bouncer.BlacklistStore]()
|
|
if !ok || bl == nil {
|
|
t.Fatal("blacklist was not published")
|
|
}
|
|
blocked, err := bl.IsBlacklisted(t.Context(), "refresh-old")
|
|
if err != nil || !blocked {
|
|
t.Fatalf("old jti blacklisted=%v err=%v", blocked, err)
|
|
}
|
|
fetch := bearer(t, controllers.Fetch(app), http.MethodGet, "/_user/api/v1/fetch", old)
|
|
if fetch.Code != http.StatusUnauthorized {
|
|
t.Fatalf("old token still fetches: %d %s", fetch.Code, fetch.Body.String())
|
|
}
|
|
|
|
stale := craftToken(t, sub, "refresh-stale", time.Now().Add(-40*24*time.Hour), time.Now().Add(-39*24*time.Hour))
|
|
denied := bearer(t, controllers.Refresh(app), http.MethodPost, "/_user/api/v1/refresh", stale)
|
|
if denied.Code != http.StatusUnauthorized || !strings.Contains(denied.Body.String(), `"error":"Could not refresh token"`) || !strings.Contains(denied.Body.String(), `"msg":`) {
|
|
t.Fatalf("stale %d %s", denied.Code, denied.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLoginRouteGroup(t *testing.T) {
|
|
cfg := sessionConfig(t)
|
|
if err := cfg.Set("http.body_limits.default_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := cfg.Set("http.body_limits.upload_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
app := backpack.New(cfg)
|
|
p := &Plugin{}
|
|
if err := p.Boot(app); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rt, err := surf.BuildRouter(app, []party.Plugin{p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]bool{
|
|
"POST /_user/api/v1/login": false,
|
|
"POST /_user/api/v1/logout": false,
|
|
"GET /_user/api/v1/fetch": false,
|
|
"POST /_user/api/v1/refresh": false,
|
|
"POST /_user/api/v1/register": false,
|
|
"GET /_user/api/v1/oauth-providers": false,
|
|
}
|
|
for _, info := range rt.Routes() {
|
|
key := info.Method + " " + info.Pattern
|
|
if _, ok := want[key]; !ok {
|
|
continue
|
|
}
|
|
want[key] = true
|
|
if len(info.Middleware) != 1 || info.Middleware[0] != "throttle:user-api" {
|
|
t.Fatalf("%s middleware = %#v", key, info.Middleware)
|
|
}
|
|
}
|
|
for key, seen := range want {
|
|
if !seen {
|
|
t.Fatalf("missing route %s", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLoginBlacklistSweep(t *testing.T) {
|
|
gdb, sqlDB := sessionDB(t)
|
|
app := newSessionApp(t, gdb, sqlDB)
|
|
_, stop := bootPlugin(t, app, "50ms")
|
|
past := time.Now().Add(-time.Minute)
|
|
if err := gdb.Exec(`INSERT INTO jwt_blacklist (jti, expires_at, valid_until) VALUES (?, ?, ?)`, "sweep-me", past, past).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
var n int64
|
|
if err := gdb.Raw(`SELECT COUNT(*) FROM jwt_blacklist WHERE jti = ?`, "sweep-me").Scan(&n).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n == 0 {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("expired jwt_blacklist row was not swept")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
stop()
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
quiet := newSessionApp(t, gdb, sqlDB)
|
|
bootPlugin(t, quiet, "0s")
|
|
later := time.Now().Add(-time.Minute)
|
|
if err := gdb.Exec(`INSERT INTO jwt_blacklist (jti, expires_at, valid_until) VALUES (?, ?, ?)`, "sweep-keep", later, later).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
var n int64
|
|
if err := gdb.Raw(`SELECT COUNT(*) FROM jwt_blacklist WHERE jti = ?`, "sweep-keep").Scan(&n).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatal("non-positive sweep interval removed a row")
|
|
}
|
|
}
|
|
|
|
type captureMail struct {
|
|
got []string
|
|
}
|
|
|
|
func (c *captureMail) Send(_ context.Context, msg postcard.Message) error {
|
|
c.got = append(c.got, msg.Template)
|
|
return nil
|
|
}
|
|
|
|
func loginToken(t *testing.T, app *backpack.App, gdb *gorm.DB, email string) string {
|
|
t.Helper()
|
|
insertUser(t, gdb, email, "correct-horse")
|
|
rec := postJSON(t, controllers.Login(app), "/_user/api/v1/login", `{"email":"`+email+`","password":"correct-horse"}`)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("login %s: %d %s", email, rec.Code, rec.Body.String())
|
|
}
|
|
var body map[string]any
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
token, _ := body["token"].(string)
|
|
if token == "" {
|
|
t.Fatal("empty token")
|
|
}
|
|
return token
|
|
}
|
|
|
|
func insertUser(t *testing.T, gdb *gorm.DB, email, password string) *models.User {
|
|
t.Helper()
|
|
hash, err := bouncer.HashPassword(10, password)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
user := &models.User{Email: email, Password: hash}
|
|
if err := gdb.Create(user).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func postJSON(t *testing.T, h http.HandlerFunc, path, body string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body))
|
|
req.Host = "example.com"
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func bearer(t *testing.T, h http.HandlerFunc, method, path, token string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(method, path, nil)
|
|
req.Host = "example.com"
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h(rec, req)
|
|
return rec
|
|
}
|
|
|
|
func parseToken(t *testing.T, raw string) jwt.MapClaims {
|
|
t.Helper()
|
|
claims := jwt.MapClaims{}
|
|
if _, err := jwt.ParseWithClaims(raw, claims, func(*jwt.Token) (any, error) {
|
|
return []byte(sessionSecret), nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return claims
|
|
}
|
|
|
|
func craftToken(t *testing.T, sub, jti string, iat, exp time.Time) string {
|
|
t.Helper()
|
|
claims := jwt.MapClaims{
|
|
"sub": sub,
|
|
"iss": "http://example.com/_user/api/v1/login",
|
|
"iat": iat.Unix(),
|
|
"nbf": iat.Unix(),
|
|
"exp": exp.Unix(),
|
|
"jti": jti,
|
|
"prv": prvHash,
|
|
}
|
|
signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(sessionSecret))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return signed
|
|
}
|
|
|
|
func sessionApp(t *testing.T, sweep string) (*backpack.App, *gorm.DB) {
|
|
t.Helper()
|
|
gdb, sqlDB := sessionDB(t)
|
|
app := backpack.New(sessionConfig(t))
|
|
if err := app.Config.Set("http.body_limits.default_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app.Config.Set("http.body_limits.upload_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sweep != "" {
|
|
if err := app.Config.Set("golem15.user.jwt.blacklist_sweep_interval", sweep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bootPlugin(t, app, sweep)
|
|
return app, gdb
|
|
}
|
|
|
|
func newSessionApp(t *testing.T, gdb *gorm.DB, sqlDB *sql.DB) *backpack.App {
|
|
t.Helper()
|
|
app := backpack.New(sessionConfig(t))
|
|
if err := app.Config.Set("http.body_limits.default_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := app.Config.Set("http.body_limits.upload_bytes", 1048576); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := lagoon.Publish(app, sqlDB, gdb); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return app
|
|
}
|
|
|
|
func bootPlugin(t *testing.T, app *backpack.App, sweep string) (*Plugin, func()) {
|
|
t.Helper()
|
|
if sweep != "" {
|
|
if err := app.Config.Set("golem15.user.jwt.blacklist_sweep_interval", sweep); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
p := &Plugin{}
|
|
if err := p.Boot(app); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stop := func() {}
|
|
if p.sweepStop != nil {
|
|
ch := p.sweepStop
|
|
var once sync.Once
|
|
stop = func() { once.Do(func() { close(ch) }) }
|
|
t.Cleanup(stop)
|
|
}
|
|
return p, stop
|
|
}
|
|
|
|
func sessionConfig(t *testing.T) *compass.Config {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: user-session\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg, err := compass.Open(compass.Options{
|
|
Dir: dir,
|
|
Environ: []string{
|
|
"SUMMER_ENV=development",
|
|
"SUMMER_GOLEM15__USER__JWT__SECRET=" + sessionSecret,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func sessionDB(t *testing.T) (*gorm.DB, *sql.DB) {
|
|
t.Helper()
|
|
sessionOnce.Do(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
ctr, err := postgres.Run(ctx, "postgres:16-alpine",
|
|
postgres.WithDatabase("user_session"),
|
|
postgres.WithUsername("user_session"),
|
|
postgres.WithPassword("user_session"),
|
|
postgres.BasicWaitStrategies(),
|
|
testcontainers.WithEnv(map[string]string{
|
|
"POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8",
|
|
}),
|
|
)
|
|
if err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
|
|
if err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
sqlDB, err := sql.Open("pgx", dsn)
|
|
if err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
gdb, err := lagoon.Use(ctx, sqlDB)
|
|
if err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
m := gormigrate.New(gdb, &gormigrate.Options{
|
|
TableName: "summer_migrations_golem15_user",
|
|
UseTransaction: true,
|
|
}, updates.All())
|
|
if err := m.Migrate(); err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
files := gormigrate.New(gdb, &gormigrate.Options{
|
|
TableName: "summer_migrations_system_files",
|
|
UseTransaction: true,
|
|
}, attach.Migrations)
|
|
if err := files.Migrate(); err != nil {
|
|
sessionErr = err
|
|
return
|
|
}
|
|
sessionGDB = gdb
|
|
sessionSQL = sqlDB
|
|
})
|
|
if sessionErr != nil {
|
|
t.Fatal(sessionErr)
|
|
}
|
|
return sessionGDB, sessionSQL
|
|
}
|