hugo/hugolib/config_test.go
Bjørn Erik Pedersen f38a2fbd2e Make hugo.toml the new config.toml
Both will of course work, but hugo.toml will win if both are set.

We should have done this a long time ago, of course, but the reason I'm picking this up now is that my VS Code setup by default picks up some
JSON config schema from some random other software which also names its config files config.toml.

Fixes #8979
2023-01-16 15:34:16 +01:00

837 lines
18 KiB
Go

// Copyright 2016-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"bytes"
"fmt"
"path/filepath"
"strings"
"testing"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/media"
"github.com/google/go-cmp/cmp"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/maps"
"github.com/spf13/afero"
)
func TestLoadConfig(t *testing.T) {
c := qt.New(t)
loadConfig := func(c *qt.C, configContent string, fromDir bool) config.Provider {
mm := afero.NewMemMapFs()
filename := "config.toml"
descriptor := ConfigSourceDescriptor{Fs: mm}
if fromDir {
filename = filepath.Join("config", "_default", filename)
descriptor.AbsConfigDir = "config"
}
writeToFs(t, mm, filename, configContent)
cfg, _, err := LoadConfig(descriptor)
c.Assert(err, qt.IsNil)
return cfg
}
c.Run("Basic", func(c *qt.C) {
c.Parallel()
// Add a random config variable for testing.
// side = page in Norwegian.
cfg := loadConfig(c, `PaginatePath = "side"`, false)
c.Assert(cfg.GetString("paginatePath"), qt.Equals, "side")
})
// Issue #8763
for _, fromDir := range []bool{false, true} {
testName := "Taxonomy overrides"
if fromDir {
testName += " from dir"
}
c.Run(testName, func(c *qt.C) {
c.Parallel()
cfg := loadConfig(c, `[taxonomies]
appellation = "appellations"
vigneron = "vignerons"`, fromDir)
c.Assert(cfg.Get("taxonomies"), qt.DeepEquals, maps.Params{
"appellation": "appellations",
"vigneron": "vignerons",
})
})
}
}
func TestLoadMultiConfig(t *testing.T) {
t.Parallel()
c := qt.New(t)
// Add a random config variable for testing.
// side = page in Norwegian.
configContentBase := `
DontChange = "same"
PaginatePath = "side"
`
configContentSub := `
PaginatePath = "top"
`
mm := afero.NewMemMapFs()
writeToFs(t, mm, "base.toml", configContentBase)
writeToFs(t, mm, "override.toml", configContentSub)
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Filename: "base.toml,override.toml"})
c.Assert(err, qt.IsNil)
c.Assert(cfg.GetString("paginatePath"), qt.Equals, "top")
c.Assert(cfg.GetString("DontChange"), qt.Equals, "same")
}
func TestLoadConfigFromThemes(t *testing.T) {
t.Parallel()
c := qt.New(t)
mainConfigTemplate := `
theme = "test-theme"
baseURL = "https://example.com/"
[frontmatter]
date = ["date","publishDate"]
[params]
MERGE_PARAMS
p1 = "p1 main"
[params.b]
b1 = "b1 main"
[params.b.c]
bc1 = "bc1 main"
[mediaTypes]
[mediaTypes."text/m1"]
suffixes = ["m1main"]
[outputFormats.o1]
mediaType = "text/m1"
baseName = "o1main"
[languages]
[languages.en]
languageName = "English"
[languages.en.params]
pl1 = "p1-en-main"
[languages.nb]
languageName = "Norsk"
[languages.nb.params]
pl1 = "p1-nb-main"
[[menu.main]]
name = "menu-main-main"
[[menu.top]]
name = "menu-top-main"
`
themeConfig := `
baseURL = "http://bep.is/"
# Can not be set in theme.
disableKinds = ["taxonomy", "term"]
# Can not be set in theme.
[frontmatter]
expiryDate = ["date"]
[params]
p1 = "p1 theme"
p2 = "p2 theme"
[params.b]
b1 = "b1 theme"
b2 = "b2 theme"
[params.b.c]
bc1 = "bc1 theme"
bc2 = "bc2 theme"
[params.b.c.d]
bcd1 = "bcd1 theme"
[mediaTypes]
[mediaTypes."text/m1"]
suffixes = ["m1theme"]
[mediaTypes."text/m2"]
suffixes = ["m2theme"]
[outputFormats.o1]
mediaType = "text/m1"
baseName = "o1theme"
[outputFormats.o2]
mediaType = "text/m2"
baseName = "o2theme"
[languages]
[languages.en]
languageName = "English2"
[languages.en.params]
pl1 = "p1-en-theme"
pl2 = "p2-en-theme"
[[languages.en.menu.main]]
name = "menu-lang-en-main"
[[languages.en.menu.theme]]
name = "menu-lang-en-theme"
[languages.nb]
languageName = "Norsk2"
[languages.nb.params]
pl1 = "p1-nb-theme"
pl2 = "p2-nb-theme"
top = "top-nb-theme"
[[languages.nb.menu.main]]
name = "menu-lang-nb-main"
[[languages.nb.menu.theme]]
name = "menu-lang-nb-theme"
[[languages.nb.menu.top]]
name = "menu-lang-nb-top"
[[menu.main]]
name = "menu-main-theme"
[[menu.thememenu]]
name = "menu-theme"
`
buildForConfig := func(t testing.TB, mainConfig, themeConfig string) *sitesBuilder {
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
return b.Build(BuildCfg{})
}
buildForStrategy := func(t testing.TB, s string) *sitesBuilder {
mainConfig := strings.ReplaceAll(mainConfigTemplate, "MERGE_PARAMS", s)
return buildForConfig(t, mainConfig, themeConfig)
}
c.Run("Merge default", func(c *qt.C) {
b := buildForStrategy(c, "")
got := b.Cfg.Get("").(maps.Params)
// Issue #8866
b.Assert(b.Cfg.Get("disableKinds"), qt.IsNil)
b.Assert(got["params"], qt.DeepEquals, maps.Params{
"b": maps.Params{
"b1": "b1 main",
"c": maps.Params{
"bc1": "bc1 main",
"bc2": "bc2 theme",
"d": maps.Params{"bcd1": string("bcd1 theme")},
},
"b2": "b2 theme",
},
"p2": "p2 theme",
"p1": "p1 main",
})
b.Assert(got["mediatypes"], qt.DeepEquals, maps.Params{
"text/m2": maps.Params{
"suffixes": []any{
"m2theme",
},
},
"text/m1": maps.Params{
"suffixes": []any{
"m1main",
},
},
})
var eq = qt.CmpEquals(
cmp.Comparer(func(m1, m2 media.Type) bool {
if m1.SubType != m2.SubType {
return false
}
return m1.FirstSuffix == m2.FirstSuffix
}),
)
mediaTypes := b.H.Sites[0].mediaTypesConfig
m1, _ := mediaTypes.GetByType("text/m1")
m2, _ := mediaTypes.GetByType("text/m2")
b.Assert(got["outputformats"], eq, maps.Params{
"o1": maps.Params{
"mediatype": m1,
"basename": "o1main",
},
"o2": maps.Params{
"basename": "o2theme",
"mediatype": m2,
},
})
b.Assert(got["languages"], qt.DeepEquals, maps.Params{
"en": maps.Params{
"languagename": "English",
"params": maps.Params{
"pl2": "p2-en-theme",
"pl1": "p1-en-main",
},
"menus": maps.Params{
"main": []any{
map[string]any{
"name": "menu-lang-en-main",
},
},
"theme": []any{
map[string]any{
"name": "menu-lang-en-theme",
},
},
},
},
"nb": maps.Params{
"languagename": "Norsk",
"params": maps.Params{
"top": "top-nb-theme",
"pl1": "p1-nb-main",
"pl2": "p2-nb-theme",
},
"menus": maps.Params{
"main": []any{
map[string]any{
"name": "menu-lang-nb-main",
},
},
"theme": []any{
map[string]any{
"name": "menu-lang-nb-theme",
},
},
"top": []any{
map[string]any{
"name": "menu-lang-nb-top",
},
},
},
},
})
c.Assert(got["baseurl"], qt.Equals, "https://example.com/")
})
c.Run("Merge shallow", func(c *qt.C) {
b := buildForStrategy(c, fmt.Sprintf("_merge=%q", "shallow"))
got := b.Cfg.Get("").(maps.Params)
// Shallow merge, only add new keys to params.
b.Assert(got["params"], qt.DeepEquals, maps.Params{
"p1": "p1 main",
"b": maps.Params{
"b1": "b1 main",
"c": maps.Params{
"bc1": "bc1 main",
},
},
"p2": "p2 theme",
})
})
c.Run("Merge no params in project", func(c *qt.C) {
b := buildForConfig(
c,
"baseURL=\"https://example.org\"\ntheme = \"test-theme\"\n",
"[params]\np1 = \"p1 theme\"\n",
)
got := b.Cfg.Get("").(maps.Params)
b.Assert(got["params"], qt.DeepEquals, maps.Params{
"p1": "p1 theme",
})
})
c.Run("Merge language no menus or params in project", func(c *qt.C) {
b := buildForConfig(
c,
`
theme = "test-theme"
baseURL = "https://example.com/"
[languages]
[languages.en]
languageName = "English"
`,
`
[languages]
[languages.en]
languageName = "EnglishTheme"
[languages.en.params]
p1="themep1"
[[languages.en.menus.main]]
name = "menu-theme"
`,
)
got := b.Cfg.Get("").(maps.Params)
b.Assert(got["languages"], qt.DeepEquals,
maps.Params{
"en": maps.Params{
"languagename": "English",
"menus": maps.Params{
"main": []any{
map[string]any{
"name": "menu-theme",
},
},
},
"params": maps.Params{
"p1": "themep1",
},
},
},
)
})
// Issue #8724
for _, mergeStrategy := range []string{"none", "shallow"} {
c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) {
smapConfigTempl := `[sitemap]
changefreq = %q
filename = "sitemap.xml"
priority = 0.5`
b := buildForConfig(
c,
fmt.Sprintf("_merge=%q\nbaseURL=\"https://example.org\"\ntheme = \"test-theme\"\n", mergeStrategy),
"baseURL=\"http://example.com\"\n"+fmt.Sprintf(smapConfigTempl, "monthly"),
)
got := b.Cfg.Get("").(maps.Params)
if mergeStrategy == "none" {
b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{
"priority": int(-1),
"filename": "sitemap.xml",
})
b.AssertFileContent("public/sitemap.xml", "schemas/sitemap")
} else {
b.Assert(got["sitemap"], qt.DeepEquals, maps.Params{
"priority": int(-1),
"filename": "sitemap.xml",
"changefreq": "monthly",
})
b.AssertFileContent("public/sitemap.xml", "<changefreq>monthly</changefreq>")
}
})
}
}
func TestLoadConfigFromThemeDir(t *testing.T) {
t.Parallel()
mainConfig := `
theme = "test-theme"
[params]
m1 = "mv1"
`
themeConfig := `
[params]
t1 = "tv1"
t2 = "tv2"
`
themeConfigDir := filepath.Join("themes", "test-theme", "config")
themeConfigDirDefault := filepath.Join(themeConfigDir, "_default")
themeConfigDirProduction := filepath.Join(themeConfigDir, "production")
projectConfigDir := "config"
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig)
b.Assert(b.Fs.Source.MkdirAll(themeConfigDirDefault, 0777), qt.IsNil)
b.Assert(b.Fs.Source.MkdirAll(themeConfigDirProduction, 0777), qt.IsNil)
b.Assert(b.Fs.Source.MkdirAll(projectConfigDir, 0777), qt.IsNil)
b.WithSourceFile(filepath.Join(projectConfigDir, "config.toml"), `[params]
m2 = "mv2"
`)
b.WithSourceFile(filepath.Join(themeConfigDirDefault, "config.toml"), `[params]
t2 = "tv2d"
t3 = "tv3d"
`)
b.WithSourceFile(filepath.Join(themeConfigDirProduction, "config.toml"), `[params]
t3 = "tv3p"
`)
b.Build(BuildCfg{})
got := b.Cfg.Get("params").(maps.Params)
b.Assert(got, qt.DeepEquals, maps.Params{
"t3": "tv3p",
"m1": "mv1",
"t1": "tv1",
"t2": "tv2d",
})
}
func TestPrivacyConfig(t *testing.T) {
t.Parallel()
c := qt.New(t)
tomlConfig := `
someOtherValue = "foo"
[privacy]
[privacy.youtube]
privacyEnhanced = true
`
b := newTestSitesBuilder(t)
b.WithConfigFile("toml", tomlConfig)
b.Build(BuildCfg{SkipRender: true})
c.Assert(b.H.Sites[0].Info.Config().Privacy.YouTube.PrivacyEnhanced, qt.Equals, true)
}
func TestLoadConfigModules(t *testing.T) {
t.Parallel()
c := qt.New(t)
// https://github.com/gohugoio/hugoThemes#themetoml
const (
// Before Hugo 0.56 each theme/component could have its own theme.toml
// with some settings, mostly used on the Hugo themes site.
// To preserve combability we read these files into the new "modules"
// section in config.toml.
o1t = `
name = "Component o1"
license = "MIT"
min_version = 0.38
`
// This is the component's config.toml, using the old theme syntax.
o1c = `
theme = ["n2"]
`
n1 = `
title = "Component n1"
[module]
description = "Component n1 description"
[module.hugoVersion]
min = "0.40.0"
max = "0.50.0"
extended = true
[[module.imports]]
path="o1"
[[module.imports]]
path="n3"
`
n2 = `
title = "Component n2"
`
n3 = `
title = "Component n3"
`
n4 = `
title = "Component n4"
`
)
b := newTestSitesBuilder(t)
writeThemeFiles := func(name, configTOML, themeTOML string) {
b.WithSourceFile(filepath.Join("themes", name, "data", "module.toml"), fmt.Sprintf("name=%q", name))
if configTOML != "" {
b.WithSourceFile(filepath.Join("themes", name, "config.toml"), configTOML)
}
if themeTOML != "" {
b.WithSourceFile(filepath.Join("themes", name, "theme.toml"), themeTOML)
}
}
writeThemeFiles("n1", n1, "")
writeThemeFiles("n2", n2, "")
writeThemeFiles("n3", n3, "")
writeThemeFiles("n4", n4, "")
writeThemeFiles("o1", o1c, o1t)
b.WithConfigFile("toml", `
[module]
[[module.imports]]
path="n1"
[[module.imports]]
path="n4"
`)
b.Build(BuildCfg{})
modulesClient := b.H.Paths.ModulesClient
var graphb bytes.Buffer
modulesClient.Graph(&graphb)
expected := `project n1
n1 o1
o1 n2
n1 n3
project n4
`
c.Assert(graphb.String(), qt.Equals, expected)
}
func TestLoadConfigWithOsEnvOverrides(t *testing.T) {
c := qt.New(t)
baseConfig := `
theme = "mytheme"
environment = "production"
enableGitInfo = true
intSlice = [5,7,9]
floatSlice = [3.14, 5.19]
stringSlice = ["a", "b"]
[outputFormats]
[outputFormats.ofbase]
mediaType = "text/plain"
[params]
paramWithNoEnvOverride="nooverride"
[params.api_config]
api_key="default_key"
another_key="default another_key"
[imaging]
anchor = "smart"
quality = 75
`
newB := func(t testing.TB) *sitesBuilder {
b := newTestSitesBuilder(t).WithConfigFile("toml", baseConfig)
b.WithSourceFile("themes/mytheme/config.toml", `
[outputFormats]
[outputFormats.oftheme]
mediaType = "text/plain"
[outputFormats.ofbase]
mediaType = "application/xml"
[params]
[params.mytheme_section]
theme_param="themevalue"
theme_param_nooverride="nooverride"
[params.mytheme_section2]
theme_param="themevalue2"
`)
return b
}
c.Run("Variations", func(c *qt.C) {
b := newB(c)
b.WithEnviron(
"HUGO_ENVIRONMENT", "test",
"HUGO_NEW", "new", // key not in config.toml
"HUGO_ENABLEGITINFO", "false",
"HUGO_IMAGING_ANCHOR", "top",
"HUGO_IMAGING_RESAMPLEFILTER", "CatmullRom",
"HUGO_STRINGSLICE", `["c", "d"]`,
"HUGO_INTSLICE", `[5, 8, 9]`,
"HUGO_FLOATSLICE", `[5.32]`,
// Issue #7829
"HUGOxPARAMSxAPI_CONFIGxAPI_KEY", "new_key",
// Delimiters are case sensitive.
"HUGOxPARAMSxAPI_CONFIGXANOTHER_KEY", "another_key",
// Issue #8346
"HUGOxPARAMSxMYTHEME_SECTIONxTHEME_PARAM", "themevalue_changed",
"HUGOxPARAMSxMYTHEME_SECTION2xTHEME_PARAM", "themevalue2_changed",
"HUGO_PARAMS_EMPTY", ``,
"HUGO_PARAMS_HTML", `<a target="_blank" />`,
// Issue #8618
"HUGO_SERVICES_GOOGLEANALYTICS_ID", `gaid`,
"HUGO_PARAMS_A_B_C", "abc",
)
b.Build(BuildCfg{})
cfg := b.H.Cfg
s := b.H.Sites[0]
scfg := s.siteConfigConfig.Services
c.Assert(cfg.Get("environment"), qt.Equals, "test")
c.Assert(cfg.GetBool("enablegitinfo"), qt.Equals, false)
c.Assert(cfg.Get("new"), qt.Equals, "new")
c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "top")
c.Assert(cfg.Get("imaging.quality"), qt.Equals, int64(75))
c.Assert(cfg.Get("imaging.resamplefilter"), qt.Equals, "CatmullRom")
c.Assert(cfg.Get("stringSlice"), qt.DeepEquals, []any{"c", "d"})
c.Assert(cfg.Get("floatSlice"), qt.DeepEquals, []any{5.32})
c.Assert(cfg.Get("intSlice"), qt.DeepEquals, []any{5, 8, 9})
c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key")
c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key")
c.Assert(cfg.Get("params.mytheme_section.theme_param"), qt.Equals, "themevalue_changed")
c.Assert(cfg.Get("params.mytheme_section.theme_param_nooverride"), qt.Equals, "nooverride")
c.Assert(cfg.Get("params.mytheme_section2.theme_param"), qt.Equals, "themevalue2_changed")
c.Assert(cfg.Get("params.empty"), qt.Equals, ``)
c.Assert(cfg.Get("params.html"), qt.Equals, `<a target="_blank" />`)
params := cfg.Get("params").(maps.Params)
c.Assert(params["paramwithnoenvoverride"], qt.Equals, "nooverride")
c.Assert(cfg.Get("params.paramwithnoenvoverride"), qt.Equals, "nooverride")
c.Assert(scfg.GoogleAnalytics.ID, qt.Equals, "gaid")
c.Assert(cfg.Get("params.a.b"), qt.DeepEquals, maps.Params{
"c": "abc",
})
ofBase, _ := s.outputFormatsConfig.GetByName("ofbase")
ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme")
c.Assert(ofBase.MediaType, qt.Equals, media.TextType)
c.Assert(ofTheme.MediaType, qt.Equals, media.TextType)
})
// Issue #8709
c.Run("Set in string", func(c *qt.C) {
b := newB(c)
b.WithEnviron(
"HUGO_ENABLEGITINFO", "false",
// imaging.anchor is a string, and it's not possible
// to set a child attribute.
"HUGO_IMAGING_ANCHOR_FOO", "top",
)
b.Build(BuildCfg{})
cfg := b.H.Cfg
c.Assert(cfg.Get("imaging.anchor"), qt.Equals, "smart")
})
}
func TestInvalidDefaultMarkdownHandler(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
[markup]
defaultMarkdownHandler = 'blackfriday'
-- content/_index.md --
## Foo
-- layouts/index.html --
{{ .Content }}
`
b, err := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
},
).BuildE()
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "Configured defaultMarkdownHandler \"blackfriday\" not found. Did you mean to use goldmark? Blackfriday was removed in Hugo v0.100.0.")
}
// Issue 8979
func TestHugoConfig(t *testing.T) {
filesTemplate := `
-- hugo.toml --
theme = "mytheme"
[params]
rootparam = "rootvalue"
-- config/_default/hugo.toml --
[params]
rootconfigparam = "rootconfigvalue"
-- themes/mytheme/config/_default/hugo.toml --
[params]
themeconfigdirparam = "themeconfigdirvalue"
-- themes/mytheme/hugo.toml --
[params]
themeparam = "themevalue"
-- layouts/index.html --
rootparam: {{ site.Params.rootparam }}
rootconfigparam: {{ site.Params.rootconfigparam }}
themeparam: {{ site.Params.themeparam }}
themeconfigdirparam: {{ site.Params.themeconfigdirparam }}
`
for _, configName := range []string{"hugo.toml", "config.toml"} {
configName := configName
t.Run(configName, func(t *testing.T) {
t.Parallel()
files := strings.ReplaceAll(filesTemplate, "hugo.toml", configName)
b, err := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
},
).BuildE()
b.Assert(err, qt.IsNil)
b.AssertFileContent("public/index.html",
"rootparam: rootvalue",
"rootconfigparam: rootconfigvalue",
"themeparam: themevalue",
"themeconfigdirparam: themeconfigdirvalue",
)
})
}
}