hugo/common/paths/path_test.go
Bjørn Erik Pedersen 34033e349a
Strip hash sign (#) from file paths/URLs
The general way Hugo does this now is:

* Sanitize the file paths so the work as URLs
* When we create the final RelPermalink/Permalink, we use Go's `url.Parse` to escape it so it work for the browser.

So, leaving anything in the first step that does not work with the second step, just doesn't work.

It's a little bit odd that `url.Parse` silently truncates this URL without any error, but that's for another day.

I have another better test coverage for this.

Fixes #12342
Fixes #4926
See #8232
2024-04-05 11:37:18 +02:00

280 lines
8.1 KiB
Go

// Copyright 2024 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 paths
import (
"fmt"
"net/url"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
func TestGetRelativePath(t *testing.T) {
tests := []struct {
path string
base string
expect any
}{
{filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
{filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
{filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
{filepath.FromSlash("/c"), "", false},
}
for i, this := range tests {
// ultimately a fancy wrapper around filepath.Rel
result, err := GetRelativePath(this.path, this.base)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
}
} else {
if err != nil {
t.Errorf("[%d] GetRelativePath failed: %s", i, err)
continue
}
if result != this.expect {
t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
}
}
}
}
func TestMakePathRelative(t *testing.T) {
type test struct {
inPath, path1, path2, output string
}
data := []test{
{"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"},
{"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"},
}
for i, d := range data {
output, _ := makePathRelative(d.inPath, d.path1, d.path2)
if d.output != output {
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
}
}
_, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
if error == nil {
t.Errorf("Test failed, expected error")
}
}
func TestMakeTitle(t *testing.T) {
type test struct {
input, expected string
}
data := []test{
{"Make-Title", "Make Title"},
{"MakeTitle", "MakeTitle"},
{"make_title", "make_title"},
}
for i, d := range data {
output := MakeTitle(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
}
}
// Replace Extension is probably poorly named, but the intent of the
// function is to accept a path and return only the file name with a
// new extension. It's intentionally designed to strip out the path
// and only provide the name. We should probably rename the function to
// be more explicit at some point.
func TestReplaceExtension(t *testing.T) {
type test struct {
input, newext, expected string
}
data := []test{
// These work according to the above definition
{"/some/random/path/file.xml", "html", "file.html"},
{"/banana.html", "xml", "banana.xml"},
{"./banana.html", "xml", "banana.xml"},
{"banana/pie/index.html", "xml", "index.xml"},
{"../pies/fish/index.html", "xml", "index.xml"},
// but these all fail
{"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
{"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
{"/directory/mydir/", "ext", ".ext"},
{"mydir/", "ext", ".ext"},
}
for i, d := range data {
output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
}
}
func TestExtNoDelimiter(t *testing.T) {
c := qt.New(t)
c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
}
func TestFilename(t *testing.T) {
type test struct {
input, expected string
}
data := []test{
{"index.html", "index"},
{"./index.html", "index"},
{"/index.html", "index"},
{"index", "index"},
{"/tmp/index.html", "index"},
{"./filename-no-ext", "filename-no-ext"},
{"/filename-no-ext", "filename-no-ext"},
{"filename-no-ext", "filename-no-ext"},
{"directory/", ""}, // no filename case??
{"directory/.hidden.ext", ".hidden"},
{"./directory/../~/banana/gold.fish", "gold"},
{"../directory/banana.man", "banana"},
{"~/mydir/filename.ext", "filename"},
{"./directory//tmp/filename.ext", "filename"},
}
for i, d := range data {
output := Filename(filepath.FromSlash(d.input))
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
}
}
func TestFileAndExt(t *testing.T) {
type test struct {
input, expectedFile, expectedExt string
}
data := []test{
{"index.html", "index", ".html"},
{"./index.html", "index", ".html"},
{"/index.html", "index", ".html"},
{"index", "index", ""},
{"/tmp/index.html", "index", ".html"},
{"./filename-no-ext", "filename-no-ext", ""},
{"/filename-no-ext", "filename-no-ext", ""},
{"filename-no-ext", "filename-no-ext", ""},
{"directory/", "", ""}, // no filename case??
{"directory/.hidden.ext", ".hidden", ".ext"},
{"./directory/../~/banana/gold.fish", "gold", ".fish"},
{"../directory/banana.man", "banana", ".man"},
{"~/mydir/filename.ext", "filename", ".ext"},
{"./directory//tmp/filename.ext", "filename", ".ext"},
}
for i, d := range data {
file, ext := fileAndExt(filepath.FromSlash(d.input), fpb)
if d.expectedFile != file {
t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file)
}
if d.expectedExt != ext {
t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext)
}
}
}
func TestSanitize(t *testing.T) {
c := qt.New(t)
tests := []struct {
input string
expected string
}{
{" Foo bar ", "Foo-bar"},
{"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo"},
{"fOO,bar:foobAR", "fOObarfoobAR"},
{"FOo/BaR.html", "FOo/BaR.html"},
{"FOo/Ba---R.html", "FOo/Ba---R.html"}, /// See #10104
{"FOo/Ba R.html", "FOo/Ba-R.html"},
{"трям/трям", "трям/трям"},
{"은행", "은행"},
{"Банковский кассир", "Банковский-кассир"},
{"संस्कृत", "संस्कृत"}, // Issue #1488
{"this+is+a+test", "this+is+a+test"}, // Issue #1290
{"~foo", "~foo"}, // Issue #2177
// Issue #2342
{"foo#bar", "foobar"},
{"foo@bar", "foo@bar"},
}
for _, test := range tests {
c.Assert(Sanitize(test.input), qt.Equals, test.expected)
// Make sure they survive the URL roundtrip, which makes sure that this works with URLs (e.g. in Permalink)
protocol := "https://"
urlString := fmt.Sprintf("%s%s", protocol, test.expected)
unescaped, err := url.PathUnescape(strings.TrimPrefix(URLEscape(urlString), protocol))
c.Assert(err, qt.IsNil)
c.Assert(unescaped, qt.Equals, test.expected)
}
// Some special cases.
c.Assert(Sanitize("a%C3%B1ame"), qt.Equals, "a%C3%B1ame") // Issue #1292
}
func BenchmarkSanitize(b *testing.B) {
const (
allAlowedPath = "foo/bar"
spacePath = "foo bar"
)
// This should not allocate any memory.
b.Run("All allowed", func(b *testing.B) {
for i := 0; i < b.N; i++ {
got := Sanitize(allAlowedPath)
if got != allAlowedPath {
b.Fatal(got)
}
}
})
// This will allocate some memory.
b.Run("Spaces", func(b *testing.B) {
for i := 0; i < b.N; i++ {
got := Sanitize(spacePath)
if got != "foo-bar" {
b.Fatal(got)
}
}
})
}
func TestDir(t *testing.T) {
c := qt.New(t)
c.Assert(Dir("/a/b/c/d"), qt.Equals, "/a/b/c")
c.Assert(Dir("/a"), qt.Equals, "/")
c.Assert(Dir("/"), qt.Equals, "/")
c.Assert(Dir(""), qt.Equals, "")
}
func TestFieldsSlash(t *testing.T) {
c := qt.New(t)
c.Assert(FieldsSlash("a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(FieldsSlash("/a/b/c"), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(FieldsSlash("/a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(FieldsSlash("a/b/c/"), qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(FieldsSlash("/"), qt.DeepEquals, []string{})
c.Assert(FieldsSlash(""), qt.DeepEquals, []string{})
}