diff --git a/internal/api/anonymous_test.go b/internal/api/anonymous_test.go index 628206954..d9b051c72 100644 --- a/internal/api/anonymous_test.go +++ b/internal/api/anonymous_test.go @@ -86,7 +86,7 @@ func (ts *AnonymousTestSuite) TestAnonymousLogins() { func (ts *AnonymousTestSuite) TestConvertAnonymousUserToPermanent() { ts.Config.External.AnonymousUsers.Enabled = true - ts.Config.Sms.TestOTP = map[string]string{"1234567890": "000000", "1234560000": "000000"} + ts.Config.Sms.TestOTP = conf.TestOTPMap{"1234567890": "000000", "1234560000": "000000"} // test OTPs still require setting up an sms provider ts.Config.Sms.Provider = "twilio" ts.Config.Sms.Twilio.AccountSid = "fake-sid" diff --git a/internal/api/phone_test.go b/internal/api/phone_test.go index dc9180fea..2936dc99a 100644 --- a/internal/api/phone_test.go +++ b/internal/api/phone_test.go @@ -105,7 +105,7 @@ func doTestSendPhoneConfirmation(ts *PhoneTestSuite, useTestOTP bool) { } if useTestOTP { - ts.API.config.Sms.TestOTP = map[string]string{ + ts.API.config.Sms.TestOTP = conf.TestOTPMap{ "123456789": "123456", } } else { diff --git a/internal/conf/configuration.go b/internal/conf/configuration.go index be3c7315c..49da81372 100644 --- a/internal/conf/configuration.go +++ b/internal/conf/configuration.go @@ -652,6 +652,39 @@ type PhoneProviderConfiguration struct { Enabled bool `json:"enabled" default:"false"` } +type TestOTPMap map[string]string + +func (m *TestOTPMap) Decode(value string) error { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return nil + } + + if err := json.Unmarshal([]byte(trimmed), (*map[string]string)(m)); err == nil { + return nil + } + + result := make(map[string]string) + for _, pair := range strings.Split(trimmed, ",") { + pair = strings.TrimSpace(pair) + var k, v string + var found bool + if k, v, found = strings.Cut(pair, "="); !found { + if k, v, found = strings.Cut(pair, ":"); !found { + continue + } + } + result[strings.TrimSpace(k)] = strings.TrimSpace(v) + } + + if len(result) == 0 { + return fmt.Errorf("invalid test OTP format") + } + + *m = TestOTPMap(result) + return nil +} + type SmsProviderConfiguration struct { Autoconfirm bool `json:"autoconfirm"` MaxFrequency time.Duration `json:"max_frequency" split_words:"true"` @@ -659,7 +692,7 @@ type SmsProviderConfiguration struct { OtpLength int `json:"otp_length" split_words:"true"` Provider string `json:"provider"` Template string `json:"template"` - TestOTP map[string]string `json:"test_otp" split_words:"true"` + TestOTP TestOTPMap `json:"test_otp" split_words:"true"` TestOTPValidUntil Time `json:"test_otp_valid_until" split_words:"true"` SMSTemplate *template.Template `json:"-"` @@ -1214,7 +1247,7 @@ func (config *GlobalConfiguration) ApplyDefaults() error { } if config.Sms.TestOTP != nil { - formatTestOtps := make(map[string]string) + formatTestOtps := make(TestOTPMap) for phone, otp := range config.Sms.TestOTP { phone = strings.ReplaceAll(strings.TrimPrefix(phone, "+"), " ", "") formatTestOtps[phone] = otp diff --git a/internal/conf/configuration_test.go b/internal/conf/configuration_test.go index c0f4b5493..3fa34eeac 100644 --- a/internal/conf/configuration_test.go +++ b/internal/conf/configuration_test.go @@ -998,7 +998,7 @@ func TestMethods(t *testing.T) { require.Equal(t, "", got) // valid - val.TestOTP = map[string]string{"13334444": "123456"} + val.TestOTP = TestOTPMap{"13334444": "123456"} got, ok = val.GetTestOTP("13334444", now) require.True(t, ok) require.Equal(t, "123456", got) @@ -1021,6 +1021,57 @@ func TestMethods(t *testing.T) { require.Equal(t, "", got) } + { + t.Run("DecodeJSON", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`{"13334444": "123456"}`) + require.NoError(t, err) + require.Equal(t, TestOTPMap{"13334444": "123456"}, m) + }) + + t.Run("DecodeColonDelimited", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`13334444:123456`) + require.NoError(t, err) + require.Equal(t, TestOTPMap{"13334444": "123456"}, m) + }) + + t.Run("DecodeEqualsDelimited", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`13334444=123456`) + require.NoError(t, err) + require.Equal(t, TestOTPMap{"13334444": "123456"}, m) + }) + + t.Run("DecodeMultipleEntries", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`13334444:123456,15550001111:000000`) + require.NoError(t, err) + require.Equal(t, TestOTPMap{"13334444": "123456", "15550001111": "000000"}, m) + }) + + t.Run("DecodeMixedFormats", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`13334444:123456,15550001111=000000`) + require.NoError(t, err) + require.Equal(t, TestOTPMap{"13334444": "123456", "15550001111": "000000"}, m) + }) + + t.Run("DecodeEmptyString", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(``) + require.NoError(t, err) + require.Nil(t, m) + }) + + t.Run("DecodeInvalidFormat", func(t *testing.T) { + var m TestOTPMap + err := m.Decode(`invalid`) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid test OTP format") + }) + } + { val := &OAuthProviderConfiguration{} @@ -1172,7 +1223,7 @@ func TestMethods(t *testing.T) { Secret: "a", }, Sms: SmsProviderConfiguration{ - TestOTP: map[string]string{"13334444": "123456"}, + TestOTP: TestOTPMap{"13334444": "123456"}, }, } err := val.ApplyDefaults()