package gist_test import ( "net/http" "net/url" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" "github.com/thomiceli/opengist/internal/config" "github.com/thomiceli/opengist/internal/db" "github.com/thomiceli/opengist/internal/git" webtest "github.com/thomiceli/opengist/internal/web/test" ) // Helper function to extract username and gist identifier from redirect URL func getGistInfoFromRedirect(resp *http.Response) (username, identifier string) { location := resp.Header.Get("Location") // Location format: /{username}/{identifier} parts := strings.Split(strings.TrimPrefix(location, "/"), "/") if len(parts) >= 2 { return parts[0], parts[1] } return "", "" } func verifyGistCreation(t *testing.T, gist *db.Gist, username, identifier string) { require.NotNil(t, gist) require.Equal(t, username, gist.User.Username) require.Equal(t, identifier, gist.Identifier()) require.NotEmpty(t, gist.Uuid) require.Greater(t, gist.NbFiles, 0) gistPath := filepath.Join(config.GetHomeDir(), git.ReposDirectory, username, gist.Uuid) _, err := os.Stat(gistPath) require.NoError(t, err, "Gist repository should exist on filesystem at %s", gistPath) } func TestGistCreationPage(t *testing.T) { s := webtest.Setup(t) defer webtest.Teardown(t) s.Register(t, "thomas") t.Run("NoAuth", func(t *testing.T) { s.Request(t, "GET", "/", nil, 302) }) t.Run("Authenticated", func(t *testing.T) { s.Login(t, "thomas") s.Request(t, "GET", "/", nil, 200) }) } func TestGistCreation(t *testing.T) { s := webtest.Setup(t) defer webtest.Teardown(t) s.Register(t, "thomas") t.Run("NoAuth", func(t *testing.T) { s.Request(t, "POST", "/", url.Values{ "title": {"Test Gist"}, "name": {"test.txt"}, "content": {"hello world"}, }, 302) // Redirects to login // Verify no gist was created count, err := db.CountAll(db.Gist{}) require.NoError(t, err) require.Equal(t, int64(0), count) }) tests := []struct { name string data url.Values expectedCode int expectGistCreated bool expectedTitle string expectedDescription string expectedURL string expectedTopics string // Expected topics string expectedNbFiles int expectedVisibility db.Visibility expectedFileNames []string // Expected filenames in the gist expectedFileContents map[string]string // Expected content for each file (filename -> content) }{ { name: "NoFiles", data: url.Values{ "title": {"Test Gist"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "EmptyContent", data: url.Values{ "title": {"Test Gist"}, "name": {"test.txt"}, "content": {""}, }, expectedCode: 400, expectGistCreated: false, }, { name: "TitleTooLong", data: url.Values{ "title": {strings.Repeat("a", 251)}, // Max is 250 "name": {"test.txt"}, "content": {"hello"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "DescriptionTooLong", data: url.Values{ "title": {"Test Gist"}, "description": {strings.Repeat("a", 1001)}, // Max is 1000 "name": {"test.txt"}, "content": {"hello"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "URLTooLong", data: url.Values{ "title": {"Test Gist"}, "url": {strings.Repeat("a", 33)}, // Max is 32 "name": {"test.txt"}, "content": {"hello"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "URLInvalidCharacters", data: url.Values{ "title": {"Test Gist"}, "url": {"invalid@url#here"}, // Only alphanumeric and dashes allowed "name": {"test.txt"}, "content": {"hello"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "InvalidVisibility", data: url.Values{ "title": {"Test Gist"}, "name": {"test.txt"}, "content": {"hello"}, "private": {"3"}, // Valid values are 0, 1, 2 }, expectedCode: 400, expectGistCreated: false, }, { name: "Valid", data: url.Values{ "title": {"My Test Gist"}, "name": {"test.txt"}, "url": {"my-custom-url-123"}, // Alphanumeric + dashes should be valid "content": {"hello world"}, "private": {"0"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "My Test Gist", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"test.txt"}, expectedFileContents: map[string]string{ "test.txt": "hello world", }, }, { name: "AutoNamedFile", data: url.Values{ "title": {"Auto Named"}, "name": {""}, "content": {"content without name"}, "private": {"0"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Auto Named", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"gistfile1.txt"}, expectedFileContents: map[string]string{ "gistfile1.txt": "content without name", }, }, { name: "MultipleFiles", data: url.Values{ "title": {"Multi File Gist"}, "name": []string{"", "file2.md"}, "content": []string{"content 1", "content 2"}, "private": {"0"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Multi File Gist", expectedNbFiles: 2, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"gistfile1.txt", "file2.md"}, expectedFileContents: map[string]string{ "gistfile1.txt": "content 1", "file2.md": "content 2", }, }, { name: "NoTitle", data: url.Values{ "name": {"readme.md"}, "content": {"# README"}, "private": {"0"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "readme.md", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"readme.md"}, expectedFileContents: map[string]string{ "readme.md": "# README", }, }, { name: "Unlisted", data: url.Values{ "title": {"Unlisted Gist"}, "name": {"secret.txt"}, "content": {"secret content"}, "private": {"1"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Unlisted Gist", expectedNbFiles: 1, expectedVisibility: db.UnlistedVisibility, expectedFileNames: []string{"secret.txt"}, expectedFileContents: map[string]string{ "secret.txt": "secret content", }, }, { name: "Private", data: url.Values{ "title": {"Private Gist"}, "name": {"secret.txt"}, "content": {"secret content"}, "private": {"2"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Private Gist", expectedNbFiles: 1, expectedVisibility: db.PrivateVisibility, expectedFileNames: []string{"secret.txt"}, expectedFileContents: map[string]string{ "secret.txt": "secret content", }, }, { name: "Topics", data: url.Values{ "title": {"Gist With Topics"}, "name": {"test.txt"}, "content": {"hello"}, "topics": {"golang testing webdev"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Gist With Topics", expectedTopics: "golang,testing,webdev", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"test.txt"}, expectedFileContents: map[string]string{ "test.txt": "hello", }, }, { name: "TopicsTooMany", data: url.Values{ "title": {"Test"}, "name": {"test.txt"}, "content": {"hello"}, "topics": {"topic1 topic2 topic3 topic4 topic5 topic6 topic7 topic8 topic9 topic10 topic11"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "TopicTooLong", data: url.Values{ "title": {"Test"}, "name": {"test.txt"}, "content": {"hello"}, "topics": {strings.Repeat("a", 51)}, // 51 chars - exceeds max of 50 }, expectedCode: 400, expectGistCreated: false, }, { name: "TopicInvalidCharacters", data: url.Values{ "title": {"Test"}, "name": {"test.txt"}, "content": {"hello"}, "topics": {"topic@name topic.name"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "TopicUnicode", data: url.Values{ "title": {"Unicode Topics"}, "name": {"test.txt"}, "content": {"hello"}, "topics": {"编程 тест"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Unicode Topics", expectedTopics: "编程,тест", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"test.txt"}, }, { name: "DuplicateFileNames", data: url.Values{ "title": {"Duplicate Files"}, "name": []string{"test.txt", "test.txt"}, "content": []string{"content1", "content2"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Duplicate Files", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"test.txt"}, expectedFileContents: map[string]string{ "test.txt": "content2", }, }, { name: "FileNameTooLong", data: url.Values{ "title": {"Too Long Filename"}, "name": {strings.Repeat("a", 256) + ".txt"}, // 260 total - exceeds 255 "content": {"hello"}, }, expectedCode: 400, expectGistCreated: false, }, { name: "FileNameWithUnicode", data: url.Values{ "title": {"Unicode Filename"}, "name": {"文件.txt"}, "content": {"hello world"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Unicode Filename", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"文件.txt"}, expectedFileContents: map[string]string{ "文件.txt": "hello world", }, }, { name: "FileNamePathTraversal", data: url.Values{ "title": {"Path Traversal"}, "name": {"../../../etc/passwd"}, "content": {"malicious"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Path Traversal", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"passwd"}, expectedFileContents: map[string]string{ "passwd": "malicious", }, }, { name: "EmptyAndValidContent", data: url.Values{ "title": {"Mixed Content"}, "name": []string{"empty.txt", "valid.txt", "also-empty.txt"}, "content": []string{"", "valid content", ""}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Mixed Content", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"valid.txt"}, expectedFileContents: map[string]string{ "valid.txt": "valid content", }, }, { name: "ContentWithSpecialCharacters", data: url.Values{ "title": {"Special Chars"}, "name": {"special.txt"}, "content": {"Line1\nLine2\tTabbed\x00NullByte😀Emoji"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Special Chars", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"special.txt"}, expectedFileContents: map[string]string{ "special.txt": "Line1\nLine2\tTabbed\x00NullByte😀Emoji", }, }, { name: "ContentMultibyteUnicode", data: url.Values{ "title": {"Unicode Content"}, "name": {"unicode.txt"}, "content": {"Hello 世界 🌍 Привет"}, }, expectedCode: 302, expectGistCreated: true, expectedTitle: "Unicode Content", expectedNbFiles: 1, expectedVisibility: db.PublicVisibility, expectedFileNames: []string{"unicode.txt"}, expectedFileContents: map[string]string{ "unicode.txt": "Hello 世界 🌍 Привет", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s.Login(t, "thomas") resp := s.Request(t, "POST", "/", tt.data, tt.expectedCode) if tt.expectGistCreated { // Get gist info from redirect username, gistIdentifier := getGistInfoFromRedirect(resp) require.Equal(t, "thomas", username) require.NotEmpty(t, gistIdentifier) // Verify gist was created gist, err := db.GetGist(username, gistIdentifier) require.NoError(t, err) // Run common verification (filesystem, git, etc.) verifyGistCreation(t, gist, username, gistIdentifier) // Verify all expected fields require.Equal(t, tt.expectedTitle, gist.Title, "Title mismatch") require.Equal(t, tt.expectedNbFiles, gist.NbFiles, "File count mismatch") require.Equal(t, tt.expectedVisibility, gist.Private, "Visibility mismatch") // Verify description if specified if tt.expectedDescription != "" { require.Equal(t, tt.expectedDescription, gist.Description, "Description mismatch") } // Verify URL if specified if tt.expectedURL != "" { require.Equal(t, tt.expectedURL, gist.Identifier(), "URL/Identifier mismatch") } // Verify topics if specified if tt.expectedTopics != "" { // Get gist topics topics, err := gist.GetTopics() require.NoError(t, err, "Failed to get gist topics") require.ElementsMatch(t, strings.Split(tt.expectedTopics, ","), topics, "Topics mismatch") } // Verify files if specified if len(tt.expectedFileNames) > 0 { files, err := gist.Files("HEAD", false) require.NoError(t, err, "Failed to get gist files") require.Len(t, files, len(tt.expectedFileNames), "File count mismatch") actualFileNames := make([]string, len(files)) for i, file := range files { actualFileNames[i] = file.Filename } require.ElementsMatch(t, tt.expectedFileNames, actualFileNames, "File names mismatch") // Verify file contents if specified if len(tt.expectedFileContents) > 0 { for filename, expectedContent := range tt.expectedFileContents { content, _, err := git.GetFileContent(username, gist.Uuid, "HEAD", filename, false) require.NoError(t, err, "Failed to get content for file %s", filename) require.Equal(t, expectedContent, content, "Content mismatch for file %s", filename) } } } } }) } }