Merge remote-tracking branch 'upstream/main' into merge-gramsrv-9106877
This commit is contained in:
commit
ac6a50c5ff
697 changed files with 100880 additions and 8052 deletions
|
|
@ -11,10 +11,16 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/links"
|
||||
)
|
||||
|
||||
const defaultConfigFile = ".env"
|
||||
const (
|
||||
defaultConfigFile = ".env"
|
||||
defaultCountryCode = "CN"
|
||||
)
|
||||
|
||||
// Config 是 telesrv 的运行配置。
|
||||
type Config struct {
|
||||
|
|
@ -31,6 +37,9 @@ type Config struct {
|
|||
RSAKeyPath string
|
||||
// DC 是本 server 的 DC ID。
|
||||
DC int
|
||||
// DefaultCountryCode 是 help.getNearestDc 返回的 ISO 3166-1 alpha-2 国家码。
|
||||
// 客户端登录页据此预选国家和国际电话区号;例如 CN 对应 +86。
|
||||
DefaultCountryCode string
|
||||
// StrictDCCheck turns on exact DC-ID validation for the permanent-key
|
||||
// exchange (default off = lenient). See mtprotoedge.Options.StrictDC doc
|
||||
// for the full rationale: telesrv is always a single physical backend,
|
||||
|
|
@ -53,15 +62,13 @@ type Config struct {
|
|||
MTProtoRPCGlobalWorkers int
|
||||
MTProtoRPCGlobalMaxTasks int
|
||||
MTProtoRPCGlobalMaxBytes int64
|
||||
// Pending ownership and completed rpc_result replay state share a three-level
|
||||
// global/raw-auth/session budget over the full MTProto duplicate horizon.
|
||||
MTProtoRPCResultCacheMaxEntries int
|
||||
MTProtoRPCResultCacheMaxBytes int64
|
||||
MTProtoRPCResultCacheAuthMaxEntries int
|
||||
MTProtoRPCResultCacheAuthMaxBytes int64
|
||||
MTProtoRPCResultCacheSessionMaxEntries int
|
||||
MTProtoRPCResultCacheSessionMaxBytes int64
|
||||
MTProtoRPCResultPendingPerAuth int
|
||||
// Pending ownership and compact completed receipts share three-level
|
||||
// global/raw-auth/session entry accounting. Result bodies are never cached
|
||||
// here; the logical-session outbox owns unacknowledged wire bytes.
|
||||
MTProtoRPCExecutionMaxEntries int
|
||||
MTProtoRPCExecutionAuthMaxEntries int
|
||||
MTProtoRPCExecutionSessionMaxEntries int
|
||||
MTProtoRPCExecutionPendingPerAuth int
|
||||
// MTProtoInboundFrameGlobalMaxBytes 是 transport wire + 最大解密 plaintext 的
|
||||
// 进程级在途预算;frame 长度读出后、payload 分配前预留。
|
||||
MTProtoInboundFrameGlobalMaxBytes int64
|
||||
|
|
@ -106,7 +113,8 @@ type Config struct {
|
|||
ScamWarning string
|
||||
FakeWarning string
|
||||
// PublicLinkWebAddr 是公开链接落地页监听地址;为空关闭。
|
||||
// 生产应只监听 loopback,并由 nginx 将 /<username>、/addstickers/、/addemoji/ 与 /addlist/ 反代到该地址。
|
||||
// 生产应只监听 loopback,并由 nginx 将 /<username>、/addstickers/、/addemoji/、
|
||||
// /addlist/ 与 hash-only /appeal/ 路由反代到该地址。
|
||||
PublicLinkWebAddr string
|
||||
// TelegramLoginEnabled mounts the self-hosted Telegram Login/OIDC provider
|
||||
// on PublicLinkWebAddr. Secrets are file-backed so they are not exposed in
|
||||
|
|
@ -131,6 +139,19 @@ type Config struct {
|
|||
AdminUIPassword string
|
||||
AdminUIToken string
|
||||
AdminSessionKey string
|
||||
// AdminUIPermissions is the permission set granted to a panel session that
|
||||
// authenticated with TELESRV_ADMIN_UI_PASSWORD / _TOKEN. The single entry "*"
|
||||
// means "every permission" and is the shipped default, so enabling RBAC never
|
||||
// silently locks an operator out of a panel that worked before.
|
||||
AdminUIPermissions []string
|
||||
// AdminScopedTokens are additional adminapi bearer tokens with a bounded
|
||||
// permission set each. They exist so an integration can be given exactly the
|
||||
// rights it needs instead of the unrestricted TELESRV_ADMIN_API_TOKEN. Parsed
|
||||
// from
|
||||
// "name:token:perm1,perm2" entries separated by ';'; a malformed entry, a
|
||||
// duplicate name or a duplicate token fails startup rather than silently
|
||||
// granting or dropping rights.
|
||||
AdminScopedTokens []AdminScopedToken
|
||||
|
||||
// PostgresDSN 是业务数据(auth_key / user / authorization 等)持久化的 PostgreSQL 连接串。
|
||||
// 依赖由 deploy/docker-compose.yml 启动;职责划分见 docs/persistence-layer.md。
|
||||
|
|
@ -236,6 +257,9 @@ type Config struct {
|
|||
StickerSeedDir string
|
||||
// StickerSeedMaxSets 限制导入的常规贴纸集数量(避免启动时导入过多包),<=0 表示不限。
|
||||
StickerSeedMaxSets int
|
||||
// PremiumPromoSeedDir 是 help.getPremiumPromo 视频与缩略图导出目录。
|
||||
// 目录缺失时保留无视频兼容响应;目录存在但内容非法时启动失败。
|
||||
PremiumPromoSeedDir string
|
||||
// BusinessAIProvider 控制服务端 Business automation 回复生成器。
|
||||
// 空值/"echo" 回显触发私聊文本,用于跑通后续 AI provider 链路;
|
||||
// "template" 使用 quick reply 模板。
|
||||
|
|
@ -344,6 +368,8 @@ type Config struct {
|
|||
CallTombstoneTTL time.Duration
|
||||
// CallMaxActivePerUser 是单用户并发非终态通话上限。
|
||||
CallMaxActivePerUser int
|
||||
// CallRegistryMaxEntries 是进程内通话 registry 的全局硬上限。
|
||||
CallRegistryMaxEntries int
|
||||
// CallSignalingMaxBytes 是 phone.sendSignalingData 单条载荷上限。
|
||||
CallSignalingMaxBytes int
|
||||
// CallSignalingRate 是单通话每秒信令转发上限(超限静默丢弃)。
|
||||
|
|
@ -391,6 +417,106 @@ type Config struct {
|
|||
StarGiftCraftDelay time.Duration
|
||||
StarGiftCraftChancePermille int
|
||||
|
||||
// RatingEnabled controls the local admin-only composite account rating.
|
||||
// Disabled keeps every local projection empty and refuses rating writes; no
|
||||
// client-facing Telegram field changes in either mode.
|
||||
RatingEnabled bool
|
||||
// RatingPendingDelay is how long a rating increase stays parked as a pending
|
||||
// local score before it becomes the visible admin level. A decrease is
|
||||
// always applied immediately: a penalty must not sit behind a delay.
|
||||
// 0 applies every change immediately.
|
||||
RatingPendingDelay time.Duration
|
||||
// RatingRecomputeInterval / RatingRecomputeBatch drive the background
|
||||
// recompute worker. The rating derives from signals owned by other
|
||||
// subsystems, so freshness is a worker property, not a write-path one.
|
||||
RatingRecomputeInterval time.Duration
|
||||
RatingRecomputeBatch int
|
||||
// RatingStaleAfter is the projection age after which the worker recomputes a
|
||||
// user.
|
||||
RatingStaleAfter time.Duration
|
||||
// Rating weights are the integer composite formula. Defaults mirror
|
||||
// domain.DefaultAccountRatingWeights() exactly, so the shipped behaviour is
|
||||
// identical whether or not these keys are set. Every weight is a magnitude:
|
||||
// the penalties are subtracted by the domain formula, so all values are
|
||||
// non-negative and a negative value fails startup.
|
||||
RatingWeightStarsReceivedPermille int64
|
||||
RatingWeightStarsSpentPermille int64
|
||||
RatingWeightMessageSent int64
|
||||
RatingWeightAccountAgeDay int64
|
||||
RatingWeightGiftReceived int64
|
||||
RatingWeightModerationCase int64
|
||||
RatingWeightScamPenalty int64
|
||||
RatingWeightFakePenalty int64
|
||||
// RatingActivityCap bounds the activity component so activity alone cannot
|
||||
// outweigh Stars and moderation; 0 leaves it uncapped.
|
||||
RatingActivityCap int64
|
||||
// VerificationEnabled controls official platform verification: the @verifybot
|
||||
// application flow and the panel's review queue. Disabled refuses every
|
||||
// verification use case explicitly; already-verified peers keep their badge,
|
||||
// because the flag lives on the peer record and is not derived from this
|
||||
// feature being on.
|
||||
VerificationEnabled bool
|
||||
// VerificationAllowUserTargets opts plain user accounts in as verification
|
||||
// subjects. Off by default: the official process verifies a public presence
|
||||
// (bot, public channel, public supergroup), and a private account has nothing
|
||||
// to check.
|
||||
VerificationAllowUserTargets bool
|
||||
// VerificationRejectCooldown is how long an applicant must wait before filing
|
||||
// the same target again after a rejection. Measured from the decision, so a
|
||||
// slow review never shortens it; 0 disables the cooldown.
|
||||
VerificationRejectCooldown time.Duration
|
||||
// VerificationApplyRateLimit / VerificationApplyRateWindow bound how many
|
||||
// applications one applicant may create per window. 0 for either disables the
|
||||
// budget.
|
||||
VerificationApplyRateLimit int
|
||||
VerificationApplyRateWindow time.Duration
|
||||
// VerificationBotRateLimit / VerificationBotRateWindow bound the @verifybot
|
||||
// dialog itself (per-applicant command rate), independently of how many
|
||||
// applications are actually created.
|
||||
VerificationBotRateLimit int
|
||||
VerificationBotRateWindow time.Duration
|
||||
// VerificationNotifyInterval / VerificationNotifyBatch drive the applicant
|
||||
// notification worker. A decision commits with its outbox row, never with a
|
||||
// message send, so delivery cadence is a worker property.
|
||||
VerificationNotifyInterval time.Duration
|
||||
VerificationNotifyBatch int
|
||||
// VerificationMaxActivePerUser bounds how many applications one applicant may
|
||||
// keep open at once; 0 disables the cap.
|
||||
VerificationMaxActivePerUser int
|
||||
|
||||
// BotVerificationEnabled controls THIRD-PARTY bot verification
|
||||
// (core.telegram.org/api/bots/verification): a verifier bot marking peers with
|
||||
// its own icon and description, projected onto
|
||||
// user/channel.bot_verification_icon and botInfo.verifier_settings. It is a
|
||||
// different mechanism from VerificationEnabled above -- that one is the
|
||||
// operator-granted platform checkmark, and the two never read each other's
|
||||
// state.
|
||||
//
|
||||
// Disabled refuses every third-party mutation (grants, revocations,
|
||||
// applications, catalogue edits) while the marks already granted keep
|
||||
// projecting: blanking one verifier's badges is what its per-verifier kill
|
||||
// switch is for.
|
||||
BotVerificationEnabled bool
|
||||
// BotVerificationMaxPerVerifier bounds how many peers one verifier bot may
|
||||
// mark. Verifier status is granted per deployment rather than earned per peer,
|
||||
// so an unbounded verifier would be an unbounded badge printer. 0 disables the
|
||||
// service-level bound and leaves only the storage bound
|
||||
// (domain.MaxCustomVerificationsPerVerifier), which is also the maximum this
|
||||
// key accepts.
|
||||
BotVerificationMaxPerVerifier int
|
||||
// BotVerificationRequestRateLimit / BotVerificationRequestRateWindow bound how
|
||||
// many verification applications one applicant may file per window, across all
|
||||
// verifier bots. 0 for either disables the budget.
|
||||
BotVerificationRequestRateLimit int
|
||||
BotVerificationRequestRateWindow time.Duration
|
||||
|
||||
// CollectibleUsernameURLTemplate is the landing URL recorded on a minted
|
||||
// collectible username when the mint request carries no explicit URL.
|
||||
// Empty derives <TELESRV_PUBLIC_BASE_URL>/nft/username/<username>; a template
|
||||
// may carry the {username} placeholder, and without it the name is appended
|
||||
// as the last path segment. No external marketplace is contacted.
|
||||
CollectibleUsernameURLTemplate string
|
||||
|
||||
// GroupCallCheckTTL 是群通话参与者保活水位的过期阈值(客户端 Connecting 态
|
||||
// 4s 一跳;M1 起 SFU liveness reporter 同样刷新该水位)。
|
||||
GroupCallCheckTTL time.Duration
|
||||
|
|
@ -440,6 +566,15 @@ type Config struct {
|
|||
SFUAdvertiseIP string
|
||||
}
|
||||
|
||||
// AdminScopedToken is one adminapi bearer token restricted to a permission set.
|
||||
// Name is the audit identity written next to actions performed with the token;
|
||||
// Permissions is the closed list of rights it carries ("*" means all).
|
||||
type AdminScopedToken struct {
|
||||
Name string
|
||||
Token string
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
type AIProviderConfig struct {
|
||||
Name string
|
||||
Kind string
|
||||
|
|
@ -465,6 +600,9 @@ func Load() (Config, error) {
|
|||
envInt64Or := fileEnv.envInt64Or
|
||||
envDurationOr := fileEnv.envDurationOr
|
||||
envAllowEmptyOr := fileEnv.envAllowEmptyOr
|
||||
if err := validateStrictMTProtoCapacityEnv(fileEnv); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
publicBaseURL, err := links.ValidateBaseURL(envOr("TELESRV_PUBLIC_BASE_URL", links.DefaultPublicBaseURL))
|
||||
if err != nil {
|
||||
|
|
@ -495,6 +633,21 @@ func Load() (Config, error) {
|
|||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_PUBLIC_DOWNLOAD_URL: %w", err)
|
||||
}
|
||||
countryCode, err := normalizeDefaultCountryCode(envOr("TELESRV_DEFAULT_COUNTRY_CODE", defaultCountryCode))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_DEFAULT_COUNTRY_CODE: %w", err)
|
||||
}
|
||||
advertiseIP, err := normalizeAdvertiseIP(envOr("TELESRV_ADVERTISE_IP", "127.0.0.1"))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_ADVERTISE_IP: %w", err)
|
||||
}
|
||||
// The composite rating weight defaults are the domain formula's own defaults;
|
||||
// see RatingWeight* below.
|
||||
defaultRatingWeights := domain.DefaultAccountRatingWeights()
|
||||
adminScopedTokens, err := parseAdminScopedTokens(envAllowEmptyOr("TELESRV_ADMIN_SCOPED_TOKENS", ""))
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
ListenAddr: envOr("TELESRV_LISTEN", "0.0.0.0:2398"),
|
||||
|
|
@ -503,33 +656,28 @@ func Load() (Config, error) {
|
|||
"http://localhost:1234",
|
||||
"http://127.0.0.1:1234",
|
||||
}),
|
||||
// AdvertiseIP 当前不影响 help.getConfig——getConfig 返回空 DCOptions,
|
||||
// 客户端使用其写死的 static DC 地址(见 compat/tdesktop/config.go)。
|
||||
// 字段与默认值保留,供未来需要显式下发 DC 地址时使用。
|
||||
AdvertiseIP: envOr("TELESRV_ADVERTISE_IP", "127.0.0.1"),
|
||||
RSAKeyPath: envOr("TELESRV_RSA_KEY", "data/server_rsa.pem"),
|
||||
DC: envIntOr("TELESRV_DC", 2),
|
||||
StrictDCCheck: envBoolOr("TELESRV_STRICT_DC_CHECK", false),
|
||||
MTProtoMaxConnections: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS", 200000),
|
||||
MTProtoMaxConnectionsPerIP: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS_PER_IP", 4096),
|
||||
MTProtoMaxConcurrentHandshakes: envIntOr("TELESRV_MTPROTO_MAX_CONCURRENT_HANDSHAKES", 256),
|
||||
MTProtoRPCMaxInflight: envIntOr("TELESRV_MTPROTO_RPC_MAX_INFLIGHT", 32),
|
||||
MTProtoRPCQueueSize: envIntOr("TELESRV_MTPROTO_RPC_QUEUE_SIZE", 64),
|
||||
MTProtoRPCTimeout: envDurationOr("TELESRV_MTPROTO_RPC_TIMEOUT", 30*time.Second),
|
||||
MTProtoRPCGlobalWorkers: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_WORKERS", 256),
|
||||
MTProtoRPCGlobalMaxTasks: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", 8192),
|
||||
MTProtoRPCGlobalMaxBytes: envInt64Or("TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES", 512<<20),
|
||||
MTProtoRPCResultCacheMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_ENTRIES", 1<<18),
|
||||
MTProtoRPCResultCacheMaxBytes: envInt64Or("TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_BYTES", 64<<20),
|
||||
MTProtoRPCResultCacheAuthMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_ENTRIES", 1<<15),
|
||||
MTProtoRPCResultCacheAuthMaxBytes: envInt64Or("TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_BYTES", 32<<20),
|
||||
MTProtoRPCResultCacheSessionMaxEntries: envIntOr(
|
||||
"TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_ENTRIES", 1<<14,
|
||||
// help.getConfig 必须下发至少一个可重连的主 DC 地址;远端部署不能
|
||||
// 沿用 loopback 默认值,需显式设置客户端实际可达的 IP。
|
||||
AdvertiseIP: advertiseIP,
|
||||
RSAKeyPath: envOr("TELESRV_RSA_KEY", "data/server_rsa.pem"),
|
||||
DC: envIntOr("TELESRV_DC", 2),
|
||||
DefaultCountryCode: countryCode,
|
||||
StrictDCCheck: envBoolOr("TELESRV_STRICT_DC_CHECK", false),
|
||||
MTProtoMaxConnections: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS", 200000),
|
||||
MTProtoMaxConnectionsPerIP: envIntOr("TELESRV_MTPROTO_MAX_CONNECTIONS_PER_IP", 4096),
|
||||
MTProtoMaxConcurrentHandshakes: envIntOr("TELESRV_MTPROTO_MAX_CONCURRENT_HANDSHAKES", 256),
|
||||
MTProtoRPCMaxInflight: envIntOr("TELESRV_MTPROTO_RPC_MAX_INFLIGHT", 32),
|
||||
MTProtoRPCQueueSize: envIntOr("TELESRV_MTPROTO_RPC_QUEUE_SIZE", 64),
|
||||
MTProtoRPCTimeout: envDurationOr("TELESRV_MTPROTO_RPC_TIMEOUT", 30*time.Second),
|
||||
MTProtoRPCGlobalWorkers: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_WORKERS", 256),
|
||||
MTProtoRPCGlobalMaxTasks: envIntOr("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", 8192),
|
||||
MTProtoRPCGlobalMaxBytes: envInt64Or("TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES", 512<<20),
|
||||
MTProtoRPCExecutionMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", 1<<18),
|
||||
MTProtoRPCExecutionAuthMaxEntries: envIntOr("TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES", 1<<15),
|
||||
MTProtoRPCExecutionSessionMaxEntries: envIntOr(
|
||||
"TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES", 1<<14,
|
||||
),
|
||||
MTProtoRPCResultCacheSessionMaxBytes: envInt64Or(
|
||||
"TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_BYTES", 16<<20,
|
||||
),
|
||||
MTProtoRPCResultPendingPerAuth: envIntOr("TELESRV_MTPROTO_RPC_RESULT_PENDING_PER_AUTH", 1<<11),
|
||||
MTProtoRPCExecutionPendingPerAuth: envIntOr("TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH", 1<<11),
|
||||
MTProtoInboundFrameGlobalMaxBytes: envInt64Or("TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES", 512<<20),
|
||||
MTProtoOutboundQueueSize: envIntOr("TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE", 128),
|
||||
MTProtoOutboundControlQueueSize: envIntOr("TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE", 32),
|
||||
|
|
@ -561,6 +709,8 @@ func Load() (Config, error) {
|
|||
TelegramLoginRetention: envDurationOr("TELESRV_TELEGRAM_LOGIN_RETENTION", 7*24*time.Hour),
|
||||
TelegramLoginSweepInterval: envDurationOr("TELESRV_TELEGRAM_LOGIN_SWEEP_INTERVAL", 5*time.Minute),
|
||||
TelegramLoginSweepBatch: envIntOr("TELESRV_TELEGRAM_LOGIN_SWEEP_BATCH", 500),
|
||||
AdminUIPermissions: envListOr("TELESRV_ADMIN_UI_PERMISSIONS", []string{adminPermissionAll}),
|
||||
AdminScopedTokens: adminScopedTokens,
|
||||
AdminUIAddr: envOr("TELESRV_ADMIN_UI_ADDR", "127.0.0.1:2600"),
|
||||
AdminUIPassword: envOr("TELESRV_ADMIN_UI_PASSWORD", ""),
|
||||
AdminUIToken: envOr("TELESRV_ADMIN_UI_TOKEN", ""),
|
||||
|
|
@ -608,6 +758,7 @@ func Load() (Config, error) {
|
|||
BlobDir: envOr("TELESRV_BLOB_DIR", "data/blobs"),
|
||||
StickerSeedDir: envOr("TELESRV_STICKER_SEED_DIR", "data/sticker-seed"),
|
||||
StickerSeedMaxSets: envIntOr("TELESRV_STICKER_SEED_MAX_SETS", 300),
|
||||
PremiumPromoSeedDir: envOr("TELESRV_PREMIUM_PROMO_SEED_DIR", "data/premium-promo"),
|
||||
MapboxToken: envOr("TELESRV_MAPBOX_TOKEN", ""),
|
||||
MapTileCacheDir: envOr("TELESRV_MAPTILE_CACHE_DIR", "data/maptiles"),
|
||||
ExternalMediaEnable: envBoolOr("TELESRV_EXTERNAL_MEDIA_ENABLE", true),
|
||||
|
|
@ -662,12 +813,13 @@ func Load() (Config, error) {
|
|||
UploadInFlightMaxParts: envIntOr("TELESRV_UPLOAD_INFLIGHT_MAX_PARTS", 8000),
|
||||
UploadInFlightMaxFiles: envIntOr("TELESRV_UPLOAD_INFLIGHT_MAX_FILES", 64),
|
||||
|
||||
CallRingTimeout: envDurationOr("TELESRV_CALL_RING_TIMEOUT", 90*time.Second),
|
||||
CallTombstoneTTL: envDurationOr("TELESRV_CALL_TOMBSTONE_TTL", 60*time.Second),
|
||||
CallMaxActivePerUser: envIntOr("TELESRV_CALL_MAX_ACTIVE_PER_USER", 4),
|
||||
CallSignalingMaxBytes: envIntOr("TELESRV_CALL_SIGNALING_MAX_BYTES", 65536),
|
||||
CallSignalingRate: envIntOr("TELESRV_CALL_SIGNALING_RATE", 50),
|
||||
CallExpiryInterval: envDurationOr("TELESRV_CALL_EXPIRY_INTERVAL", time.Second),
|
||||
CallRingTimeout: envDurationOr("TELESRV_CALL_RING_TIMEOUT", 90*time.Second),
|
||||
CallTombstoneTTL: envDurationOr("TELESRV_CALL_TOMBSTONE_TTL", 60*time.Second),
|
||||
CallMaxActivePerUser: envIntOr("TELESRV_CALL_MAX_ACTIVE_PER_USER", 4),
|
||||
CallRegistryMaxEntries: envIntOr("TELESRV_CALL_REGISTRY_MAX_ENTRIES", 10_000),
|
||||
CallSignalingMaxBytes: envIntOr("TELESRV_CALL_SIGNALING_MAX_BYTES", 65536),
|
||||
CallSignalingRate: envIntOr("TELESRV_CALL_SIGNALING_RATE", 50),
|
||||
CallExpiryInterval: envDurationOr("TELESRV_CALL_EXPIRY_INTERVAL", time.Second),
|
||||
|
||||
PremiumGrantMonths: envIntOr("TELESRV_PREMIUM_GRANT_MONTHS", 3),
|
||||
DefaultStickerSetID: envInt64Or("TELESRV_DEFAULT_STICKER_SET_ID", 0),
|
||||
|
|
@ -689,6 +841,47 @@ func Load() (Config, error) {
|
|||
StarGiftCraftDelay: envDurationOr("TELESRV_STARGIFT_CRAFT_DELAY", 0),
|
||||
StarGiftCraftChancePermille: envIntOr("TELESRV_STARGIFT_CRAFT_CHANCE_PERMILLE", 250),
|
||||
|
||||
RatingEnabled: envBoolOr("TELESRV_RATING_ENABLED", true),
|
||||
RatingPendingDelay: envDurationOr("TELESRV_RATING_PENDING_DELAY", 24*time.Hour),
|
||||
RatingRecomputeInterval: envDurationOr("TELESRV_RATING_RECOMPUTE_INTERVAL", 15*time.Minute),
|
||||
RatingRecomputeBatch: envIntOr("TELESRV_RATING_RECOMPUTE_BATCH", 500),
|
||||
RatingStaleAfter: envDurationOr("TELESRV_RATING_STALE_AFTER", 6*time.Hour),
|
||||
// Weight defaults are read from the domain formula itself so the shipped
|
||||
// behaviour cannot drift from domain.DefaultAccountRatingWeights().
|
||||
RatingWeightStarsReceivedPermille: envInt64Or("TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE", defaultRatingWeights.StarsReceivedPermille),
|
||||
RatingWeightStarsSpentPermille: envInt64Or("TELESRV_RATING_WEIGHT_STARS_SPENT_PERMILLE", defaultRatingWeights.StarsSpentPermille),
|
||||
RatingWeightMessageSent: envInt64Or("TELESRV_RATING_WEIGHT_MESSAGE_SENT", defaultRatingWeights.PerMessageSent),
|
||||
RatingWeightAccountAgeDay: envInt64Or("TELESRV_RATING_WEIGHT_ACCOUNT_AGE_DAY", defaultRatingWeights.PerAccountAgeDay),
|
||||
RatingWeightGiftReceived: envInt64Or("TELESRV_RATING_WEIGHT_GIFT_RECEIVED", defaultRatingWeights.PerGiftReceived),
|
||||
RatingWeightModerationCase: envInt64Or("TELESRV_RATING_WEIGHT_MODERATION_CASE", defaultRatingWeights.PerModerationCase),
|
||||
RatingWeightScamPenalty: envInt64Or("TELESRV_RATING_WEIGHT_SCAM_PENALTY", defaultRatingWeights.ScamPenalty),
|
||||
RatingWeightFakePenalty: envInt64Or("TELESRV_RATING_WEIGHT_FAKE_PENALTY", defaultRatingWeights.FakePenalty),
|
||||
RatingActivityCap: envInt64Or("TELESRV_RATING_ACTIVITY_CAP", defaultRatingWeights.ActivityCap),
|
||||
CollectibleUsernameURLTemplate: strings.TrimSpace(envAllowEmptyOr("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE", "")),
|
||||
|
||||
// Official verification defaults ship the feature on with the official bar
|
||||
// in place: user accounts are not accepted, a rejection costs a month, and
|
||||
// an applicant can neither flood the queue nor keep an unbounded number of
|
||||
// applications open.
|
||||
VerificationEnabled: envBoolOr("TELESRV_VERIFICATION_ENABLED", true),
|
||||
VerificationAllowUserTargets: envBoolOr("TELESRV_VERIFICATION_ALLOW_USER_TARGETS", false),
|
||||
VerificationRejectCooldown: envDurationOr("TELESRV_VERIFICATION_REJECT_COOLDOWN", 720*time.Hour),
|
||||
VerificationApplyRateLimit: envIntOr("TELESRV_VERIFICATION_APPLY_RATE_LIMIT", 3),
|
||||
VerificationApplyRateWindow: envDurationOr("TELESRV_VERIFICATION_APPLY_RATE_WINDOW", 24*time.Hour),
|
||||
VerificationBotRateLimit: envIntOr("TELESRV_VERIFICATION_BOT_RATE_LIMIT", 30),
|
||||
VerificationBotRateWindow: envDurationOr("TELESRV_VERIFICATION_BOT_RATE_WINDOW", time.Minute),
|
||||
VerificationNotifyInterval: envDurationOr("TELESRV_VERIFICATION_NOTIFY_INTERVAL", 15*time.Second),
|
||||
VerificationNotifyBatch: envIntOr("TELESRV_VERIFICATION_NOTIFY_BATCH", 50),
|
||||
VerificationMaxActivePerUser: envIntOr("TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER", 3),
|
||||
|
||||
BotVerificationEnabled: envBoolOr("TELESRV_BOT_VERIFICATION_ENABLED", true),
|
||||
BotVerificationMaxPerVerifier: envIntOr("TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER", domain.MaxCustomVerificationsPerVerifier),
|
||||
// The applicant budget is deliberately looser than the official one
|
||||
// (TELESRV_VERIFICATION_APPLY_RATE_LIMIT=3): a deployment can run many
|
||||
// verifier bots, and filing with a second company is not a retry of the first.
|
||||
BotVerificationRequestRateLimit: envIntOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", 5),
|
||||
BotVerificationRequestRateWindow: envDurationOr("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", 24*time.Hour),
|
||||
|
||||
GroupCallCheckTTL: envDurationOr("TELESRV_GROUPCALL_CHECK_TTL", 45*time.Second),
|
||||
GroupCallSweepInterval: envDurationOr("TELESRV_GROUPCALL_SWEEP_INTERVAL", 10*time.Second),
|
||||
GroupCallMaxParticipants: envIntOr("TELESRV_GROUPCALL_MAX_PARTICIPANTS", 32),
|
||||
|
|
@ -716,18 +909,54 @@ func Load() (Config, error) {
|
|||
if err := validateLoginEmailConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateRPCResultCacheConfig(cfg); err != nil {
|
||||
if err := validateRPCExecutionConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateStarGiftConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateAccountRatingConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateCollectibleUsernameConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateVerificationConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateAdminRBACConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if err := validateTelegramLoginConfig(cfg); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func normalizeDefaultCountryCode(raw string) (string, error) {
|
||||
code := strings.ToUpper(strings.TrimSpace(raw))
|
||||
if len(code) != 2 || code[0] < 'A' || code[0] > 'Z' || code[1] < 'A' || code[1] > 'Z' {
|
||||
return "", fmt.Errorf("must be a two-letter ISO 3166-1 alpha-2 code")
|
||||
}
|
||||
region, err := language.ParseRegion(code)
|
||||
if err != nil || !region.IsCountry() {
|
||||
return "", fmt.Errorf("must identify a country or autonomous area")
|
||||
}
|
||||
return region.String(), nil
|
||||
}
|
||||
|
||||
func normalizeAdvertiseIP(raw string) (string, error) {
|
||||
addr, err := netip.ParseAddr(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("must be an IPv4 or IPv6 address: %w", err)
|
||||
}
|
||||
addr = addr.Unmap()
|
||||
if addr.IsUnspecified() || addr.IsMulticast() || addr.Zone() != "" {
|
||||
return "", fmt.Errorf("must be a unicast address usable by clients")
|
||||
}
|
||||
return addr.String(), nil
|
||||
}
|
||||
|
||||
func validateTelegramLoginConfig(cfg Config) error {
|
||||
if !cfg.TelegramLoginEnabled {
|
||||
return nil
|
||||
|
|
@ -798,35 +1027,262 @@ func validateStarGiftConfig(cfg Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
const mtProtoRPCResultMinBytes = int64((1 << 24) - (2 << 10))
|
||||
// AccountRatingWeights renders the configured composite rating formula. It is
|
||||
// the single conversion point between env keys and the domain formula, so the
|
||||
// app service and the admin explanation always use the same numbers.
|
||||
func (c Config) AccountRatingWeights() domain.AccountRatingWeights {
|
||||
return domain.AccountRatingWeights{
|
||||
StarsReceivedPermille: c.RatingWeightStarsReceivedPermille,
|
||||
StarsSpentPermille: c.RatingWeightStarsSpentPermille,
|
||||
PerMessageSent: c.RatingWeightMessageSent,
|
||||
PerAccountAgeDay: c.RatingWeightAccountAgeDay,
|
||||
PerGiftReceived: c.RatingWeightGiftReceived,
|
||||
PerModerationCase: c.RatingWeightModerationCase,
|
||||
ScamPenalty: c.RatingWeightScamPenalty,
|
||||
FakePenalty: c.RatingWeightFakePenalty,
|
||||
ActivityCap: c.RatingActivityCap,
|
||||
}
|
||||
}
|
||||
|
||||
func validateRPCResultCacheConfig(cfg Config) error {
|
||||
if cfg.MTProtoRPCResultCacheMaxEntries <= 0 || cfg.MTProtoRPCResultCacheAuthMaxEntries <= 0 ||
|
||||
cfg.MTProtoRPCResultCacheSessionMaxEntries <= 0 {
|
||||
return fmt.Errorf("MTProto rpc_result entry limits must be positive")
|
||||
// validateAccountRatingConfig rejects a formula or worker cadence that cannot
|
||||
// produce a reproducible rating. Weights are validated even when the feature is
|
||||
// disabled: enabling it later must not be the moment a typo is discovered.
|
||||
func validateAccountRatingConfig(cfg Config) error {
|
||||
if err := cfg.AccountRatingWeights().Validate(); err != nil {
|
||||
return fmt.Errorf("TELESRV_RATING_WEIGHT_* and TELESRV_RATING_ACTIVITY_CAP must be non-negative: %w", err)
|
||||
}
|
||||
if cfg.MTProtoRPCResultCacheMaxEntries < cfg.MTProtoRPCResultCacheAuthMaxEntries ||
|
||||
cfg.MTProtoRPCResultCacheAuthMaxEntries < cfg.MTProtoRPCResultCacheSessionMaxEntries {
|
||||
return fmt.Errorf("MTProto rpc_result entry hierarchy must satisfy global >= auth >= session: %d/%d/%d",
|
||||
cfg.MTProtoRPCResultCacheMaxEntries, cfg.MTProtoRPCResultCacheAuthMaxEntries, cfg.MTProtoRPCResultCacheSessionMaxEntries)
|
||||
if cfg.RatingPendingDelay < 0 {
|
||||
return fmt.Errorf("TELESRV_RATING_PENDING_DELAY must be non-negative")
|
||||
}
|
||||
if cfg.MTProtoRPCResultCacheMaxBytes < mtProtoRPCResultMinBytes ||
|
||||
cfg.MTProtoRPCResultCacheAuthMaxBytes < mtProtoRPCResultMinBytes ||
|
||||
cfg.MTProtoRPCResultCacheSessionMaxBytes < mtProtoRPCResultMinBytes {
|
||||
return fmt.Errorf("MTProto rpc_result byte limits must each be at least %d: %d/%d/%d",
|
||||
mtProtoRPCResultMinBytes, cfg.MTProtoRPCResultCacheMaxBytes,
|
||||
cfg.MTProtoRPCResultCacheAuthMaxBytes, cfg.MTProtoRPCResultCacheSessionMaxBytes)
|
||||
const maxRatingPendingDelay = 30 * 24 * time.Hour
|
||||
if cfg.RatingPendingDelay > maxRatingPendingDelay {
|
||||
return fmt.Errorf("TELESRV_RATING_PENDING_DELAY must not exceed 720h")
|
||||
}
|
||||
if cfg.MTProtoRPCResultCacheMaxBytes < cfg.MTProtoRPCResultCacheAuthMaxBytes ||
|
||||
cfg.MTProtoRPCResultCacheAuthMaxBytes < cfg.MTProtoRPCResultCacheSessionMaxBytes {
|
||||
return fmt.Errorf("MTProto rpc_result byte hierarchy must satisfy global >= auth >= session: %d/%d/%d",
|
||||
cfg.MTProtoRPCResultCacheMaxBytes, cfg.MTProtoRPCResultCacheAuthMaxBytes, cfg.MTProtoRPCResultCacheSessionMaxBytes)
|
||||
if cfg.RatingRecomputeInterval <= 0 {
|
||||
return fmt.Errorf("TELESRV_RATING_RECOMPUTE_INTERVAL must be positive")
|
||||
}
|
||||
if cfg.MTProtoRPCGlobalMaxTasks <= 0 || cfg.MTProtoRPCResultPendingPerAuth <= 0 ||
|
||||
cfg.MTProtoRPCResultPendingPerAuth > cfg.MTProtoRPCGlobalMaxTasks ||
|
||||
cfg.MTProtoRPCResultPendingPerAuth > cfg.MTProtoRPCResultCacheAuthMaxEntries {
|
||||
return fmt.Errorf("MTProto rpc_result pending-per-auth %d must be positive and <= global pending %d and auth entries %d",
|
||||
cfg.MTProtoRPCResultPendingPerAuth, cfg.MTProtoRPCGlobalMaxTasks, cfg.MTProtoRPCResultCacheAuthMaxEntries)
|
||||
if cfg.RatingStaleAfter <= 0 {
|
||||
return fmt.Errorf("TELESRV_RATING_STALE_AFTER must be positive")
|
||||
}
|
||||
if cfg.RatingRecomputeBatch <= 0 || cfg.RatingRecomputeBatch > 10000 {
|
||||
return fmt.Errorf("TELESRV_RATING_RECOMPUTE_BATCH must be 1..10000")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// adminPermissionAll is the wildcard permission: a session or token carrying it
|
||||
// may perform every admin action.
|
||||
const adminPermissionAll = "*"
|
||||
|
||||
// validateVerificationConfig rejects a verification policy that cannot be
|
||||
// enforced, for both mechanisms: the operator-granted platform badge and the
|
||||
// third-party bot verification marks. It runs even when either feature is
|
||||
// disabled, so enabling it later is not the moment a typo is discovered.
|
||||
func validateVerificationConfig(cfg Config) error {
|
||||
if cfg.VerificationRejectCooldown < 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_REJECT_COOLDOWN must be non-negative")
|
||||
}
|
||||
const maxVerificationRejectCooldown = 365 * 24 * time.Hour
|
||||
if cfg.VerificationRejectCooldown > maxVerificationRejectCooldown {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_REJECT_COOLDOWN must not exceed 8760h")
|
||||
}
|
||||
if cfg.VerificationApplyRateLimit < 0 || cfg.VerificationBotRateLimit < 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_APPLY_RATE_LIMIT and TELESRV_VERIFICATION_BOT_RATE_LIMIT must be non-negative")
|
||||
}
|
||||
if cfg.VerificationApplyRateWindow < 0 || cfg.VerificationBotRateWindow < 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_APPLY_RATE_WINDOW and TELESRV_VERIFICATION_BOT_RATE_WINDOW must be non-negative")
|
||||
}
|
||||
// A positive limit with a zero window is not "unlimited", it is a limiter that
|
||||
// can never refill: reject it instead of shipping a permanent lockout.
|
||||
if cfg.VerificationApplyRateLimit > 0 && cfg.VerificationApplyRateWindow <= 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_APPLY_RATE_WINDOW must be positive when TELESRV_VERIFICATION_APPLY_RATE_LIMIT is set")
|
||||
}
|
||||
if cfg.VerificationBotRateLimit > 0 && cfg.VerificationBotRateWindow <= 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_BOT_RATE_WINDOW must be positive when TELESRV_VERIFICATION_BOT_RATE_LIMIT is set")
|
||||
}
|
||||
if cfg.VerificationNotifyInterval <= 0 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_NOTIFY_INTERVAL must be positive")
|
||||
}
|
||||
if cfg.VerificationNotifyBatch <= 0 || cfg.VerificationNotifyBatch > 500 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_NOTIFY_BATCH must be 1..500")
|
||||
}
|
||||
if cfg.VerificationMaxActivePerUser < 0 || cfg.VerificationMaxActivePerUser > 50 {
|
||||
return fmt.Errorf("TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER must be 0..50")
|
||||
}
|
||||
// Third-party bot verification. The ceiling is the storage bound: a value above
|
||||
// it would be silently unreachable, and a configuration key that cannot do what
|
||||
// it says is worse than no key.
|
||||
if cfg.BotVerificationMaxPerVerifier < 0 {
|
||||
return fmt.Errorf("TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER must be non-negative")
|
||||
}
|
||||
if cfg.BotVerificationMaxPerVerifier > domain.MaxCustomVerificationsPerVerifier {
|
||||
return fmt.Errorf("TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER must not exceed %d", domain.MaxCustomVerificationsPerVerifier)
|
||||
}
|
||||
if cfg.BotVerificationRequestRateLimit < 0 {
|
||||
return fmt.Errorf("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT must be non-negative")
|
||||
}
|
||||
if cfg.BotVerificationRequestRateWindow < 0 {
|
||||
return fmt.Errorf("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW must be non-negative")
|
||||
}
|
||||
// Same trap as the official budget above: a positive limit with a zero window is
|
||||
// not "unlimited", it is a limiter that can never refill.
|
||||
if cfg.BotVerificationRequestRateLimit > 0 && cfg.BotVerificationRequestRateWindow <= 0 {
|
||||
return fmt.Errorf("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW must be positive when TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT is set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAdminRBACConfig checks the panel/adminapi permission configuration.
|
||||
// An unparsable permission name is refused rather than ignored: a silently
|
||||
// dropped permission is either a lockout or an unintended grant.
|
||||
func validateAdminRBACConfig(cfg Config) error {
|
||||
if len(cfg.AdminUIPermissions) == 0 {
|
||||
return fmt.Errorf("TELESRV_ADMIN_UI_PERMISSIONS must not be empty; use * to grant every permission")
|
||||
}
|
||||
for _, permission := range cfg.AdminUIPermissions {
|
||||
if !validAdminPermission(permission) {
|
||||
return fmt.Errorf("TELESRV_ADMIN_UI_PERMISSIONS contains invalid permission %q", permission)
|
||||
}
|
||||
}
|
||||
names := make(map[string]struct{}, len(cfg.AdminScopedTokens))
|
||||
tokens := make(map[string]struct{}, len(cfg.AdminScopedTokens))
|
||||
for _, scoped := range cfg.AdminScopedTokens {
|
||||
if _, dup := names[strings.ToLower(scoped.Name)]; dup {
|
||||
return fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS has duplicate name %q", scoped.Name)
|
||||
}
|
||||
names[strings.ToLower(scoped.Name)] = struct{}{}
|
||||
if _, dup := tokens[scoped.Token]; dup {
|
||||
return fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS reuses one token for several names")
|
||||
}
|
||||
tokens[scoped.Token] = struct{}{}
|
||||
if scoped.Token == cfg.AdminAPIToken && strings.TrimSpace(cfg.AdminAPIToken) != "" {
|
||||
return fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q reuses TELESRV_ADMIN_API_TOKEN, which would silently widen it to every permission", scoped.Name)
|
||||
}
|
||||
if len(scoped.Permissions) == 0 {
|
||||
return fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q has no permissions", scoped.Name)
|
||||
}
|
||||
for _, permission := range scoped.Permissions {
|
||||
if !validAdminPermission(permission) {
|
||||
return fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q has invalid permission %q", scoped.Name, permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseAdminScopedTokens reads "name:token:perm1,perm2" entries separated by ';'.
|
||||
//
|
||||
// The shape is strict on purpose: the value carries credentials, and a
|
||||
// half-understood entry must fail startup rather than produce a token whose
|
||||
// rights nobody can predict. The permission list is the last field, so a token
|
||||
// itself may not contain ':' -- which is also why it is validated here rather
|
||||
// than being re-split later by a consumer.
|
||||
func parseAdminScopedTokens(raw string) ([]AdminScopedToken, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]AdminScopedToken, 0, 4)
|
||||
for _, entry := range strings.Split(raw, ";") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(entry, ":")
|
||||
if len(parts) != 3 {
|
||||
return nil, fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q must be name:token:perm1,perm2", entry)
|
||||
}
|
||||
name := strings.TrimSpace(parts[0])
|
||||
token := strings.TrimSpace(parts[1])
|
||||
if name == "" || token == "" {
|
||||
return nil, fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q must carry a non-empty name and token", entry)
|
||||
}
|
||||
if strings.ContainsAny(token, " \t\r\n") {
|
||||
return nil, fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q has whitespace inside its token", name)
|
||||
}
|
||||
permissions := make([]string, 0, 4)
|
||||
for _, permission := range strings.Split(parts[2], ",") {
|
||||
permission = strings.TrimSpace(permission)
|
||||
if permission == "" {
|
||||
continue
|
||||
}
|
||||
permissions = append(permissions, permission)
|
||||
}
|
||||
if len(permissions) == 0 {
|
||||
return nil, fmt.Errorf("TELESRV_ADMIN_SCOPED_TOKENS entry %q must list at least one permission", name)
|
||||
}
|
||||
out = append(out, AdminScopedToken{Name: name, Token: token, Permissions: permissions})
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// validAdminPermission accepts the wildcard and dotted/namespaced permission
|
||||
// names such as "users.read" or "verification:decide".
|
||||
func validAdminPermission(permission string) bool {
|
||||
if permission == adminPermissionAll {
|
||||
return true
|
||||
}
|
||||
if permission == "" || len(permission) > 64 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(permission); i++ {
|
||||
c := permission[i]
|
||||
switch {
|
||||
case c >= 'a' && c <= 'z':
|
||||
case c >= 'A' && c <= 'Z':
|
||||
case c >= '0' && c <= '9':
|
||||
case c == '_' || c == '-':
|
||||
case (c == '.' || c == ':') && i != 0 && i != len(permission)-1:
|
||||
case c == '*' && i == len(permission)-1 && i > 0 && (permission[i-1] == '.' || permission[i-1] == ':'):
|
||||
// A trailing "namespace.*" grants a whole namespace.
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// validateCollectibleUsernameConfig checks the optional mint URL template. An
|
||||
// empty template is the documented default (the public-link route is derived
|
||||
// from TELESRV_PUBLIC_BASE_URL); a configured one must be a client-openable
|
||||
// absolute http(s) URL that still fits the registry's url column.
|
||||
func validateCollectibleUsernameConfig(cfg Config) error {
|
||||
template := strings.TrimSpace(cfg.CollectibleUsernameURLTemplate)
|
||||
if template == "" {
|
||||
return nil
|
||||
}
|
||||
if len(template)+domain.MaxCollectibleUsernameLength > domain.MaxCollectibleUsernameURLLength {
|
||||
return fmt.Errorf("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE is too long to hold a rendered username")
|
||||
}
|
||||
// Render the placeholder before parsing so a templated path segment is
|
||||
// validated in its final shape.
|
||||
rendered := strings.ReplaceAll(template, "{username}", "username")
|
||||
parsed, err := url.Parse(rendered)
|
||||
if err != nil || parsed.Host == "" || parsed.User != nil ||
|
||||
(parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return fmt.Errorf("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE must be an absolute http(s) URL without userinfo")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRPCExecutionConfig(cfg Config) error {
|
||||
if cfg.MTProtoRPCExecutionMaxEntries <= 0 || cfg.MTProtoRPCExecutionAuthMaxEntries <= 0 ||
|
||||
cfg.MTProtoRPCExecutionSessionMaxEntries <= 0 {
|
||||
return fmt.Errorf("MTProto rpc execution entry limits must be positive")
|
||||
}
|
||||
if cfg.MTProtoRPCExecutionMaxEntries < cfg.MTProtoRPCExecutionAuthMaxEntries ||
|
||||
cfg.MTProtoRPCExecutionAuthMaxEntries < cfg.MTProtoRPCExecutionSessionMaxEntries {
|
||||
return fmt.Errorf("MTProto rpc execution entry hierarchy must satisfy global >= auth >= session: %d/%d/%d",
|
||||
cfg.MTProtoRPCExecutionMaxEntries, cfg.MTProtoRPCExecutionAuthMaxEntries, cfg.MTProtoRPCExecutionSessionMaxEntries)
|
||||
}
|
||||
if cfg.MTProtoRPCGlobalMaxTasks <= 0 || cfg.MTProtoRPCExecutionPendingPerAuth <= 0 ||
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth > cfg.MTProtoRPCGlobalMaxTasks ||
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth > cfg.MTProtoRPCExecutionAuthMaxEntries {
|
||||
return fmt.Errorf("MTProto rpc execution pending-per-auth %d must be positive and <= global pending %d and auth entries %d",
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth, cfg.MTProtoRPCGlobalMaxTasks, cfg.MTProtoRPCExecutionAuthMaxEntries)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1139,6 +1595,43 @@ func (e envSource) envIntOr(key string, def int) int {
|
|||
return def
|
||||
}
|
||||
|
||||
func validateStrictMTProtoCapacityEnv(e envSource) error {
|
||||
for _, key := range []string{
|
||||
"TELESRV_MTPROTO_MAX_CONNECTIONS",
|
||||
"TELESRV_MTPROTO_MAX_CONNECTIONS_PER_IP",
|
||||
"TELESRV_MTPROTO_MAX_CONCURRENT_HANDSHAKES",
|
||||
"TELESRV_MTPROTO_RPC_MAX_INFLIGHT",
|
||||
"TELESRV_MTPROTO_RPC_QUEUE_SIZE",
|
||||
"TELESRV_MTPROTO_RPC_GLOBAL_WORKERS",
|
||||
"TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS",
|
||||
"TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES",
|
||||
"TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES",
|
||||
"TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES",
|
||||
"TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH",
|
||||
"TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE",
|
||||
"TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE",
|
||||
} {
|
||||
if raw := e.envOr(key, ""); raw != "" {
|
||||
if _, err := strconv.Atoi(raw); err != nil {
|
||||
return fmt.Errorf("%s must be a base-10 integer: %w", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, key := range []string{
|
||||
"TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES",
|
||||
"TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES",
|
||||
"TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES",
|
||||
"TELESRV_MTPROTO_OUTBOUND_WRITE_GLOBAL_MAX_BYTES",
|
||||
} {
|
||||
if raw := e.envOr(key, ""); raw != "" {
|
||||
if _, err := strconv.ParseInt(raw, 10, 64); err != nil {
|
||||
return fmt.Errorf("%s must be a base-10 int64: %w", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e envSource) envInt64Or(key string, def int64) int64 {
|
||||
if v := e.envOr(key, ""); v != "" {
|
||||
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
|
||||
|
|
@ -28,12 +30,31 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
|
|||
if cfg.PublicAppLinkBase != "" {
|
||||
t.Fatalf("PublicAppLinkBase = %q, want disabled", cfg.PublicAppLinkBase)
|
||||
}
|
||||
if cfg.PublicWebBaseURL != "https://web.telesrv.net" {
|
||||
t.Fatalf("PublicWebBaseURL = %q, want https://web.telesrv.net", cfg.PublicWebBaseURL)
|
||||
if cfg.PublicWebBaseURL != "https://weba.telesrv.net" {
|
||||
t.Fatalf("PublicWebBaseURL = %q, want https://weba.telesrv.net", cfg.PublicWebBaseURL)
|
||||
}
|
||||
if cfg.PublicAppName != "telesrv" {
|
||||
t.Fatalf("PublicAppName = %q, want telesrv", cfg.PublicAppName)
|
||||
}
|
||||
if cfg.CallRegistryMaxEntries != 10_000 {
|
||||
t.Fatalf("CallRegistryMaxEntries = %d, want 10000", cfg.CallRegistryMaxEntries)
|
||||
}
|
||||
if cfg.PremiumPromoSeedDir != "data/premium-promo" {
|
||||
t.Fatalf("PremiumPromoSeedDir = %q, want data/premium-promo", cfg.PremiumPromoSeedDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPremiumPromoSeedDirOverride(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_PREMIUM_PROMO_SEED_DIR", `D:\seed\premium-promo`)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.PremiumPromoSeedDir != `D:\seed\premium-promo` {
|
||||
t.Fatalf("PremiumPromoSeedDir = %q", cfg.PremiumPromoSeedDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesExplicitAdvertiseIP(t *testing.T) {
|
||||
|
|
@ -49,6 +70,101 @@ func TestLoadUsesExplicitAdvertiseIP(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadCanonicalizesAdvertiseIP(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_ADVERTISE_IP", " 2001:0db8::1 ")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.AdvertiseIP != "2001:db8::1" {
|
||||
t.Fatalf("AdvertiseIP = %q, want canonical IPv6", cfg.AdvertiseIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsUnusableAdvertiseIP(t *testing.T) {
|
||||
for _, value := range []string{"example.com", "0.0.0.0", "::", "224.0.0.1", "fe80::1%eth0"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_ADVERTISE_IP", value)
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted TELESRV_ADVERTISE_IP=%q", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDefaultCountryCode(t *testing.T) {
|
||||
t.Run("default", func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", "")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.DefaultCountryCode != "CN" {
|
||||
t.Fatalf("DefaultCountryCode = %q, want CN", cfg.DefaultCountryCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("normalized override", func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", " us ")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.DefaultCountryCode != "US" {
|
||||
t.Fatalf("DefaultCountryCode = %q, want US", cfg.DefaultCountryCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidDefaultCountryCode(t *testing.T) {
|
||||
for _, value := range []string{"+86", "CHN", "C1", "中", "ZZ"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_DEFAULT_COUNTRY_CODE", value)
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted TELESRV_DEFAULT_COUNTRY_CODE=%q", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadStrictDCCheck(t *testing.T) {
|
||||
t.Run("defaults off", func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_STRICT_DC_CHECK", "")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.StrictDCCheck {
|
||||
t.Fatal("StrictDCCheck = true, want default false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("explicitly enabled", func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_STRICT_DC_CHECK", "true")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if !cfg.StrictDCCheck {
|
||||
t.Fatal("StrictDCCheck = false, want true")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_MTPROTO_MAX_CONNECTIONS", "12345")
|
||||
|
|
@ -60,13 +176,10 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
|
|||
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_WORKERS", "33")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", "444")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_GLOBAL_MAX_BYTES", "555555")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_ENTRIES", "555")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_BYTES", "70000000")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_ENTRIES", "444")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_BYTES", "40000000")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_ENTRIES", "333")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_BYTES", "20000000")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_RESULT_PENDING_PER_AUTH", "222")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", "555")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_AUTH_MAX_ENTRIES", "444")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_SESSION_MAX_ENTRIES", "333")
|
||||
t.Setenv("TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH", "222")
|
||||
t.Setenv("TELESRV_MTPROTO_INBOUND_FRAME_GLOBAL_MAX_BYTES", "777777")
|
||||
t.Setenv("TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE", "88")
|
||||
t.Setenv("TELESRV_MTPROTO_OUTBOUND_CONTROL_QUEUE_SIZE", "22")
|
||||
|
|
@ -87,15 +200,15 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
|
|||
cfg.MTProtoRPCGlobalWorkers != 33 || cfg.MTProtoRPCGlobalMaxTasks != 444 || cfg.MTProtoRPCGlobalMaxBytes != 555555 {
|
||||
t.Fatalf("rpc budget config = %d/%d/%v/%d/%d/%d", cfg.MTProtoRPCMaxInflight, cfg.MTProtoRPCQueueSize, cfg.MTProtoRPCTimeout, cfg.MTProtoRPCGlobalWorkers, cfg.MTProtoRPCGlobalMaxTasks, cfg.MTProtoRPCGlobalMaxBytes)
|
||||
}
|
||||
if cfg.MTProtoRPCResultCacheMaxEntries != 555 || cfg.MTProtoRPCResultCacheMaxBytes != 70000000 ||
|
||||
cfg.MTProtoRPCResultCacheAuthMaxEntries != 444 || cfg.MTProtoRPCResultCacheAuthMaxBytes != 40000000 ||
|
||||
cfg.MTProtoRPCResultCacheSessionMaxEntries != 333 || cfg.MTProtoRPCResultCacheSessionMaxBytes != 20000000 ||
|
||||
cfg.MTProtoRPCResultPendingPerAuth != 222 {
|
||||
t.Fatalf("rpc result cache config = global:%d/%d auth:%d/%d session:%d/%d pending/auth:%d",
|
||||
cfg.MTProtoRPCResultCacheMaxEntries, cfg.MTProtoRPCResultCacheMaxBytes,
|
||||
cfg.MTProtoRPCResultCacheAuthMaxEntries, cfg.MTProtoRPCResultCacheAuthMaxBytes,
|
||||
cfg.MTProtoRPCResultCacheSessionMaxEntries, cfg.MTProtoRPCResultCacheSessionMaxBytes,
|
||||
cfg.MTProtoRPCResultPendingPerAuth)
|
||||
if cfg.MTProtoRPCExecutionMaxEntries != 555 ||
|
||||
cfg.MTProtoRPCExecutionAuthMaxEntries != 444 ||
|
||||
cfg.MTProtoRPCExecutionSessionMaxEntries != 333 ||
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth != 222 {
|
||||
t.Fatalf("rpc execution ledger config = global:%d auth:%d session:%d pending/auth:%d",
|
||||
cfg.MTProtoRPCExecutionMaxEntries,
|
||||
cfg.MTProtoRPCExecutionAuthMaxEntries,
|
||||
cfg.MTProtoRPCExecutionSessionMaxEntries,
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth)
|
||||
}
|
||||
if cfg.MTProtoInboundFrameGlobalMaxBytes != 777777 {
|
||||
t.Fatalf("inbound frame budget config = %d", cfg.MTProtoInboundFrameGlobalMaxBytes)
|
||||
|
|
@ -108,34 +221,32 @@ func TestLoadMTProtoAdmissionAndRPCBudgets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadRPCResultFairBudgetDefaults(t *testing.T) {
|
||||
func TestLoadRPCExecutionFairBudgetDefaults(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.MTProtoRPCResultCacheMaxEntries != 1<<18 || cfg.MTProtoRPCResultCacheMaxBytes != 64<<20 ||
|
||||
cfg.MTProtoRPCResultCacheAuthMaxEntries != 1<<15 || cfg.MTProtoRPCResultCacheAuthMaxBytes != 32<<20 ||
|
||||
cfg.MTProtoRPCResultCacheSessionMaxEntries != 1<<14 || cfg.MTProtoRPCResultCacheSessionMaxBytes != 16<<20 ||
|
||||
cfg.MTProtoRPCResultPendingPerAuth != 1<<11 {
|
||||
t.Fatalf("rpc_result fair defaults = global:%d/%d auth:%d/%d session:%d/%d pending/auth:%d",
|
||||
cfg.MTProtoRPCResultCacheMaxEntries, cfg.MTProtoRPCResultCacheMaxBytes,
|
||||
cfg.MTProtoRPCResultCacheAuthMaxEntries, cfg.MTProtoRPCResultCacheAuthMaxBytes,
|
||||
cfg.MTProtoRPCResultCacheSessionMaxEntries, cfg.MTProtoRPCResultCacheSessionMaxBytes,
|
||||
cfg.MTProtoRPCResultPendingPerAuth)
|
||||
if cfg.MTProtoRPCExecutionMaxEntries != 1<<18 ||
|
||||
cfg.MTProtoRPCExecutionAuthMaxEntries != 1<<15 ||
|
||||
cfg.MTProtoRPCExecutionSessionMaxEntries != 1<<14 ||
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth != 1<<11 {
|
||||
t.Fatalf("rpc execution receipt defaults = global:%d auth:%d session:%d pending/auth:%d",
|
||||
cfg.MTProtoRPCExecutionMaxEntries,
|
||||
cfg.MTProtoRPCExecutionAuthMaxEntries,
|
||||
cfg.MTProtoRPCExecutionSessionMaxEntries,
|
||||
cfg.MTProtoRPCExecutionPendingPerAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidRPCResultFairBudgets(t *testing.T) {
|
||||
func TestLoadRejectsInvalidRPCExecutionFairBudgets(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{name: "entry hierarchy", key: "TELESRV_MTPROTO_RPC_RESULT_CACHE_MAX_ENTRIES", value: "1024"},
|
||||
{name: "byte below outbound body", key: "TELESRV_MTPROTO_RPC_RESULT_CACHE_SESSION_MAX_BYTES", value: "16700000"},
|
||||
{name: "byte hierarchy", key: "TELESRV_MTPROTO_RPC_RESULT_CACHE_AUTH_MAX_BYTES", value: "70000000"},
|
||||
{name: "pending hierarchy", key: "TELESRV_MTPROTO_RPC_RESULT_PENDING_PER_AUTH", value: "9000"},
|
||||
{name: "entry hierarchy", key: "TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", value: "1024"},
|
||||
{name: "pending hierarchy", key: "TELESRV_MTPROTO_RPC_EXECUTION_PENDING_PER_AUTH", value: "9000"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
|
@ -148,6 +259,27 @@ func TestLoadRejectsInvalidRPCResultFairBudgets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsMalformedMTProtoCapacity(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{name: "worker tasks malformed", key: "TELESRV_MTPROTO_RPC_GLOBAL_MAX_TASKS", value: "lots"},
|
||||
{name: "receipt entries overflow", key: "TELESRV_MTPROTO_RPC_EXECUTION_MAX_ENTRIES", value: "999999999999999999999999"},
|
||||
{name: "tracked bytes overflow", key: "TELESRV_MTPROTO_OUTBOUND_TRACKED_GLOBAL_MAX_BYTES", value: "999999999999999999999999"},
|
||||
{name: "outbound queue malformed", key: "TELESRV_MTPROTO_OUTBOUND_QUEUE_SIZE", value: "many"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv(test.key, test.value)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted %s=%q", test.key, test.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOutboxPoisonPolicy(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_OUTBOX_POISON_RETENTION", "2m")
|
||||
|
|
@ -666,6 +798,359 @@ func TestValidateStarGiftConfigRejectsNegativeInternalTONGrant(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadAccountRatingDefaults(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if !cfg.RatingEnabled {
|
||||
t.Fatal("RatingEnabled = false, want the feature on by default")
|
||||
}
|
||||
if cfg.RatingPendingDelay != 24*time.Hour || cfg.RatingRecomputeInterval != 15*time.Minute ||
|
||||
cfg.RatingRecomputeBatch != 500 || cfg.RatingStaleAfter != 6*time.Hour {
|
||||
t.Fatalf("rating worker defaults = %v/%v/%d/%v, want 24h/15m/500/6h",
|
||||
cfg.RatingPendingDelay, cfg.RatingRecomputeInterval, cfg.RatingRecomputeBatch, cfg.RatingStaleAfter)
|
||||
}
|
||||
if got, want := cfg.AccountRatingWeights(), domain.DefaultAccountRatingWeights(); got != want {
|
||||
t.Fatalf("rating weights = %#v, want the domain defaults %#v", got, want)
|
||||
}
|
||||
if cfg.CollectibleUsernameURLTemplate != "" {
|
||||
t.Fatalf("CollectibleUsernameURLTemplate = %q, want empty (derived from the public base URL)",
|
||||
cfg.CollectibleUsernameURLTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAccountRatingOverrides(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_RATING_ENABLED", "false")
|
||||
t.Setenv("TELESRV_RATING_PENDING_DELAY", "1h")
|
||||
t.Setenv("TELESRV_RATING_RECOMPUTE_INTERVAL", "90s")
|
||||
t.Setenv("TELESRV_RATING_RECOMPUTE_BATCH", "42")
|
||||
t.Setenv("TELESRV_RATING_STALE_AFTER", "30m")
|
||||
t.Setenv("TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE", "500")
|
||||
t.Setenv("TELESRV_RATING_WEIGHT_MESSAGE_SENT", "0")
|
||||
t.Setenv("TELESRV_RATING_ACTIVITY_CAP", "0")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.RatingEnabled {
|
||||
t.Fatal("RatingEnabled = true, want the explicit override")
|
||||
}
|
||||
weights := cfg.AccountRatingWeights()
|
||||
if weights.StarsReceivedPermille != 500 || weights.PerMessageSent != 0 || weights.ActivityCap != 0 {
|
||||
t.Fatalf("weights = %#v, want the overridden values", weights)
|
||||
}
|
||||
if weights.StarsSpentPermille != domain.DefaultAccountRatingWeights().StarsSpentPermille {
|
||||
t.Fatalf("unset weight = %d, want the domain default", weights.StarsSpentPermille)
|
||||
}
|
||||
if cfg.RatingPendingDelay != time.Hour || cfg.RatingRecomputeInterval != 90*time.Second ||
|
||||
cfg.RatingRecomputeBatch != 42 || cfg.RatingStaleAfter != 30*time.Minute {
|
||||
t.Fatalf("rating worker overrides = %v/%v/%d/%v",
|
||||
cfg.RatingPendingDelay, cfg.RatingRecomputeInterval, cfg.RatingRecomputeBatch, cfg.RatingStaleAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidAccountRatingConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{name: "negative stars weight", key: "TELESRV_RATING_WEIGHT_STARS_RECEIVED_PERMILLE", value: "-1"},
|
||||
{name: "negative moderation weight", key: "TELESRV_RATING_WEIGHT_MODERATION_CASE", value: "-150"},
|
||||
{name: "negative scam penalty", key: "TELESRV_RATING_WEIGHT_SCAM_PENALTY", value: "-1"},
|
||||
{name: "negative activity cap", key: "TELESRV_RATING_ACTIVITY_CAP", value: "-5000"},
|
||||
{name: "negative pending delay", key: "TELESRV_RATING_PENDING_DELAY", value: "-1h"},
|
||||
{name: "zero recompute interval", key: "TELESRV_RATING_RECOMPUTE_INTERVAL", value: "0s"},
|
||||
{name: "zero stale horizon", key: "TELESRV_RATING_STALE_AFTER", value: "0s"},
|
||||
{name: "zero recompute batch", key: "TELESRV_RATING_RECOMPUTE_BATCH", value: "0"},
|
||||
{name: "oversized recompute batch", key: "TELESRV_RATING_RECOMPUTE_BATCH", value: "20000"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv(test.key, test.value)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted invalid %s=%s", test.key, test.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCollectibleUsernameURLTemplate(t *testing.T) {
|
||||
t.Run("absolute template accepted", func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE", " https://frag.example/u/{username} ")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.CollectibleUsernameURLTemplate != "https://frag.example/u/{username}" {
|
||||
t.Fatalf("template = %q, want the trimmed value", cfg.CollectibleUsernameURLTemplate)
|
||||
}
|
||||
})
|
||||
|
||||
for _, invalid := range []string{"/nft/{username}", "ftp://frag.example/{username}", "https://user:pass@frag.example/{username}"} {
|
||||
t.Run("rejects "+invalid, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_COLLECTIBLE_USERNAME_URL_TEMPLATE", invalid)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted template %q", invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVerificationDefaults(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if !cfg.VerificationEnabled {
|
||||
t.Fatal("VerificationEnabled = false, want the feature shipped on")
|
||||
}
|
||||
if cfg.VerificationAllowUserTargets {
|
||||
t.Fatal("VerificationAllowUserTargets = true, want user targets opt-in")
|
||||
}
|
||||
if cfg.VerificationRejectCooldown != 720*time.Hour {
|
||||
t.Fatalf("VerificationRejectCooldown = %v, want 720h", cfg.VerificationRejectCooldown)
|
||||
}
|
||||
if cfg.VerificationApplyRateLimit != 3 || cfg.VerificationApplyRateWindow != 24*time.Hour {
|
||||
t.Fatalf("apply rate = %d/%v, want 3/24h", cfg.VerificationApplyRateLimit, cfg.VerificationApplyRateWindow)
|
||||
}
|
||||
if cfg.VerificationBotRateLimit != 30 || cfg.VerificationBotRateWindow != time.Minute {
|
||||
t.Fatalf("bot rate = %d/%v, want 30/1m", cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow)
|
||||
}
|
||||
if cfg.VerificationNotifyInterval != 15*time.Second || cfg.VerificationNotifyBatch != 50 {
|
||||
t.Fatalf("notify = %v/%d, want 15s/50", cfg.VerificationNotifyInterval, cfg.VerificationNotifyBatch)
|
||||
}
|
||||
if cfg.VerificationMaxActivePerUser != 3 {
|
||||
t.Fatalf("VerificationMaxActivePerUser = %d, want 3", cfg.VerificationMaxActivePerUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVerificationOverrides(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_VERIFICATION_ENABLED", "false")
|
||||
t.Setenv("TELESRV_VERIFICATION_ALLOW_USER_TARGETS", "true")
|
||||
t.Setenv("TELESRV_VERIFICATION_REJECT_COOLDOWN", "48h")
|
||||
t.Setenv("TELESRV_VERIFICATION_APPLY_RATE_LIMIT", "7")
|
||||
t.Setenv("TELESRV_VERIFICATION_APPLY_RATE_WINDOW", "12h")
|
||||
t.Setenv("TELESRV_VERIFICATION_BOT_RATE_LIMIT", "0")
|
||||
t.Setenv("TELESRV_VERIFICATION_BOT_RATE_WINDOW", "0s")
|
||||
t.Setenv("TELESRV_VERIFICATION_NOTIFY_INTERVAL", "5s")
|
||||
t.Setenv("TELESRV_VERIFICATION_NOTIFY_BATCH", "200")
|
||||
t.Setenv("TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER", "0")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.VerificationEnabled || !cfg.VerificationAllowUserTargets {
|
||||
t.Fatalf("enabled=%v allowUserTargets=%v", cfg.VerificationEnabled, cfg.VerificationAllowUserTargets)
|
||||
}
|
||||
if cfg.VerificationRejectCooldown != 48*time.Hour {
|
||||
t.Fatalf("cooldown = %v", cfg.VerificationRejectCooldown)
|
||||
}
|
||||
if cfg.VerificationApplyRateLimit != 7 || cfg.VerificationApplyRateWindow != 12*time.Hour {
|
||||
t.Fatalf("apply rate = %d/%v", cfg.VerificationApplyRateLimit, cfg.VerificationApplyRateWindow)
|
||||
}
|
||||
// A zero limit disables the budget, so a zero window is accepted with it.
|
||||
if cfg.VerificationBotRateLimit != 0 || cfg.VerificationBotRateWindow != 0 {
|
||||
t.Fatalf("bot rate = %d/%v", cfg.VerificationBotRateLimit, cfg.VerificationBotRateWindow)
|
||||
}
|
||||
if cfg.VerificationNotifyInterval != 5*time.Second || cfg.VerificationNotifyBatch != 200 {
|
||||
t.Fatalf("notify = %v/%d", cfg.VerificationNotifyInterval, cfg.VerificationNotifyBatch)
|
||||
}
|
||||
if cfg.VerificationMaxActivePerUser != 0 {
|
||||
t.Fatalf("maxActive = %d, want the cap disabled", cfg.VerificationMaxActivePerUser)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidVerificationConfig(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{"TELESRV_VERIFICATION_REJECT_COOLDOWN", "-1h"},
|
||||
{"TELESRV_VERIFICATION_REJECT_COOLDOWN", "9000h"},
|
||||
{"TELESRV_VERIFICATION_APPLY_RATE_LIMIT", "-1"},
|
||||
{"TELESRV_VERIFICATION_APPLY_RATE_WINDOW", "0s"},
|
||||
{"TELESRV_VERIFICATION_APPLY_RATE_WINDOW", "-5m"},
|
||||
{"TELESRV_VERIFICATION_BOT_RATE_LIMIT", "-2"},
|
||||
{"TELESRV_VERIFICATION_BOT_RATE_WINDOW", "0s"},
|
||||
{"TELESRV_VERIFICATION_NOTIFY_INTERVAL", "0s"},
|
||||
{"TELESRV_VERIFICATION_NOTIFY_BATCH", "0"},
|
||||
{"TELESRV_VERIFICATION_NOTIFY_BATCH", "501"},
|
||||
{"TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER", "-1"},
|
||||
{"TELESRV_VERIFICATION_MAX_ACTIVE_PER_USER", "51"},
|
||||
} {
|
||||
t.Run(test.key+"="+test.value, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv(test.key, test.value)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted invalid %s=%s", test.key, test.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBotVerificationDefaults(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if !cfg.BotVerificationEnabled {
|
||||
t.Fatal("BotVerificationEnabled = false, want the feature shipped on")
|
||||
}
|
||||
// The default is the storage bound itself, so the shipped behaviour is the same
|
||||
// whether or not the key is set.
|
||||
if cfg.BotVerificationMaxPerVerifier != domain.MaxCustomVerificationsPerVerifier {
|
||||
t.Fatalf("BotVerificationMaxPerVerifier = %d, want %d", cfg.BotVerificationMaxPerVerifier, domain.MaxCustomVerificationsPerVerifier)
|
||||
}
|
||||
if cfg.BotVerificationRequestRateLimit != 5 || cfg.BotVerificationRequestRateWindow != 24*time.Hour {
|
||||
t.Fatalf("request rate = %d/%v, want 5/24h", cfg.BotVerificationRequestRateLimit, cfg.BotVerificationRequestRateWindow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBotVerificationOverrides(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_ENABLED", "false")
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER", "0")
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", "0")
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", "0s")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.BotVerificationEnabled {
|
||||
t.Fatal("BotVerificationEnabled = true, want the override honoured")
|
||||
}
|
||||
if cfg.BotVerificationMaxPerVerifier != 0 {
|
||||
t.Fatalf("BotVerificationMaxPerVerifier = %d, want the service bound disabled", cfg.BotVerificationMaxPerVerifier)
|
||||
}
|
||||
// A zero limit disables the budget, so a zero window is accepted with it.
|
||||
if cfg.BotVerificationRequestRateLimit != 0 || cfg.BotVerificationRequestRateWindow != 0 {
|
||||
t.Fatalf("request rate = %d/%v, want the budget disabled", cfg.BotVerificationRequestRateLimit, cfg.BotVerificationRequestRateWindow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidBotVerificationConfig(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{"TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER", "-1"},
|
||||
{"TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER", "10001"},
|
||||
{"TELESRV_BOT_VERIFICATION_REQUEST_RATE_LIMIT", "-1"},
|
||||
{"TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", "-5m"},
|
||||
// A positive limit with no window is a limiter that never refills.
|
||||
{"TELESRV_BOT_VERIFICATION_REQUEST_RATE_WINDOW", "0s"},
|
||||
} {
|
||||
t.Run(test.key+"="+test.value, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv(test.key, test.value)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted invalid %s=%s", test.key, test.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadValidatesBotVerificationWhileDisabled pins that the policy is checked
|
||||
// even with the feature off, so switching it on later is not the moment a typo is
|
||||
// discovered.
|
||||
func TestLoadValidatesBotVerificationWhileDisabled(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_ENABLED", "false")
|
||||
t.Setenv("TELESRV_BOT_VERIFICATION_MAX_PER_VERIFIER", "-3")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("Load accepted a negative per-verifier bound while the feature was disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAdminRBACDefaults(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(cfg.AdminUIPermissions) != 1 || cfg.AdminUIPermissions[0] != "*" {
|
||||
t.Fatalf("AdminUIPermissions = %v, want the wildcard default", cfg.AdminUIPermissions)
|
||||
}
|
||||
if len(cfg.AdminScopedTokens) != 0 {
|
||||
t.Fatalf("AdminScopedTokens = %+v, want none by default", cfg.AdminScopedTokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadAdminScopedTokens(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_ADMIN_UI_PERMISSIONS", "users.read, verification:decide")
|
||||
t.Setenv("TELESRV_ADMIN_SCOPED_TOKENS", "ops:tok-ops-1:users.read,users.write; audit:tok-audit-2:verification.*")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if len(cfg.AdminUIPermissions) != 2 ||
|
||||
cfg.AdminUIPermissions[0] != "users.read" || cfg.AdminUIPermissions[1] != "verification:decide" {
|
||||
t.Fatalf("AdminUIPermissions = %v", cfg.AdminUIPermissions)
|
||||
}
|
||||
if len(cfg.AdminScopedTokens) != 2 {
|
||||
t.Fatalf("AdminScopedTokens = %+v, want 2 entries", cfg.AdminScopedTokens)
|
||||
}
|
||||
first := cfg.AdminScopedTokens[0]
|
||||
if first.Name != "ops" || first.Token != "tok-ops-1" ||
|
||||
len(first.Permissions) != 2 || first.Permissions[0] != "users.read" || first.Permissions[1] != "users.write" {
|
||||
t.Fatalf("first scoped token = %+v", first)
|
||||
}
|
||||
second := cfg.AdminScopedTokens[1]
|
||||
if second.Name != "audit" || second.Token != "tok-audit-2" ||
|
||||
len(second.Permissions) != 1 || second.Permissions[0] != "verification.*" {
|
||||
t.Fatalf("second scoped token = %+v", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidAdminRBACConfig(t *testing.T) {
|
||||
for name, env := range map[string]map[string]string{
|
||||
"missing permissions field": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-ops-1"},
|
||||
"too many fields": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok:extra:users.read"},
|
||||
"empty name": {"TELESRV_ADMIN_SCOPED_TOKENS": ":tok-ops-1:users.read"},
|
||||
"empty token": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops::users.read"},
|
||||
"no permissions listed": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-ops-1:"},
|
||||
"invalid permission": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-ops-1:users read"},
|
||||
"duplicate name": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-a:users.read;OPS:tok-b:users.read"},
|
||||
"duplicate token": {"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-a:users.read;audit:tok-a:users.read"},
|
||||
"reuses the admin api token": {
|
||||
"TELESRV_ADMIN_API_TOKEN": "tok-a",
|
||||
"TELESRV_ADMIN_SCOPED_TOKENS": "ops:tok-a:users.read",
|
||||
},
|
||||
"invalid ui permission": {"TELESRV_ADMIN_UI_PERMISSIONS": "users/read"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
for key, value := range env {
|
||||
t.Setenv(key, value)
|
||||
}
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load accepted %v", env)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func writeConfigFile(t *testing.T, path, body string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue