diff --git a/Makefile b/Makefile index a3c8066..3c726c5 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,19 @@ UNIX_BINARY=${MYNAME} WINDOWS_BINARY=${MYNAME}.exe RMCMD=rm -rf +# Default target +.PHONY: help +help: + @echo "aleesa-misc-go build system" + @echo "" + @echo "Available targets:" + @echo " all Clean and build (default)" + @echo " build Build binary only" + @echo " clean Remove built artifacts" + @echo " test Run unit tests" + @echo " upgrade Update dependencies (get, tidy, vendor)" + @echo "" + # На windows имя бинарника может зависеть не только от платформы, но и от выбранной цели, для linux-а суффикс .exe # не нужен ifeq ($(OS),Windows_NT) @@ -30,6 +43,7 @@ define IFS endef +.PHONY: all build clean test upgrade help all: clean build @@ -69,6 +83,10 @@ else # not ifeq ($(OS),Windows_NT) endif +test: + go test ./... + + upgrade: go get -u ./... go mod tidy diff --git a/internal/misc/msgParser_test.go b/internal/misc/msgParser_test.go new file mode 100644 index 0000000..c20b51f --- /dev/null +++ b/internal/misc/msgParser_test.go @@ -0,0 +1,351 @@ +package misc + +import ( + "context" + "encoding/json" + "strings" + "testing" +) + +func TestMsgParser_Validation(t *testing.T) { + tests := []struct { + name string + inputJSON string + expectPanic bool + }{ + { + name: "empty message should not panic", + inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"","plugin":"test","mode":"test"}`, + expectPanic: false, + }, + { + name: "short message less than csign", + inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!","plugin":"test","mode":"test","misc":{"csign":"!!!"}}`, + expectPanic: false, + }, + { + name: "valid message with single char csign", + inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!karma","plugin":"test","mode":"test","misc":{"csign":"!"}}`, + expectPanic: false, + }, + { + name: "message shorter than multi char csign", + inputJSON: `{"from":"test","chatid":"1","userid":"1","message":"!","plugin":"test","mode":"test","misc":{"csign":"!!"}}`, + expectPanic: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.expectPanic { + t.Errorf("unexpected panic: %v", r) + } + } + }() + + // We only test that parsing doesn't panic + // Actual publishing requires Redis + var msg rMsg + if err := json.Unmarshal([]byte(tt.inputJSON), &msg); err != nil { + t.Logf("JSON parse error (expected for some tests): %v", err) + } + }) + } +} + +func TestMsgParser_CommandRouting(t *testing.T) { + // Save original config + origConfig := Config + Config = myConfig{ + Csign: "!", + ForwardChannels: struct { + Games string `json:"games,omitempty"` + Phrases string `json:"phrases,omitempty"` + Webapp string `json:"webapp,omitempty"` + WebappGo string `json:"webapp-go,omitempty"` + Craniac string `json:"craniac,omitempty"` + }{ + Games: "games", + Phrases: "phrases", + Webapp: "webapp", + WebappGo: "webapp-go", + Craniac: "craniac", + }, + } + defer func() { Config = origConfig }() + + tests := []struct { + name string + message string + expected string + }{ + // Phrases commands + {"friday command", "!friday", "phrases"}, + {"fortune command", "!fortune", "phrases"}, + {"karma command", "!karma", "phrases"}, + {"russian friday", "!пятница", "phrases"}, + {"russian karma", "!карма", "phrases"}, + + // Webapp-go commands + {"frog command", "!frog", "webapp-go"}, + {"cat command", "!cat", "webapp-go"}, + {"weather command", "!w Moskow", "webapp-go"}, + {"russian weather", "!погода Москва", "webapp-go"}, + + // Games commands + {"dig command", "!dig", "games"}, + {"fish command", "!fish", "games"}, + {"russian dig", "!копать", "games"}, + + // Default fallback + {"unknown command", "!unknown", "craniac"}, + {"no command prefix", "just some text", "craniac"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := rMsg{ + From: "test", + Chatid: "1", + Userid: "1", + Message: tt.message, + Plugin: "test", + Mode: "test", + Misc: struct { + Answer int64 `json:"answer,omitempty"` + Botnick string `json:"bot_nick,omitempty"` + Csign string `json:"csign,omitempty"` + Fwdcnt *int64 `json:"fwd_cnt,omitempty"` + GoodMorning int64 `json:"good_morning,omitempty"` + Msgformat int64 `json:"msg_format,omitempty"` + Username string `json:"username,omitempty"` + }{ + Csign: Config.Csign, + Fwdcnt: new(int64), + }, + } + *msg.Misc.Fwdcnt = 1 + + result := determineChannel(msg.Message, msg.Misc.Csign) + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + +func TestMsgParser_KarmaPatterns(t *testing.T) { + origConfig := Config + Config = myConfig{ + Csign: "!", + ForwardChannels: struct { + Games string `json:"games,omitempty"` + Phrases string `json:"phrases,omitempty"` + Webapp string `json:"webapp,omitempty"` + WebappGo string `json:"webapp-go,omitempty"` + Craniac string `json:"craniac,omitempty"` + }{ + Phrases: "phrases", + Craniac: "craniac", + }, + } + defer func() { Config = origConfig }() + + tests := []struct { + name string + message string + expected string + }{ + {"karma increment", "something++", "phrases"}, + {"karma decrement", "something--", "phrases"}, + {"russian plus plus", "что-то++", "phrases"}, + {"no karma pattern", "just text", "craniac"}, + {"multiple lines no karma", "line1\nline2++", "craniac"}, + {"single line with karma", "line1++", "phrases"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := determineKarmaChannel(tt.message) + if result != tt.expected { + t.Errorf("expected %s, got %s", tt.expected, result) + } + }) + } +} + +func TestMsgParser_FwdcntPointer(t *testing.T) { + // Test that Fwdcnt pointer handling works correctly + fwdcnt := new(int64) + *fwdcnt = 0 + + if fwdcnt == nil { + t.Error("fwdcnt should not be nil after new(int64)") + } + + if *fwdcnt != 0 { + t.Errorf("expected 0, got %d", *fwdcnt) + } + + *fwdcnt++ + if *fwdcnt != 1 { + t.Errorf("expected 1, got %d", *fwdcnt) + } +} + +// determineChannel is extracted logic for testing command routing +func determineChannel(message, csign string) string { + sendTo := "craniac" + + if len(message) >= len(csign) && message[0:len(csign)] == csign { + cmd := message[len(csign):] + + cmds := []string{"friday", "пятница", "proverb", "пословица", "пословиться", "fortune", "фортунка", "f", "ф", + "karma", "карма", "rum", "ром", "vodka", "водка", "beer", "пиво", "tequila", "текила", "whisky", "виски", + "absinthe", "абсент", "fuck"} + + for _, command := range cmds { + if cmd == command { + return "phrases" + } + } + + cmds = []string{"frog", "лягушка", "horse", "лошадь", "лошадка", "rabbit", "bunny", "кролик", + "snail", "улитка", "cat", "кис", "fox", "лис", "buni", "anek", "анек", "анекдот", + "xkcd", "monkeyuser", "tits", "boobs", "tities", "boobies", "сиси", "сисечки", "butt", + "booty", "ass", "попа", "попка", "drink", "праздник", "owl", "сова", "сыч", "w", "п", "погода", + "погодка", "погадка"} + + for _, command := range cmds { + if cmd == command { + return "webapp-go" + } + } + + cmds = []string{"dig", "копать", "fish", "fishing", "рыба", "рыбка", "рыбалка"} + + for _, command := range cmds { + if cmd == command { + return "games" + } + } + + cmdLen := len(cmd) + cmds = []string{"w ", "п ", "погода ", "погодка ", "погадка ", "weather "} + + for _, command := range cmds { + if cmdLen > len(command) && cmd[0:len(command)] == command { + return "webapp-go" + } + } + + cmds = []string{"karma ", "карма ", "rum ", "ром ", "vodka ", "водка ", "beer ", "пиво ", "tequila ", + "текила ", "whisky ", "виски ", "absinthe ", "абсент "} + + for _, command := range cmds { + if cmdLen > len(command) && cmd[0:len(command)] == command { + return "phrases" + } + } + } + + return sendTo +} + +// determineKarmaChannel is extracted logic for testing karma patterns +func determineKarmaChannel(message string) string { + msgLen := len(message) + + if msgLen > 2 { + if message[msgLen-2:] == "--" || message[msgLen-2:] == "++" { + // Предполагается, что менять карму мы будем для одной фразы, то есть для 1 строки + if len(strings.Split(message, "\n")) == 1 { + return "phrases" + } + } + } + + return "craniac" +} + +func TestMsgParser_BuildsMsgCorrectly(t *testing.T) { + // Test that message building produces correct output + fwdcnt := new(int64) + *fwdcnt = 2 + + input := rMsg{ + From: "telegram", + Chatid: "123", + Userid: "456", + Threadid: "789", + Message: "!karma user", + Plugin: "telegram", + Mode: "private", + Misc: struct { + Answer int64 `json:"answer,omitempty"` + Botnick string `json:"bot_nick,omitempty"` + Csign string `json:"csign,omitempty"` + Fwdcnt *int64 `json:"fwd_cnt,omitempty"` + GoodMorning int64 `json:"good_morning,omitempty"` + Msgformat int64 `json:"msg_format,omitempty"` + Username string `json:"username,omitempty"` + }{ + Answer: 1, + Botnick: "aleesa", + Csign: "!", + Fwdcnt: fwdcnt, + Username: "testuser", + }, + } + + // Simulate the message building logic + var message sMsg + message.From = input.From + message.Userid = input.Userid + message.Chatid = input.Chatid + message.Threadid = input.Threadid + message.Message = input.Message + message.Plugin = input.Plugin + message.Mode = input.Mode + message.Misc.Fwdcnt = *input.Misc.Fwdcnt + message.Misc.Csign = input.Misc.Csign + message.Misc.Username = input.Misc.Username + message.Misc.Answer = input.Misc.Answer + message.Misc.Botnick = input.Misc.Botnick + message.Misc.Msgformat = input.Misc.Msgformat + message.Misc.GoodMorning = input.Misc.GoodMorning + + // Verify + if message.Misc.Fwdcnt != 2 { + t.Errorf("expected Fwdcnt 2, got %d", message.Misc.Fwdcnt) + } + if message.Misc.Answer != 1 { + t.Errorf("expected Answer 1, got %d", message.Misc.Answer) + } + if message.Misc.Username != "testuser" { + t.Errorf("expected Username 'testuser', got %s", message.Misc.Username) + } + + // Verify JSON serialization works + data, err := json.Marshal(message) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + // Verify we can unmarshal it back + var result sMsg + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + if result.From != message.From { + t.Errorf("From mismatch: got %s", result.From) + } + if result.Misc.Fwdcnt != message.Misc.Fwdcnt { + t.Errorf("Fwdcnt mismatch: got %d", result.Misc.Fwdcnt) + } +} + +var _ = context.Background // for import diff --git a/internal/misc/readConfig_test.go b/internal/misc/readConfig_test.go new file mode 100644 index 0000000..313fd2f --- /dev/null +++ b/internal/misc/readConfig_test.go @@ -0,0 +1,327 @@ +package misc + +import ( + "encoding/json" + "testing" +) + +func TestConfigValidation(t *testing.T) { + tests := []struct { + name string + config myConfig + expectedServer string + expectedPort int + expectValid bool + }{ + { + name: "full valid config", + config: myConfig{ + Server: "redis.example.com", + Port: 6380, + Channel: "test-channel", + Csign: "/", + }, + expectedServer: "redis.example.com", + expectedPort: 6380, + expectValid: true, + }, + { + name: "config with defaults", + config: myConfig{ + Channel: "test-channel", + Csign: "!", + }, + expectedServer: "localhost", + expectedPort: 6379, + expectValid: true, + }, + { + name: "missing channel", + config: myConfig{ + Server: "localhost", + Port: 6379, + Csign: "!", + }, + expectValid: false, + }, + { + name: "missing csign", + config: myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + }, + expectValid: false, + }, + { + name: "zero port uses default", + config: myConfig{ + Server: "localhost", + Port: 0, + Channel: "test", + Csign: "!", + }, + expectedServer: "localhost", + expectedPort: 6379, + expectValid: true, + }, + { + name: "zero timeout uses default", + config: myConfig{ + Server: "localhost", + Port: 6379, + Timeout: 0, + Channel: "test", + Csign: "!", + }, + expectedServer: "localhost", + expectedPort: 6379, + expectValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := tt.config + valid := validateConfig(&cfg) + + if valid != tt.expectValid { + t.Errorf("expected valid=%v, got %v", tt.expectValid, valid) + } + + if tt.expectValid { + applyDefaults(&cfg) + if cfg.Server != tt.expectedServer { + t.Errorf("expected Server=%s, got %s", tt.expectedServer, cfg.Server) + } + if cfg.Port != tt.expectedPort { + t.Errorf("expected Port=%d, got %d", tt.expectedPort, cfg.Port) + } + } + }) + } +} + +func TestForwardChannelsDefaults(t *testing.T) { + cfg := myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + Csign: "!", + } + + applyDefaults(&cfg) + + if cfg.ForwardChannels.Games != "games" { + t.Errorf("expected games='games', got '%s'", cfg.ForwardChannels.Games) + } + if cfg.ForwardChannels.Phrases != "phrases" { + t.Errorf("expected phrases='phrases', got '%s'", cfg.ForwardChannels.Phrases) + } + if cfg.ForwardChannels.Webapp != "webapp" { + t.Errorf("expected webapp='webapp', got '%s'", cfg.ForwardChannels.Webapp) + } + if cfg.ForwardChannels.WebappGo != "webapp-go" { + t.Errorf("expected webapp-go='webapp-go', got '%s'", cfg.ForwardChannels.WebappGo) + } + if cfg.ForwardChannels.Craniac != "craniac" { + t.Errorf("expected craniac='craniac', got '%s'", cfg.ForwardChannels.Craniac) + } +} + +func TestForwardChannelsCustomValues(t *testing.T) { + cfg := myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + Csign: "!", + ForwardChannels: struct { + Games string `json:"games,omitempty"` + Phrases string `json:"phrases,omitempty"` + Webapp string `json:"webapp,omitempty"` + WebappGo string `json:"webapp-go,omitempty"` + Craniac string `json:"craniac,omitempty"` + }{ + Games: "my-games", + Phrases: "my-phrases", + Webapp: "my-webapp", + WebappGo: "my-webapp-go", + Craniac: "my-craniac", + }, + } + + applyDefaults(&cfg) + + if cfg.ForwardChannels.Games != "my-games" { + t.Errorf("expected games='my-games', got '%s'", cfg.ForwardChannels.Games) + } + if cfg.ForwardChannels.Phrases != "my-phrases" { + t.Errorf("expected phrases='my-phrases', got '%s'", cfg.ForwardChannels.Phrases) + } +} + +// validateConfig simulates the validation logic from ReadConfig +func validateConfig(cfg *myConfig) bool { + if cfg.Channel == "" { + return false + } + if cfg.Csign == "" { + return false + } + return true +} + +// applyDefaults simulates the default application logic from ReadConfig +func applyDefaults(cfg *myConfig) { + if cfg.Server == "" { + cfg.Server = "localhost" + } + if cfg.Port == 0 { + cfg.Port = 6379 + } + if cfg.Timeout == 0 { + cfg.Timeout = 10 + } + if cfg.Loglevel == "" { + cfg.Loglevel = "info" + } + if cfg.ForwardChannels.Games == "" { + cfg.ForwardChannels.Games = "games" + } + if cfg.ForwardChannels.Phrases == "" { + cfg.ForwardChannels.Phrases = "phrases" + } + if cfg.ForwardChannels.Webapp == "" { + cfg.ForwardChannels.Webapp = "webapp" + } + if cfg.ForwardChannels.WebappGo == "" { + cfg.ForwardChannels.WebappGo = "webapp-go" + } + if cfg.ForwardChannels.Craniac == "" { + cfg.ForwardChannels.Craniac = "craniac" + } + if cfg.Csign == "" { + cfg.Csign = "!" + } + if cfg.ForwardsMax == 0 { + cfg.ForwardsMax = 5 + } +} + +func TestLoglevelValidation(t *testing.T) { + tests := []struct { + name string + loglevel string + expectedOK bool + }{ + {"valid error", "error", true}, + {"valid warn", "warn", true}, + {"valid info", "info", true}, + {"valid debug", "debug", true}, + {"invalid level", "trace", false}, + {"empty uses default", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := &myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + Csign: "!", + } + + if tt.loglevel != "" { + cfg.Loglevel = tt.loglevel + } + + applyDefaults(cfg) + + // Valid loglevels should result in a non-empty loglevel + // Invalid ones would still get set to "info" as default + if cfg.Loglevel == "" && tt.expectedOK { + t.Error("expected non-empty loglevel") + } + }) + } +} + +func TestForwardsMaxDefault(t *testing.T) { + cfg := myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + Csign: "!", + } + + applyDefaults(&cfg) + + if cfg.ForwardsMax != 5 { + t.Errorf("expected ForwardsMax=5, got %d", cfg.ForwardsMax) + } + + cfg.ForwardsMax = 10 + applyDefaults(&cfg) + + if cfg.ForwardsMax != 10 { + t.Errorf("expected ForwardsMax=10, got %d", cfg.ForwardsMax) + } +} + +func TestConfigJSONRoundtrip(t *testing.T) { + original := myConfig{ + Server: "redis.example.com", + Port: 6380, + Timeout: 15, + Loglevel: "debug", + Log: "/var/log/test.log", + Channel: "test-channel", + Csign: "/", + ForwardsMax: 10, + ForwardChannels: struct { + Games string `json:"games,omitempty"` + Phrases string `json:"phrases,omitempty"` + Webapp string `json:"webapp,omitempty"` + WebappGo string `json:"webapp-go,omitempty"` + Craniac string `json:"craniac,omitempty"` + }{ + Games: "custom-games", + Phrases: "custom-phrases", + Webapp: "custom-webapp", + WebappGo: "custom-webapp-go", + Craniac: "custom-craniac", + }, + } + + // Marshal + data, err := json.Marshal(original) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + // Unmarshal + var restored myConfig + if err := json.Unmarshal(data, &restored); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + // Compare + if restored.Server != original.Server { + t.Errorf("Server: expected %s, got %s", original.Server, restored.Server) + } + if restored.Port != original.Port { + t.Errorf("Port: expected %d, got %d", original.Port, restored.Port) + } + if restored.Channel != original.Channel { + t.Errorf("Channel: expected %s, got %s", original.Channel, restored.Channel) + } + if restored.Csign != original.Csign { + t.Errorf("Csign: expected %s, got %s", original.Csign, restored.Csign) + } + if restored.ForwardsMax != original.ForwardsMax { + t.Errorf("ForwardsMax: expected %d, got %d", original.ForwardsMax, restored.ForwardsMax) + } + if restored.ForwardChannels.Games != original.ForwardChannels.Games { + t.Errorf("ForwardChannels.Games: expected %s, got %s", original.ForwardChannels.Games, restored.ForwardChannels.Games) + } +} diff --git a/internal/misc/types_test.go b/internal/misc/types_test.go new file mode 100644 index 0000000..c728558 --- /dev/null +++ b/internal/misc/types_test.go @@ -0,0 +1,348 @@ +package misc + +import ( + "encoding/json" + "testing" +) + +func TestRMsgJSONDeserialization(t *testing.T) { + tests := []struct { + name string + jsonInput string + expectedFrom string + expectedChatid string + expectedPlugin string + expectCsignNil bool + }{ + { + name: "full message with all fields", + jsonInput: `{"from":"telegram","chatid":"123","userid":"456","message":"hello","plugin":"telegram","mode":"private","misc":{"answer":1,"bot_nick":"aleesa","csign":"!","fwd_cnt":1,"good_morning":0,"msg_format":1,"username":"user"}}`, + expectedFrom: "telegram", + expectedChatid: "123", + expectedPlugin: "telegram", + expectCsignNil: false, + }, + { + name: "minimal message", + jsonInput: `{"from":"irc","chatid":"#room","userid":"nick","message":"!karma","plugin":"irc","mode":"channel"}`, + expectedFrom: "irc", + expectedChatid: "#room", + expectedPlugin: "irc", + expectCsignNil: true, + }, + { + name: "message with fwd_cnt zero", + jsonInput: `{"from":"jabber","chatid":"room@conference","userid":"user","message":"test","plugin":"jabber","mode":"group","misc":{"fwd_cnt":0}}`, + expectedFrom: "jabber", + expectedChatid: "room@conference", + expectedPlugin: "jabber", + expectCsignNil: true, + }, + { + name: "message with threadid", + jsonInput: `{"from":"telegram","chatid":"123","userid":"456","threadid":"789","message":"reply","plugin":"telegram","mode":"thread"}`, + expectedFrom: "telegram", + expectedChatid: "123", + expectedPlugin: "telegram", + expectCsignNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var msg rMsg + if err := json.Unmarshal([]byte(tt.jsonInput), &msg); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + if msg.From != tt.expectedFrom { + t.Errorf("From: expected %s, got %s", tt.expectedFrom, msg.From) + } + if msg.Chatid != tt.expectedChatid { + t.Errorf("Chatid: expected %s, got %s", tt.expectedChatid, msg.Chatid) + } + if msg.Plugin != tt.expectedPlugin { + t.Errorf("Plugin: expected %s, got %s", tt.expectedPlugin, msg.Plugin) + } + if tt.expectCsignNil && msg.Misc.Csign != "" { + t.Errorf("Csign: expected empty, got %s", msg.Misc.Csign) + } + }) + } +} + +func TestSMsgJSONSerialization(t *testing.T) { + tests := []struct { + name string + message sMsg + }{ + { + name: "full message", + message: sMsg{ + From: "telegram", + Chatid: "123", + Userid: "456", + Threadid: "789", + Message: "Hello world", + Plugin: "telegram", + Mode: "private", + Misc: struct { + Answer int64 `json:"answer"` + Botnick string `json:"bot_nick"` + Csign string `json:"csign"` + Fwdcnt int64 `json:"fwd_cnt"` + GoodMorning int64 `json:"good_morning"` + Msgformat int64 `json:"msg_format"` + Username string `json:"username"` + }{ + Answer: 1, + Botnick: "aleesa", + Csign: "!", + Fwdcnt: 2, + GoodMorning: 0, + Msgformat: 1, + Username: "testuser", + }, + }, + }, + { + name: "minimal message", + message: sMsg{ + From: "irc", + Chatid: "#room", + Userid: "nick", + Message: "!help", + Plugin: "irc", + Mode: "channel", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.message) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + // Verify it's valid JSON + var result map[string]interface{} + if err := json.Unmarshal(data, &result); err != nil { + t.Fatalf("marshaled data is not valid JSON: %v", err) + } + + // Verify key fields are present + if _, ok := result["from"]; !ok { + t.Error("missing 'from' field") + } + if _, ok := result["chatid"]; !ok { + t.Error("missing 'chatid' field") + } + if _, ok := result["message"]; !ok { + t.Error("missing 'message' field") + } + }) + } +} + +func TestRMsgToSMsgConversion(t *testing.T) { + fwdcnt := new(int64) + *fwdcnt = 3 + + input := rMsg{ + From: "telegram", + Chatid: "123", + Userid: "456", + Threadid: "789", + Message: "!friday", + Plugin: "telegram", + Mode: "private", + Misc: struct { + Answer int64 `json:"answer,omitempty"` + Botnick string `json:"bot_nick,omitempty"` + Csign string `json:"csign,omitempty"` + Fwdcnt *int64 `json:"fwd_cnt,omitempty"` + GoodMorning int64 `json:"good_morning,omitempty"` + Msgformat int64 `json:"msg_format,omitempty"` + Username string `json:"username,omitempty"` + }{ + Answer: 0, + Botnick: "aleesa", + Csign: "!", + Fwdcnt: fwdcnt, + GoodMorning: 1, + Msgformat: 1, + Username: "testuser", + }, + } + + // Convert + output := convertToSMsg(input) + + // Verify all fields transferred + if output.From != input.From { + t.Errorf("From: expected %s, got %s", input.From, output.From) + } + if output.Chatid != input.Chatid { + t.Errorf("Chatid: expected %s, got %s", input.Chatid, output.Chatid) + } + if output.Userid != input.Userid { + t.Errorf("Userid: expected %s, got %s", input.Userid, output.Userid) + } + if output.Threadid != input.Threadid { + t.Errorf("Threadid: expected %s, got %s", input.Threadid, output.Threadid) + } + if output.Message != input.Message { + t.Errorf("Message: expected %s, got %s", input.Message, output.Message) + } + if output.Plugin != input.Plugin { + t.Errorf("Plugin: expected %s, got %s", input.Plugin, output.Plugin) + } + if output.Mode != input.Mode { + t.Errorf("Mode: expected %s, got %s", input.Mode, output.Mode) + } + if output.Misc.Csign != input.Misc.Csign { + t.Errorf("Misc.Csign: expected %s, got %s", input.Misc.Csign, output.Misc.Csign) + } + if output.Misc.Fwdcnt != *input.Misc.Fwdcnt { + t.Errorf("Misc.Fwdcnt: expected %d, got %d", *input.Misc.Fwdcnt, output.Misc.Fwdcnt) + } + if output.Misc.Username != input.Misc.Username { + t.Errorf("Misc.Username: expected %s, got %s", input.Misc.Username, output.Misc.Username) + } + if output.Misc.Answer != input.Misc.Answer { + t.Errorf("Misc.Answer: expected %d, got %d", input.Misc.Answer, output.Misc.Answer) + } + if output.Misc.Botnick != input.Misc.Botnick { + t.Errorf("Misc.Botnick: expected %s, got %s", input.Misc.Botnick, output.Misc.Botnick) + } + if output.Misc.Msgformat != input.Misc.Msgformat { + t.Errorf("Misc.Msgformat: expected %d, got %d", input.Misc.Msgformat, output.Misc.Msgformat) + } + if output.Misc.GoodMorning != input.Misc.GoodMorning { + t.Errorf("Misc.GoodMorning: expected %d, got %d", input.Misc.GoodMorning, output.Misc.GoodMorning) + } +} + +// convertToSMsg simulates the conversion logic from msgParser.go +func convertToSMsg(j rMsg) sMsg { + var message sMsg + message.From = j.From + message.Userid = j.Userid + message.Chatid = j.Chatid + message.Threadid = j.Threadid + message.Message = j.Message + message.Plugin = j.Plugin + message.Mode = j.Mode + message.Misc.Fwdcnt = *j.Misc.Fwdcnt + message.Misc.Csign = j.Misc.Csign + message.Misc.Username = j.Misc.Username + message.Misc.Answer = j.Misc.Answer + message.Misc.Botnick = j.Misc.Botnick + message.Misc.Msgformat = j.Misc.Msgformat + message.Misc.GoodMorning = j.Misc.GoodMorning + return message +} + +func TestJSONFieldNaming(t *testing.T) { + // Test that sMsg uses lowercase "misc" in JSON + msg := sMsg{ + From: "test", + Chatid: "1", + Userid: "2", + Message: "hello", + Plugin: "test", + Mode: "test", + } + + data, err := json.Marshal(msg) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + // Check for lowercase "misc" + if !contains(string(data), `"misc"`) { + t.Errorf("expected lowercase 'misc' in JSON, got: %s", string(data)) + } + + // Ensure it doesn't contain capitalized "Misc" + if contains(string(data), `"Misc"`) { + t.Errorf("should not contain capitalized 'Misc' in JSON, got: %s", string(data)) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsAt(s, substr)) +} + +func containsAt(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func TestRMsgFwdcntPointer(t *testing.T) { + // Test that Fwdcnt as pointer works correctly with JSON + jsonInput := `{"from":"test","chatid":"1","userid":"1","message":"test","plugin":"test","mode":"test","misc":{"fwd_cnt":5}}` + + var msg rMsg + if err := json.Unmarshal([]byte(jsonInput), &msg); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + if msg.Misc.Fwdcnt == nil { + t.Fatal("Fwdcnt should not be nil") + } + + if *msg.Misc.Fwdcnt != 5 { + t.Errorf("expected Fwdcnt=5, got %d", *msg.Misc.Fwdcnt) + } +} + +func TestRMsgFwdcntNilWhenOmitted(t *testing.T) { + jsonInput := `{"from":"test","chatid":"1","userid":"1","message":"test","plugin":"test","mode":"test"}` + + var msg rMsg + if err := json.Unmarshal([]byte(jsonInput), &msg); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + if msg.Misc.Fwdcnt != nil { + t.Errorf("Fwdcnt should be nil when omitted, got %d", *msg.Misc.Fwdcnt) + } +} + +func TestMyConfigForwardChannels(t *testing.T) { + cfg := myConfig{ + Server: "localhost", + Port: 6379, + Channel: "test", + Csign: "!", + } + + // Test that struct can hold custom values + cfg.ForwardChannels.Games = "custom-games" + cfg.ForwardChannels.Phrases = "custom-phrases" + cfg.ForwardChannels.Webapp = "custom-webapp" + cfg.ForwardChannels.WebappGo = "custom-webapp-go" + cfg.ForwardChannels.Craniac = "custom-craniac" + + if cfg.ForwardChannels.Games != "custom-games" { + t.Errorf("Games: expected custom-games, got %s", cfg.ForwardChannels.Games) + } + if cfg.ForwardChannels.Phrases != "custom-phrases" { + t.Errorf("Phrases: expected custom-phrases, got %s", cfg.ForwardChannels.Phrases) + } + if cfg.ForwardChannels.Webapp != "custom-webapp" { + t.Errorf("Webapp: expected custom-webapp, got %s", cfg.ForwardChannels.Webapp) + } + if cfg.ForwardChannels.WebappGo != "custom-webapp-go" { + t.Errorf("WebappGo: expected custom-webapp-go, got %s", cfg.ForwardChannels.WebappGo) + } + if cfg.ForwardChannels.Craniac != "custom-craniac" { + t.Errorf("Craniac: expected custom-craniac, got %s", cfg.ForwardChannels.Craniac) + } +}