Skip to content

Commit 1dd2999

Browse files
committed
Add empty folder export, folder comments, and / escaping in folder names
- Add AddFolder(folder, comment) API for creating empty folders and folder comments - Add "Folder Comment" CSV column - Support / in folder names by escaping as \/ in the joined path - Reject folder segments ending with \ to prevent ambiguity with \/ escape - Replace json.Marshal with jsonMarshal (SetEscapeHTML=false) to avoid mangling &, <, > in JSON fields - Preserve folder insertion order instead of sorting alphabetically - Add functional test examples for manual import verification - Add *.csv to .gitignore
1 parent 91795f7 commit 1dd2999

File tree

4 files changed

+367
-34
lines changed

4 files changed

+367
-34
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313

1414
# .env files
1515
.env
16+
17+
# Generated CSV files
18+
*.csv

examples/functional/main.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
qascsv "github.com/hypersequent/qasphere-csv"
7+
)
8+
9+
func main() {
10+
generateFolderComments()
11+
generateEmptyFolders()
12+
generateEscaping()
13+
}
14+
15+
func generateFolderComments() {
16+
q := qascsv.NewQASphereCSV()
17+
18+
if err := q.AddFolder([]string{"Commented Folder"}, "This folder has a comment but also contains test cases"); err != nil {
19+
log.Fatal(err)
20+
}
21+
if err := q.AddTestCase(qascsv.TestCase{
22+
Title: "Test inside commented folder",
23+
Folder: []string{"Commented Folder"},
24+
Priority: qascsv.PriorityHigh,
25+
}); err != nil {
26+
log.Fatal(err)
27+
}
28+
29+
if err := q.AddFolder([]string{"Another Folder"}, "Standalone comment on a folder with no test cases"); err != nil {
30+
log.Fatal(err)
31+
}
32+
33+
if err := q.AddFolder([]string{"Parent", "Child With Comment"}, "Nested folder comment"); err != nil {
34+
log.Fatal(err)
35+
}
36+
if err := q.AddTestCase(qascsv.TestCase{
37+
Title: "Test in parent folder",
38+
Folder: []string{"Parent"},
39+
Priority: qascsv.PriorityMedium,
40+
}); err != nil {
41+
log.Fatal(err)
42+
}
43+
44+
if err := q.WriteCSVToFile("folder_comments.csv"); err != nil {
45+
log.Fatal(err)
46+
}
47+
log.Println("wrote folder_comments.csv")
48+
}
49+
50+
func generateEmptyFolders() {
51+
q := qascsv.NewQASphereCSV()
52+
53+
if err := q.AddFolder([]string{"Empty Root Folder"}, ""); err != nil {
54+
log.Fatal(err)
55+
}
56+
if err := q.AddFolder([]string{"Parent", "Empty Child"}, ""); err != nil {
57+
log.Fatal(err)
58+
}
59+
if err := q.AddFolder([]string{"Parent", "Another Empty Child"}, ""); err != nil {
60+
log.Fatal(err)
61+
}
62+
if err := q.AddTestCase(qascsv.TestCase{
63+
Title: "Test in parent alongside empty children",
64+
Folder: []string{"Parent"},
65+
Priority: qascsv.PriorityLow,
66+
}); err != nil {
67+
log.Fatal(err)
68+
}
69+
70+
if err := q.AddFolder([]string{"Deep", "Nested", "Empty"}, ""); err != nil {
71+
log.Fatal(err)
72+
}
73+
74+
if err := q.WriteCSVToFile("empty_folders.csv"); err != nil {
75+
log.Fatal(err)
76+
}
77+
log.Println("wrote empty_folders.csv")
78+
}
79+
80+
func generateEscaping() {
81+
q := qascsv.NewQASphereCSV()
82+
83+
if err := q.AddTestCase(qascsv.TestCase{
84+
Title: "Test in folder with slash",
85+
Folder: []string{"Features/Bugs", "Login"},
86+
Priority: qascsv.PriorityHigh,
87+
}); err != nil {
88+
log.Fatal(err)
89+
}
90+
if err := q.AddTestCase(qascsv.TestCase{
91+
Title: "Test in folder with multiple slashes",
92+
Folder: []string{"A/B/C", "D/E"},
93+
Priority: qascsv.PriorityMedium,
94+
}); err != nil {
95+
log.Fatal(err)
96+
}
97+
98+
if err := q.AddFolder([]string{"Empty/Slash/Folder"}, "This empty folder name contains slashes"); err != nil {
99+
log.Fatal(err)
100+
}
101+
102+
if err := q.AddTestCase(qascsv.TestCase{
103+
Title: "Test in normal folder for comparison",
104+
Folder: []string{"Normal Folder", "Subfolder"},
105+
Priority: qascsv.PriorityLow,
106+
}); err != nil {
107+
log.Fatal(err)
108+
}
109+
110+
if err := q.WriteCSVToFile("escaping.csv"); err != nil {
111+
log.Fatal(err)
112+
}
113+
log.Println("wrote escaping.csv")
114+
}

qacsv_test.go

Lines changed: 159 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ var successTestCases = []TestCase{
125125
},
126126
}
127127

128-
const successTestCasesCSV = `Folder,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,Step 2,Expected 2
129-
root,standalone,tc-with-minimal-fields,,false,high,,,,,,,,,,,
130-
root,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,,,action-1,,,expected-2
131-
root/child,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,,,action-1,expected-1,action-2,expected-2
132-
root/child,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,,"action.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","expected.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,
128+
const successTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,Step 2,Expected 2
129+
root/child,,standalone,tc-with-all-fields,legacy-id,false,high,"tag1,tag2",[req1](http://req1),"[link-1](http://link1),[link-2](http://link2)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",preconditions,,,action-1,expected-1,action-2,expected-2
130+
root/child,,standalone,"tc-with-special-chars.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",legacy-id,false,high,"tag1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","[req.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;]()","[link-1.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;](http://link1)","[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]","preconditions.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,,"action.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;","expected.,<>/@$%""""''*&()[]{}+-[BACKTICK]!~;",,
131+
root,,standalone,tc-with-minimal-fields,,false,high,,,,,,,,,,,
132+
root,,standalone,tc-with-partial-fields,,true,low,,[](http://req1),,"[{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10},{""fileName"":""file-1.csv"",""id"":""file-id"",""url"":""http://file1"",""mimeType"":""text/csv"",""size"":10}]",,,,action-1,,,expected-2
133133
`
134134

