aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2025-11-18 19:51:02 -0800
committerJeff Halter <868228+jhalter@users.noreply.github.com>2025-11-18 19:52:02 -0800
commit16f0a8527a0b9b736afbd4e71e6aac55e31b60e8 (patch)
treeaf793d94f0bebcbc590fb7fcfe67bae81c1fbb37 /internal
parent331edfa762e2e53fa0d5bd09fc1e4977f6c9de43 (diff)
Add support for GIF banners with validation and improved error messages
- Add custom validator for banner file extensions (.jpg, .jpeg, .gif) - Update HandleTranAgreed to dynamically detect banner type from file extension - Export FileTypeFromFilename function for use across packages - Add comprehensive test coverage for banner validation and type detection - Improve error messages to clearly communicate allowed file extensions
Diffstat (limited to 'internal')
-rw-r--r--internal/mobius/config.go19
-rw-r--r--internal/mobius/config_test.go95
-rw-r--r--internal/mobius/transaction_handlers.go3
-rw-r--r--internal/mobius/transaction_handlers_test.go50
4 files changed, 166 insertions, 1 deletions
diff --git a/internal/mobius/config.go b/internal/mobius/config.go
index d04b14d..c798e54 100644
--- a/internal/mobius/config.go
+++ b/internal/mobius/config.go
@@ -7,6 +7,7 @@ import (
"gopkg.in/yaml.v3"
"os"
"path/filepath"
+ "strings"
)
var ConfigSearchOrder = []string{
@@ -28,7 +29,25 @@ func LoadConfig(path string) (*hotline.Config, error) {
}
validate := validator.New()
+ if err = validate.RegisterValidation("bannerext", func(fl validator.FieldLevel) bool {
+ filename := fl.Field().String()
+ if filename == "" {
+ return true // Allow empty since BannerFile is optional
+ }
+ ext := strings.ToLower(filepath.Ext(filename))
+ return ext == ".jpg" || ext == ".jpeg" || ext == ".gif"
+ }); err != nil {
+ return nil, fmt.Errorf("register validation: %v", err)
+ }
if err = validate.Struct(config); err != nil {
+ // Check if this is a BannerFile validation error and provide a better message
+ if validationErrs, ok := err.(validator.ValidationErrors); ok {
+ for _, fieldErr := range validationErrs {
+ if fieldErr.Field() == "BannerFile" && fieldErr.Tag() == "bannerext" {
+ return nil, fmt.Errorf("BannerFile must have a .jpg, .jpeg, or .gif extension (got: %s)", config.BannerFile)
+ }
+ }
+ }
return nil, fmt.Errorf("validate config: %v", err)
}
diff --git a/internal/mobius/config_test.go b/internal/mobius/config_test.go
new file mode 100644
index 0000000..b76b16d
--- /dev/null
+++ b/internal/mobius/config_test.go
@@ -0,0 +1,95 @@
+package mobius
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestLoadConfig_InvalidBannerFileExtension(t *testing.T) {
+ // Create a temporary directory for test files
+ tmpDir, err := os.MkdirTemp("", "mobius-config-test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Create a test config file with an invalid banner file extension
+ configContent := `
+Name: "Test Server"
+Description: "Test Description"
+BannerFile: "banner.png"
+FileRoot: "files"
+`
+ configPath := filepath.Join(tmpDir, "config.yaml")
+ if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
+ t.Fatalf("Failed to write config file: %v", err)
+ }
+
+ // Attempt to load the config
+ _, err = LoadConfig(configPath)
+
+ // Verify that we get the improved error message
+ if err == nil {
+ t.Fatal("Expected error for invalid banner file extension, got nil")
+ }
+
+ expectedMsg := "BannerFile must have a .jpg, .jpeg, or .gif extension (got: banner.png)"
+ if !strings.Contains(err.Error(), expectedMsg) {
+ t.Errorf("Expected error message to contain %q, got: %v", expectedMsg, err)
+ }
+}
+
+func TestLoadConfig_ValidBannerFileExtensions(t *testing.T) {
+ tests := []struct {
+ name string
+ bannerFile string
+ }{
+ {"jpg extension", "banner.jpg"},
+ {"jpeg extension", "banner.jpeg"},
+ {"gif extension", "banner.gif"},
+ {"empty banner", ""},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ // Create a temporary directory for test files
+ tmpDir, err := os.MkdirTemp("", "mobius-config-test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Create files subdirectory
+ filesDir := filepath.Join(tmpDir, "files")
+ if err := os.Mkdir(filesDir, 0755); err != nil {
+ t.Fatalf("Failed to create files dir: %v", err)
+ }
+
+ // Create a test config file with a valid banner file extension
+ configContent := `
+Name: "Test Server"
+Description: "Test Description"
+BannerFile: "` + tt.bannerFile + `"
+FileRoot: "files"
+`
+ configPath := filepath.Join(tmpDir, "config.yaml")
+ if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
+ t.Fatalf("Failed to write config file: %v", err)
+ }
+
+ // Attempt to load the config
+ config, err := LoadConfig(configPath)
+
+ // Verify that we don't get a validation error
+ if err != nil {
+ t.Errorf("Expected no error for %s, got: %v", tt.bannerFile, err)
+ }
+
+ if config.BannerFile != tt.bannerFile {
+ t.Errorf("Expected BannerFile to be %q, got %q", tt.bannerFile, config.BannerFile)
+ }
+ })
+ }
+}
diff --git a/internal/mobius/transaction_handlers.go b/internal/mobius/transaction_handlers.go
index c5b1bbb..52ea29a 100644
--- a/internal/mobius/transaction_handlers.go
+++ b/internal/mobius/transaction_handlers.go
@@ -1095,7 +1095,8 @@ func HandleTranAgreed(cc *hotline.ClientConn, t *hotline.Transaction) (res []hot
res = append(res, trans...)
if cc.Server.Config.BannerFile != "" {
- res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte("JPEG"))))
+ bannerType := hotline.FileTypeFromFilename(cc.Server.Config.BannerFile).TypeCode
+ res = append(res, hotline.NewTransaction(hotline.TranServerBanner, cc.ID, hotline.NewField(hotline.FieldBannerType, []byte(bannerType))))
}
res = append(res, cc.NewReply(t))
diff --git a/internal/mobius/transaction_handlers_test.go b/internal/mobius/transaction_handlers_test.go
index 0d975a7..1c3fac0 100644
--- a/internal/mobius/transaction_handlers_test.go
+++ b/internal/mobius/transaction_handlers_test.go
@@ -2873,6 +2873,56 @@ func TestHandleTranAgreed(t *testing.T) {
},
},
},
+ {
+ name: "with gif banner",
+ args: args{
+ cc: &hotline.ClientConn{
+ Account: &hotline.Account{
+ Access: func() hotline.AccessBitmap {
+ var bits hotline.AccessBitmap
+ bits.Set(hotline.AccessDisconUser)
+ bits.Set(hotline.AccessAnyName)
+ return bits
+ }()},
+ Icon: []byte{0, 1},
+ Flags: [2]byte{0, 1},
+ Version: []byte{0, 1},
+ ID: [2]byte{0, 1},
+ Logger: NewTestLogger(),
+ Server: &hotline.Server{
+ Config: hotline.Config{
+ BannerFile: "Banner.gif",
+ },
+ ClientMgr: func() *hotline.MockClientMgr {
+ m := hotline.MockClientMgr{}
+ m.On("List").Return([]*hotline.ClientConn{},
+ )
+ return &m
+ }(),
+ },
+ },
+ t: hotline.NewTransaction(
+ hotline.TranAgreed, [2]byte{},
+ hotline.NewField(hotline.FieldUserName, []byte("username")),
+ hotline.NewField(hotline.FieldUserIconID, []byte{0, 1}),
+ hotline.NewField(hotline.FieldOptions, []byte{0, 0}),
+ ),
+ },
+ wantRes: []hotline.Transaction{
+ {
+ ClientID: [2]byte{0, 1},
+ Type: [2]byte{0, 0x7a},
+ Fields: []hotline.Field{
+ hotline.NewField(hotline.FieldBannerType, []byte("GIFf")),
+ },
+ },
+ {
+ ClientID: [2]byte{0, 1},
+ IsReply: 0x01,
+ Fields: []hotline.Field{},
+ },
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {