1// Copyright 2015 The Hugo Authors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package helpers
15
16import (
17	"fmt"
18	"io/ioutil"
19	"os"
20	"path/filepath"
21	"reflect"
22	"runtime"
23	"strconv"
24	"strings"
25	"testing"
26	"time"
27
28	"github.com/gohugoio/hugo/langs"
29
30	qt "github.com/frankban/quicktest"
31
32	"github.com/gohugoio/hugo/hugofs"
33	"github.com/spf13/afero"
34)
35
36func TestMakePath(t *testing.T) {
37	c := qt.New(t)
38	tests := []struct {
39		input         string
40		expected      string
41		removeAccents bool
42	}{
43		{"  Foo bar  ", "Foo-bar", true},
44		{"Foo.Bar/foo_Bar-Foo", "Foo.Bar/foo_Bar-Foo", true},
45		{"fOO,bar:foobAR", "fOObarfoobAR", true},
46		{"FOo/BaR.html", "FOo/BaR.html", true},
47		{"трям/трям", "трям/трям", true},
48		{"은행", "은행", true},
49		{"Банковский кассир", "Банковскии-кассир", true},
50		// Issue #1488
51		{"संस्कृत", "संस्कृत", false},
52		{"a%C3%B1ame", "a%C3%B1ame", false},         // Issue #1292
53		{"this+is+a+test", "this+is+a+test", false}, // Issue #1290
54		{"~foo", "~foo", false},                     // Issue #2177
55
56	}
57
58	for _, test := range tests {
59		v := newTestCfg()
60		v.Set("removePathAccents", test.removeAccents)
61
62		l := langs.NewDefaultLanguage(v)
63		p, err := NewPathSpec(hugofs.NewMem(v), l, nil)
64		c.Assert(err, qt.IsNil)
65
66		output := p.MakePath(test.input)
67		if output != test.expected {
68			t.Errorf("Expected %#v, got %#v\n", test.expected, output)
69		}
70	}
71}
72
73func TestMakePathSanitized(t *testing.T) {
74	v := newTestCfg()
75
76	p, _ := NewPathSpec(hugofs.NewMem(v), v, nil)
77
78	tests := []struct {
79		input    string
80		expected string
81	}{
82		{"  FOO bar  ", "foo-bar"},
83		{"Foo.Bar/fOO_bAr-Foo", "foo.bar/foo_bar-foo"},
84		{"FOO,bar:FooBar", "foobarfoobar"},
85		{"foo/BAR.HTML", "foo/bar.html"},
86		{"трям/трям", "трям/трям"},
87		{"은행", "은행"},
88	}
89
90	for _, test := range tests {
91		output := p.MakePathSanitized(test.input)
92		if output != test.expected {
93			t.Errorf("Expected %#v, got %#v\n", test.expected, output)
94		}
95	}
96}
97
98func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
99	v := newTestCfg()
100
101	v.Set("disablePathToLower", true)
102
103	l := langs.NewDefaultLanguage(v)
104	p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
105
106	tests := []struct {
107		input    string
108		expected string
109	}{
110		{"  FOO bar  ", "FOO-bar"},
111		{"Foo.Bar/fOO_bAr-Foo", "Foo.Bar/fOO_bAr-Foo"},
112		{"FOO,bar:FooBar", "FOObarFooBar"},
113		{"foo/BAR.HTML", "foo/BAR.HTML"},
114		{"трям/трям", "трям/трям"},
115		{"은행", "은행"},
116	}
117
118	for _, test := range tests {
119		output := p.MakePathSanitized(test.input)
120		if output != test.expected {
121			t.Errorf("Expected %#v, got %#v\n", test.expected, output)
122		}
123	}
124}
125
126func TestMakePathRelative(t *testing.T) {
127	type test struct {
128		inPath, path1, path2, output string
129	}
130
131	data := []test{
132		{"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"},
133		{"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"},
134	}
135
136	for i, d := range data {
137		output, _ := makePathRelative(d.inPath, d.path1, d.path2)
138		if d.output != output {
139			t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
140		}
141	}
142	_, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
143
144	if error == nil {
145		t.Errorf("Test failed, expected error")
146	}
147}
148
149func TestGetDottedRelativePath(t *testing.T) {
150	// on Windows this will receive both kinds, both country and western ...
151	for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} {
152		doTestGetDottedRelativePath(f, t)
153	}
154}
155
156func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
157	type test struct {
158		input, expected string
159	}
160	data := []test{
161		{"", "./"},
162		{urlFixer("/"), "./"},
163		{urlFixer("post"), "../"},
164		{urlFixer("/post"), "../"},
165		{urlFixer("post/"), "../"},
166		{urlFixer("tags/foo.html"), "../"},
167		{urlFixer("/tags/foo.html"), "../"},
168		{urlFixer("/post/"), "../"},
169		{urlFixer("////post/////"), "../"},
170		{urlFixer("/foo/bar/index.html"), "../../"},
171		{urlFixer("/foo/bar/foo/"), "../../../"},
172		{urlFixer("/foo/bar/foo"), "../../../"},
173		{urlFixer("foo/bar/foo/"), "../../../"},
174		{urlFixer("foo/bar/foo/bar"), "../../../../"},
175		{"404.html", "./"},
176		{"404.xml", "./"},
177		{"/404.html", "./"},
178	}
179	for i, d := range data {
180		output := GetDottedRelativePath(d.input)
181		if d.expected != output {
182			t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
183		}
184	}
185}
186
187func TestMakeTitle(t *testing.T) {
188	type test struct {
189		input, expected string
190	}
191	data := []test{
192		{"Make-Title", "Make Title"},
193		{"MakeTitle", "MakeTitle"},
194		{"make_title", "make_title"},
195	}
196	for i, d := range data {
197		output := MakeTitle(d.input)
198		if d.expected != output {
199			t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
200		}
201	}
202}
203
204func TestDirExists(t *testing.T) {
205	type test struct {
206		input    string
207		expected bool
208	}
209
210	data := []test{
211		{".", true},
212		{"./", true},
213		{"..", true},
214		{"../", true},
215		{"./..", true},
216		{"./../", true},
217		{os.TempDir(), true},
218		{os.TempDir() + FilePathSeparator, true},
219		{"/", true},
220		{"/some-really-random-directory-name", false},
221		{"/some/really/random/directory/name", false},
222		{"./some-really-random-local-directory-name", false},
223		{"./some/really/random/local/directory/name", false},
224	}
225
226	for i, d := range data {
227		exists, _ := DirExists(filepath.FromSlash(d.input), new(afero.OsFs))
228		if d.expected != exists {
229			t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists)
230		}
231	}
232}
233
234func TestIsDir(t *testing.T) {
235	type test struct {
236		input    string
237		expected bool
238	}
239	data := []test{
240		{"./", true},
241		{"/", true},
242		{"./this-directory-does-not-existi", false},
243		{"/this-absolute-directory/does-not-exist", false},
244	}
245
246	for i, d := range data {
247
248		exists, _ := IsDir(d.input, new(afero.OsFs))
249		if d.expected != exists {
250			t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists)
251		}
252	}
253}
254
255func TestIsEmpty(t *testing.T) {
256	zeroSizedFile, _ := createZeroSizedFileInTempDir()
257	defer deleteFileInTempDir(zeroSizedFile)
258	nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir()
259	defer deleteFileInTempDir(nonZeroSizedFile)
260	emptyDirectory, _ := createEmptyTempDir()
261	defer deleteTempDir(emptyDirectory)
262	nonEmptyZeroLengthFilesDirectory, _ := createTempDirWithZeroLengthFiles()
263	defer deleteTempDir(nonEmptyZeroLengthFilesDirectory)
264	nonEmptyNonZeroLengthFilesDirectory, _ := createTempDirWithNonZeroLengthFiles()
265	defer deleteTempDir(nonEmptyNonZeroLengthFilesDirectory)
266	nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt"
267	nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/"
268
269	fileDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentFile)
270	dirDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentDir)
271
272	type test struct {
273		input          string
274		expectedResult bool
275		expectedErr    error
276	}
277
278	data := []test{
279		{zeroSizedFile.Name(), true, nil},
280		{nonZeroSizedFile.Name(), false, nil},
281		{emptyDirectory, true, nil},
282		{nonEmptyZeroLengthFilesDirectory, false, nil},
283		{nonEmptyNonZeroLengthFilesDirectory, false, nil},
284		{nonExistentFile, false, fileDoesNotExist},
285		{nonExistentDir, false, dirDoesNotExist},
286	}
287	for i, d := range data {
288		exists, err := IsEmpty(d.input, new(afero.OsFs))
289		if d.expectedResult != exists {
290			t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists)
291		}
292		if d.expectedErr != nil {
293			if d.expectedErr.Error() != err.Error() {
294				t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err)
295			}
296		} else {
297			if d.expectedErr != err {
298				t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err)
299			}
300		}
301	}
302}
303
304func createZeroSizedFileInTempDir() (*os.File, error) {
305	filePrefix := "_path_test_"
306	f, e := ioutil.TempFile("", filePrefix) // dir is os.TempDir()
307	if e != nil {
308		// if there was an error no file was created.
309		// => no requirement to delete the file
310		return nil, e
311	}
312	return f, nil
313}
314
315func createNonZeroSizedFileInTempDir() (*os.File, error) {
316	f, err := createZeroSizedFileInTempDir()
317	if err != nil {
318		// no file ??
319		return nil, err
320	}
321	byteString := []byte("byteString")
322	err = ioutil.WriteFile(f.Name(), byteString, 0644)
323	if err != nil {
324		// delete the file
325		deleteFileInTempDir(f)
326		return nil, err
327	}
328	return f, nil
329}
330
331func deleteFileInTempDir(f *os.File) {
332	_ = os.Remove(f.Name())
333}
334
335func createEmptyTempDir() (string, error) {
336	dirPrefix := "_dir_prefix_"
337	d, e := ioutil.TempDir("", dirPrefix) // will be in os.TempDir()
338	if e != nil {
339		// no directory to delete - it was never created
340		return "", e
341	}
342	return d, nil
343}
344
345func createTempDirWithZeroLengthFiles() (string, error) {
346	d, dirErr := createEmptyTempDir()
347	if dirErr != nil {
348		return "", dirErr
349	}
350	filePrefix := "_path_test_"
351	_, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir()
352	if fileErr != nil {
353		// if there was an error no file was created.
354		// but we need to remove the directory to clean-up
355		deleteTempDir(d)
356		return "", fileErr
357	}
358	// the dir now has one, zero length file in it
359	return d, nil
360}
361
362func createTempDirWithNonZeroLengthFiles() (string, error) {
363	d, dirErr := createEmptyTempDir()
364	if dirErr != nil {
365		return "", dirErr
366	}
367	filePrefix := "_path_test_"
368	f, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir()
369	if fileErr != nil {
370		// if there was an error no file was created.
371		// but we need to remove the directory to clean-up
372		deleteTempDir(d)
373		return "", fileErr
374	}
375	byteString := []byte("byteString")
376
377	fileErr = ioutil.WriteFile(f.Name(), byteString, 0644)
378	if fileErr != nil {
379		// delete the file
380		deleteFileInTempDir(f)
381		// also delete the directory
382		deleteTempDir(d)
383		return "", fileErr
384	}
385
386	// the dir now has one, zero length file in it
387	return d, nil
388}
389
390func deleteTempDir(d string) {
391	_ = os.RemoveAll(d)
392}
393
394func TestExists(t *testing.T) {
395	zeroSizedFile, _ := createZeroSizedFileInTempDir()
396	defer deleteFileInTempDir(zeroSizedFile)
397	nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir()
398	defer deleteFileInTempDir(nonZeroSizedFile)
399	emptyDirectory, _ := createEmptyTempDir()
400	defer deleteTempDir(emptyDirectory)
401	nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt"
402	nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/"
403
404	type test struct {
405		input          string
406		expectedResult bool
407		expectedErr    error
408	}
409
410	data := []test{
411		{zeroSizedFile.Name(), true, nil},
412		{nonZeroSizedFile.Name(), true, nil},
413		{emptyDirectory, true, nil},
414		{nonExistentFile, false, nil},
415		{nonExistentDir, false, nil},
416	}
417	for i, d := range data {
418		exists, err := Exists(d.input, new(afero.OsFs))
419		if d.expectedResult != exists {
420			t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists)
421		}
422		if d.expectedErr != err {
423			t.Errorf("Test %d failed. Expected %q got %q", i, d.expectedErr, err)
424		}
425	}
426}
427
428func TestAbsPathify(t *testing.T) {
429	type test struct {
430		inPath, workingDir, expected string
431	}
432	data := []test{
433		{os.TempDir(), filepath.FromSlash("/work"), filepath.Clean(os.TempDir())}, // TempDir has trailing slash
434		{"dir", filepath.FromSlash("/work"), filepath.FromSlash("/work/dir")},
435	}
436
437	windowsData := []test{
438		{"c:\\banana\\..\\dir", "c:\\foo", "c:\\dir"},
439		{"\\dir", "c:\\foo", "c:\\foo\\dir"},
440		{"c:\\", "c:\\foo", "c:\\"},
441	}
442
443	unixData := []test{
444		{"/banana/../dir/", "/work", "/dir"},
445	}
446
447	for i, d := range data {
448		// todo see comment in AbsPathify
449		ps := newTestDefaultPathSpec("workingDir", d.workingDir)
450
451		expected := ps.AbsPathify(d.inPath)
452		if d.expected != expected {
453			t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected)
454		}
455	}
456	t.Logf("Running platform specific path tests for %s", runtime.GOOS)
457	if runtime.GOOS == "windows" {
458		for i, d := range windowsData {
459			ps := newTestDefaultPathSpec("workingDir", d.workingDir)
460
461			expected := ps.AbsPathify(d.inPath)
462			if d.expected != expected {
463				t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected)
464			}
465		}
466	} else {
467		for i, d := range unixData {
468			ps := newTestDefaultPathSpec("workingDir", d.workingDir)
469
470			expected := ps.AbsPathify(d.inPath)
471			if d.expected != expected {
472				t.Errorf("Test %d failed. Expected %q but got %q", i, d.expected, expected)
473			}
474		}
475	}
476}
477
478func TestExtractAndGroupRootPaths(t *testing.T) {
479	in := []string{
480		filepath.FromSlash("/a/b/c/d"),
481		filepath.FromSlash("/a/b/c/e"),
482		filepath.FromSlash("/a/b/e/f"),
483		filepath.FromSlash("/a/b"),
484		filepath.FromSlash("/a/b/c/b/g"),
485		filepath.FromSlash("/c/d/e"),
486	}
487
488	inCopy := make([]string, len(in))
489	copy(inCopy, in)
490
491	result := ExtractAndGroupRootPaths(in)
492
493	c := qt.New(t)
494	c.Assert(fmt.Sprint(result), qt.Equals, filepath.FromSlash("[/a/b/{c,e} /c/d/e]"))
495
496	// Make sure the original is preserved
497	c.Assert(in, qt.DeepEquals, inCopy)
498}
499
500func TestExtractRootPaths(t *testing.T) {
501	tests := []struct {
502		input    []string
503		expected []string
504	}{{
505		[]string{
506			filepath.FromSlash("a/b"), filepath.FromSlash("a/b/c/"), "b",
507			filepath.FromSlash("/c/d"), filepath.FromSlash("d/"), filepath.FromSlash("//e//"),
508		},
509		[]string{"a", "a", "b", "c", "d", "e"},
510	}}
511
512	for _, test := range tests {
513		output := ExtractRootPaths(test.input)
514		if !reflect.DeepEqual(output, test.expected) {
515			t.Errorf("Expected %#v, got %#v\n", test.expected, output)
516		}
517	}
518}
519
520func TestFindCWD(t *testing.T) {
521	type test struct {
522		expectedDir string
523		expectedErr error
524	}
525
526	// cwd, _ := os.Getwd()
527	data := []test{
528		//{cwd, nil},
529		// Commenting this out. It doesn't work properly.
530		// There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to.
531		// I really don't know a better way to test this function. - SPF 2014.11.04
532	}
533	for i, d := range data {
534		dir, err := FindCWD()
535		if d.expectedDir != dir {
536			t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
537		}
538		if d.expectedErr != err {
539			t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err)
540		}
541	}
542}
543
544func TestSafeWriteToDisk(t *testing.T) {
545	emptyFile, _ := createZeroSizedFileInTempDir()
546	defer deleteFileInTempDir(emptyFile)
547	tmpDir, _ := createEmptyTempDir()
548	defer deleteTempDir(tmpDir)
549
550	randomString := "This is a random string!"
551	reader := strings.NewReader(randomString)
552
553	fileExists := fmt.Errorf("%v already exists", emptyFile.Name())
554
555	type test struct {
556		filename    string
557		expectedErr error
558	}
559
560	now := time.Now().Unix()
561	nowStr := strconv.FormatInt(now, 10)
562	data := []test{
563		{emptyFile.Name(), fileExists},
564		{tmpDir + "/" + nowStr, nil},
565	}
566
567	for i, d := range data {
568		e := SafeWriteToDisk(d.filename, reader, new(afero.OsFs))
569		if d.expectedErr != nil {
570			if d.expectedErr.Error() != e.Error() {
571				t.Errorf("Test %d failed. Expected error %q but got %q", i, d.expectedErr.Error(), e.Error())
572			}
573		} else {
574			if d.expectedErr != e {
575				t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, e)
576			}
577			contents, _ := ioutil.ReadFile(d.filename)
578			if randomString != string(contents) {
579				t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents))
580			}
581		}
582		reader.Seek(0, 0)
583	}
584}
585
586func TestWriteToDisk(t *testing.T) {
587	emptyFile, _ := createZeroSizedFileInTempDir()
588	defer deleteFileInTempDir(emptyFile)
589	tmpDir, _ := createEmptyTempDir()
590	defer deleteTempDir(tmpDir)
591
592	randomString := "This is a random string!"
593	reader := strings.NewReader(randomString)
594
595	type test struct {
596		filename    string
597		expectedErr error
598	}
599
600	now := time.Now().Unix()
601	nowStr := strconv.FormatInt(now, 10)
602	data := []test{
603		{emptyFile.Name(), nil},
604		{tmpDir + "/" + nowStr, nil},
605	}
606
607	for i, d := range data {
608		e := WriteToDisk(d.filename, reader, new(afero.OsFs))
609		if d.expectedErr != e {
610			t.Errorf("Test %d failed. WriteToDisk Error Expected %q but got %q", i, d.expectedErr, e)
611		}
612		contents, e := ioutil.ReadFile(d.filename)
613		if e != nil {
614			t.Errorf("Test %d failed. Could not read file %s. Reason: %s\n", i, d.filename, e)
615		}
616		if randomString != string(contents) {
617			t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents))
618		}
619		reader.Seek(0, 0)
620	}
621}
622
623func TestGetTempDir(t *testing.T) {
624	dir := os.TempDir()
625	if FilePathSeparator != dir[len(dir)-1:] {
626		dir = dir + FilePathSeparator
627	}
628	testDir := "hugoTestFolder" + FilePathSeparator
629	tests := []struct {
630		input    string
631		expected string
632	}{
633		{"", dir},
634		{testDir + "  Foo bar  ", dir + testDir + "  Foo bar  " + FilePathSeparator},
635		{testDir + "Foo.Bar/foo_Bar-Foo", dir + testDir + "Foo.Bar/foo_Bar-Foo" + FilePathSeparator},
636		{testDir + "fOO,bar:foo%bAR", dir + testDir + "fOObarfoo%bAR" + FilePathSeparator},
637		{testDir + "fOO,bar:foobAR", dir + testDir + "fOObarfoobAR" + FilePathSeparator},
638		{testDir + "FOo/BaR.html", dir + testDir + "FOo/BaR.html" + FilePathSeparator},
639		{testDir + "трям/трям", dir + testDir + "трям/трям" + FilePathSeparator},
640		{testDir + "은행", dir + testDir + "은행" + FilePathSeparator},
641		{testDir + "Банковский кассир", dir + testDir + "Банковский кассир" + FilePathSeparator},
642	}
643
644	for _, test := range tests {
645		output := GetTempDir(test.input, new(afero.MemMapFs))
646		if output != test.expected {
647			t.Errorf("Expected %#v, got %#v\n", test.expected, output)
648		}
649	}
650}
651