135135
var failureTestCases = []TestCase{
@@ -146,12 +146,12 @@ var failureTestCases = []TestCase{
146146
Folder: []string{},
147147
Priority: "high",
148148
}, {
149-
Title: "folder with empty title",
150-
Folder: []string{"root/child"},
149+
Title: "folder with empty segment",
150+
Folder: []string{"root", ""},
151151
Priority: "high",
152152
}, {
153-
Title: "folder title with slash",
154-
Folder: []string{"root/child"},
153+
Title: "folder segment ending with backslash",
154+
Folder: []string{"root\\"},
155155
Priority: "high",
156156
}, {
157157
Title: "wrong priority",
@@ -353,12 +353,12 @@ var customFieldSuccessTestCases = []TestCase{
353353
},
354354
}
355355

356-
const customFieldSuccessTestCasesCSV = `Folder,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes
357-
custom-fields,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",,
358-
custom-fields,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,,,Execute test,Test passes,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^\u0026*()"",""isDefault"":false}"
359-
custom-fields,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,,"{""value"":"""",""isDefault"":false}"
360-
custom-fields,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,,"{""value"":"""",""isDefault"":false}",
361-
custom-fields/comprehensive,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,,,Step 1,Result 1,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}",
356+
const customFieldSuccessTestCasesCSV = `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params,Step 1,Expected 1,custom_field_dropdown_test_env,custom_field_dropdown_automation,custom_field_text_notes
357+
custom-fields,,standalone,tc-with-single-custom-field,,false,medium,,,,,,,,,,"{""value"":""staging"",""isDefault"":false}",,
358+
custom-fields,,standalone,tc-with-multiple-custom-fields,,false,high,"regression,smoke",,,,,,,Execute test,Test passes,"{""value"":""production"",""isDefault"":false}","{""value"":""Automated"",""isDefault"":false}","{""value"":""This is a test note with special chars: !@#$%^&*()"",""isDefault"":false}"
359+
custom-fields,,standalone,tc-with-empty-custom-field-value,,false,low,,,,,,,,,,,,"{""value"":"""",""isDefault"":false}"
360+
custom-fields,,standalone,tc-with-default-custom-field,,false,medium,,,,,,,,,,,"{""value"":"""",""isDefault"":false}",
361+
custom-fields/comprehensive,,standalone,tc-with-all-fields-and-custom-fields,CF-001,false,high,"custom,comprehensive",[CF Requirements](http://cf-req),[CF Link](http://cf-link),"[{""fileName"":""cf-test.txt"",""id"":""cf-file-id"",""url"":""http://cf-file"",""mimeType"":""text/plain"",""size"":100}]",Custom field test setup,,,Step 1,Result 1,"{""value"":""development"",""isDefault"":false}","{""value"":""In Progress"",""isDefault"":false}",
362362
`
363363

364364
var customFieldFailureTestCases = []TestCase{
@@ -456,3 +456,147 @@ func TestCustomFieldFailureTestCases(t *testing.T) {
456456
})
457457
}
458458
}
459+
460+
func TestFolderSlashEscaping(t *testing.T) {
461+
qasCSV := NewQASphereCSV()
462+
463+
err := qasCSV.AddTestCase(TestCase{
464+
Title: "tc-in-slash-folder",
465+
Folder: []string{"root/parent", "child/leaf"},
466+
Priority: "high",
467+
})
468+
require.NoError(t, err)
469+
470+
csv, err := qasCSV.GenerateCSV()
471+
require.NoError(t, err)
472+
473+
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
474+
root\/parent/child\/leaf,,standalone,tc-in-slash-folder,,false,high,,,,,,,
475+
`
476+
require.Equal(t, expected, csv)
477+
}
478+
479+
func TestFolderSegmentEndingWithBackslash(t *testing.T) {
480+
qasCSV := NewQASphereCSV()
481+
482+
err := qasCSV.AddTestCase(TestCase{
483+
Title: "tc-bad-backslash",
484+
Folder: []string{"root\\"},
485+
Priority: "high",
486+
})
487+
require.Error(t, err)
488+
require.Contains(t, err.Error(), "must not end with '\\'")
489+
}
490+
491+
func TestAddFolderEmpty(t *testing.T) {
492+
qasCSV := NewQASphereCSV()
493+
494+
err := qasCSV.AddFolder([]string{"empty-folder"}, "")
495+
require.NoError(t, err)
496+
497+
csv, err := qasCSV.GenerateCSV()
498+
require.NoError(t, err)
499+
500+
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
501+
empty-folder,,,,,,,,,,,,,
502+
`
503+
require.Equal(t, expected, csv)
504+
}
505+
506+
func TestAddFolderWithComment(t *testing.T) {
507+
qasCSV := NewQASphereCSV()
508+
509+
err := qasCSV.AddFolder([]string{"commented-folder"}, "This is a folder comment")
510+
require.NoError(t, err)
511+
512+
csv, err := qasCSV.GenerateCSV()
513+
require.NoError(t, err)
514+
515+
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
516+
commented-folder,This is a folder comment,,,,,,,,,,,,
517+
`
518+
require.Equal(t, expected, csv)
519+
}
520+
521+
func TestAddFolderWithCommentAndTestCases(t *testing.T) {
522+
qasCSV := NewQASphereCSV()
523+
524+
err := qasCSV.AddFolder([]string{"my-folder"}, "Folder description")
525+
require.NoError(t, err)
526+
527+
err = qasCSV.AddTestCase(TestCase{
528+
Title: "tc-in-commented-folder",
529+
Folder: []string{"my-folder"},
530+
Priority: "high",
531+
})
532+
require.NoError(t, err)
533+
534+
csv, err := qasCSV.GenerateCSV()
535+
require.NoError(t, err)
536+
537+
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
538+
my-folder,Folder description,,,,,,,,,,,,
539+
my-folder,,standalone,tc-in-commented-folder,,false,high,,,,,,,
540+
`
541+
require.Equal(t, expected, csv)
542+
}
543+
544+
func TestAddFolderValidation(t *testing.T) {
545+
t.Run("empty folder path", func(t *testing.T) {
546+
qasCSV := NewQASphereCSV()
547+
err := qasCSV.AddFolder([]string{}, "comment")
548+
require.Error(t, err)
549+
require.Contains(t, err.Error(), "folder path must not be empty")
550+
})
551+
552+
t.Run("empty folder segment", func(t *testing.T) {
553+
qasCSV := NewQASphereCSV()
554+
err := qasCSV.AddFolder([]string{"root", ""}, "comment")
555+
require.Error(t, err)
556+
require.Contains(t, err.Error(), "folder segment must not be empty")
557+
})
558+
559+
t.Run("folder segment ending with backslash", func(t *testing.T) {
560+
qasCSV := NewQASphereCSV()
561+
err := qasCSV.AddFolder([]string{"root\\"}, "comment")
562+
require.Error(t, err)
563+
require.Contains(t, err.Error(), "must not end with '\\'")
564+
})
565+
566+
t.Run("duplicate folder", func(t *testing.T) {
567+
qasCSV := NewQASphereCSV()
568+
err := qasCSV.AddFolder([]string{"root"}, "comment")
569+
require.NoError(t, err)
570+
err = qasCSV.AddFolder([]string{"root"}, "another comment")
571+
require.Error(t, err)
572+
require.Contains(t, err.Error(), "already exists")
573+
})
574+
575+
t.Run("folder already has test cases", func(t *testing.T) {
576+
qasCSV := NewQASphereCSV()
577+
err := qasCSV.AddTestCase(TestCase{
578+
Title: "tc",
579+
Folder: []string{"root"},
580+
Priority: "high",
581+
})
582+
require.NoError(t, err)
583+
err = qasCSV.AddFolder([]string{"root"}, "comment")
584+
require.Error(t, err)
585+
require.Contains(t, err.Error(), "already exists")
586+
})
587+
}
588+
589+
func TestAddFolderWithSlashInName(t *testing.T) {
590+
qasCSV := NewQASphereCSV()
591+
592+
err := qasCSV.AddFolder([]string{"folder/with/slashes", "child"}, "slash comment")
593+
require.NoError(t, err)
594+
595+
csv, err := qasCSV.GenerateCSV()
596+
require.NoError(t, err)
597+
598+
expected := `Folder,Folder Comment,Type,Name,Legacy ID,Draft,Priority,Tags,Requirements,Links,Files,Preconditions,Parameter Values,Template Suffix Params
599+
folder\/with\/slashes/child,slash comment,,,,,,,,,,,,
600+
`
601+
require.Equal(t, expected, csv)
602+
}

0 commit comments

Comments
 (0)