chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:02 +08:00
commit 171e2a4a0d
1315 changed files with 266099 additions and 0 deletions
@@ -0,0 +1,87 @@
package forms
import (
"regexp"
"strings"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/golang-jwt/jwt/v5"
"github.com/pocketbase/pocketbase/core"
)
var privateKeyRegex = regexp.MustCompile(`(?m)-----BEGIN PRIVATE KEY----[\s\S]+-----END PRIVATE KEY-----`)
// AppleClientSecretCreate is a form struct to generate a new Apple Client Secret.
//
// Reference: https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens
type AppleClientSecretCreate struct {
app core.App
// ClientId is the identifier of your app (aka. Service ID).
ClientId string `form:"clientId" json:"clientId"`
// TeamId is a 10-character string associated with your developer account
// (usually could be found next to your name in the Apple Developer site).
TeamId string `form:"teamId" json:"teamId"`
// KeyId is a 10-character key identifier generated for the "Sign in with Apple"
// private key associated with your developer account.
KeyId string `form:"keyId" json:"keyId"`
// PrivateKey is the private key associated to your app.
// Usually wrapped within -----BEGIN PRIVATE KEY----- X -----END PRIVATE KEY-----.
PrivateKey string `form:"privateKey" json:"privateKey"`
// Duration specifies how long the generated JWT should be considered valid.
// The specified value must be in seconds and max 15777000 (~6months).
Duration int `form:"duration" json:"duration"`
}
// NewAppleClientSecretCreate creates a new [AppleClientSecretCreate] form with initializer
// config created from the provided [core.App] instances.
func NewAppleClientSecretCreate(app core.App) *AppleClientSecretCreate {
form := &AppleClientSecretCreate{
app: app,
}
return form
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *AppleClientSecretCreate) Validate() error {
return validation.ValidateStruct(form,
validation.Field(&form.ClientId, validation.Required),
validation.Field(&form.TeamId, validation.Required, validation.Length(10, 10)),
validation.Field(&form.KeyId, validation.Required, validation.Length(10, 10)),
validation.Field(&form.PrivateKey, validation.Required, validation.Match(privateKeyRegex)),
validation.Field(&form.Duration, validation.Required, validation.Min(1), validation.Max(15777000)),
)
}
// Submit validates the form and returns a new Apple Client Secret JWT.
func (form *AppleClientSecretCreate) Submit() (string, error) {
if err := form.Validate(); err != nil {
return "", err
}
signKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(strings.TrimSpace(form.PrivateKey)))
if err != nil {
return "", err
}
now := time.Now()
claims := &jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"https://appleid.apple.com"},
Subject: form.ClientId,
Issuer: form.TeamId,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(time.Duration(form.Duration) * time.Second)),
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = form.KeyId
return token.SignedString(signKey)
}
@@ -0,0 +1,119 @@
package forms_test
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tests"
)
func TestAppleClientSecretCreateValidateAndSubmit(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
encodedKey, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatal(err)
}
privatePem := pem.EncodeToMemory(
&pem.Block{
Type: "PRIVATE KEY",
Bytes: encodedKey,
},
)
scenarios := []struct {
name string
formData map[string]any
expectError bool
}{
{
"empty data",
map[string]any{},
true,
},
{
"invalid data",
map[string]any{
"clientId": "",
"teamId": "123456789",
"keyId": "123456789",
"privateKey": "-----BEGIN PRIVATE KEY----- invalid -----END PRIVATE KEY-----",
"duration": -1,
},
true,
},
{
"valid data",
map[string]any{
"clientId": "123",
"teamId": "1234567890",
"keyId": "1234567891",
"privateKey": string(privatePem),
"duration": 1,
},
false,
},
}
for _, s := range scenarios {
form := forms.NewAppleClientSecretCreate(app)
rawData, marshalErr := json.Marshal(s.formData)
if marshalErr != nil {
t.Errorf("[%s] Failed to marshalize the scenario data: %v", s.name, marshalErr)
continue
}
// load data
loadErr := json.Unmarshal(rawData, form)
if loadErr != nil {
t.Errorf("[%s] Failed to load form data: %v", s.name, loadErr)
continue
}
secret, err := form.Submit()
hasErr := err != nil
if hasErr != s.expectError {
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, err)
}
if hasErr {
continue
}
if secret == "" {
t.Errorf("[%s] Expected non-empty secret", s.name)
}
claims := jwt.MapClaims{}
token, _, err := jwt.NewParser().ParseUnverified(secret, claims)
if err != nil {
t.Errorf("[%s] Failed to parse token: %v", s.name, err)
}
if alg := token.Header["alg"]; alg != "ES256" {
t.Errorf("[%s] Expected %q alg header, got %q", s.name, "ES256", alg)
}
if kid := token.Header["kid"]; kid != form.KeyId {
t.Errorf("[%s] Expected %q kid header, got %q", s.name, form.KeyId, kid)
}
}
}
@@ -0,0 +1,316 @@
package forms
import (
"context"
"errors"
"fmt"
"slices"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/core/validators"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/spf13/cast"
)
const (
accessLevelDefault = iota
accessLevelManager
accessLevelSuperuser
)
type RecordUpsert struct {
ctx context.Context
app core.App
record *core.Record
accessLevel int
// extra password fields
disablePasswordValidations bool
password string
passwordConfirm string
oldPassword string
}
// NewRecordUpsert creates a new [RecordUpsert] form from the provided [core.App] and [core.Record] instances
// (for create you could pass a pointer to an empty Record - core.NewRecord(collection)).
func NewRecordUpsert(app core.App, record *core.Record) *RecordUpsert {
form := &RecordUpsert{
ctx: context.Background(),
app: app,
record: record,
}
return form
}
// SetContext assigns ctx as context of the current form.
func (form *RecordUpsert) SetContext(ctx context.Context) {
form.ctx = ctx
}
// SetApp replaces the current form app instance.
//
// This could be used for example if you want to change at later stage
// before submission to change from regular -> transactional app instance.
func (form *RecordUpsert) SetApp(app core.App) {
form.app = app
}
// SetRecord replaces the current form record instance.
func (form *RecordUpsert) SetRecord(record *core.Record) {
form.record = record
}
// ResetAccess resets the form access level to the accessLevelDefault.
func (form *RecordUpsert) ResetAccess() {
form.accessLevel = accessLevelDefault
}
// GrantManagerAccess updates the form access level to "manager" allowing
// directly changing some system record fields (often used with auth collection records).
func (form *RecordUpsert) GrantManagerAccess() {
form.accessLevel = accessLevelManager
}
// GrantSuperuserAccess updates the form access level to "superuser" allowing
// directly changing all system record fields, including those marked as "Hidden".
func (form *RecordUpsert) GrantSuperuserAccess() {
form.accessLevel = accessLevelSuperuser
}
// HasManageAccess reports whether the form has "manager" or "superuser" level access.
func (form *RecordUpsert) HasManageAccess() bool {
return form.accessLevel == accessLevelManager || form.accessLevel == accessLevelSuperuser
}
// Load loads the provided data into the form and the related record.
func (form *RecordUpsert) Load(data map[string]any) {
excludeFields := []string{core.FieldNameExpand}
isAuth := form.record.Collection().IsAuth()
// load the special auth form fields
if isAuth {
if v, ok := data["password"]; ok {
form.password = cast.ToString(v)
}
if v, ok := data["passwordConfirm"]; ok {
form.passwordConfirm = cast.ToString(v)
}
if v, ok := data["oldPassword"]; ok {
form.oldPassword = cast.ToString(v)
}
excludeFields = append(excludeFields, "passwordConfirm", "oldPassword") // skip non-schema password fields
}
for k, v := range data {
if slices.Contains(excludeFields, k) {
continue
}
// set only known collection fields
field := form.record.SetIfFieldExists(k, v)
// restore original value if hidden field (with exception of the auth "password")
//
// note: this is an extra measure against loading hidden fields
// but usually is not used by the default route handlers since
// they filter additionally the data before calling Load
if form.accessLevel != accessLevelSuperuser && field != nil && field.GetHidden() && (!isAuth || field.GetName() != core.FieldNamePassword) {
form.record.SetRaw(field.GetName(), form.record.Original().GetRaw(field.GetName()))
}
}
}
func (form *RecordUpsert) validateFormFields() error {
isAuth := form.record.Collection().IsAuth()
if !isAuth {
return nil
}
form.syncPasswordFields()
isNew := form.record.IsNew()
original := form.record.Original()
validateData := map[string]any{
"email": form.record.Email(),
"verified": form.record.Verified(),
"password": form.password,
"passwordConfirm": form.passwordConfirm,
"oldPassword": form.oldPassword,
}
return validation.Validate(validateData,
validation.Map(
validation.Key(
"email",
// don't allow direct email updates if the form doesn't have manage access permissions
// (aka. allow only admin or authorized auth models to directly update the field)
validation.When(
!isNew && !form.HasManageAccess(),
validation.By(validators.Equal(original.Email())),
),
),
validation.Key(
"verified",
// don't allow changing verified if the form doesn't have manage access permissions
// (aka. allow only admin or authorized auth models to directly change the field)
validation.When(
!form.HasManageAccess(),
validation.By(validators.Equal(original.Verified())),
),
),
validation.Key(
"password",
validation.When(
!form.disablePasswordValidations && (isNew || form.passwordConfirm != "" || form.oldPassword != ""),
validation.Required,
),
),
validation.Key(
"passwordConfirm",
validation.When(
!form.disablePasswordValidations && (isNew || form.password != "" || form.oldPassword != ""),
validation.Required,
),
validation.When(!form.disablePasswordValidations, validation.By(validators.Equal(form.password))),
),
validation.Key(
"oldPassword",
// require old password only on update when:
// - form.HasManageAccess() is not satisfied
// - changing the existing password
validation.When(
!form.disablePasswordValidations && !isNew && !form.HasManageAccess() && (form.password != "" || form.passwordConfirm != ""),
validation.Required,
validation.By(form.checkOldPassword),
),
),
),
)
}
func (form *RecordUpsert) checkOldPassword(value any) error {
v, ok := value.(string)
if !ok {
return validators.ErrUnsupportedValueType
}
if !form.record.Original().ValidatePassword(v) {
return validation.NewError("validation_invalid_old_password", "Missing or invalid old password.")
}
return nil
}
// Deprecated: It was previously used as part of the record create action but it is not needed anymore and will be removed in the future.
//
// DrySubmit performs a temp form submit within a transaction and reverts it at the end.
// For actual record persistence, check the [RecordUpsert.Submit()] method.
//
// This method doesn't perform validations, handle file uploads/deletes or trigger app save events!
func (form *RecordUpsert) DrySubmit(callback func(txApp core.App, drySavedRecord *core.Record) error) error {
isNew := form.record.IsNew()
clone := form.record.Clone()
// set an id if it doesn't have already
// (the value doesn't matter; it is used only during the manual delete/update rollback)
if clone.IsNew() && clone.Id == "" {
clone.Id = "_temp_" + security.PseudorandomString(15)
}
app := form.app.UnsafeWithoutHooks()
isTransactional := app.IsTransactional()
if !isTransactional {
return app.RunInTransaction(func(txApp core.App) error {
tx, ok := txApp.DB().(*dbx.Tx)
if !ok {
return errors.New("failed to get transaction db")
}
defer tx.Rollback()
if err := txApp.SaveNoValidateWithContext(form.ctx, clone); err != nil {
return validators.NormalizeUniqueIndexError(err, clone.Collection().Name, clone.Collection().Fields.FieldNames())
}
if callback != nil {
return callback(txApp, clone)
}
return nil
})
}
// already in a transaction
// (manual rollback to avoid starting another transaction)
// ---------------------------------------------------------------
err := app.SaveNoValidateWithContext(form.ctx, clone)
if err != nil {
return validators.NormalizeUniqueIndexError(err, clone.Collection().Name, clone.Collection().Fields.FieldNames())
}
manualRollback := func() error {
if isNew {
err = app.DeleteWithContext(form.ctx, clone)
if err != nil {
return fmt.Errorf("failed to rollback dry submit created record: %w", err)
}
} else {
clone.Load(clone.Original().FieldsData())
err = app.SaveNoValidateWithContext(form.ctx, clone)
if err != nil {
return fmt.Errorf("failed to rollback dry submit updated record: %w", err)
}
}
return nil
}
if callback != nil {
return errors.Join(callback(app, clone), manualRollback())
}
return manualRollback()
}
// Submit validates the form specific validations and attempts to save the form record.
func (form *RecordUpsert) Submit() error {
err := form.validateFormFields()
if err != nil {
return err
}
// run record validations and persist in db
return form.app.SaveWithContext(form.ctx, form.record)
}
// syncPasswordFields syncs the form's auth password fields with their
// corresponding record field values.
//
// This could be useful in case the password fields were programmatically set
// directly by modifying the related record model.
func (form *RecordUpsert) syncPasswordFields() {
if !form.record.Collection().IsAuth() {
return // not an auth collection
}
form.disablePasswordValidations = false
rawPassword := form.record.GetRaw(core.FieldNamePassword)
if v, ok := rawPassword.(*core.PasswordFieldValue); ok && v != nil {
if
// programmatically set custom plain password value
(v.Plain != "" && v.Plain != form.password) ||
// random generated password for new record
(v.Plain == "" && v.Hash != "" && form.record.IsNew()) {
form.disablePasswordValidations = true
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
package forms
import (
"errors"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/mails"
"github.com/pocketbase/pocketbase/tools/types"
)
const (
TestTemplateVerification = "verification"
TestTemplatePasswordReset = "password-reset"
TestTemplateEmailChange = "email-change"
TestTemplateOTP = "otp"
TestTemplateAuthAlert = "login-alert"
)
// TestEmailSend is a email template test request form.
type TestEmailSend struct {
app core.App
Email string `form:"email" json:"email"`
Template string `form:"template" json:"template"`
Collection string `form:"collection" json:"collection"` // optional, fallbacks to _superusers
}
// NewTestEmailSend creates and initializes new TestEmailSend form.
func NewTestEmailSend(app core.App) *TestEmailSend {
return &TestEmailSend{app: app}
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *TestEmailSend) Validate() error {
return validation.ValidateStruct(form,
validation.Field(
&form.Collection,
validation.Length(1, 255),
validation.By(form.checkAuthCollection),
),
validation.Field(
&form.Email,
validation.Required,
validation.Length(1, 255),
is.EmailFormat,
),
validation.Field(
&form.Template,
validation.Required,
validation.In(
TestTemplateVerification,
TestTemplatePasswordReset,
TestTemplateEmailChange,
TestTemplateOTP,
TestTemplateAuthAlert,
),
),
)
}
func (form *TestEmailSend) checkAuthCollection(value any) error {
v, _ := value.(string)
if v == "" {
return nil // nothing to check
}
c, _ := form.app.FindCollectionByNameOrId(v)
if c == nil || !c.IsAuth() {
return validation.NewError("validation_invalid_auth_collection", "Must be a valid auth collection id or name.")
}
return nil
}
// Submit validates and sends a test email to the form.Email address.
func (form *TestEmailSend) Submit() error {
if err := form.Validate(); err != nil {
return err
}
collectionIdOrName := form.Collection
if collectionIdOrName == "" {
collectionIdOrName = core.CollectionNameSuperusers
}
collection, err := form.app.FindCollectionByNameOrId(collectionIdOrName)
if err != nil {
return err
}
record := core.NewRecord(collection)
for _, field := range collection.Fields {
if field.GetHidden() {
continue
}
record.Set(field.GetName(), "__pb_test_"+field.GetName()+"__")
}
record.RefreshTokenKey()
record.SetEmail(form.Email)
switch form.Template {
case TestTemplateVerification:
return mails.SendRecordVerification(form.app, record)
case TestTemplatePasswordReset:
return mails.SendRecordPasswordReset(form.app, record)
case TestTemplateEmailChange:
return mails.SendRecordChangeEmail(form.app, record, form.Email)
case TestTemplateOTP:
return mails.SendRecordOTP(form.app, record, "_PB_TEST_OTP_ID_", "123456")
case TestTemplateAuthAlert:
testEvent := types.NowDateTime().String() + " - TEST_IP TEST_USER_AGENT"
return mails.SendRecordAuthAlert(form.app, record, testEvent)
default:
return errors.New("unknown template " + form.Template)
}
}
@@ -0,0 +1,96 @@
package forms_test
import (
"fmt"
"strings"
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tests"
)
func TestEmailSendValidateAndSubmit(t *testing.T) {
t.Parallel()
scenarios := []struct {
template string
email string
collection string
expectedErrors []string
}{
{"", "", "", []string{"template", "email"}},
{"invalid", "test@example.com", "", []string{"template"}},
{forms.TestTemplateVerification, "invalid", "", []string{"email"}},
{forms.TestTemplateVerification, "test@example.com", "invalid", []string{"collection"}},
{forms.TestTemplateVerification, "test@example.com", "demo1", []string{"collection"}},
{forms.TestTemplateVerification, "test@example.com", "users", nil},
{forms.TestTemplatePasswordReset, "test@example.com", "", nil},
{forms.TestTemplateEmailChange, "test@example.com", "", nil},
{forms.TestTemplateOTP, "test@example.com", "", nil},
{forms.TestTemplateAuthAlert, "test@example.com", "", nil},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("%d_%s", i, s.template), func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
form := forms.NewTestEmailSend(app)
form.Email = s.email
form.Template = s.template
form.Collection = s.collection
result := form.Submit()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Fatalf("Failed to parse errors %v", result)
}
// check errors
if len(errs) > len(s.expectedErrors) {
t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs)
}
for _, k := range s.expectedErrors {
if _, ok := errs[k]; !ok {
t.Fatalf("Missing expected error key %q in %v", k, errs)
}
}
expectedEmails := 1
if len(s.expectedErrors) > 0 {
expectedEmails = 0
}
if app.TestMailer.TotalSend() != expectedEmails {
t.Fatalf("Expected %d email(s) to be sent, got %d", expectedEmails, app.TestMailer.TotalSend())
}
if len(s.expectedErrors) > 0 {
return
}
var expectedContent string
switch s.template {
case forms.TestTemplatePasswordReset:
expectedContent = "Reset password"
case forms.TestTemplateEmailChange:
expectedContent = "Confirm new email"
case forms.TestTemplateVerification:
expectedContent = "Verify"
case forms.TestTemplateOTP:
expectedContent = "one-time password"
case forms.TestTemplateAuthAlert:
expectedContent = "from a new location"
default:
expectedContent = "__UNKNOWN_TEMPLATE__"
}
if !strings.Contains(app.TestMailer.LastMessage().HTML, expectedContent) {
t.Errorf("Expected the email to contains %q, got\n%v", expectedContent, app.TestMailer.LastMessage().HTML)
}
})
}
}
@@ -0,0 +1,120 @@
package forms
import (
"errors"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/filesystem"
"github.com/pocketbase/pocketbase/tools/security"
)
const (
s3FilesystemStorage = "storage"
s3FilesystemBackups = "backups"
)
// TestS3Filesystem defines a S3 filesystem connection test.
type TestS3Filesystem struct {
app core.App
// The name of the filesystem - storage or backups
Filesystem string `form:"filesystem" json:"filesystem"`
// Optional S3 config fields to test unsaved settings
// If provided, these override the saved settings for testing
Bucket string `form:"bucket" json:"bucket"`
Region string `form:"region" json:"region"`
Endpoint string `form:"endpoint" json:"endpoint"`
AccessKey string `form:"accessKey" json:"accessKey"`
Secret string `form:"secret" json:"secret"`
ForcePathStyle bool `form:"forcePathStyle" json:"forcePathStyle"`
}
// NewTestS3Filesystem creates and initializes new TestS3Filesystem form.
func NewTestS3Filesystem(app core.App) *TestS3Filesystem {
return &TestS3Filesystem{app: app}
}
// Validate makes the form validatable by implementing [validation.Validatable] interface.
func (form *TestS3Filesystem) Validate() error {
return validation.ValidateStruct(form,
validation.Field(
&form.Filesystem,
validation.Required,
validation.In(s3FilesystemStorage, s3FilesystemBackups),
),
)
}
// Submit validates and performs a S3 filesystem connection test.
func (form *TestS3Filesystem) Submit() error {
if err := form.Validate(); err != nil {
return err
}
var s3Config core.S3Config
// Check if form fields are provided (for testing unsaved settings)
if form.Endpoint != "" {
// Use the form fields directly
s3Config = core.S3Config{
Enabled: true,
Bucket: form.Bucket,
Region: form.Region,
Endpoint: form.Endpoint,
AccessKey: form.AccessKey,
Secret: form.Secret,
ForcePathStyle: form.ForcePathStyle,
}
// If secret is not provided, use the saved secret from settings
if s3Config.Secret == "" {
if form.Filesystem == s3FilesystemBackups {
s3Config.Secret = form.app.Settings().Backups.S3.Secret
} else {
s3Config.Secret = form.app.Settings().S3.Secret
}
}
} else {
// Fall back to saved settings
if form.Filesystem == s3FilesystemBackups {
s3Config = form.app.Settings().Backups.S3
} else {
s3Config = form.app.Settings().S3
}
}
if !s3Config.Enabled {
return errors.New("S3 storage filesystem is not enabled")
}
fsys, err := filesystem.NewS3(
s3Config.Bucket,
s3Config.Region,
s3Config.Endpoint,
s3Config.AccessKey,
s3Config.Secret,
s3Config.ForcePathStyle,
)
if err != nil {
return fmt.Errorf("failed to initialize the S3 filesystem: %w", err)
}
defer fsys.Close()
testPrefix := "pb_settings_test_" + security.PseudorandomString(5)
testFileKey := testPrefix + "/test.txt"
// try to upload a test file
if err := fsys.Upload([]byte("test"), testFileKey); err != nil {
return fmt.Errorf("failed to upload a test file: %w", err)
}
// test prefix deletion (ensures that both bucket list and delete works)
if errs := fsys.DeletePrefix(testPrefix); len(errs) > 0 {
return fmt.Errorf("failed to delete a test file: %w", errs[0])
}
return nil
}
@@ -0,0 +1,107 @@
package forms_test
import (
"testing"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/pocketbase/pocketbase/forms"
"github.com/pocketbase/pocketbase/tests"
)
func TestS3FilesystemValidate(t *testing.T) {
t.Parallel()
scenarios := []struct {
name string
filesystem string
expectedErrors []string
}{
{
"empty filesystem",
"",
[]string{"filesystem"},
},
{
"invalid filesystem",
"something",
[]string{"filesystem"},
},
{
"backups filesystem",
"backups",
[]string{},
},
{
"storage filesystem",
"storage",
[]string{},
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
app, _ := tests.NewTestApp()
defer app.Cleanup()
form := forms.NewTestS3Filesystem(app)
form.Filesystem = s.filesystem
result := form.Validate()
// parse errors
errs, ok := result.(validation.Errors)
if !ok && result != nil {
t.Fatalf("Failed to parse errors %v", result)
}
// check errors
if len(errs) > len(s.expectedErrors) {
t.Fatalf("Expected error keys %v, got %v", s.expectedErrors, errs)
}
for _, k := range s.expectedErrors {
if _, ok := errs[k]; !ok {
t.Fatalf("Missing expected error key %q in %v", k, errs)
}
}
})
}
}
func TestS3FilesystemSubmitFailure(t *testing.T) {
t.Parallel()
app, _ := tests.NewTestApp()
defer app.Cleanup()
// check if validate was called
{
form := forms.NewTestS3Filesystem(app)
form.Filesystem = ""
result := form.Submit()
if result == nil {
t.Fatal("Expected error, got nil")
}
if _, ok := result.(validation.Errors); !ok {
t.Fatalf("Expected validation.Error, got %v", result)
}
}
// check with valid storage and disabled s3
{
form := forms.NewTestS3Filesystem(app)
form.Filesystem = "storage"
result := form.Submit()
if result == nil {
t.Fatal("Expected error, got nil")
}
if _, ok := result.(validation.Error); ok {
t.Fatalf("Didn't expect validation.Error, got %v", result)
}
}
}