docs: sync public link configuration reference
This commit is contained in:
parent
4390ebf5a9
commit
c0088f1160
13 changed files with 857 additions and 100 deletions
|
|
@ -67,6 +67,13 @@ type Config struct {
|
|||
// PublicBaseURL 是所有客户端可见 telesrv 链接的公开根 URL。
|
||||
// 生产默认 https://telesrv.net;本地可设为 http://127.0.0.1:2401。
|
||||
PublicBaseURL string
|
||||
// PublicAppScheme 是公开落地页自动唤起自建客户端时使用的 URL scheme。
|
||||
// 必须与 TDesktop/Android 客户端构建时注册的 scheme 一致,且不能占用 tg/http/https。
|
||||
PublicAppScheme string
|
||||
// PublicWebBaseURL 是公开 username 页面“Open in Web”按钮指向的 Web 客户端根 URL。
|
||||
PublicWebBaseURL string
|
||||
// PublicAppName 是公开落地页展示的产品名,不参与协议路由。
|
||||
PublicAppName string
|
||||
// PublicLinkWebAddr 是公开链接落地页监听地址;为空关闭。
|
||||
// 生产应只监听 loopback,并由 nginx 将 /<username>、/addstickers/、/addemoji/ 与 /addlist/ 反代到该地址。
|
||||
PublicLinkWebAddr string
|
||||
|
|
@ -353,6 +360,18 @@ func Load() (Config, error) {
|
|||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_PUBLIC_BASE_URL: %w", err)
|
||||
}
|
||||
publicAppScheme, err := links.ValidateAppScheme(envOr("TELESRV_PUBLIC_APP_SCHEME", links.DefaultAppScheme))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_SCHEME: %w", err)
|
||||
}
|
||||
publicWebBaseURL, err := links.ValidateBaseURL(envOr("TELESRV_PUBLIC_WEB_BASE_URL", links.DefaultWebBaseURL))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_PUBLIC_WEB_BASE_URL: %w", err)
|
||||
}
|
||||
publicAppName, err := links.ValidateAppName(envOr("TELESRV_PUBLIC_APP_NAME", links.DefaultAppName))
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("TELESRV_PUBLIC_APP_NAME: %w", err)
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
ListenAddr: envOr("TELESRV_LISTEN", "0.0.0.0:2398"),
|
||||
|
|
@ -386,6 +405,9 @@ func Load() (Config, error) {
|
|||
AdminAPIAddr: envAllowEmptyOr("TELESRV_ADMIN_API_ADDR", ""),
|
||||
AdminAPIToken: envOr("TELESRV_ADMIN_API_TOKEN", ""),
|
||||
PublicBaseURL: publicBaseURL,
|
||||
PublicAppScheme: publicAppScheme,
|
||||
PublicWebBaseURL: publicWebBaseURL,
|
||||
PublicAppName: publicAppName,
|
||||
PublicLinkWebAddr: envAllowEmptyOr("TELESRV_PUBLIC_LINK_WEB_ADDR", ""),
|
||||
AdminUIAddr: envOr("TELESRV_ADMIN_UI_ADDR", "127.0.0.1:2600"),
|
||||
AdminUIPassword: envOr("TELESRV_ADMIN_UI_PASSWORD", ""),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,15 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
|
|||
if cfg.PublicBaseURL != "https://telesrv.net" {
|
||||
t.Fatalf("PublicBaseURL = %q, want https://telesrv.net", cfg.PublicBaseURL)
|
||||
}
|
||||
if cfg.PublicAppScheme != "telesrv" {
|
||||
t.Fatalf("PublicAppScheme = %q, want telesrv", cfg.PublicAppScheme)
|
||||
}
|
||||
if cfg.PublicWebBaseURL != "https://web.telesrv.net" {
|
||||
t.Fatalf("PublicWebBaseURL = %q, want https://web.telesrv.net", cfg.PublicWebBaseURL)
|
||||
}
|
||||
if cfg.PublicAppName != "telesrv" {
|
||||
t.Fatalf("PublicAppName = %q, want telesrv", cfg.PublicAppName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesExplicitAdvertiseIP(t *testing.T) {
|
||||
|
|
@ -248,6 +257,9 @@ TELESRV_POSTGRES_MAX_CONNS=77
|
|||
TELESRV_WEBSOCKET_ALLOWED_ORIGINS=https://one.example, https://two.example
|
||||
TELESRV_CALL_RING_TIMEOUT=2m
|
||||
TELESRV_PUBLIC_BASE_URL=links.example.test/root
|
||||
TELESRV_PUBLIC_APP_SCHEME=example-chat
|
||||
TELESRV_PUBLIC_WEB_BASE_URL=web.example.test/client
|
||||
TELESRV_PUBLIC_APP_NAME=Example Chat
|
||||
TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401
|
||||
`)
|
||||
t.Setenv("TELESRV_CONFIG", path)
|
||||
|
|
@ -274,6 +286,15 @@ TELESRV_PUBLIC_LINK_WEB_ADDR=127.0.0.1:2401
|
|||
if cfg.PublicBaseURL != "https://links.example.test/root" {
|
||||
t.Fatalf("PublicBaseURL = %q, want https://links.example.test/root", cfg.PublicBaseURL)
|
||||
}
|
||||
if cfg.PublicAppScheme != "example-chat" {
|
||||
t.Fatalf("PublicAppScheme = %q, want example-chat", cfg.PublicAppScheme)
|
||||
}
|
||||
if cfg.PublicWebBaseURL != "https://web.example.test/client" {
|
||||
t.Fatalf("PublicWebBaseURL = %q, want https://web.example.test/client", cfg.PublicWebBaseURL)
|
||||
}
|
||||
if cfg.PublicAppName != "Example Chat" {
|
||||
t.Fatalf("PublicAppName = %q, want Example Chat", cfg.PublicAppName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNormalizesLocalPublicBaseURL(t *testing.T) {
|
||||
|
|
@ -298,6 +319,29 @@ func TestLoadRejectsInvalidPublicBaseURL(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidPublicLinkClientConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
value string
|
||||
}{
|
||||
{name: "official scheme", key: "TELESRV_PUBLIC_APP_SCHEME", value: "tg"},
|
||||
{name: "malformed scheme", key: "TELESRV_PUBLIC_APP_SCHEME", value: "bad scheme"},
|
||||
{name: "invalid web base", key: "TELESRV_PUBLIC_WEB_BASE_URL", value: "file:///tmp/client"},
|
||||
{name: "empty app name after trim", key: "TELESRV_PUBLIC_APP_NAME", value: " "},
|
||||
{name: "control in app name", key: "TELESRV_PUBLIC_APP_NAME", value: "bad\nname"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv(tc.key, tc.value)
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("Load succeeded with %s=%q", tc.key, tc.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadExplicitEmptyEnvironmentDisablesNullableListeners(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "telesrv.env")
|
||||
writeConfigFile(t, path, `
|
||||
|
|
|
|||
|
|
@ -6,9 +6,52 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const DefaultPublicBaseURL = "https://telesrv.net"
|
||||
const (
|
||||
DefaultPublicBaseURL = "https://telesrv.net"
|
||||
DefaultWebBaseURL = "https://web.telesrv.net"
|
||||
DefaultAppScheme = "telesrv"
|
||||
DefaultAppName = "telesrv"
|
||||
)
|
||||
const MaxChatlistSlugBytes = 128
|
||||
|
||||
// ValidateAppScheme normalizes the client-visible custom URL scheme used by
|
||||
// public landing pages. Standard Web schemes and Telegram's official tg scheme
|
||||
// are deliberately rejected: the latter remains a manual compatibility link
|
||||
// and must never become the automatic open target.
|
||||
func ValidateAppScheme(raw string) (string, error) {
|
||||
scheme := strings.ToLower(strings.TrimSpace(raw))
|
||||
if scheme == "" {
|
||||
scheme = DefaultAppScheme
|
||||
}
|
||||
for i, r := range scheme {
|
||||
if (r >= 'a' && r <= 'z') || (i > 0 && ((r >= '0' && r <= '9') || r == '+' || r == '-' || r == '.')) {
|
||||
continue
|
||||
}
|
||||
return "", fmt.Errorf("must match [a-z][a-z0-9+.-]*")
|
||||
}
|
||||
switch scheme {
|
||||
case "http", "https", "tg":
|
||||
return "", fmt.Errorf("reserved scheme %q is not allowed", scheme)
|
||||
}
|
||||
return scheme, nil
|
||||
}
|
||||
|
||||
func ValidateAppName(raw string) (string, error) {
|
||||
name := strings.TrimSpace(raw)
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("must not be empty")
|
||||
}
|
||||
if len([]rune(name)) > 64 {
|
||||
return "", fmt.Errorf("must not exceed 64 characters")
|
||||
}
|
||||
for _, r := range name {
|
||||
if r < 0x20 || r == 0x7f {
|
||||
return "", fmt.Errorf("must not contain control characters")
|
||||
}
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func NormalizeBaseURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package links
|
|||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
|
@ -59,6 +60,45 @@ func TestValidateBaseURL(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestValidateAppScheme(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "default", raw: "", want: "telesrv"},
|
||||
{name: "normalized", raw: " My-App+Dev ", want: "my-app+dev"},
|
||||
{name: "starts with digit", raw: "1app", wantErr: true},
|
||||
{name: "colon", raw: "myapp:", wantErr: true},
|
||||
{name: "official tg", raw: "tg", wantErr: true},
|
||||
{name: "http", raw: "http", wantErr: true},
|
||||
{name: "https", raw: "https", wantErr: true},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ValidateAppScheme(tc.raw)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("ValidateAppScheme(%q) error = %v, wantErr %v", tc.raw, err, tc.wantErr)
|
||||
}
|
||||
if got != tc.want {
|
||||
t.Fatalf("ValidateAppScheme(%q) = %q, want %q", tc.raw, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAppName(t *testing.T) {
|
||||
if got, err := ValidateAppName(" Example Chat "); err != nil || got != "Example Chat" {
|
||||
t.Fatalf("ValidateAppName valid = %q, %v", got, err)
|
||||
}
|
||||
for _, raw := range []string{"", " ", "bad\nname", strings.Repeat("x", 65)} {
|
||||
if got, err := ValidateAppName(raw); err == nil {
|
||||
t.Fatalf("ValidateAppName(%q) = %q, want error", raw, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPreservesBasePathAndQuery(t *testing.T) {
|
||||
got := Build("http://127.0.0.1:2401/root/", "/call/abc", url.Values{"slug": []string{"abc"}})
|
||||
if want := "http://127.0.0.1:2401/root/call/abc?slug=abc"; got != want {
|
||||
|
|
|
|||
|
|
@ -20,9 +20,18 @@ import (
|
|||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
"telesrv/internal/web/stickerlinks"
|
||||
publicweb "telesrv/internal/web"
|
||||
)
|
||||
|
||||
func newStickerLinkHandler(t *testing.T, files publicweb.StickerSetResolver) http.Handler {
|
||||
t.Helper()
|
||||
h, err := publicweb.NewHandler(publicweb.Config{StickerSets: files, PublicBaseURL: "https://telesrv.net"})
|
||||
if err != nil {
|
||||
t.Fatalf("new public Web handler: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestCustomStickerPackLinkInstallAndSendSmoke(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
|
|
@ -73,7 +82,7 @@ func TestCustomStickerPackLinkInstallAndSendSmoke(t *testing.T) {
|
|||
t.Fatalf("created = %T, want *tg.MessagesStickerSet", created)
|
||||
}
|
||||
|
||||
web := stickerlinks.NewHandler(files, "https://telesrv.net")
|
||||
web := newStickerLinkHandler(t, files)
|
||||
rr := httptest.NewRecorder()
|
||||
web.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/addstickers/alice_fresh_pack", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
|
|
@ -201,7 +210,7 @@ func TestStickersBotCreatePackLinkInstallIsolationSmoke(t *testing.T) {
|
|||
sendStickersBotText(t, r, alice, "Alice Bot Pack", 9102)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "Lottie JSON")
|
||||
sendStickersBotDocument(t, r, alice, 401, 4401, 9103)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "Now send the emoji")
|
||||
waitForStickersReply(t, messageStore, alice.ID, "emoji")
|
||||
sendStickersBotText(t, r, alice, "🙂", 9104)
|
||||
waitForStickersReply(t, messageStore, alice.ID, "Added")
|
||||
sendStickersBotText(t, r, alice, "/publish", 9105)
|
||||
|
|
@ -224,7 +233,7 @@ func TestStickersBotCreatePackLinkInstallIsolationSmoke(t *testing.T) {
|
|||
t.Fatalf("bob getAllStickers before install = %v, want empty", got)
|
||||
}
|
||||
|
||||
web := stickerlinks.NewHandler(files, "https://telesrv.net")
|
||||
web := newStickerLinkHandler(t, files)
|
||||
rr := httptest.NewRecorder()
|
||||
web.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/addstickers/alice_bot_pack", nil))
|
||||
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "https://telesrv.net/addstickers/alice_bot_pack") {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
package stickerlinks
|
||||
// Package web serves telesrv's read-only public link landing pages.
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -22,13 +23,17 @@ import (
|
|||
type Config struct {
|
||||
Addr string
|
||||
PublicBaseURL string
|
||||
AppScheme string
|
||||
WebBaseURL string
|
||||
AppName string
|
||||
StickerSets StickerSetResolver
|
||||
Users UsernameResolver
|
||||
Channels PublicChannelResolver
|
||||
Privacy AnonymousPrivacyResolver
|
||||
Photos ProfilePhotoResolver
|
||||
}
|
||||
|
||||
type Resolver interface {
|
||||
type StickerSetResolver interface {
|
||||
ResolveStickerSet(ctx context.Context, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error)
|
||||
}
|
||||
|
||||
|
|
@ -52,18 +57,18 @@ type ProfilePhotoResolver interface {
|
|||
GetFile(ctx context.Context, req domain.FileDownloadRequest) (domain.FileChunk, bool, error)
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logger) (*http.Server, error) {
|
||||
func Start(ctx context.Context, cfg Config, logger *zap.Logger) (*http.Server, error) {
|
||||
addr := strings.TrimSpace(cfg.Addr)
|
||||
if addr == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if resolver == nil {
|
||||
return nil, fmt.Errorf("sticker links resolver is nil")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
handler := newHandler(resolver, cfg.Users, cfg.Channels, cfg.Privacy, cfg.Photos, cfg.PublicBaseURL, logger)
|
||||
handler, err := newHandler(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srv := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: handler,
|
||||
|
|
@ -78,7 +83,11 @@ func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logge
|
|||
return nil, err
|
||||
}
|
||||
go func() {
|
||||
logger.Info("Public link Web endpoint enabled", zap.String("addr", addr), zap.String("public_base_url", normalizePublicBaseURL(cfg.PublicBaseURL)))
|
||||
logger.Info("Public link Web endpoint enabled",
|
||||
zap.String("addr", addr),
|
||||
zap.String("public_base_url", cfg.PublicBaseURL),
|
||||
zap.String("app_scheme", cfg.AppScheme),
|
||||
zap.String("web_base_url", cfg.WebBaseURL))
|
||||
if err := srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Warn("Public link Web endpoint exited", zap.Error(err))
|
||||
}
|
||||
|
|
@ -92,44 +101,46 @@ func Start(ctx context.Context, cfg Config, resolver Resolver, logger *zap.Logge
|
|||
return srv, nil
|
||||
}
|
||||
|
||||
func NewHandler(resolver Resolver, publicBaseURL string) http.Handler {
|
||||
return newHandler(resolver, nil, nil, nil, nil, publicBaseURL, zap.NewNop())
|
||||
func NewHandler(cfg Config) (http.Handler, error) {
|
||||
return newHandler(cfg, zap.NewNop())
|
||||
}
|
||||
|
||||
func NewHandlerWithUsers(resolver Resolver, users UsernameResolver, publicBaseURL string) http.Handler {
|
||||
return newHandler(resolver, users, nil, nil, nil, publicBaseURL, zap.NewNop())
|
||||
}
|
||||
|
||||
func NewHandlerWithPublicPeers(
|
||||
resolver Resolver,
|
||||
users UsernameResolver,
|
||||
channels PublicChannelResolver,
|
||||
privacy AnonymousPrivacyResolver,
|
||||
photos ProfilePhotoResolver,
|
||||
publicBaseURL string,
|
||||
) http.Handler {
|
||||
return newHandler(resolver, users, channels, privacy, photos, publicBaseURL, zap.NewNop())
|
||||
}
|
||||
|
||||
func newHandler(
|
||||
resolver Resolver,
|
||||
users UsernameResolver,
|
||||
channels PublicChannelResolver,
|
||||
privacy AnonymousPrivacyResolver,
|
||||
photos ProfilePhotoResolver,
|
||||
publicBaseURL string,
|
||||
logger *zap.Logger,
|
||||
) http.Handler {
|
||||
func newHandler(cfg Config, logger *zap.Logger) (http.Handler, error) {
|
||||
var err error
|
||||
if cfg.StickerSets == nil {
|
||||
return nil, fmt.Errorf("public Web sticker set resolver is nil")
|
||||
}
|
||||
if strings.TrimSpace(cfg.WebBaseURL) == "" {
|
||||
cfg.WebBaseURL = links.DefaultWebBaseURL
|
||||
}
|
||||
if strings.TrimSpace(cfg.AppName) == "" {
|
||||
cfg.AppName = links.DefaultAppName
|
||||
}
|
||||
if cfg.PublicBaseURL, err = links.ValidateBaseURL(cfg.PublicBaseURL); err != nil {
|
||||
return nil, fmt.Errorf("public base URL: %w", err)
|
||||
}
|
||||
if cfg.AppScheme, err = links.ValidateAppScheme(cfg.AppScheme); err != nil {
|
||||
return nil, fmt.Errorf("app scheme: %w", err)
|
||||
}
|
||||
if cfg.WebBaseURL, err = links.ValidateBaseURL(cfg.WebBaseURL); err != nil {
|
||||
return nil, fmt.Errorf("Web base URL: %w", err)
|
||||
}
|
||||
if cfg.AppName, err = links.ValidateAppName(cfg.AppName); err != nil {
|
||||
return nil, fmt.Errorf("app name: %w", err)
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
h := &handler{
|
||||
resolver: resolver,
|
||||
users: users,
|
||||
channels: channels,
|
||||
privacy: privacy,
|
||||
photos: photos,
|
||||
publicBaseURL: normalizePublicBaseURL(publicBaseURL),
|
||||
stickerSets: cfg.StickerSets,
|
||||
users: cfg.Users,
|
||||
channels: cfg.Channels,
|
||||
privacy: cfg.Privacy,
|
||||
photos: cfg.Photos,
|
||||
publicBaseURL: cfg.PublicBaseURL,
|
||||
appScheme: cfg.AppScheme,
|
||||
webBaseURL: cfg.WebBaseURL,
|
||||
appName: cfg.AppName,
|
||||
logger: logger,
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -140,16 +151,19 @@ func newHandler(
|
|||
mux.HandleFunc("GET /addlist/{slug}", h.addList)
|
||||
mux.HandleFunc("GET /{username}", h.usernameLink)
|
||||
mux.HandleFunc("GET /{username}/{$}", h.usernameLink)
|
||||
return publicSecurityHeaders(mux)
|
||||
return publicSecurityHeaders(mux), nil
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
resolver Resolver
|
||||
stickerSets StickerSetResolver
|
||||
users UsernameResolver
|
||||
channels PublicChannelResolver
|
||||
privacy AnonymousPrivacyResolver
|
||||
photos ProfilePhotoResolver
|
||||
publicBaseURL string
|
||||
appScheme string
|
||||
webBaseURL string
|
||||
appName string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
|
|
@ -172,8 +186,9 @@ func (h *handler) addList(w http.ResponseWriter, r *http.Request) {
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
app := appURL("addlist", "slug", slug)
|
||||
app := h.appURL("addlist", "slug", slug)
|
||||
data := pageData{
|
||||
AppName: h.appName,
|
||||
Title: "Shared Folder",
|
||||
KindLabel: "shared folder",
|
||||
Subtitle: slug,
|
||||
|
|
@ -212,13 +227,15 @@ func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
params.Set("domain", peer.username)
|
||||
app := schemeURLValues("telesrv", "resolve", params)
|
||||
app := schemeURLValues(h.appScheme, "resolve", params)
|
||||
legacy := schemeURLValues("tg", "resolve", params)
|
||||
description := peer.about
|
||||
if description == "" {
|
||||
description = peer.fallbackDescription()
|
||||
description = peer.fallbackDescription(h.appName)
|
||||
}
|
||||
data := usernamePageData{
|
||||
AppName: h.appName,
|
||||
AppInitial: appInitial(h.appName),
|
||||
Title: peer.title,
|
||||
Username: peer.username,
|
||||
Verified: peer.verified,
|
||||
|
|
@ -228,7 +245,7 @@ func (h *handler) usernameLink(w http.ResponseWriter, r *http.Request) {
|
|||
HomeURL: h.publicBaseURL + "/",
|
||||
AppURL: template.URL(app),
|
||||
LegacyTgURL: template.URL(legacy),
|
||||
WebURL: template.URL(publicWebAppURL(legacy)),
|
||||
WebURL: template.URL(publicWebAppURL(h.webBaseURL, legacy)),
|
||||
ButtonLabel: peer.buttonLabel(),
|
||||
Initials: peer.initials(),
|
||||
}
|
||||
|
|
@ -319,7 +336,7 @@ func (h *handler) serveSet(w http.ResponseWriter, r *http.Request, pathKind stri
|
|||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
set, docs, found, err := h.resolver.ResolveStickerSet(r.Context(), domain.StickerSetRef{
|
||||
set, docs, found, err := h.stickerSets.ResolveStickerSet(r.Context(), domain.StickerSetRef{
|
||||
Kind: domain.StickerSetRefByShortName,
|
||||
ShortName: shortName,
|
||||
})
|
||||
|
|
@ -340,8 +357,9 @@ func (h *handler) serveSet(w http.ResponseWriter, r *http.Request, pathKind stri
|
|||
if count == 0 {
|
||||
count = len(docs)
|
||||
}
|
||||
app := appURL(canonicalKind, "set", set.ShortName)
|
||||
app := h.appURL(canonicalKind, "set", set.ShortName)
|
||||
data := pageData{
|
||||
AppName: h.appName,
|
||||
Title: fallbackTitle(set),
|
||||
KindLabel: kindLabel(set),
|
||||
Subtitle: fmt.Sprintf("@%s · %d %s", set.ShortName, count, itemNoun(set, count)),
|
||||
|
|
@ -548,16 +566,16 @@ func (p publicPeer) extra() string {
|
|||
}
|
||||
}
|
||||
|
||||
func (p publicPeer) fallbackDescription() string {
|
||||
func (p publicPeer) fallbackDescription(appName string) string {
|
||||
switch p.kind {
|
||||
case publicPeerBot:
|
||||
return "Open telesrv to start a chat with this bot."
|
||||
return "Open " + appName + " to start a chat with this bot."
|
||||
case publicPeerChannel:
|
||||
return "Open telesrv to view and join this channel."
|
||||
return "Open " + appName + " to view and join this channel."
|
||||
case publicPeerSupergroup:
|
||||
return "Open telesrv to view and join this group."
|
||||
return "Open " + appName + " to view and join this group."
|
||||
default:
|
||||
return "Open telesrv to send a message to @" + p.username + "."
|
||||
return "Open " + appName + " to send a message to @" + p.username + "."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,6 +616,13 @@ func plural(n int, one, many string) string {
|
|||
return many
|
||||
}
|
||||
|
||||
func appInitial(name string) string {
|
||||
for _, r := range name {
|
||||
return strings.ToUpper(string(r))
|
||||
}
|
||||
return "T"
|
||||
}
|
||||
|
||||
const (
|
||||
maxPublicLinkRawQuery = 2048
|
||||
maxPublicLinkParams = 16
|
||||
|
|
@ -703,8 +728,8 @@ func schemeURLValues(scheme, kind string, values url.Values) string {
|
|||
return (&url.URL{Scheme: scheme, Host: kind, RawQuery: values.Encode()}).String()
|
||||
}
|
||||
|
||||
func publicWebAppURL(legacyURL string) string {
|
||||
return "https://web.telesrv.net/#?tgaddr=" + url.QueryEscape(legacyURL)
|
||||
func publicWebAppURL(webBaseURL, legacyURL string) string {
|
||||
return strings.TrimRight(webBaseURL, "/") + "/#?tgaddr=" + url.QueryEscape(legacyURL)
|
||||
}
|
||||
|
||||
func publicSecurityHeaders(next http.Handler) http.Handler {
|
||||
|
|
@ -718,14 +743,6 @@ func publicSecurityHeaders(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func normalizePublicBaseURL(raw string) string {
|
||||
normalized, err := links.ValidateBaseURL(raw)
|
||||
if err != nil {
|
||||
return links.DefaultPublicBaseURL
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func validShortNamePath(shortName string) bool {
|
||||
if shortName == "" || len(shortName) > 64 {
|
||||
return false
|
||||
|
|
@ -803,8 +820,8 @@ func itemNoun(set domain.StickerSet, count int) string {
|
|||
return "stickers"
|
||||
}
|
||||
|
||||
func appURL(kind, key, value string) string {
|
||||
return schemeURL("telesrv", kind, key, value)
|
||||
func (h *handler) appURL(kind, key, value string) string {
|
||||
return schemeURL(h.appScheme, kind, key, value)
|
||||
}
|
||||
|
||||
func legacyTgURL(kind, key, value string) string {
|
||||
|
|
@ -816,6 +833,7 @@ func schemeURL(scheme, kind, key, value string) string {
|
|||
}
|
||||
|
||||
type pageData struct {
|
||||
AppName string
|
||||
Title string
|
||||
KindLabel string
|
||||
Subtitle string
|
||||
|
|
@ -827,6 +845,8 @@ type pageData struct {
|
|||
}
|
||||
|
||||
type usernamePageData struct {
|
||||
AppName string
|
||||
AppInitial string
|
||||
Title string
|
||||
Username string
|
||||
Verified bool
|
||||
|
|
@ -850,7 +870,8 @@ func (h *handler) serveUsernameNotFound(w http.ResponseWriter, username string)
|
|||
if err := usernameNotFoundTemplate.Execute(w, struct {
|
||||
Username string
|
||||
HomeURL string
|
||||
}{Username: username, HomeURL: h.publicBaseURL + "/"}); err != nil {
|
||||
AppName string
|
||||
}{Username: username, HomeURL: h.publicBaseURL + "/", AppName: h.appName}); err != nil {
|
||||
h.logger.Error("Render public username not-found page failed", zap.String("username", username), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -861,12 +882,12 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0e1621">
|
||||
<title>{{.Title}} (@{{.Username}}) - telesrv</title>
|
||||
<title>{{.Title}} (@{{.Username}}) - {{.AppName}}</title>
|
||||
<meta name="description" content="{{.Description}}">
|
||||
<meta name="robots" content="index,follow,max-image-preview:large">
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:site_name" content="telesrv">
|
||||
<meta property="og:site_name" content="{{.AppName}}">
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:description" content="{{.Description}}">
|
||||
<meta property="og:url" content="{{.CanonicalURL}}">
|
||||
|
|
@ -922,7 +943,7 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<a class="brand" href="{{.HomeURL}}" aria-label="telesrv home"><span class="brand-mark">t</span><span>telesrv</span></a>
|
||||
<a class="brand" href="{{.HomeURL}}" aria-label="{{.AppName}} home"><span class="brand-mark">{{.AppInitial}}</span><span>{{.AppName}}</span></a>
|
||||
<main>
|
||||
<article class="card">
|
||||
<div class="avatar">{{if .PhotoURL}}<img src="{{.PhotoURL}}" alt="{{.Title}} profile photo" width="112" height="112">{{else}}<span class="initials" aria-hidden="true">{{.Initials}}</span>{{end}}</div>
|
||||
|
|
@ -937,7 +958,7 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
<p class="legacy">Old test clients only: <a href="{{.LegacyTgURL}}">open with tg://</a></p>
|
||||
</article>
|
||||
</main>
|
||||
<footer>If you have telesrv, this page can open the chat directly.</footer>
|
||||
<footer>If you have {{.AppName}}, this page can open the chat directly.</footer>
|
||||
</div>
|
||||
<script>window.setTimeout(function () { window.location.href = {{.AppURLJS}}; }, 250);</script>
|
||||
</body>
|
||||
|
|
@ -946,16 +967,16 @@ var usernameLandingTemplate = template.Must(template.New("username-landing").Par
|
|||
|
||||
var usernameNotFoundTemplate = template.Must(template.New("username-not-found").Parse(`<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex,nofollow"><title>Username not found - telesrv</title>
|
||||
<meta name="robots" content="noindex,nofollow"><title>Username not found - {{.AppName}}</title>
|
||||
<style>:root{color-scheme:dark;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{margin:0;min-height:100svh;display:grid;place-items:center;padding:24px;background:#0e1621;color:#f5f8fb}.card{width:min(100%,420px);padding:34px 28px;border:1px solid rgba(255,255,255,.08);border-radius:22px;background:#17212b;text-align:center}h1{margin:0 0 12px;font-size:26px}p{margin:0;color:#9fb0bf;line-height:1.55;overflow-wrap:anywhere}a{display:inline-block;margin-top:24px;color:#67bff9;text-decoration:none}</style>
|
||||
</head><body><main class="card"><h1>Username not found</h1><p>{{if .Username}}@{{.Username}} is not an active public telesrv username.{{else}}This is not a valid public telesrv username.{{end}}</p><a href="{{.HomeURL}}">Back to telesrv</a></main></body></html>`))
|
||||
</head><body><main class="card"><h1>Username not found</h1><p>{{if .Username}}@{{.Username}} is not an active public {{.AppName}} username.{{else}}This is not a valid public {{.AppName}} username.{{end}}</p><a href="{{.HomeURL}}">Back to {{.AppName}}</a></main></body></html>`))
|
||||
|
||||
var landingTemplate = template.Must(template.New("landing").Parse(`<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Title}} - telesrv</title>
|
||||
<title>{{.Title}} - {{.AppName}}</title>
|
||||
<link rel="canonical" href="{{.CanonicalURL}}">
|
||||
<meta property="og:title" content="{{.Title}}">
|
||||
<meta property="og:description" content="{{.Description}}">
|
||||
|
|
@ -984,7 +1005,7 @@ var landingTemplate = template.Must(template.New("landing").Parse(`<!doctype htm
|
|||
<p class="meta">{{.KindLabel}}</p>
|
||||
<h1>{{.Title}}</h1>
|
||||
<p class="meta">{{.Subtitle}}</p>
|
||||
<p><a class="button" href="{{.AppURL}}">Open in telesrv</a></p>
|
||||
<p><a class="button" href="{{.AppURL}}">Open in {{.AppName}}</a></p>
|
||||
<p>{{.Description}}</p>
|
||||
<p class="meta">Old test clients only: <a class="raw" href="{{.LegacyTgURL}}">open with tg://</a></p>
|
||||
<p class="meta"><a class="raw" href="{{.CanonicalURL}}">{{.CanonicalURL}}</a></p>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package stickerlinks
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -11,6 +11,39 @@ import (
|
|||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func newTestHandler(t *testing.T, resolver StickerSetResolver, publicBaseURL string) http.Handler {
|
||||
t.Helper()
|
||||
h, err := NewHandler(Config{StickerSets: resolver, PublicBaseURL: publicBaseURL})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandler: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func newTestHandlerWithPublicPeers(
|
||||
t *testing.T,
|
||||
resolver StickerSetResolver,
|
||||
users UsernameResolver,
|
||||
channels PublicChannelResolver,
|
||||
privacy AnonymousPrivacyResolver,
|
||||
photos ProfilePhotoResolver,
|
||||
publicBaseURL string,
|
||||
) http.Handler {
|
||||
t.Helper()
|
||||
h, err := NewHandler(Config{
|
||||
StickerSets: resolver,
|
||||
Users: users,
|
||||
Channels: channels,
|
||||
Privacy: privacy,
|
||||
Photos: photos,
|
||||
PublicBaseURL: publicBaseURL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandler: %v", err)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func TestHandlerServesStickerSetLandingPage(t *testing.T) {
|
||||
resolver := fakeResolver{
|
||||
"fresh_pack": {
|
||||
|
|
@ -24,7 +57,7 @@ func TestHandlerServesStickerSetLandingPage(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/addstickers/fresh_pack", nil)
|
||||
|
||||
NewHandler(resolver, "https://telesrv.net/").ServeHTTP(rr, req)
|
||||
newTestHandler(t, resolver, "https://telesrv.net/").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
|
|
@ -63,7 +96,7 @@ func TestHandlerServesEmojiLandingPage(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/addemoji/emoji_pack", nil)
|
||||
|
||||
NewHandler(resolver, "https://example.test/base").ServeHTTP(rr, req)
|
||||
newTestHandler(t, resolver, "https://example.test/base").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
|
|
@ -85,7 +118,7 @@ func TestHandlerServesChatlistLandingPage(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/addlist/zNhytIbwRwjaC2GH", nil)
|
||||
|
||||
NewHandler(fakeResolver{}, "http://127.0.0.1:2401").ServeHTTP(rr, req)
|
||||
newTestHandler(t, fakeResolver{}, "http://127.0.0.1:2401").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
|
|
@ -119,7 +152,7 @@ func TestHandlerServesBotUsernameLandingPage(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/TetrisBot", nil)
|
||||
|
||||
NewHandlerWithPublicPeers(fakeResolver{}, users, nil, nil, nil, "http://127.0.0.1:2401").ServeHTTP(rr, req)
|
||||
newTestHandlerWithPublicPeers(t, fakeResolver{}, users, nil, nil, nil, "http://127.0.0.1:2401").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
|
|
@ -146,6 +179,74 @@ func TestHandlerServesBotUsernameLandingPage(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandlerUsesConfiguredClientLinksAndBrand(t *testing.T) {
|
||||
h, err := NewHandler(Config{
|
||||
StickerSets: fakeResolver{
|
||||
"stickers_pack": {ShortName: "stickers_pack", Title: "Stickers", Kind: domain.StickerSetKindStickers},
|
||||
"emoji_pack": {ShortName: "emoji_pack", Title: "Emoji", Kind: domain.StickerSetKindEmoji, Emojis: true},
|
||||
},
|
||||
Users: fakeUsers{"alice": {ID: 2001, Username: "Alice", FirstName: "Alice"}},
|
||||
PublicBaseURL: "https://links.example.test",
|
||||
AppScheme: "example-chat",
|
||||
WebBaseURL: "https://web.example.test/client/",
|
||||
AppName: "Example Chat",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandler: %v", err)
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/Alice?start=hello", nil))
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
|
||||
}
|
||||
body := rr.Body.String()
|
||||
for _, want := range []string{
|
||||
"example-chat://resolve?domain=Alice&start=hello",
|
||||
"https://web.example.test/client/#?tgaddr=",
|
||||
"Example Chat",
|
||||
"Open Example Chat to send a message to @Alice.",
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Fatalf("body missing %q:\n%s", want, body)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "telesrv://") || strings.Contains(body, "https://web.telesrv.net") {
|
||||
t.Fatalf("body contains stale default client link:\n%s", body)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{path: "/addstickers/stickers_pack", want: "example-chat://addstickers?set=stickers_pack"},
|
||||
{path: "/addemoji/emoji_pack", want: "example-chat://addemoji?set=emoji_pack"},
|
||||
{path: "/addlist/shared-folder", want: "example-chat://addlist?slug=shared-folder"},
|
||||
} {
|
||||
rr := httptest.NewRecorder()
|
||||
h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, tc.path, nil))
|
||||
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), tc.want) || !strings.Contains(rr.Body.String(), "Example Chat") {
|
||||
t.Fatalf("%s response = %d %q, want configured link %q and brand", tc.path, rr.Code, rr.Body.String(), tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHandlerRejectsInvalidClientLinkConfig(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
cfg Config
|
||||
}{
|
||||
{name: "missing sticker resolver", cfg: Config{}},
|
||||
{name: "official scheme", cfg: Config{StickerSets: fakeResolver{}, AppScheme: "tg"}},
|
||||
{name: "invalid Web base URL", cfg: Config{StickerSets: fakeResolver{}, WebBaseURL: "file:///tmp/web"}},
|
||||
{name: "invalid app name", cfg: Config{StickerSets: fakeResolver{}, AppName: "bad\nname"}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := NewHandler(tc.cfg); err == nil {
|
||||
t.Fatal("NewHandler succeeded, want error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerServesUserChannelAndSupergroupLandingPages(t *testing.T) {
|
||||
users := fakeUsers{
|
||||
"alice": {
|
||||
|
|
@ -183,7 +284,7 @@ func TestHandlerServesUserChannelAndSupergroupLandingPages(t *testing.T) {
|
|||
photos := &fakePhotos{byID: map[int64]domain.Photo{
|
||||
301: {ID: 301, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "c", W: 640, H: 640, Size: 12}}},
|
||||
}}
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, users, channels, nil, photos, "https://telesrv.net")
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, users, channels, nil, photos, "https://telesrv.net")
|
||||
|
||||
for _, tc := range []struct {
|
||||
path string
|
||||
|
|
@ -233,7 +334,7 @@ func TestHandlerServesUserChannelAndSupergroupLandingPages(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerPreservesBoundedResolveQueryAndOverridesDomain(t *testing.T) {
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"tetrisbot": {ID: 2001, Username: "TetrisBot", FirstName: "Tetris", Bot: true},
|
||||
}, nil, nil, nil, "https://telesrv.net")
|
||||
rr := httptest.NewRecorder()
|
||||
|
|
@ -283,7 +384,7 @@ func TestHandlerHonorsAnonymousAboutAndPhotoPrivacy(t *testing.T) {
|
|||
domain.PrivacyKeyAbout: false,
|
||||
domain.PrivacyKeyProfilePhoto: false,
|
||||
}
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"alice": {ID: userID, Username: "Alice", FirstName: "Alice", About: "private biography"},
|
||||
}, nil, privacy, photos, "https://telesrv.net")
|
||||
rr := httptest.NewRecorder()
|
||||
|
|
@ -314,7 +415,7 @@ func TestHandlerServesBoundedCurrentAvatarWithETag(t *testing.T) {
|
|||
"photo:99:c": {Bytes: jpeg, MimeType: "image/jpeg", Total: int64(len(jpeg))},
|
||||
},
|
||||
}
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"alice": {ID: userID, Username: "Alice", FirstName: "Alice"},
|
||||
}, nil, nil, photos, "https://telesrv.net")
|
||||
|
||||
|
|
@ -345,7 +446,7 @@ func TestHandlerServesBoundedCurrentAvatarWithETag(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerFailsFastForAmbiguousUsernameOwner(t *testing.T) {
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"sharedname": {ID: 2001, Username: "SharedName", FirstName: "User"},
|
||||
}, fakeChannels{
|
||||
"sharedname": {ID: 3001, Username: "SharedName", Title: "Channel", Broadcast: true},
|
||||
|
|
@ -358,7 +459,7 @@ func TestHandlerFailsFastForAmbiguousUsernameOwner(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerReturnsTrustedUsernameNotFoundPage(t *testing.T) {
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{}, fakeChannels{}, nil, nil, "https://telesrv.net")
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{}, fakeChannels{}, nil, nil, "https://telesrv.net")
|
||||
for _, path := range []string{"/MissingName", "/bad-name", "/Nope"} {
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
|
|
@ -394,7 +495,7 @@ func TestPublicAvatarRejectsOversizedOrUnsafeBlob(t *testing.T) {
|
|||
},
|
||||
files: map[string]domain.FileChunk{"photo:99:c": tc.chunk},
|
||||
}
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"alice": {ID: userID, Username: "Alice", FirstName: "Alice"},
|
||||
}, nil, nil, photos, "https://telesrv.net")
|
||||
rr := httptest.NewRecorder()
|
||||
|
|
@ -419,7 +520,7 @@ func TestHandlerRedirectsMismatchedKindToCanonicalURL(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/addstickers/emoji_pack", nil)
|
||||
|
||||
NewHandler(resolver, "https://telesrv.net").ServeHTTP(rr, req)
|
||||
newTestHandler(t, resolver, "https://telesrv.net").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusPermanentRedirect {
|
||||
t.Fatalf("status = %d, want 308; body=%s", rr.Code, rr.Body.String())
|
||||
|
|
@ -430,7 +531,7 @@ func TestHandlerRedirectsMismatchedKindToCanonicalURL(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestHandlerNotFoundForMissingOrInvalidShortName(t *testing.T) {
|
||||
handler := NewHandlerWithPublicPeers(fakeResolver{}, fakeUsers{
|
||||
handler := newTestHandlerWithPublicPeers(t, fakeResolver{}, fakeUsers{
|
||||
"alice": {
|
||||
ID: 2001,
|
||||
Username: "Alice",
|
||||
|
|
@ -460,7 +561,7 @@ func TestHandlerLookupErrorIsInternalServerError(t *testing.T) {
|
|||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/addstickers/fresh_pack", nil)
|
||||
|
||||
NewHandler(errorResolver{}, "https://telesrv.net").ServeHTTP(rr, req)
|
||||
newTestHandler(t, errorResolver{}, "https://telesrv.net").ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("status = %d, want 500", rr.Code)
|
||||
Loading…
Add table
Add a link
Reference in a new issue