package handlers import ( "encoding/json" "io" "testing" "time" "amnezia-share/internal/config" "amnezia-share/internal/models" "amnezia-share/internal/share" "amnezia-share/internal/web" ) // TestRenderAllPages executes every page template against a realistic view // struct (as built by the real handlers) without needing a database. It // catches execution-time template errors (bad field/method names, nil // dereferences) that plain `go build` and template parsing cannot catch. func TestRenderAllPages(t *testing.T) { tmpl, err := web.LoadTemplates(config.Config{WebDir: "../../../web"}) if err != nil { t.Fatalf("LoadTemplates: %v", err) } now := time.Now() future := now.Add(48 * time.Hour) note := "test note" linkID := 7 email := "user@example.com" validityDays := 30 serverInfo := models.ServerInfo{ID: 1, Label: "Germany", Protocols: []string{"wireguard", "awg2"}, Flag: "de", Speed: "1 Gbps"} sampleLink := models.ShareLink{ ID: 1, Token: "abc123", MaxUses: 5, UseCount: 2, CreatedAt: now, ExpiresAt: &future, ValidityDays: &validityDays, AllowedServerIDs: json.RawMessage(`[1,2]`), } bundle := share.Bundle{ Base: "peer1", Conf: &share.FilePart{Filename: "peer1.conf", Body: "conf-body", Mime: "text/plain"}, Vpn: &share.FilePart{Filename: "peer1.vpn", Body: "vpn-body", Mime: "text/plain"}, CreationID: 42, CreatedAt: now, Protocol: "wireguard", ConnectionName: "peer1", ServerID: 1, } adminUser := &models.User{ID: 1, Username: "admin"} cases := []struct { name string data any }{ {"page_install", installView{}}, {"page_install", installView{Installed: true}}, {"page_install", installView{Error: "test error"}}, {"page_login", loginView{PocketEnabled: true, NoAdmins: false}}, {"page_login", loginView{Error: "bad creds", NoAdmins: true}}, {"page_cabinet_auth", cabinetAuthView{}}, {"page_cabinet_auth", cabinetAuthView{Error: "bad creds"}}, {"page_cabinet", cabinetView{ Member: &models.Member{ID: 1, Username: "member1", Email: &email, CreatedAt: now}, Sub: &models.MemberSubscription{MemberID: 1, ExpiresAt: &future, MaxConfigs: 5, ConfigCount: 2}, SubLabel: "до " + future.Format("2006-01-02"), ConfigsRemaining: 3, Servers: []models.ServerInfo{serverInfo}, Bundles: []share.Bundle{bundle}, OK: "ok message", }}, {"page_cabinet", cabinetView{ Member: &models.Member{ID: 1, Username: "member1", CreatedAt: now}, Error: "some error", }}, {"page_share", shareView{Token: "abc123", NotFound: true}}, {"page_share", shareView{ Token: "abc123", Link: &sampleLink, Servers: []models.ServerInfo{serverInfo}, Bundles: []share.Bundle{bundle}, OK: "created", }}, {"page_share", shareView{ Token: "abc123", Link: &sampleLink, Servers: []models.ServerInfo{serverInfo}, Expired: true, LimitReached: true, Error: "some error", }}, {"page_faq", faqView{Items: []faqEntry{{Question: "Q1", Answer: "A1"}}}}, {"page_rules", nil}, {"page_status", statusView{Servers: []serverStatus{{ServerInfo: serverInfo, Alive: true, Ms: 42}, {ServerInfo: serverInfo, Alive: false, Err: "timeout"}}}}, {"page_admin_index", adminIndexView{ adminLayout: adminLayout{Admin: adminUser, Active: "index"}, TotalLinks: 10, ActiveLinks: 5, ExpiredLinks: 2, CleanedLinks: 1, LimitLinks: 2, CodesCount: 3, PanelURL: "https://panel.example.com", PanelOK: true, Maintenance: false, }}, {"page_admin_index", adminIndexView{ adminLayout: adminLayout{Admin: adminUser, Active: "index"}, PanelErr: "connection refused", }}, {"page_admin_links", adminLinksView{ adminLayout: adminLayout{Admin: adminUser, Active: "links", OK: "saved"}, StatusFilter: "active", Page: 1, Total: 1, PerPage: 20, AllowedDurations: []int{7, 30, 90}, Links: []linkRow{ {ShareLink: sampleLink, Status: share.StatusActive, ActiveConfigs: 2}, }, Detail: &sampleLink, DetailStatus: share.StatusActive, Creations: []models.ShareCreation{{ID: 42, ShareLinkID: 1, ServerID: 1, Protocol: "wireguard", ConnectionName: "peer1", CreatedAt: now}}, DeletedCreations: []models.ShareCreation{{ID: 41, ShareLinkID: 1, ServerID: 1, Protocol: "wireguard", ConnectionName: "peer0", CreatedAt: now}}, AllowedServerIDs: []int{1, 2}, AllServers: []models.ServerInfo{serverInfo}, }}, {"page_admin_links", adminLinksView{ adminLayout: adminLayout{Admin: adminUser, Active: "links"}, PerPage: 20, }}, {"page_admin_renewal", adminRenewalView{ adminLayout: adminLayout{Admin: adminUser, Active: "renewal"}, Codes: []models.RenewalCode{ {ID: 1, Code: "ABCD1234", AddDays: 30, MaxUses: 1, UseCount: 0, ShareLinkID: &linkID, CodeExpiresAt: &future, Note: ¬e, CodeTarget: "guest", CreatedAt: now}, {ID: 2, Code: "WXYZ9999", AddDays: 30, MaxUses: 5, UseCount: 1, CodeTarget: "member", CreatedAt: now}, }, }}, {"page_admin_renewal", adminRenewalView{adminLayout: adminLayout{Admin: adminUser, Active: "renewal"}}}, {"page_admin_configs", adminConfigsView{ adminLayout: adminLayout{Admin: adminUser, Active: "configs"}, Rows: []configRow{ {ShareCreation: models.ShareCreation{ID: 42, ShareLinkID: 1, ServerID: 1, Protocol: "wireguard", ConnectionName: "peer1", CreatedAt: now}, LinkID: 1, LinkToken: "abc123"}, }, LinkIDFilter: 1, Truncated: true, }}, {"page_admin_configs", adminConfigsView{adminLayout: adminLayout{Admin: adminUser, Active: "configs"}}}, {"page_admin_servers", adminServersView{ adminLayout: adminLayout{Admin: adminUser, Active: "servers"}, AllProtocols: share.GuestProtocols, Rows: []serverRow{ {ID: 1, Title: "Germany", Protocols: []string{"wireguard", "awg2"}, Flag: "🇩🇪", Speed: "1 Gbps", Disabled: false, PanelName: "de-1", PanelHost: "1.2.3.4"}, {ID: 2, Title: "France", Protocols: nil, Disabled: true}, }, PanelReachable: true, }}, {"page_admin_servers", adminServersView{ adminLayout: adminLayout{Admin: adminUser, Active: "servers"}, AllProtocols: share.GuestProtocols, PanelErr: "connection refused", }}, {"page_admin_settings", adminSettingsView{ adminLayout: adminLayout{Admin: adminUser, Active: "settings"}, PanelURL: "https://panel.example.com", APIToken: "awp_secret", ServerLabelsJSON: `{"1":"Germany"}`, TrafficNotices: `{}`, Maintenance: true, PocketURL: "https://id.example.com", PocketClientID: "client-id", PocketSecret: "secret", MemberDays: "30", MemberMaxUses: "5", TestResult: "Соединение установлено. Серверов обнаружено: 3.", }}, {"page_admin_settings", adminSettingsView{ adminLayout: adminLayout{Admin: adminUser, Active: "settings"}, TestError: "connection refused", }}, } for i, c := range cases { clone, err := tmpl.Clone() if err != nil { t.Fatalf("clone: %v", err) } if err := clone.ExecuteTemplate(io.Discard, c.name, c.data); err != nil { t.Errorf("case %d (%s): execute failed: %v", i, c.name, err) } } }