mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-09 17:23:22 +03:00
fix: Fixes and performance improvements in utils package (#331)
This commit is contained in:
committed by
GitHub
parent
b483e2e92f
commit
348192b9d7
23
.github/workflows/e2e-tests.yml
vendored
23
.github/workflows/e2e-tests.yml
vendored
@@ -33,6 +33,29 @@ jobs:
|
||||
name: docker-image
|
||||
path: /tmp/docker-image.tar
|
||||
|
||||
test-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: 'backend/go.mod'
|
||||
cache-dependency-path: 'backend/go.sum'
|
||||
- name: Install dependencies
|
||||
working-directory: backend
|
||||
run: |
|
||||
go get ./...
|
||||
- name: Run backend unit tests
|
||||
working-directory: backend
|
||||
run: |
|
||||
go test -v ./... | tee /tmp/TestResults.log
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: backend-unit-tests
|
||||
path: /tmp/TestResults.log
|
||||
retention-days: 15
|
||||
|
||||
test-sqlite:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/resources"
|
||||
)
|
||||
|
||||
func GetFileExtension(filename string) string {
|
||||
splitted := strings.Split(filename, ".")
|
||||
return splitted[len(splitted)-1]
|
||||
ext := filepath.Ext(filename)
|
||||
if len(ext) > 0 && ext[0] == '.' {
|
||||
return ext[1:]
|
||||
}
|
||||
return filename
|
||||
}
|
||||
|
||||
func GetImageMimeType(ext string) string {
|
||||
|
||||
73
backend/internal/utils/file_util_test.go
Normal file
73
backend/internal/utils/file_util_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetFileExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filename string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Simple file with extension",
|
||||
filename: "document.pdf",
|
||||
want: "pdf",
|
||||
},
|
||||
{
|
||||
name: "File with path",
|
||||
filename: "/path/to/document.txt",
|
||||
want: "txt",
|
||||
},
|
||||
{
|
||||
name: "File with path (Windows style)",
|
||||
filename: "C:\\path\\to\\document.jpg",
|
||||
want: "jpg",
|
||||
},
|
||||
{
|
||||
name: "Multiple extensions",
|
||||
filename: "archive.tar.gz",
|
||||
want: "gz",
|
||||
},
|
||||
{
|
||||
name: "Hidden file with extension",
|
||||
filename: ".config.json",
|
||||
want: "json",
|
||||
},
|
||||
{
|
||||
name: "Filename with dots",
|
||||
filename: "version.1.2.3.txt",
|
||||
want: "txt",
|
||||
},
|
||||
{
|
||||
name: "File with uppercase extension",
|
||||
filename: "image.JPG",
|
||||
want: "JPG",
|
||||
},
|
||||
{
|
||||
name: "File without extension",
|
||||
filename: "README",
|
||||
want: "README",
|
||||
},
|
||||
{
|
||||
name: "Hidden file without extension",
|
||||
filename: ".gitignore",
|
||||
want: "gitignore",
|
||||
},
|
||||
{
|
||||
name: "Empty filename",
|
||||
filename: "",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetFileExtension(tt.filename)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetFileExtension(%q) = %q, want %q", tt.filename, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func CreateDefaultProfilePicture(firstName, lastName string) (*bytes.Buffer, err
|
||||
var buf bytes.Buffer
|
||||
err = imaging.Encode(&buf, img, imaging.PNG)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode image: %v", err)
|
||||
return nil, fmt.Errorf("failed to encode image: %w", err)
|
||||
}
|
||||
|
||||
return &buf, nil
|
||||
|
||||
@@ -2,8 +2,9 @@ package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -13,23 +14,41 @@ import (
|
||||
// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length
|
||||
func GenerateRandomAlphanumericString(length int) (string, error) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const charsetLength = int64(len(charset))
|
||||
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("length must be a positive integer")
|
||||
return "", errors.New("length must be a positive integer")
|
||||
}
|
||||
|
||||
result := make([]byte, length)
|
||||
// The algorithm below is adapted from https://stackoverflow.com/a/35615565
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
)
|
||||
|
||||
for i := range result {
|
||||
num, err := rand.Int(rand.Reader, big.NewInt(charsetLength))
|
||||
if err != nil {
|
||||
return "", err
|
||||
result := strings.Builder{}
|
||||
result.Grow(length)
|
||||
// Because we discard a bunch of bytes, we read more in the buffer to minimize the changes of performing additional IO
|
||||
bufferSize := int(float64(length) * 1.3)
|
||||
randomBytes := make([]byte, bufferSize)
|
||||
for i, j := 0, 0; i < length; j++ {
|
||||
// Fill the buffer if needed
|
||||
if j%bufferSize == 0 {
|
||||
_, err := io.ReadFull(rand.Reader, randomBytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Discard bytes that are outside of the range
|
||||
// This allows making sure that we maintain uniform distribution
|
||||
idx := int(randomBytes[j%length] & letterIdxMask)
|
||||
if idx < len(charset) {
|
||||
result.WriteByte(charset[idx])
|
||||
i++
|
||||
}
|
||||
result[i] = charset[num.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
func GetHostnameFromURL(rawURL string) string {
|
||||
@@ -45,30 +64,40 @@ func StringPointer(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func CapitalizeFirstLetter(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
func CapitalizeFirstLetter(str string) string {
|
||||
if str == "" {
|
||||
return ""
|
||||
}
|
||||
runes := []rune(s)
|
||||
runes[0] = unicode.ToUpper(runes[0])
|
||||
return string(runes)
|
||||
|
||||
result := strings.Builder{}
|
||||
result.Grow(len(str))
|
||||
for i, r := range str {
|
||||
if i == 0 {
|
||||
result.WriteRune(unicode.ToUpper(r))
|
||||
} else {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
func CamelCaseToSnakeCase(s string) string {
|
||||
var result []rune
|
||||
for i, r := range s {
|
||||
func CamelCaseToSnakeCase(str string) string {
|
||||
result := strings.Builder{}
|
||||
result.Grow(int(float32(len(str)) * 1.1))
|
||||
for i, r := range str {
|
||||
if unicode.IsUpper(r) && i > 0 {
|
||||
result = append(result, '_')
|
||||
result.WriteByte('_')
|
||||
}
|
||||
result = append(result, unicode.ToLower(r))
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
return string(result)
|
||||
return result.String()
|
||||
}
|
||||
|
||||
var camelCaseToScreamingSnakeCaseRe = regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
|
||||
func CamelCaseToScreamingSnakeCase(s string) string {
|
||||
// Insert underscores before uppercase letters (except the first one)
|
||||
re := regexp.MustCompile(`([a-z0-9])([A-Z])`)
|
||||
snake := re.ReplaceAllString(s, `${1}_${2}`)
|
||||
snake := camelCaseToScreamingSnakeCaseRe.ReplaceAllString(s, `${1}_${2}`)
|
||||
|
||||
// Convert to uppercase
|
||||
return strings.ToUpper(snake)
|
||||
|
||||
105
backend/internal/utils/string_util_test.go
Normal file
105
backend/internal/utils/string_util_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateRandomAlphanumericString(t *testing.T) {
|
||||
t.Run("valid length returns correct string", func(t *testing.T) {
|
||||
const length = 10
|
||||
str, err := GenerateRandomAlphanumericString(length)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(str) != length {
|
||||
t.Errorf("Expected length %d, got %d", length, len(str))
|
||||
}
|
||||
|
||||
matched, err := regexp.MatchString(`^[a-zA-Z0-9]+$`, str)
|
||||
if err != nil {
|
||||
t.Errorf("Regex match failed: %v", err)
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("String contains non-alphanumeric characters: %s", str)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero length returns error", func(t *testing.T) {
|
||||
_, err := GenerateRandomAlphanumericString(0)
|
||||
if err == nil {
|
||||
t.Error("Expected error for zero length, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative length returns error", func(t *testing.T) {
|
||||
_, err := GenerateRandomAlphanumericString(-1)
|
||||
if err == nil {
|
||||
t.Error("Expected error for negative length, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("generates different strings", func(t *testing.T) {
|
||||
str1, _ := GenerateRandomAlphanumericString(10)
|
||||
str2, _ := GenerateRandomAlphanumericString(10)
|
||||
if str1 == str2 {
|
||||
t.Error("Generated strings should be different")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapitalizeFirstLetter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"empty string", "", ""},
|
||||
{"lowercase first letter", "hello", "Hello"},
|
||||
{"already capitalized", "Hello", "Hello"},
|
||||
{"single lowercase letter", "h", "H"},
|
||||
{"single uppercase letter", "H", "H"},
|
||||
{"starts with number", "123abc", "123abc"},
|
||||
{"unicode character", "étoile", "Étoile"},
|
||||
{"special character", "_test", "_test"},
|
||||
{"multi-word", "hello world", "Hello world"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CapitalizeFirstLetter(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CapitalizeFirstLetter(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCamelCaseToSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"empty string", "", ""},
|
||||
{"simple camelCase", "camelCase", "camel_case"},
|
||||
{"PascalCase", "PascalCase", "pascal_case"},
|
||||
{"multipleWordsInCamelCase", "multipleWordsInCamelCase", "multiple_words_in_camel_case"},
|
||||
{"consecutive uppercase", "HTTPRequest", "h_t_t_p_request"},
|
||||
{"single lowercase word", "word", "word"},
|
||||
{"single uppercase word", "WORD", "w_o_r_d"},
|
||||
{"with numbers", "camel123Case", "camel123_case"},
|
||||
{"with numbers in middle", "model2Name", "model2_name"},
|
||||
{"mixed case", "iPhone6sPlus", "i_phone6s_plus"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CamelCaseToSnakeCase(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CamelCaseToSnakeCase(%q) = %q, want %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user