1
0
Fork 0
mirror of https://github.com/Eggbertx/gochan.git synced 2025-09-18 13:46:23 -07:00
gochan/pkg/building/building_test.go
2025-07-10 16:28:13 -07:00

449 lines
14 KiB
Go

package building
import (
"bytes"
"database/sql"
"database/sql/driver"
"io"
"os"
"path"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/PuerkitoBio/goquery"
"github.com/gochan-org/gochan/pkg/config"
"github.com/gochan-org/gochan/pkg/gcsql"
_ "github.com/gochan-org/gochan/pkg/gcsql/initsql"
"github.com/gochan-org/gochan/pkg/gctemplates"
"github.com/gochan-org/gochan/pkg/gcutil/testutil"
_ "github.com/gochan-org/gochan/pkg/posting/uploads/inituploads"
"github.com/gochan-org/gochan/pkg/server/serverutil"
"github.com/stretchr/testify/assert"
)
var (
pageHeaderTestCases = []pageHeaderTestCase{
{
desc: "Front page with includes",
pageTitle: "Gochan",
includeJS: []config.IncludeScript{{Location: "test.js", Defer: true}, {Location: "test2.js", Defer: false}},
includeCSS: []string{"test.css"},
expectTitleText: "Gochan",
misc: map[string]any{
"documentTitle": "Gochan",
},
hasPosts: true,
},
{
desc: "Front page without includes",
pageTitle: "Gochan",
board: "",
expectTitleText: "Gochan",
misc: map[string]any{
"documentTitle": "Gochan",
},
hasPosts: true,
},
{
desc: "Regular ban page",
misc: map[string]any{
"ban": gcsql.IPBan{},
},
expectTitleText: "YOU ARE BANNED :(",
},
{
desc: "Unappealable permaban",
misc: map[string]any{
"ban": gcsql.IPBan{
IPBanBase: gcsql.IPBanBase{
CanAppeal: false,
Permanent: true,
IsActive: true,
},
},
},
expectTitleText: "YOU'RE PERMABANNED,\u00a0IDIOT!",
},
{
desc: "Board page",
pageTitle: "Gochan",
board: "test",
misc: map[string]any{
"documentTitle": "/test/ - Testing board",
},
expectTitleText: "/test/ - Testing board",
hasPosts: true,
},
}
)
func TestBuildJS(t *testing.T) {
testRoot, err := testutil.GoToGochanRoot(t)
if !assert.NoError(t, err) {
t.FailNow()
}
outDir := t.TempDir()
config.InitTestConfig()
systemCriticalCfg := config.GetSystemCriticalConfig()
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
systemCriticalCfg.WebRoot = "/chan"
systemCriticalCfg.TimeZone = 8
config.SetSystemCriticalConfig(systemCriticalCfg)
boardCfg := config.GetBoardConfig("")
boardCfg.Styles = []config.Style{
{Name: "test1", Filename: "test1.css"},
{Name: "test2", Filename: "test2.css"},
}
boardCfg.DefaultStyle = "test1.css"
serverutil.InitMinifier()
os.MkdirAll(path.Join(systemCriticalCfg.DocumentRoot, "js"), config.DirFileMode)
if err = BuildJS(); !assert.NoError(t, err) {
return
}
jsFile, err := os.Open(path.Join(systemCriticalCfg.DocumentRoot, "js/consts.js"))
if !assert.NoError(t, err) {
return
}
defer jsFile.Close()
ba, err := io.ReadAll(jsFile)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, expectedMinifiedJS, string(ba))
siteCfg := config.GetSiteConfig()
siteCfg.MinifyJS = false
if err = BuildJS(); !assert.NoError(t, err) {
return
}
jsFile.Seek(0, io.SeekStart)
if ba, err = io.ReadAll(jsFile); !assert.NoError(t, err) {
return
}
assert.NoError(t, jsFile.Close())
assert.Equal(t, expectedUnminifiedJS, string(ba))
}
func mockSetupBoards(t *testing.T, mock sqlmock.Sqlmock, expectRecentPosts bool) {
mock.ExpectPrepare(`SELECT\s*` +
`boards.id, section_id, uri, dir, navbar_position, title, subtitle, description,\s*` +
`max_file_size, max_threads, default_style, boards\.locked, created_at, anonymous_name, force_anonymous,\s*` +
`autosage_after, no_images_after, max_message_length, min_message_length, allow_embeds, redirect_to_thread,\s*` +
`require_file, enable_catalog\s*` +
`FROM boards\s*` +
`INNER JOIN \(\s*` +
`SELECT id, hidden FROM sections\s*` +
`\) s ON boards.section_id = s.id\s*` +
`WHERE s\.hidden = FALSE\s*` +
`ORDER BY navbar_position ASC, boards.id ASC`).ExpectQuery().WillReturnRows(
sqlmock.NewRows([]string{
"boards.id", "section_id", "uri", "dir", "navbar_position", "title", "subtitle", "description",
"max_file_size", "max_threads", "default_style", "locked", "created_at", "anonymous_name", "force_anonymous",
"autosage_after", "no_images_after", "max_message_length", "min_message_length", "allow_embeds", "redirect_to_thread",
"require_file", "enable_catalog",
}).AddRows([]driver.Value{
1, 1, "test", "test", 1, "Testing board", "Board for testing", "Board for testing description",
15000, 100, "pipes.css", false, time.Now(), "Anonymous", false,
1500, 2000, 1500, 0, true, false, false, true,
}).AddRows([]driver.Value{
1, 1, "test2", "test2", 1, "Testing board 2", "Board for testing 2", "Board for testing description 2",
15000, 100, "pipes.css", false, time.Now(), "Anonymous", false,
1500, 2000, 1500, 0, true, false, false, true,
}),
)
mock.ExpectPrepare(
`SELECT id, name, abbreviation, position, hidden FROM sections WHERE hidden = FALSE ORDER BY position ASC, name ASC`,
).ExpectQuery().
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "abbreviation", "position", "hidden"}).
AddRows([]driver.Value{1, "Main", "main", 1, false}))
if expectRecentPosts {
mockSetupPosts(mock)
}
err := gcsql.ResetBoardSectionArrays()
if !assert.NoError(t, err) {
t.FailNow()
}
}
func mockSetupPosts(mock sqlmock.Sqlmock) {
mock.ExpectPrepare(`SELECT id, message_raw, dir, filename, original_filename, op_id FROM v_front_page_posts ORDER BY id DESC LIMIT 15`).ExpectQuery().WillReturnRows(
sqlmock.NewRows([]string{"posts.id", "posts.message_raw", "dir", "filename", "original_filename", "op.id"}).
AddRows(
[]driver.Value{6, "message_raw 6", "test", "filename.png", "12345.png", 1},
[]driver.Value{5, "message_raw 5", "test", "", "", 1},
[]driver.Value{4, "message_raw 4", "test", "deleted", "deleted", 1},
[]driver.Value{3, "message_raw 3", "test2", "embed:rawvideo", "http://example.com/video.webm", 1},
[]driver.Value{2, "message_raw 2", "test2", "embed:youtube", "abcd", 1},
[]driver.Value{1, "message_raw 1", "test2", "embed:youtube", "wxyz", 1},
))
}
func doFrontBuildingTest(t *testing.T, mock sqlmock.Sqlmock) {
serverutil.InitMinifier()
mockSetupBoards(t, mock, true)
err := BuildFrontPage()
if !assert.NoError(t, err) {
t.FailNow()
}
if !assert.NoError(t, mock.ExpectationsWereMet()) {
t.FailNow()
}
frontFile, err := os.Open(path.Join(config.GetSystemCriticalConfig().DocumentRoot, "index.html"))
if !assert.NoError(t, err) {
t.FailNow()
}
defer frontFile.Close()
doc, err := goquery.NewDocumentFromReader(frontFile)
if !assert.NoError(t, err) {
t.FailNow()
}
boardsDiv := doc.Find("div#frontpage div.section-block:nth-of-type(2)")
if !assert.Equal(t, 1, boardsDiv.Length()) {
t.FailNow()
}
assert.Equal(t, "Boards", boardsDiv.Find("div.section-title-block").Text())
sectionUl := boardsDiv.Find("div.section-body ul")
if !assert.Equal(t, 1, sectionUl.Length()) {
t.FailNow()
}
li := sectionUl.Find("li")
assert.Equal(t, 3, li.Length())
assert.Equal(t, config.GetSiteConfig().SiteName, doc.Find("title").Text())
assert.Equal(t, config.GetSiteConfig().SiteName, doc.Find("div#top-pane h1").Text())
assert.Equal(t, config.GetSiteConfig().SiteSlogan, doc.Find("div#top-pane span#site-slogan").Text())
assert.Equal(t, "Main", li.Eq(0).Text())
assert.Equal(t, "/test/ — Testing board", li.Eq(1).Text())
assert.Equal(t, "/test2/ — Testing board 2", li.Eq(2).Text())
assert.Equal(t, "/chan/test/", li.Eq(1).Find("a").AttrOr("href", ""))
assert.Equal(t, "/chan/test2/", li.Eq(2).Find("a").AttrOr("href", ""))
recentPostsContainer := doc.Find("div#frontpage div.section-block:nth-of-type(3)")
if !assert.Equal(t, 1, recentPostsContainer.Length()) {
t.FailNow()
}
assert.Equal(t, "Recent Posts", recentPostsContainer.Find("div.section-title-block").Text())
recentPosts := recentPostsContainer.Find("div.section-body div.recent-post")
if !assert.Equal(t, 6, recentPosts.Length()) {
t.FailNow()
}
assert.Regexp(t, `/test/\s*message_raw 6`, recentPosts.Eq(0).Text())
assert.Equal(t, 1, recentPosts.Eq(0).Find(`img[src="/chan/test/thumb/filenamet.png"]`).Length())
assert.Equal(t, 1, recentPosts.Eq(1).Find("div.file-deleted-box").Length())
assert.Equal(t, 1, recentPosts.Eq(2).Find("div.file-deleted-box").Length())
assert.Equal(t, 1, recentPosts.Eq(3).Find("div.file-deleted-box").Length())
assert.Equal(t, "Post embed", recentPosts.Eq(3).Find("div.file-deleted-box").Text())
assert.Equal(t, 1, recentPosts.Eq(4).Find("img").Length())
assert.Equal(t, 1, recentPosts.Eq(5).Find("img").Length())
assert.NoError(t, frontFile.Close())
}
func TestBuildFrontPage(t *testing.T) {
testRoot, err := testutil.GoToGochanRoot(t)
if !assert.NoError(t, err) {
t.FailNow()
}
for _, driver := range sql.Drivers() {
if driver == "sqlmock" {
continue
}
t.Run(driver, func(t *testing.T) {
outDir := t.TempDir()
config.InitTestConfig()
systemCriticalCfg := config.GetSystemCriticalConfig()
systemCriticalCfg.DocumentRoot = path.Join(outDir, "html")
systemCriticalCfg.TemplateDir = path.Join(testRoot, "templates")
systemCriticalCfg.LogDir = path.Join(outDir, "logs")
systemCriticalCfg.WebRoot = "/chan"
systemCriticalCfg.TimeZone = 8
config.SetSystemCriticalConfig(systemCriticalCfg)
siteConfig := config.GetSiteConfig()
siteConfig.SiteName = "Gochan"
siteConfig.SiteSlogan = "Gochan description"
siteConfig.RecentPostsWithNoFile = true
config.SetSiteConfig(siteConfig)
boardCfg := config.GetBoardConfig("")
boardCfg.Styles = []config.Style{{Name: "test1", Filename: "test1.css"}}
boardCfg.DefaultStyle = "test1.css"
rawVideoSubmatchIndex := 0
boardCfg.EmbedMatchers = map[string]config.EmbedMatcher{
"rawvideo": {
URLRegex: "^https?://.*\\.(?:webm|mp4)$",
EmbedTemplate: `<video class="embed embed-{{.HandlerID}}" src="{{.MediaID}}" style="max-width:{{.ThumbWidth}}px; max-height:{{.ThumbHeight}}px"></video>`,
MediaIDSubmatchIndex: &rawVideoSubmatchIndex,
MediaURLTemplate: "{{.MediaID}}",
},
"youtube": {
URLRegex: `^https?://(?:(?:(?:www\.)?youtube\.com/watch\?v=)|(?:youtu\.be/))([^&]+)`,
EmbedTemplate: `<iframe class="embed embed-{{.HandlerID}}" width={{.ThumbWidth}} height={{.ThumbHeight}} src="{{.MediaID}}" </iframe>`,
ThumbnailURLTemplate: "{{.MediaID}}",
MediaURLTemplate: "https://www.youtube.com/watch?v={{.MediaID}}",
},
}
if !assert.NoError(t, config.SetBoardConfig("", boardCfg)) {
t.FailNow()
}
if !assert.NoError(t, os.MkdirAll(systemCriticalCfg.DocumentRoot, config.DirFileMode)) {
t.FailNow()
}
mock, err := gcsql.SetupMockDB(driver)
if !assert.NoError(t, err) {
t.FailNow()
}
siteCfg := config.GetSiteConfig()
t.Run("with minification", func(t *testing.T) {
siteCfg.MinifyHTML = true
config.SetSiteConfig(siteCfg)
doFrontBuildingTest(t, mock)
})
t.Run("without minification", func(t *testing.T) {
siteCfg.MinifyHTML = false
config.SetSiteConfig(siteCfg)
doFrontBuildingTest(t, mock)
})
})
}
}
type pageHeaderTestCase struct {
desc string
pageTitle string
board string
misc map[string]any
includeJS []config.IncludeScript
includeCSS []string
expectError bool
expectTitleText string
hasPosts bool
}
func (p *pageHeaderTestCase) runTest(t *testing.T, sqlDriver string) {
mock, err := gcsql.SetupMockDB(sqlDriver)
if !assert.NoError(t, err) {
t.FailNow()
}
boardCfg := config.GetBoardConfig(p.board)
boardCfg.IncludeGlobalStyles = p.includeCSS
boardCfg.IncludeScripts = p.includeJS
config.SetBoardConfig(p.board, boardCfg)
mockSetupBoards(t, mock, false)
err = gctemplates.InitTemplates()
if !assert.NoError(t, err) {
t.FailNow()
}
var buf bytes.Buffer
err = BuildPageHeader(&buf, p.pageTitle, p.board, p.misc)
if p.expectError {
assert.Error(t, err)
} else {
if !assert.NoError(t, err) {
t.FailNow()
}
}
if !assert.NoError(t, mock.ExpectationsWereMet()) {
t.FailNow()
}
doc, err := goquery.NewDocumentFromReader(&buf)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Equal(t, len(p.includeJS)+2, doc.Find("script").Length())
for i, js := range p.includeJS {
script := doc.Find("script").Eq(i + 2)
assert.Equal(t, js.Location, script.AttrOr("src", ""))
_, hasDefer := script.Attr("defer")
assert.Equal(t, js.Defer, hasDefer)
}
assert.Equal(t, len(p.includeCSS)+2, doc.Find(`link[rel="stylesheet"]`).Length())
assert.Equal(t, p.expectTitleText, doc.Find("title").Text())
if _, ok := p.misc["ban"]; !ok {
topbarItems := doc.Find("a.topbar-item")
assert.Equal(t, 3, topbarItems.Length())
assert.Equal(t, "home", topbarItems.Eq(0).Text())
assert.Equal(t, "/test/", topbarItems.Eq(1).Text())
assert.Equal(t, "/test2/", topbarItems.Eq(2).Text())
}
}
func TestBuildPageHeader(t *testing.T) {
config.InitTestConfig()
_, err := testutil.GoToGochanRoot(t)
if !assert.NoError(t, err) {
return
}
systemCriticalConfig := config.GetSystemCriticalConfig()
systemCriticalConfig.TemplateDir = "templates"
config.SetSystemCriticalConfig(systemCriticalConfig)
for _, tc := range pageHeaderTestCases {
for _, driver := range sql.Drivers() {
if driver == "sqlmock" {
continue
}
t.Run(tc.desc+" - "+driver, func(t *testing.T) {
tc.runTest(t, driver)
})
}
}
}
func TestBuildPageFooter(t *testing.T) {
config.InitTestConfig()
_, err := testutil.GoToGochanRoot(t)
if !assert.NoError(t, err) {
t.FailNow()
}
systemCriticalConfig := config.GetSystemCriticalConfig()
systemCriticalConfig.TemplateDir = "templates"
config.SetSystemCriticalConfig(systemCriticalConfig)
mock, err := gcsql.SetupMockDB("sqlite3")
if !assert.NoError(t, err) {
t.FailNow()
}
var buf bytes.Buffer
if !assert.NoError(t, BuildPageFooter(&buf)) {
t.FailNow()
}
if !assert.NoError(t, mock.ExpectationsWereMet()) {
t.FailNow()
}
doc, err := goquery.NewDocumentFromReader(&buf)
if !assert.NoError(t, err) {
t.FailNow()
}
assert.Regexp(t, `Powered by Gochan \d+\.\d+\.\d+`, doc.Find("footer").Text())
}