added config for virtual phone number
This commit is contained in:
parent
aa2fc45bdd
commit
1eaa06d961
10 changed files with 268 additions and 31 deletions
|
|
@ -3,6 +3,7 @@ package account
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
|
@ -242,14 +243,18 @@ func (s *Service) phoneChangeCaller(ctx context.Context, userID int64, authKeyID
|
|||
// collision-retry loop so a store failure can't spin forever.
|
||||
const maxEmailSignupPhoneAttempts = 20
|
||||
|
||||
// assignEmailSignupDisplayPhone generates a short "888" display number for
|
||||
// an email-signup account rebinding to a new email (see
|
||||
// assignEmailSignupDisplayPhone generates a short display number for an
|
||||
// email-signup account rebinding to a new email (see
|
||||
// domain.NewEmailSignupDisplayPhone / auth.Service's SignUp counterpart),
|
||||
// re-rolling on the astronomically unlikely collision with an existing
|
||||
// account's phone.
|
||||
func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, error) {
|
||||
prefix, err := randomEmailSignupPhonePrefix(s.emailSignupPhonePrefixes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := 0; i < maxEmailSignupPhoneAttempts; i++ {
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone()
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone(prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -262,6 +267,25 @@ func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, er
|
|||
return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts)
|
||||
}
|
||||
|
||||
// randomEmailSignupPhonePrefix mirrors auth.Service's identical helper
|
||||
// (unexported to each package, but must pick with the same fairness): pick
|
||||
// one entry at random from prefixes, falling back to domain.EmailPhonePrefix
|
||||
// ("888") when the list is empty.
|
||||
func randomEmailSignupPhonePrefix(prefixes []string) (string, error) {
|
||||
if len(prefixes) == 0 {
|
||||
return domain.EmailPhonePrefix, nil
|
||||
}
|
||||
if len(prefixes) == 1 {
|
||||
return prefixes[0], nil
|
||||
}
|
||||
var b [8]byte
|
||||
if _, err := rand.Read(b[:]); err != nil {
|
||||
return "", fmt.Errorf("pick email signup phone prefix: %w", err)
|
||||
}
|
||||
idx := binary.LittleEndian.Uint64(b[:]) % uint64(len(prefixes))
|
||||
return prefixes[idx], nil
|
||||
}
|
||||
|
||||
func phoneChangeHash() (string, error) {
|
||||
var raw [8]byte
|
||||
if _, err := rand.Read(raw[:]); err != nil {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ type Service struct {
|
|||
// 验证码走 loginEmailSender 发到解码出的邮箱;非 888 号码一律拒绝——本服务器没有真实
|
||||
// 短信通道,放行会让账号的邮箱身份绑定被绕过(见迁移前的密码找回验证码漏洞教训)。
|
||||
emailSignupEnabled bool
|
||||
// emailSignupPhonePrefixes 是改绑邮箱时随机挑选的账号展示号码号段前缀
|
||||
// 列表(domain.NewEmailSignupDisplayPhone),与 auth.Service 的同名字段
|
||||
// 同一份服务端配置来源。为空时退回默认 "888"。
|
||||
emailSignupPhonePrefixes []string
|
||||
}
|
||||
|
||||
// ServiceOption 调整 account 服务依赖。
|
||||
|
|
@ -163,6 +167,14 @@ func WithEmailSignup(enabled bool) ServiceOption {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEmailSignupPhonePrefixes 设置改绑邮箱时展示号码随机挑选的号段前缀列表
|
||||
// (见 emailSignupPhonePrefixes 字段注释)。
|
||||
func WithEmailSignupPhonePrefixes(prefixes []string) ServiceOption {
|
||||
return func(s *Service) {
|
||||
s.emailSignupPhonePrefixes = prefixes
|
||||
}
|
||||
}
|
||||
|
||||
// NewService 创建 account 服务。
|
||||
func NewService(passwords store.PasswordStore, opts ...ServiceOption) *Service {
|
||||
s := &Service{
|
||||
|
|
|
|||
|
|
@ -90,6 +90,11 @@ type Service struct {
|
|||
// 在 sendCode 阶段直接解码出邮箱、复用登录邮箱同一投递通道发码,与
|
||||
// loginEmailEnabled(手机号账号的第二验证渠道)是两条独立开关。
|
||||
emailSignupEnabled bool
|
||||
// emailSignupPhonePrefixes 是 SignUp 时随机挑选的账号展示号码号段前缀
|
||||
// 列表(domain.NewEmailSignupDisplayPhone);与 emailSignupEnabled 本身
|
||||
// 用的合成 wire 号码前缀(固定 "888",见 domain.EncodeEmailPhone)无关。
|
||||
// 为空时退回默认 "888"。
|
||||
emailSignupPhonePrefixes []string
|
||||
// premiumGrantMonths 是新注册账号默认赠送的会员月数;0 表示关闭赠送。
|
||||
premiumGrantMonths int
|
||||
}
|
||||
|
|
@ -194,6 +199,14 @@ func WithEmailSignup(enabled bool) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEmailSignupPhonePrefixes 设置 SignUp 展示号码随机挑选的号段前缀列表
|
||||
// (见 emailSignupPhonePrefixes 字段注释)。
|
||||
func WithEmailSignupPhonePrefixes(prefixes []string) Option {
|
||||
return func(s *Service) {
|
||||
s.emailSignupPhonePrefixes = prefixes
|
||||
}
|
||||
}
|
||||
|
||||
// NewService 创建登录服务。fixedCode 为开发固定验证码。
|
||||
func NewService(users store.UserStore, auths store.AuthorizationStore, codes store.CodeStore, authKeys store.AuthKeyStore, tempKeys store.TempAuthKeyBindingStore, fixedCode string, opts ...Option) *Service {
|
||||
s := &Service{users: users, auths: auths, codes: codes, authKeys: authKeys, tempKeys: tempKeys, fixedCode: fixedCode, codeTTL: 5 * time.Minute, codeMaxAttempts: 5, loginEmailCodeLength: 6}
|
||||
|
|
@ -1510,8 +1523,12 @@ func normalizePhone(phone string) string {
|
|||
const maxEmailSignupPhoneAttempts = 20
|
||||
|
||||
func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, error) {
|
||||
prefix, err := randomEmailSignupPhonePrefix(s.emailSignupPhonePrefixes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := 0; i < maxEmailSignupPhoneAttempts; i++ {
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone()
|
||||
candidate, err := domain.NewEmailSignupDisplayPhone(prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -1524,6 +1541,26 @@ func (s *Service) assignEmailSignupDisplayPhone(ctx context.Context) (string, er
|
|||
return "", fmt.Errorf("assign email signup display phone: exhausted %d attempts", maxEmailSignupPhoneAttempts)
|
||||
}
|
||||
|
||||
// randomEmailSignupPhonePrefix picks one entry at random from prefixes,
|
||||
// falling back to domain.EmailPhonePrefix ("888") when the list is empty —
|
||||
// config validation requires a non-empty list whenever email signup is
|
||||
// enabled, but store-agnostic callers (tests, WithEmailSignupPhonePrefixes
|
||||
// simply never called) shouldn't crash for lack of one.
|
||||
func randomEmailSignupPhonePrefix(prefixes []string) (string, error) {
|
||||
if len(prefixes) == 0 {
|
||||
return domain.EmailPhonePrefix, nil
|
||||
}
|
||||
if len(prefixes) == 1 {
|
||||
return prefixes[0], nil
|
||||
}
|
||||
n, err := randomInt64()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
idx := uint64(n) % uint64(len(prefixes))
|
||||
return prefixes[idx], nil
|
||||
}
|
||||
|
||||
func randomHex(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"hash/crc32"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -64,10 +65,11 @@ const defaultAppConfigHash = 23 // 默认 app config 内容变更时必须递增
|
|||
// (登录页/启动配置是高频握手路径)。运维改库需重启生效。timezones/emoji 等其余目录走
|
||||
// internal/seed/catalog(go:embed 一次解析),本就在内存。
|
||||
type Service struct {
|
||||
appConfigs store.AppConfigStore
|
||||
countries store.CountryStore
|
||||
mapboxToken string
|
||||
emailSignupEnable bool
|
||||
appConfigs store.AppConfigStore
|
||||
countries store.CountryStore
|
||||
mapboxToken string
|
||||
emailSignupEnable bool
|
||||
emailSignupPhonePrefixes []string
|
||||
|
||||
appConfigOnce sync.Once
|
||||
appConfigCache domain.AppConfig
|
||||
|
|
@ -93,6 +95,16 @@ func WithEmailSignupEnable(enabled bool) Option {
|
|||
}
|
||||
}
|
||||
|
||||
// WithEmailSignupPhonePrefixes 下发 email_signup_phone_prefixes:账号展示号码
|
||||
// (domain.NewEmailSignupDisplayPhone)随机挑选的号段前缀列表,纯信息性——当前
|
||||
// 客户端并不需要读它就能工作(该号码全程由服务端生成/下发),暴露出来只是让
|
||||
// 管理员改动列表天然对所有已适配客户端可见,不需要客户端升级。
|
||||
func WithEmailSignupPhonePrefixes(prefixes []string) Option {
|
||||
return func(s *Service) {
|
||||
s.emailSignupPhonePrefixes = prefixes
|
||||
}
|
||||
}
|
||||
|
||||
// NewService 创建 help 服务。
|
||||
func NewService(appConfigs store.AppConfigStore, countries store.CountryStore, opts ...Option) *Service {
|
||||
s := &Service{appConfigs: appConfigs, countries: countries}
|
||||
|
|
@ -104,15 +116,20 @@ func NewService(appConfigs store.AppConfigStore, countries store.CountryStore, o
|
|||
return s
|
||||
}
|
||||
|
||||
func defaultAppConfig(mapboxToken string, emailSignupEnable bool) domain.AppConfig {
|
||||
jsonBytes := defaultAppConfigJSON(mapboxToken, emailSignupEnable)
|
||||
return domain.AppConfig{Client: tdesktopClient, Hash: defaultAppConfigHashFor(mapboxToken, emailSignupEnable), JSON: jsonBytes}
|
||||
func defaultAppConfig(mapboxToken string, emailSignupEnable bool, emailSignupPhonePrefixes []string) domain.AppConfig {
|
||||
jsonBytes := defaultAppConfigJSON(mapboxToken, emailSignupEnable, emailSignupPhonePrefixes)
|
||||
return domain.AppConfig{Client: tdesktopClient, Hash: defaultAppConfigHashFor(mapboxToken, emailSignupEnable, emailSignupPhonePrefixes), JSON: jsonBytes}
|
||||
}
|
||||
|
||||
func defaultAppConfigJSON(mapboxToken string, emailSignupEnable bool) []byte {
|
||||
func defaultAppConfigJSON(mapboxToken string, emailSignupEnable bool, emailSignupPhonePrefixes []string) []byte {
|
||||
base := tdesktopDefaultAppConfigBase
|
||||
if emailSignupEnable {
|
||||
base += `,"email_signup_enabled":true`
|
||||
if len(emailSignupPhonePrefixes) > 0 {
|
||||
if prefixesJSON, err := json.Marshal(emailSignupPhonePrefixes); err == nil {
|
||||
base += `,"email_signup_phone_prefixes":` + string(prefixesJSON)
|
||||
}
|
||||
}
|
||||
}
|
||||
if mapboxToken == "" {
|
||||
return []byte(base + `}`)
|
||||
|
|
@ -125,10 +142,13 @@ func defaultAppConfigJSON(mapboxToken string, emailSignupEnable bool) []byte {
|
|||
return []byte(base + `,"tdesktop_config_map":{"maps":` + tokenJSON + `,"geo":` + tokenJSON + `,"bmaps":` + tokenJSON + `,"bgeo":` + tokenJSON + `}}`)
|
||||
}
|
||||
|
||||
func defaultAppConfigHashFor(mapboxToken string, emailSignupEnable bool) int {
|
||||
func defaultAppConfigHashFor(mapboxToken string, emailSignupEnable bool, emailSignupPhonePrefixes []string) int {
|
||||
h := defaultAppConfigHash
|
||||
if emailSignupEnable {
|
||||
h += 1000003 // large odd offset so toggling the flag always changes the hash
|
||||
if len(emailSignupPhonePrefixes) > 0 {
|
||||
h += 1 + int(crc32.ChecksumIEEE([]byte(strings.Join(emailSignupPhonePrefixes, ",")))&0x3fffffff)
|
||||
}
|
||||
}
|
||||
if mapboxToken == "" {
|
||||
return h
|
||||
|
|
@ -144,9 +164,9 @@ func (s *Service) GetAppConfig(ctx context.Context, hash int) (domain.AppConfig,
|
|||
|
||||
func (s *Service) loadAppConfig(ctx context.Context) domain.AppConfig {
|
||||
if s == nil {
|
||||
return defaultAppConfig("", false)
|
||||
return defaultAppConfig("", false, nil)
|
||||
}
|
||||
defaultCfg := defaultAppConfig(s.mapboxToken, s.emailSignupEnable)
|
||||
defaultCfg := defaultAppConfig(s.mapboxToken, s.emailSignupEnable, s.emailSignupPhonePrefixes)
|
||||
s.appConfigOnce.Do(func() {
|
||||
if s.appConfigs == nil {
|
||||
s.appConfigCache = defaultCfg
|
||||
|
|
|
|||
59
internal/app/help/service_email_signup_test.go
Normal file
59
internal/app/help/service_email_signup_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package help
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestAppConfigEmailSignupPhonePrefixes asserts email_signup_phone_prefixes
|
||||
// is only present alongside email_signup_enabled=true, carries exactly the
|
||||
// configured prefix list, and that changing the list bumps the hash so
|
||||
// clients don't cache a stale list.
|
||||
func TestAppConfigEmailSignupPhonePrefixes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
disabled := NewService(nil, nil)
|
||||
cfg, _, err := disabled.GetAppConfig(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAppConfig (disabled): %v", err)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal(cfg.JSON, &decoded); err != nil {
|
||||
t.Fatalf("app config json invalid: %v", err)
|
||||
}
|
||||
if _, present := decoded["email_signup_phone_prefixes"]; present {
|
||||
t.Fatalf("email_signup_phone_prefixes present while email signup disabled: %+v", decoded["email_signup_phone_prefixes"])
|
||||
}
|
||||
|
||||
enabled := NewService(nil, nil,
|
||||
WithEmailSignupEnable(true),
|
||||
WithEmailSignupPhonePrefixes([]string{"888", "380", "373"}))
|
||||
cfg2, _, err := enabled.GetAppConfig(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAppConfig (enabled): %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(cfg2.JSON, &decoded); err != nil {
|
||||
t.Fatalf("app config json invalid: %v", err)
|
||||
}
|
||||
rawPrefixes, ok := decoded["email_signup_phone_prefixes"].([]any)
|
||||
if !ok || len(rawPrefixes) != 3 {
|
||||
t.Fatalf("email_signup_phone_prefixes = %+v, want [888 380 373]", decoded["email_signup_phone_prefixes"])
|
||||
}
|
||||
for i, want := range []string{"888", "380", "373"} {
|
||||
if rawPrefixes[i] != want {
|
||||
t.Fatalf("email_signup_phone_prefixes[%d] = %v, want %q", i, rawPrefixes[i], want)
|
||||
}
|
||||
}
|
||||
|
||||
other := NewService(nil, nil,
|
||||
WithEmailSignupEnable(true),
|
||||
WithEmailSignupPhonePrefixes([]string{"888"}))
|
||||
cfg3, _, err := other.GetAppConfig(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAppConfig (different prefixes): %v", err)
|
||||
}
|
||||
if cfg3.Hash == cfg2.Hash {
|
||||
t.Fatalf("hash unchanged (%d) despite a different prefix list, clients would stay cached on the old list", cfg3.Hash)
|
||||
}
|
||||
}
|
||||
|
|
@ -124,6 +124,14 @@ type Config struct {
|
|||
// 不变),验证码通过 SMTP 发到解码出的邮箱而非发短信。要求 SMTP 配置可用(与
|
||||
// LoginEmailEnable 共用同一组 TELESRV_SMTP_* 变量)。
|
||||
EmailSignupEnable bool
|
||||
// EmailSignupPhonePrefixes 是账号实际可见的 users.phone 短号码
|
||||
// (domain.NewEmailSignupDisplayPhone)随机选用的号段前缀列表,逗号分隔,
|
||||
// 默认仅 "888"。注意这与合成 wire 号码(domain.EncodeEmailPhone,sendCode
|
||||
// 阶段用于携带邮箱本身)无关——那个前缀恒为 "888" 且从不下发给客户端;
|
||||
// 这里只影响注册后账号真正落库/展示的号码好不好看,可通过
|
||||
// help.getAppConfig 的 email_signup_phone_prefixes 下发给客户端,管理员
|
||||
// 改动此列表不需要客户端升级。
|
||||
EmailSignupPhonePrefixes []string
|
||||
// SMTP* 是登录邮箱验证码的出站邮件配置。LoginEmailEnable=true 时必须可用。
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
|
|
@ -459,6 +467,7 @@ func Load() (Config, error) {
|
|||
LoginEmailEnable: envBoolOr("TELESRV_LOGIN_EMAIL_ENABLE", false),
|
||||
LoginEmailRequireSetup: envBoolOr("TELESRV_LOGIN_EMAIL_REQUIRE_SETUP", false),
|
||||
EmailSignupEnable: envBoolOr("TELESRV_EMAIL_SIGNUP_ENABLE", false),
|
||||
EmailSignupPhonePrefixes: envListOr("TELESRV_EMAIL_SIGNUP_PHONE_PREFIXES", []string{"888"}),
|
||||
LoginEmailCodeLength: envIntOr("TELESRV_LOGIN_EMAIL_CODE_LENGTH", 6),
|
||||
SMTPHost: envOr("TELESRV_SMTP_HOST", ""),
|
||||
SMTPPort: envIntOr("TELESRV_SMTP_PORT", 587),
|
||||
|
|
@ -588,6 +597,16 @@ func validateLoginEmailConfig(cfg Config) error {
|
|||
default:
|
||||
return fmt.Errorf("TELESRV_SMTP_TLS must be starttls, tls, or none")
|
||||
}
|
||||
if cfg.EmailSignupEnable {
|
||||
if len(cfg.EmailSignupPhonePrefixes) == 0 {
|
||||
return fmt.Errorf("TELESRV_EMAIL_SIGNUP_PHONE_PREFIXES must not be empty when TELESRV_EMAIL_SIGNUP_ENABLE=true")
|
||||
}
|
||||
for _, prefix := range cfg.EmailSignupPhonePrefixes {
|
||||
if !isDigitsOnly(prefix) || len(prefix) < 1 || len(prefix) > 4 {
|
||||
return fmt.Errorf("TELESRV_EMAIL_SIGNUP_PHONE_PREFIXES entry %q must be 1-4 digits", prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !cfg.LoginEmailEnable && !cfg.EmailSignupEnable {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -799,6 +818,18 @@ func (e envSource) envAllowEmptyOr(key, def string) string {
|
|||
return def
|
||||
}
|
||||
|
||||
func isDigitsOnly(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (e envSource) envListOr(key string, def []string) []string {
|
||||
v := e.envOr(key, "")
|
||||
if v == "" {
|
||||
|
|
|
|||
|
|
@ -139,33 +139,53 @@ func NormalizeEmailForPhone(email string) string {
|
|||
return strings.ToLower(strings.TrimSpace(email))
|
||||
}
|
||||
|
||||
// emailSignupDisplayPhoneDigits is how many random digits follow the "888"
|
||||
// prefix in a NewEmailSignupDisplayPhone result, e.g. "888" + 8 digits =
|
||||
// emailSignupDisplayPhoneDigits is how many random digits follow the prefix
|
||||
// in a NewEmailSignupDisplayPhone result, e.g. prefix "888" + 8 digits =
|
||||
// "88812345678" (formats on-screen as something like "+888 1234 5678").
|
||||
const emailSignupDisplayPhoneDigits = 8
|
||||
|
||||
// NewEmailSignupDisplayPhone generates a short, all-digit "888" phone number
|
||||
// for an email-signup account's users.phone column. Unlike EncodeEmailPhone
|
||||
// this carries no information about the email — it is purely a
|
||||
// normal-looking display/identity number — so the caller must separately
|
||||
// persist the email->user association (see User.SignupEmail) for returning
|
||||
// logins to be found. Because the result is all digits, IsEmailSignupPhone
|
||||
// on it is always false: once assigned, it behaves exactly like a real phone
|
||||
// number everywhere else in the system (contacts, search, ByPhone lookups).
|
||||
func NewEmailSignupDisplayPhone() (string, error) {
|
||||
// NewEmailSignupDisplayPhone generates a short, all-digit phone number for an
|
||||
// email-signup account's users.phone column, using the given prefix (one of
|
||||
// the server's configured EmailSignupPhonePrefixes — see
|
||||
// help.getAppConfig's email_signup_phone_prefixes, config.go). This is
|
||||
// unrelated to EncodeEmailPhone's own fixed "888" wire prefix: that one only
|
||||
// ever travels internally between client and server during sendCode/signIn
|
||||
// and is never shown to anyone, so it has no reason to be configurable,
|
||||
// unlike this display number, which is the account's actual, permanent,
|
||||
// user-visible phone. The caller must separately persist the email->user
|
||||
// association (see User.SignupEmail) for returning logins to be found.
|
||||
// Because the result is all digits, IsEmailSignupPhone on it is always
|
||||
// false: once assigned, it behaves exactly like a real phone number
|
||||
// everywhere else in the system (contacts, search, ByPhone lookups).
|
||||
func NewEmailSignupDisplayPhone(prefix string) (string, error) {
|
||||
if !isAllASCIIDigits(prefix) {
|
||||
return "", fmt.Errorf("email signup display phone prefix %q must be non-empty digits", prefix)
|
||||
}
|
||||
b := make([]byte, emailSignupDisplayPhoneDigits)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", fmt.Errorf("generate email signup display phone: %w", err)
|
||||
}
|
||||
var out strings.Builder
|
||||
out.Grow(len(EmailPhonePrefix) + emailSignupDisplayPhoneDigits)
|
||||
out.WriteString(EmailPhonePrefix)
|
||||
out.Grow(len(prefix) + emailSignupDisplayPhoneDigits)
|
||||
out.WriteString(prefix)
|
||||
for _, v := range b {
|
||||
out.WriteByte('0' + v%10)
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
func isAllASCIIDigits(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range s {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsEmailSignupPhone reports whether phone was produced by EncodeEmailPhone.
|
||||
// Every encoded value contains at least one letter (the mandatory '@'
|
||||
// escape's 'q' marker byte), which real, all-digit phone numbers — even
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ func TestDecodeEmailPhoneRejectsNonEmailNumbers(t *testing.T) {
|
|||
func TestNewEmailSignupDisplayPhoneLooksLikeARealPhoneNumber(t *testing.T) {
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 200; i++ {
|
||||
phone, err := NewEmailSignupDisplayPhone()
|
||||
phone, err := NewEmailSignupDisplayPhone(EmailPhonePrefix)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEmailSignupDisplayPhone: %v", err)
|
||||
}
|
||||
|
|
@ -94,3 +94,26 @@ func TestNewEmailSignupDisplayPhoneLooksLikeARealPhoneNumber(t *testing.T) {
|
|||
t.Fatalf("only %d distinct values out of 200 draws, generator looks non-random", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmailSignupDisplayPhoneHonorsConfiguredPrefix(t *testing.T) {
|
||||
for _, prefix := range []string{"380", "1", "7777"} {
|
||||
phone, err := NewEmailSignupDisplayPhone(prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEmailSignupDisplayPhone(%q): %v", prefix, err)
|
||||
}
|
||||
if !strings.HasPrefix(phone, prefix) {
|
||||
t.Fatalf("phone %q missing configured prefix %q", phone, prefix)
|
||||
}
|
||||
if !ValidPhone(phone) {
|
||||
t.Fatalf("phone %q fails ValidPhone", phone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEmailSignupDisplayPhoneRejectsInvalidPrefix(t *testing.T) {
|
||||
for _, prefix := range []string{"", "abc", "88q", "-1"} {
|
||||
if _, err := NewEmailSignupDisplayPhone(prefix); err == nil {
|
||||
t.Fatalf("NewEmailSignupDisplayPhone(%q) err = nil, want error", prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue