506 lines
12 KiB
Go
506 lines
12 KiB
Go
package metadata
|
|
|
|
import (
|
|
"regexp"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestGenerateIdentifier validates timestamp format (YYYYMMDDThhmmss)
|
|
// Maps to dt-denote-identifier-p from original tests
|
|
func TestGenerateIdentifier(t *testing.T) {
|
|
id := GenerateIdentifier()
|
|
|
|
// Validate format
|
|
pattern := regexp.MustCompile(`^\d{8}T\d{6}$`)
|
|
if !pattern.MatchString(id) {
|
|
t.Errorf("GenerateIdentifier() = %q, want format YYYYMMDDThhmmss", id)
|
|
}
|
|
|
|
// Validate it's a valid timestamp
|
|
_, err := time.Parse("20060102T150405", id)
|
|
if err != nil {
|
|
t.Errorf("GenerateIdentifier() = %q, not a valid timestamp: %v", id, err)
|
|
}
|
|
}
|
|
|
|
// TestSlugifyTitle validates title slugification
|
|
// Maps to dt-denote-sluggify-title and dt-denote-sluggify from original tests
|
|
func TestSlugifyTitle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "basic spaces to hyphens",
|
|
input: "Hello World",
|
|
want: "hello-world",
|
|
},
|
|
{
|
|
name: "underscores to hyphens",
|
|
input: "Test_File Name",
|
|
want: "test-file-name",
|
|
},
|
|
{
|
|
name: "remove punctuation",
|
|
input: "Test File!",
|
|
want: "test-file",
|
|
},
|
|
{
|
|
name: "mixed case",
|
|
input: "Mixed CASE 123",
|
|
want: "mixed-case-123",
|
|
},
|
|
{
|
|
name: "special characters",
|
|
input: "Special@#$Chars",
|
|
want: "specialchars",
|
|
},
|
|
{
|
|
name: "multiple spaces",
|
|
input: "Multiple Spaces",
|
|
want: "multiple---spaces",
|
|
},
|
|
{
|
|
name: "leading and trailing spaces",
|
|
input: " Trim Me ",
|
|
want: "--trim-me--",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "numbers only",
|
|
input: "12345",
|
|
want: "12345",
|
|
},
|
|
{
|
|
name: "hyphens preserved",
|
|
input: "already-hyphenated",
|
|
want: "already-hyphenated",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := slugifyTitle(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("slugifyTitle(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFormatKeywords validates keyword formatting for filenames
|
|
// Maps to dt-denote-sluggify-keywords from original tests
|
|
func TestFormatKeywords(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "multiple keywords",
|
|
input: []string{"tag1", "tag2"},
|
|
want: "__tag1_tag2",
|
|
},
|
|
{
|
|
name: "single keyword",
|
|
input: []string{"single"},
|
|
want: "__single",
|
|
},
|
|
{
|
|
name: "empty keywords",
|
|
input: []string{},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "nil keywords",
|
|
input: nil,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "three keywords",
|
|
input: []string{"one", "two", "three"},
|
|
want: "__one_two_three",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := formatKeywords(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("formatKeywords(%v) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSlugifySignature validates signature slugification
|
|
func TestSlugifySignature(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "basic spaces to equals",
|
|
input: "Hello World",
|
|
want: "hello==world",
|
|
},
|
|
{
|
|
name: "underscores to equals",
|
|
input: "test_file",
|
|
want: "test==file",
|
|
},
|
|
{
|
|
name: "remove special characters",
|
|
input: "a!@#b",
|
|
want: "ab",
|
|
},
|
|
{
|
|
name: "normalize consecutive equals",
|
|
input: "a===b",
|
|
want: "a==b",
|
|
},
|
|
{
|
|
name: "mixed",
|
|
input: "Hello World_Test",
|
|
want: "hello==world==test",
|
|
},
|
|
{
|
|
name: "trim trailing equals",
|
|
input: "test_",
|
|
want: "test",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "lowercase already",
|
|
input: "already",
|
|
want: "already",
|
|
},
|
|
{
|
|
name: "numbers preserved",
|
|
input: "test123",
|
|
want: "test123",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := slugifySignature(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("slugifySignature(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestBuildFilename validates filename construction
|
|
// Maps to dt-denote-format-file-name from original tests
|
|
func TestBuildFilename(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
identifier string
|
|
signature string
|
|
title string
|
|
keywords []string
|
|
ext string
|
|
want string
|
|
ftype FileType
|
|
}{
|
|
{
|
|
name: "complete filename with keywords",
|
|
identifier: "20231225T120000",
|
|
signature: "",
|
|
title: "My Title",
|
|
keywords: []string{"tag1", "tag2"},
|
|
ext: ".md",
|
|
want: "20231225T120000--my-title__tag1_tag2.md",
|
|
ftype: FileTypeMdYaml,
|
|
},
|
|
{
|
|
name: "filename without keywords",
|
|
identifier: "20231225T120000",
|
|
signature: "",
|
|
title: "My Title",
|
|
keywords: []string{},
|
|
ext: ".md",
|
|
want: "20231225T120000--my-title.md",
|
|
ftype: FileTypeMdYaml,
|
|
},
|
|
{
|
|
name: "filename with signature",
|
|
identifier: "20231225T120000",
|
|
signature: "hello",
|
|
title: "My Title",
|
|
keywords: []string{"tag1"},
|
|
ext: ".md",
|
|
want: "20231225T120000==hello--my-title__tag1.md",
|
|
ftype: FileTypeMdYaml,
|
|
},
|
|
{
|
|
name: "filename with signature and no keywords",
|
|
identifier: "20231225T120000",
|
|
signature: "test",
|
|
title: "My Title",
|
|
keywords: []string{},
|
|
ext: ".md",
|
|
want: "20231225T120000==test--my-title.md",
|
|
ftype: FileTypeMdYaml,
|
|
},
|
|
{
|
|
name: "filename with multi-part signature",
|
|
identifier: "20231225T120000",
|
|
signature: "a==b",
|
|
title: "My Title",
|
|
keywords: []string{"work"},
|
|
ext: ".md",
|
|
want: "20231225T120000==a==b--my-title__work.md",
|
|
ftype: FileTypeMdYaml,
|
|
},
|
|
{
|
|
name: "filename with special chars in title",
|
|
identifier: "20231225T120000",
|
|
signature: "",
|
|
title: "Special!@# Title",
|
|
keywords: []string{"work"},
|
|
ext: ".org",
|
|
want: "20231225T120000--special-title__work.org",
|
|
ftype: FileTypeOrg,
|
|
},
|
|
{
|
|
name: "org format",
|
|
identifier: "20240101T000000",
|
|
signature: "",
|
|
title: "Org Note",
|
|
keywords: []string{"emacs"},
|
|
ext: ".org",
|
|
want: "20240101T000000--org-note__emacs.org",
|
|
ftype: FileTypeOrg,
|
|
},
|
|
{
|
|
name: "txt format",
|
|
identifier: "20240101T000000",
|
|
signature: "",
|
|
title: "Plain Text",
|
|
keywords: []string{},
|
|
ext: ".txt",
|
|
want: "20240101T000000--plain-text.txt",
|
|
ftype: FileTypeTxt,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fm := NewFrontMatter(tt.title, tt.signature, tt.keywords, tt.identifier)
|
|
got := BuildFilename(fm, tt.ext)
|
|
if got != tt.want {
|
|
t.Errorf("BuildFilename() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParseFilename validates filename parsing
|
|
// Maps to dt-denote-retrieve-filename-* tests from original
|
|
func TestParseFilename(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
wantIdentifier string
|
|
wantSignature string
|
|
wantTitle string
|
|
wantTags []string
|
|
}{
|
|
{
|
|
name: "complete filename with tags",
|
|
path: "/home/notes/20231225T120000--my-title__tag1_tag2.md",
|
|
wantIdentifier: "20231225T120000",
|
|
wantSignature: "",
|
|
wantTitle: "my title",
|
|
wantTags: []string{"tag1", "tag2"},
|
|
},
|
|
{
|
|
name: "filename without tags",
|
|
path: "/home/notes/20231225T120000--simple-title.md",
|
|
wantIdentifier: "20231225T120000",
|
|
wantSignature: "",
|
|
wantTitle: "simple title",
|
|
wantTags: nil,
|
|
},
|
|
{
|
|
name: "filename with signature",
|
|
path: "20240101T000000==hello--note__work.org",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "hello",
|
|
wantTitle: "note",
|
|
wantTags: []string{"work"},
|
|
},
|
|
{
|
|
name: "filename with signature and no tags",
|
|
path: "20240101T000000==test--title.md",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "test",
|
|
wantTitle: "title",
|
|
wantTags: nil,
|
|
},
|
|
{
|
|
name: "filename with multi-part signature",
|
|
path: "20240101T000000==a==b--note__tag.md",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "a==b",
|
|
wantTitle: "note",
|
|
wantTags: []string{"tag"},
|
|
},
|
|
{
|
|
name: "filename with single tag",
|
|
path: "20240101T000000--note__work.org",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "",
|
|
wantTitle: "note",
|
|
wantTags: []string{"work"},
|
|
},
|
|
{
|
|
name: "identifier only",
|
|
path: "20240101T000000.txt",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "",
|
|
wantTitle: "",
|
|
wantTags: nil,
|
|
},
|
|
{
|
|
name: "multi-word title",
|
|
path: "20240101T000000--multi-word-title__personal_ideas.md",
|
|
wantIdentifier: "20240101T000000",
|
|
wantSignature: "",
|
|
wantTitle: "multi word title",
|
|
wantTags: []string{"personal", "ideas"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ParseFilename(tt.path)
|
|
|
|
if got.Identifier != tt.wantIdentifier {
|
|
t.Errorf("ParseFilename(%q).Identifier = %q, want %q",
|
|
tt.path, got.Identifier, tt.wantIdentifier)
|
|
}
|
|
|
|
if got.Signature != tt.wantSignature {
|
|
t.Errorf("ParseFilename(%q).Signature = %q, want %q",
|
|
tt.path, got.Signature, tt.wantSignature)
|
|
}
|
|
|
|
if got.Title != tt.wantTitle {
|
|
t.Errorf("ParseFilename(%q).Title = %q, want %q",
|
|
tt.path, got.Title, tt.wantTitle)
|
|
}
|
|
|
|
if !slices.Equal(got.Tags, tt.wantTags) {
|
|
t.Errorf("ParseFilename(%q).Tags = %v, want %v",
|
|
tt.path, got.Tags, tt.wantTags)
|
|
}
|
|
|
|
if got.Path != tt.path {
|
|
t.Errorf("ParseFilename(%q).Path = %q, want %q",
|
|
tt.path, got.Path, tt.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSort validates sorting functionality
|
|
func TestSort(t *testing.T) {
|
|
notes := Results{
|
|
{Identifier: "20240103T120000", Title: "Charlie"},
|
|
{Identifier: "20240101T120000", Title: "Alice"},
|
|
{Identifier: "20240102T120000", Title: "Bob"},
|
|
}
|
|
|
|
t.Run("sort by id ascending", func(t *testing.T) {
|
|
testData := make(Results, len(notes))
|
|
copy(testData, notes)
|
|
|
|
Sort(testData, SortById, SortOrderAsc)
|
|
|
|
if testData[0].Identifier != "20240101T120000" {
|
|
t.Errorf("First item identifier = %q, want %q", testData[0].Identifier, "20240101T120000")
|
|
}
|
|
if testData[2].Identifier != "20240103T120000" {
|
|
t.Errorf("Last item identifier = %q, want %q", testData[2].Identifier, "20240103T120000")
|
|
}
|
|
})
|
|
|
|
t.Run("sort by id descending", func(t *testing.T) {
|
|
testData := make(Results, len(notes))
|
|
copy(testData, notes)
|
|
|
|
Sort(testData, SortById, SortOrderDesc)
|
|
|
|
if testData[0].Identifier != "20240103T120000" {
|
|
t.Errorf("First item identifier = %q, want %q", testData[0].Identifier, "20240103T120000")
|
|
}
|
|
if testData[2].Identifier != "20240101T120000" {
|
|
t.Errorf("Last item identifier = %q, want %q", testData[2].Identifier, "20240101T120000")
|
|
}
|
|
})
|
|
|
|
t.Run("sort by title ascending", func(t *testing.T) {
|
|
testData := make(Results, len(notes))
|
|
copy(testData, notes)
|
|
|
|
Sort(testData, SortByTitle, SortOrderAsc)
|
|
|
|
if testData[0].Title != "Alice" {
|
|
t.Errorf("First item title = %q, want %q", testData[0].Title, "Alice")
|
|
}
|
|
if testData[2].Title != "Charlie" {
|
|
t.Errorf("Last item title = %q, want %q", testData[2].Title, "Charlie")
|
|
}
|
|
})
|
|
|
|
t.Run("sort by title descending", func(t *testing.T) {
|
|
testData := make(Results, len(notes))
|
|
copy(testData, notes)
|
|
|
|
Sort(testData, SortByTitle, SortOrderDesc)
|
|
|
|
if testData[0].Title != "Charlie" {
|
|
t.Errorf("First item title = %q, want %q", testData[0].Title, "Charlie")
|
|
}
|
|
if testData[2].Title != "Alice" {
|
|
t.Errorf("Last item title = %q, want %q", testData[2].Title, "Alice")
|
|
}
|
|
})
|
|
|
|
t.Run("sort by title case insensitive", func(t *testing.T) {
|
|
testData := Results{
|
|
{Identifier: "1", Title: "zebra"},
|
|
{Identifier: "2", Title: "Apple"},
|
|
{Identifier: "3", Title: "banana"},
|
|
}
|
|
|
|
Sort(testData, SortByTitle, SortOrderAsc)
|
|
|
|
// Should be: Apple, banana, zebra (case-insensitive)
|
|
if testData[0].Title != "Apple" {
|
|
t.Errorf("First item title = %q, want %q", testData[0].Title, "Apple")
|
|
}
|
|
if testData[1].Title != "banana" {
|
|
t.Errorf("Second item title = %q, want %q", testData[1].Title, "banana")
|
|
}
|
|
if testData[2].Title != "zebra" {
|
|
t.Errorf("Third item title = %q, want %q", testData[2].Title, "zebra")
|
|
}
|
|
})
|
|
} |