diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2025-11-28 11:46:53 -0800 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2025-11-28 11:46:53 -0800 |
| commit | 1f5717031ac2b56301092e9a090b78a2ff55480a (patch) | |
| tree | fc8a8c5a7b67c16e32ad5ea4790419721d7232c1 /hotline | |
| parent | 8ddb9bb228389b198a76d6df21de005da4fad66b (diff) | |
Mobius client library quality of life improvements
- Introduce Logger interface to replace direct slog dependency in Client
- Implement Write methods for NewsArtListData and NewsCategoryListData15 with support for partial/chunked writes
- Enhance transaction logging with human-readable type names
Diffstat (limited to 'hotline')
| -rw-r--r-- | hotline/client.go | 21 | ||||
| -rw-r--r-- | hotline/logger.go | 7 | ||||
| -rw-r--r-- | hotline/news.go | 153 | ||||
| -rw-r--r-- | hotline/news_test.go | 514 | ||||
| -rw-r--r-- | hotline/transaction.go | 1 |
5 files changed, 679 insertions, 17 deletions
diff --git a/hotline/client.go b/hotline/client.go index e300bb8..544219c 100644 --- a/hotline/client.go +++ b/hotline/client.go @@ -7,7 +7,6 @@ import ( "encoding/binary" "fmt" "io" - "log/slog" "net" "time" ) @@ -27,7 +26,7 @@ func (cp *ClientPrefs) IconBytes() []byte { type Client struct { Connection net.Conn - Logger *slog.Logger + Logger Logger Pref *ClientPrefs Handlers map[[2]byte]ClientHandler activeTasks map[[4]byte]*Transaction @@ -40,7 +39,7 @@ func (c *Client) HandleFunc(tranType [2]byte, handler ClientHandler) { c.Handlers[tranType] = handler } -func NewClient(username string, logger *slog.Logger) *Client { +func NewClient(username string, logger Logger) *Client { c := &Client{ Logger: logger, activeTasks: make(map[[4]byte]*Transaction), @@ -143,7 +142,7 @@ func (c *Client) Handshake() error { } func (c *Client) Send(t Transaction) error { - requestNum := binary.BigEndian.Uint16(t.Type[:]) + //requestNum := binary.BigEndian.Uint16(t.Type[:]) // if transaction is NOT reply, add it to the list to transactions we're expecting a response for if t.IsReply == 0 { @@ -155,11 +154,8 @@ func (c *Client) Send(t Transaction) error { return fmt.Errorf("error sending transaction: %w", err) } - c.Logger.Debug("Sent Transaction", - "IsReply", t.IsReply, - "type", requestNum, - "sentBytes", n, - ) + c.Logger.Info(tranTypeNames[t.Type], "IsReply", t.IsReply, "type", t.Type[:], "sentBytes", n) + return nil } @@ -171,11 +167,8 @@ func (c *Client) HandleTransaction(ctx context.Context, t *Transaction) error { } if handler, ok := c.Handlers[t.Type]; ok { - c.Logger.Debug( - "Received transaction", - "IsReply", t.IsReply, - "type", t.Type[:], - ) + c.Logger.Info(tranTypeNames[t.Type], "IsReply", t.IsReply, "type", t.Type[:]) + outT, err := handler(ctx, c, t) if err != nil { c.Logger.Error("error handling transaction", "err", err) diff --git a/hotline/logger.go b/hotline/logger.go new file mode 100644 index 0000000..22ba09f --- /dev/null +++ b/hotline/logger.go @@ -0,0 +1,7 @@ +package hotline + +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Error(msg string, args ...any) +} diff --git a/hotline/news.go b/hotline/news.go index 1ddeebd..f247db0 100644 --- a/hotline/news.go +++ b/hotline/news.go @@ -3,9 +3,10 @@ package hotline import ( "cmp" "encoding/binary" - "github.com/stretchr/testify/mock" "io" "slices" + + "github.com/stretchr/testify/mock" ) var ( @@ -38,7 +39,9 @@ type NewsCategoryListData15 struct { AddSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused. DeleteSN [4]byte `yaml:"-"` // What does this do? Undocumented and seeming unused. - readOffset int // Internal offset to track read progress + readOffset int // Internal offset to track read progress + writeOffset int // Internal offset to track write progress + writeBuf []byte // Buffer for accumulating partial writes } func (newscat *NewsCategoryListData15) GetNewsArtListData() (NewsArtListData, error) { @@ -110,7 +113,9 @@ type NewsArtListData struct { NewsArtList []byte // List of articles Optional (if article count > 0) Count int - readOffset int // Internal offset to track read progress + readOffset int // Internal offset to track read progress + writeOffset int // Internal offset to track write progress + writeBuf []byte // Buffer for accumulating partial writes } func (nald *NewsArtListData) Read(p []byte) (int, error) { @@ -136,6 +141,82 @@ func (nald *NewsArtListData) Read(p []byte) (int, error) { return n, nil } +func (nald *NewsArtListData) Write(p []byte) (int, error) { + // Accumulate incoming bytes into the write buffer + nald.writeBuf = append(nald.writeBuf, p...) + bytesConsumed := len(p) + + // If we've already parsed the header (writeOffset > 0), just accumulate article list data + if nald.writeOffset > 0 { + // Append new data to existing NewsArtList + nald.NewsArtList = append(nald.NewsArtList, p...) + return bytesConsumed, nil + } + + // Minimum size: ID(4) + Count(4) + NameLen(1) + DescLen(1) = 10 bytes + if len(nald.writeBuf) < 10 { + return bytesConsumed, nil // Need more data + } + + offset := 0 + + // Read ID (4 bytes) + copy(nald.ID[:], nald.writeBuf[offset:offset+4]) + offset += 4 + + // Read Count (4 bytes) + nald.Count = int(binary.BigEndian.Uint32(nald.writeBuf[offset : offset+4])) + offset += 4 + + // Read Name length (1 byte) + nameLen := int(nald.writeBuf[offset]) + offset += 1 + + // Check if we have enough data for Name + if len(nald.writeBuf) < offset+nameLen { + return bytesConsumed, nil // Need more data + } + + // Read Name (nameLen bytes) + nald.Name = make([]byte, nameLen) + copy(nald.Name, nald.writeBuf[offset:offset+nameLen]) + offset += nameLen + + // Check if we have Description length byte + if len(nald.writeBuf) < offset+1 { + return bytesConsumed, nil // Need more data + } + + // Read Description length (1 byte) + descLen := int(nald.writeBuf[offset]) + offset += 1 + + // Check if we have enough data for Description + if len(nald.writeBuf) < offset+descLen { + return bytesConsumed, nil // Need more data + } + + // Read Description (descLen bytes) + nald.Description = make([]byte, descLen) + copy(nald.Description, nald.writeBuf[offset:offset+descLen]) + offset += descLen + + // Read remaining bytes as NewsArtList + if len(nald.writeBuf) > offset { + nald.NewsArtList = make([]byte, len(nald.writeBuf)-offset) + copy(nald.NewsArtList, nald.writeBuf[offset:]) + } else { + nald.NewsArtList = []byte{} + } + + // Mark that we've successfully parsed the header + nald.writeOffset = offset + // Clear the buffer as we've successfully parsed all data + nald.writeBuf = nil + + return bytesConsumed, nil +} + // NewsArtList is a summarized version of a NewArtData record for display in list view type NewsArtList struct { ID [4]byte @@ -223,6 +304,72 @@ func (newscat *NewsCategoryListData15) Read(p []byte) (int, error) { return n, nil } +func (newscat *NewsCategoryListData15) Write(p []byte) (int, error) { + // Accumulate incoming bytes into the write buffer + newscat.writeBuf = append(newscat.writeBuf, p...) + bytesConsumed := len(p) + + // Minimum size: Type(2) + Count(2) + NameLen(1) = 5 bytes + if len(newscat.writeBuf) < 5 { + return bytesConsumed, nil // Need more data + } + + offset := 0 + + // Read Type (2 bytes) + copy(newscat.Type[:], newscat.writeBuf[offset:offset+2]) + offset += 2 + + // Read count (2 bytes) - stored but not directly used as it's derived from maps + _ = binary.BigEndian.Uint16(newscat.writeBuf[offset : offset+2]) + offset += 2 + + // If Type is NewsCategory, read GUID, AddSN, DeleteSN + if newscat.Type == NewsCategory { + // Need additional 24 bytes: GUID(16) + AddSN(4) + DeleteSN(4) + if len(newscat.writeBuf) < offset+24 { + return bytesConsumed, nil // Need more data + } + + copy(newscat.GUID[:], newscat.writeBuf[offset:offset+16]) + offset += 16 + + copy(newscat.AddSN[:], newscat.writeBuf[offset:offset+4]) + offset += 4 + + copy(newscat.DeleteSN[:], newscat.writeBuf[offset:offset+4]) + offset += 4 + } + + // Read name length (1 byte) + if len(newscat.writeBuf) < offset+1 { + return bytesConsumed, nil // Need more data + } + nameLen := int(newscat.writeBuf[offset]) + offset += 1 + + // Read name (nameLen bytes) + if len(newscat.writeBuf) < offset+nameLen { + return bytesConsumed, nil // Need more data + } + newscat.Name = string(newscat.writeBuf[offset : offset+nameLen]) + offset += nameLen + + // Initialize maps if needed + if newscat.Articles == nil { + newscat.Articles = make(map[uint32]*NewsArtData) + } + if newscat.SubCats == nil { + newscat.SubCats = make(map[string]NewsCategoryListData15) + } + + // Clear the buffer as we've successfully parsed all expected data + newscat.writeBuf = newscat.writeBuf[offset:] + newscat.writeOffset = offset + + return bytesConsumed, nil +} + func (newscat *NewsCategoryListData15) nameLen() []byte { return []byte{uint8(len(newscat.Name))} } diff --git a/hotline/news_test.go b/hotline/news_test.go index 46df2e3..2af3cd2 100644 --- a/hotline/news_test.go +++ b/hotline/news_test.go @@ -270,6 +270,306 @@ func TestNewsArtListData_Read(t *testing.T) { } } +func TestNewsArtListData_Write(t *testing.T) { + tests := []struct { + name string + input []byte + wantID [4]byte + wantCount int + wantName []byte + wantDesc []byte + wantArtList []byte + wantN int + wantErr bool + }{ + { + name: "basic data", + input: []byte{ + 0x00, 0x01, 0x02, 0x03, // ID + 0x00, 0x00, 0x00, 0x05, // Count: 5 + 0x04, // Name length: 4 + 0x74, 0x65, 0x73, 0x74, // Name: "test" + 0x04, // Description length: 4 + 0x64, 0x65, 0x73, 0x63, // Description: "desc" + }, + wantID: [4]byte{0x00, 0x01, 0x02, 0x03}, + wantCount: 5, + wantName: []byte("test"), + wantDesc: []byte("desc"), + wantArtList: []byte{}, + wantN: 18, + wantErr: false, + }, + { + name: "with article list", + input: []byte{ + 0x00, 0x01, 0x02, 0x03, // ID + 0x00, 0x00, 0x00, 0x01, // Count: 1 + 0x04, // Name length: 4 + 0x74, 0x65, 0x73, 0x74, // Name: "test" + 0x04, // Description length: 4 + 0x64, 0x65, 0x73, 0x63, // Description: "desc" + 0xAA, 0xBB, 0xCC, // NewsArtList data + }, + wantID: [4]byte{0x00, 0x01, 0x02, 0x03}, + wantCount: 1, + wantName: []byte("test"), + wantDesc: []byte("desc"), + wantArtList: []byte{0xAA, 0xBB, 0xCC}, + wantN: 21, + wantErr: false, + }, + { + name: "empty name and description", + input: []byte{ + 0xFF, 0xFE, 0xFD, 0xFC, // ID + 0x00, 0x00, 0x00, 0x00, // Count: 0 + 0x00, // Name length: 0 + 0x00, // Description length: 0 + }, + wantID: [4]byte{0xFF, 0xFE, 0xFD, 0xFC}, + wantCount: 0, + wantName: []byte{}, + wantDesc: []byte{}, + wantArtList: []byte{}, + wantN: 10, + wantErr: false, + }, + { + name: "long name and description", + input: []byte{ + 0x00, 0x00, 0x00, 0x00, // ID + 0x00, 0x00, 0x00, 0x0A, // Count: 10 + 0x0A, // Name length: 10 + 0x4C, 0x6F, 0x6E, 0x67, 0x65, 0x72, 0x4E, 0x61, 0x6D, 0x65, // Name: "LongerName" + 0x0B, // Description length: 11 + 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6F, 0x6E, // Description: "Description" + }, + wantID: [4]byte{0x00, 0x00, 0x00, 0x00}, + wantCount: 10, + wantName: []byte("LongerName"), + wantDesc: []byte("Description"), + wantArtList: []byte{}, + wantN: 31, // 4 (ID) + 4 (count) + 1 (name len) + 10 (name) + 1 (desc len) + 11 (desc) + wantErr: false, + }, + { + name: "with large article list", + input: []byte{ + 0x12, 0x34, 0x56, 0x78, // ID + 0x00, 0x00, 0x00, 0x03, // Count: 3 + 0x02, // Name length: 2 + 0x41, 0x42, // Name: "AB" + 0x02, // Description length: 2 + 0x43, 0x44, // Description: "CD" + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, // NewsArtList: 10 bytes + }, + wantID: [4]byte{0x12, 0x34, 0x56, 0x78}, + wantCount: 3, + wantName: []byte("AB"), + wantDesc: []byte("CD"), + wantArtList: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}, + wantN: 24, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nald := &NewsArtListData{} + gotN, err := nald.Write(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.wantN, gotN) + assert.Equal(t, tt.wantID, nald.ID) + assert.Equal(t, tt.wantCount, nald.Count) + assert.Equal(t, tt.wantName, nald.Name) + assert.Equal(t, tt.wantDesc, nald.Description) + assert.Equal(t, tt.wantArtList, nald.NewsArtList) + }) + } +} + +func TestNewsArtListData_Write_PartialData(t *testing.T) { + tests := []struct { + name string + chunks [][]byte + wantID [4]byte + wantCount int + wantName []byte + wantDesc []byte + wantArtList []byte + finalBytes int + }{ + { + name: "split across ID boundary", + chunks: [][]byte{ + {0x00, 0x01}, // First 2 bytes of ID + {0x02, 0x03}, // Last 2 bytes of ID + {0x00, 0x00, 0x00, 0x02}, // Count: 2 + {0x03}, // Name length: 3 + {0x66, 0x6f, 0x6f}, // Name: "foo" + {0x03}, // Description length: 3 + {0x62, 0x61, 0x72}, // Description: "bar" + }, + wantID: [4]byte{0x00, 0x01, 0x02, 0x03}, + wantCount: 2, + wantName: []byte("foo"), + wantDesc: []byte("bar"), + wantArtList: []byte{}, + finalBytes: 16, // 2 + 2 + 4 + 1 + 3 + 1 + 3 + }, + { + name: "split across name", + chunks: [][]byte{ + {0x00, 0x00, 0x00, 0x00}, // ID + {0x00, 0x00, 0x00, 0x01}, // Count: 1 + {0x05}, // Name length: 5 + {0x68, 0x65}, // "he" + {0x6c, 0x6c, 0x6f}, // "llo" + {0x00}, // Description length: 0 + }, + wantID: [4]byte{0x00, 0x00, 0x00, 0x00}, + wantCount: 1, + wantName: []byte("hello"), + wantDesc: []byte{}, + wantArtList: []byte{}, + finalBytes: 15, + }, + { + name: "split with article list", + chunks: [][]byte{ + {0xAA, 0xBB, 0xCC, 0xDD}, // ID + {0x00, 0x00, 0x00, 0x05}, // Count: 5 + {0x01, 0x41}, // Name length: 1, Name: "A" + {0x01, 0x42}, // Description length: 1, Description: "B" + {0x11, 0x22}, // Article list part 1 + {0x33, 0x44, 0x55}, // Article list part 2 + }, + wantID: [4]byte{0xAA, 0xBB, 0xCC, 0xDD}, + wantCount: 5, + wantName: []byte("A"), + wantDesc: []byte("B"), + wantArtList: []byte{0x11, 0x22, 0x33, 0x44, 0x55}, + finalBytes: 17, + }, + { + name: "single byte chunks", + chunks: [][]byte{ + {0x01}, {0x02}, {0x03}, {0x04}, // ID + {0x00}, {0x00}, {0x00}, {0x00}, // Count: 0 + {0x02}, // Name length: 2 + {0x41}, {0x42}, // Name: "AB" + {0x00}, // Description length: 0 + }, + wantID: [4]byte{0x01, 0x02, 0x03, 0x04}, + wantCount: 0, + wantName: []byte("AB"), + wantDesc: []byte{}, + wantArtList: []byte{}, + finalBytes: 12, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nald := &NewsArtListData{} + totalBytes := 0 + for _, chunk := range tt.chunks { + n, err := nald.Write(chunk) + assert.NoError(t, err) + assert.Equal(t, len(chunk), n) + totalBytes += n + } + assert.Equal(t, tt.finalBytes, totalBytes) + assert.Equal(t, tt.wantID, nald.ID) + assert.Equal(t, tt.wantCount, nald.Count) + assert.Equal(t, tt.wantName, nald.Name) + assert.Equal(t, tt.wantDesc, nald.Description) + assert.Equal(t, tt.wantArtList, nald.NewsArtList) + }) + } +} + +func TestNewsArtListData_WriteRead_RoundTrip(t *testing.T) { + tests := []struct { + name string + nald NewsArtListData + wantErr bool + }{ + { + name: "basic round trip", + nald: NewsArtListData{ + ID: [4]byte{0x00, 0x01, 0x02, 0x03}, + Name: []byte("Test Name"), + Description: []byte("Test Description"), + NewsArtList: []byte{}, + Count: 0, + }, + wantErr: false, + }, + { + name: "with article list", + nald: NewsArtListData{ + ID: [4]byte{0xFF, 0xEE, 0xDD, 0xCC}, + Name: []byte("Articles"), + Description: []byte("Article Description"), + NewsArtList: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, + Count: 5, + }, + wantErr: false, + }, + { + name: "empty fields", + nald: NewsArtListData{ + ID: [4]byte{0x00, 0x00, 0x00, 0x00}, + Name: []byte{}, + Description: []byte{}, + NewsArtList: []byte{}, + Count: 0, + }, + wantErr: false, + }, + { + name: "max length name and description", + nald: NewsArtListData{ + ID: [4]byte{0x12, 0x34, 0x56, 0x78}, + Name: []byte("This is a very long name for testing purposes with lots of characters"), + Description: []byte("This is an equally long description to ensure we handle variable length fields properly"), + NewsArtList: []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, + Count: 100, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Read the struct into bytes + data, err := io.ReadAll(&tt.nald) + if (err != nil) != tt.wantErr { + t.Errorf("ReadAll() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Write the bytes back into a new struct + result := &NewsArtListData{} + n, err := result.Write(data) + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify the round trip + assert.Equal(t, len(data), n) + assert.Equal(t, tt.nald.ID, result.ID) + assert.Equal(t, tt.nald.Count, result.Count) + assert.Equal(t, tt.nald.Name, result.Name) + assert.Equal(t, tt.nald.Description, result.Description) + assert.Equal(t, tt.nald.NewsArtList, result.NewsArtList) + }) + } +} + func TestNewsArtList_Read(t *testing.T) { tests := []struct { name string @@ -368,3 +668,217 @@ func TestNewsPathScanner(t *testing.T) { }) } } + +func TestNewsCategoryListData15_Write(t *testing.T) { + tests := []struct { + name string + input []byte + wantType [2]byte + wantName string + wantGUID [16]byte + wantN int + wantErr bool + }{ + { + name: "bundle type with name", + input: []byte{ + 0x00, 0x02, // Type: Bundle + 0x00, 0x01, // Count: 1 + 0x03, // Name length: 3 + 0x66, 0x6f, 0x6f, // Name: "foo" + }, + wantType: [2]byte{0x00, 0x02}, + wantName: "foo", + wantN: 8, // 2 (type) + 2 (count) + 1 (name len) + 3 (name) + wantErr: false, + }, + { + name: "category type with GUID, AddSN, DeleteSN", + input: []byte{ + 0x00, 0x03, // Type: Category + 0x00, 0x01, // Count: 1 + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // GUID part 1 + 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, // GUID part 2 + 0x01, 0x02, 0x03, 0x04, // AddSN + 0x05, 0x06, 0x07, 0x08, // DeleteSN + 0x03, // Name length: 3 + 0x62, 0x61, 0x72, // Name: "bar" + }, + wantType: [2]byte{0x00, 0x03}, + wantName: "bar", + wantGUID: [16]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00}, + wantN: 32, // 2 (type) + 2 (count) + 16 (GUID) + 4 (AddSN) + 4 (DeleteSN) + 1 (name len) + 3 (name) + wantErr: false, + }, + { + name: "empty name", + input: []byte{ + 0x00, 0x02, // Type: Bundle + 0x00, 0x00, // Count: 0 + 0x00, // Name length: 0 + }, + wantType: [2]byte{0x00, 0x02}, + wantName: "", + wantN: 5, + wantErr: false, + }, + { + name: "long name", + input: []byte{ + 0x00, 0x02, // Type: Bundle + 0x00, 0x05, // Count: 5 + 0x0a, // Name length: 10 + 0x4c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, // Name: "LongerName" + }, + wantType: [2]byte{0x00, 0x02}, + wantName: "LongerName", + wantN: 15, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newscat := &NewsCategoryListData15{} + gotN, err := newscat.Write(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.wantN, gotN) + assert.Equal(t, tt.wantType, newscat.Type) + assert.Equal(t, tt.wantName, newscat.Name) + if tt.wantType == NewsCategory { + assert.Equal(t, tt.wantGUID, newscat.GUID) + } + assert.NotNil(t, newscat.Articles) + assert.NotNil(t, newscat.SubCats) + }) + } +} + +func TestNewsCategoryListData15_WriteRead_RoundTrip(t *testing.T) { + tests := []struct { + name string + newscat NewsCategoryListData15 + wantErr bool + }{ + { + name: "bundle round trip", + newscat: NewsCategoryListData15{ + Type: NewsBundle, + Name: "Test Bundle", + Articles: map[uint32]*NewsArtData{ + 1: {Title: "Article 1"}, + }, + SubCats: make(map[string]NewsCategoryListData15), + }, + wantErr: false, + }, + { + name: "category round trip", + newscat: NewsCategoryListData15{ + Type: NewsCategory, + Name: "Test Category", + GUID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}, + AddSN: [4]byte{0x00, 0x00, 0x00, 0x01}, + DeleteSN: [4]byte{0x00, 0x00, 0x00, 0x02}, + Articles: make(map[uint32]*NewsArtData), + SubCats: map[string]NewsCategoryListData15{ + "subcat1": { + Type: NewsBundle, + Name: "Subcategory", + }, + }, + }, + wantErr: false, + }, + { + name: "empty name round trip", + newscat: NewsCategoryListData15{ + Type: NewsBundle, + Name: "", + Articles: make(map[uint32]*NewsArtData), + SubCats: make(map[string]NewsCategoryListData15), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Read the struct into bytes + data, err := io.ReadAll(&tt.newscat) + if (err != nil) != tt.wantErr { + t.Errorf("ReadAll() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Write the bytes back into a new struct + result := &NewsCategoryListData15{} + n, err := result.Write(data) + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Verify the round trip + assert.Equal(t, len(data), n) + assert.Equal(t, tt.newscat.Type, result.Type) + assert.Equal(t, tt.newscat.Name, result.Name) + if tt.newscat.Type == NewsCategory { + assert.Equal(t, tt.newscat.GUID, result.GUID) + assert.Equal(t, tt.newscat.AddSN, result.AddSN) + assert.Equal(t, tt.newscat.DeleteSN, result.DeleteSN) + } + }) + } +} + +func TestNewsCategoryListData15_Write_PartialData(t *testing.T) { + tests := []struct { + name string + chunks [][]byte + wantType [2]byte + wantName string + finalBytes int + }{ + { + name: "split across type boundary", + chunks: [][]byte{ + {0x00}, // First byte of type + {0x02}, // Second byte of type + {0x00, 0x01}, // Count + {0x03}, // Name length + {0x66, 0x6f, 0x6f}, // Name: "foo" + }, + wantType: [2]byte{0x00, 0x02}, + wantName: "foo", + finalBytes: 8, // 1 + 1 + 2 + 1 + 3 + }, + { + name: "split across name", + chunks: [][]byte{ + {0x00, 0x02, 0x00, 0x01, 0x05}, // Type, count, name length: 5 + {0x68, 0x65}, // "he" + {0x6c, 0x6c, 0x6f}, // "llo" + }, + wantType: [2]byte{0x00, 0x02}, + wantName: "hello", + finalBytes: 10, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newscat := &NewsCategoryListData15{} + totalBytes := 0 + for _, chunk := range tt.chunks { + n, err := newscat.Write(chunk) + assert.NoError(t, err) + assert.Equal(t, len(chunk), n) + totalBytes += n + } + assert.Equal(t, tt.finalBytes, totalBytes) + assert.Equal(t, tt.wantType, newscat.Type) + assert.Equal(t, tt.wantName, newscat.Name) + }) + } +} diff --git a/hotline/transaction.go b/hotline/transaction.go index a03030d..068a083 100644 --- a/hotline/transaction.go +++ b/hotline/transaction.go @@ -97,6 +97,7 @@ var tranTypeNames = map[TranType]string{ TranUserAccess: "User access", TranNotifyDeleteUser: "User left", TranAgreed: "Accept agreement", + TranLogin: "Log In", TranChatSend: "Send chat", TranDelNewsArt: "Delete news article", TranDelNewsItem: "Delete news item", |