aboutsummaryrefslogtreecommitdiff
path: root/hotline
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-05 15:19:15 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2025-07-05 15:19:15 -0700
commitb6bca2b7fe943ca4fd8f62a44bcfcc0c9f5a6de1 (patch)
treea689744f07dbd61e6e33e1b081dc335a43c1fb57 /hotline
parent1f3e5ec139022c20be8ca61b467b96ae7e78a36a (diff)
Add comprehensive test coverage for news.go functions
- Add table-driven tests for GetNewsArtListData, DataSize, NewsArtListData.Read, NewsArtList.Read, and newsPathScanner - Improve test coverage from 0% to 87.5%-100% for these functions - Add test for Field.DecodeNewsPath function - Fix NewsArtList.Read to return nil instead of io.EOF for proper io.Reader behavior - Add constants for NewsFlavorCount and improve code documentation
Diffstat (limited to 'hotline')
-rw-r--r--hotline/field_test.go67
-rw-r--r--hotline/news.go12
-rw-r--r--hotline/news_test.go271
3 files changed, 344 insertions, 6 deletions
diff --git a/hotline/field_test.go b/hotline/field_test.go
index 676098c..3440b0d 100644
--- a/hotline/field_test.go
+++ b/hotline/field_test.go
@@ -229,3 +229,70 @@ func TestField_DecodeInt(t *testing.T) {
})
}
}
+
+func TestField_DecodeNewsPath(t *testing.T) {
+ type fields struct {
+ Data []byte
+ }
+ tests := []struct {
+ name string
+ fields fields
+ want []string
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "empty field data",
+ fields: fields{Data: []byte{}},
+ want: []string{},
+ wantErr: assert.NoError,
+ },
+ {
+ name: "single path",
+ fields: fields{Data: []byte{
+ 0x00, 0x01, // path count = 1
+ 0x00, 0x00, 0x05, // 2 bytes unused + 1 byte length (5)
+ 0x48, 0x65, 0x6c, 0x6c, 0x6f, // "Hello"
+ }},
+ want: []string{"Hello"},
+ wantErr: assert.NoError,
+ },
+ {
+ name: "multiple paths",
+ fields: fields{Data: []byte{
+ 0x00, 0x02, // path count = 2
+ 0x00, 0x00, 0x05, // 2 bytes unused + 1 byte length (5)
+ 0x48, 0x65, 0x6c, 0x6c, 0x6f, // "Hello"
+ 0x00, 0x00, 0x05, // 2 bytes unused + 1 byte length (5)
+ 0x57, 0x6f, 0x72, 0x6c, 0x64, // "World"
+ }},
+ want: []string{"Hello", "World"},
+ wantErr: assert.NoError,
+ },
+ {
+ name: "example from comments - nested categories",
+ fields: fields{Data: []byte{
+ 0x00, 0x03, // path count = 3
+ 0x00, 0x00, 0x10, // 2 bytes unused + 1 byte length (16)
+ 0x54, 0x6f, 0x70, 0x20, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x20, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, // "Top Level Bundle"
+ 0x00, 0x00, 0x13, // 2 bytes unused + 1 byte length (19)
+ 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x20, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, // "Second Level Bundle"
+ 0x00, 0x00, 0x0f, // 2 bytes unused + 1 byte length (15)
+ 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x20, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, // "Nested Category"
+ }},
+ want: []string{"Top Level Bundle", "Second Level Bundle", "Nested Category"},
+ wantErr: assert.NoError,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ f := &Field{
+ Data: tt.fields.Data,
+ }
+ got, err := f.DecodeNewsPath()
+ if !tt.wantErr(t, err, "DecodeNewsPath()") {
+ return
+ }
+ assert.Equalf(t, tt.want, got, "DecodeNewsPath()")
+ })
+ }
+}
diff --git a/hotline/news.go b/hotline/news.go
index c61a76f..1ddeebd 100644
--- a/hotline/news.go
+++ b/hotline/news.go
@@ -47,7 +47,7 @@ func (newscat *NewsCategoryListData15) GetNewsArtListData() (NewsArtListData, er
for i, art := range newscat.Articles {
id := make([]byte, 4)
- binary.BigEndian.PutUint32(id, i)
+ binary.BigEndian.PutUint32(id, i) // The article's map key in the Articles map is its ID.
newsArts = append(newsArts, NewsArtList{
ID: [4]byte(id),
@@ -156,8 +156,8 @@ type NewsArtList struct {
}
var (
- NewsFlavorLen = []byte{0x0a}
- NewsFlavor = []byte("text/plain")
+ NewsFlavor = []byte("text/plain") // NewsFlavor is always "text/plain"
+ NewsFlavorCount = []byte{0, 1} // NewsFlavorCount is always 1
)
func (nal *NewsArtList) Read(p []byte) (int, error) {
@@ -166,12 +166,12 @@ func (nal *NewsArtList) Read(p []byte) (int, error) {
nal.TimeStamp[:],
nal.ParentID[:],
nal.Flags[:],
- []byte{0, 1}, // Flavor Count TODO: make this not hardcoded
+ NewsFlavorCount,
[]byte{uint8(len(nal.Title))},
nal.Title,
[]byte{uint8(len(nal.Poster))},
nal.Poster,
- NewsFlavorLen,
+ []byte{uint8(len(NewsFlavor))},
NewsFlavor,
nal.ArticleSize[:],
)
@@ -183,7 +183,7 @@ func (nal *NewsArtList) Read(p []byte) (int, error) {
n := copy(p, out[nal.readOffset:])
nal.readOffset += n
- return n, io.EOF
+ return n, nil
}
type NewsFlavorList struct {
diff --git a/hotline/news_test.go b/hotline/news_test.go
index d1b043e..46df2e3 100644
--- a/hotline/news_test.go
+++ b/hotline/news_test.go
@@ -97,3 +97,274 @@ func TestNewsCategoryListData15_MarshalBinary(t *testing.T) {
})
}
}
+
+func TestNewsCategoryListData15_GetNewsArtListData(t *testing.T) {
+ tests := []struct {
+ name string
+ newscat NewsCategoryListData15
+ wantData NewsArtListData
+ wantErr bool
+ }{
+ {
+ name: "empty articles",
+ newscat: NewsCategoryListData15{
+ Articles: map[uint32]*NewsArtData{},
+ },
+ wantData: NewsArtListData{
+ Count: 0,
+ Name: []byte{},
+ Description: []byte{},
+ NewsArtList: []byte{},
+ },
+ wantErr: false,
+ },
+ {
+ name: "single article",
+ newscat: NewsCategoryListData15{
+ Articles: map[uint32]*NewsArtData{
+ 1: {
+ Title: "Test Title",
+ Poster: "Test Poster",
+ Date: [8]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
+ Data: "Test content",
+ },
+ },
+ },
+ wantData: NewsArtListData{
+ Count: 1,
+ Name: []byte{},
+ Description: []byte{},
+ },
+ wantErr: false,
+ },
+ {
+ name: "multiple articles",
+ newscat: NewsCategoryListData15{
+ Articles: map[uint32]*NewsArtData{
+ 2: {
+ Title: "Second Article",
+ Poster: "Author2",
+ Date: [8]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08},
+ Data: "Second content",
+ },
+ 1: {
+ Title: "First Article",
+ Poster: "Author1",
+ Date: [8]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
+ Data: "First content",
+ },
+ },
+ },
+ wantData: NewsArtListData{
+ Count: 2,
+ Name: []byte{},
+ Description: []byte{},
+ },
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotData, err := tt.newscat.GetNewsArtListData()
+ if (err != nil) != tt.wantErr {
+ t.Errorf("GetNewsArtListData() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ assert.Equal(t, tt.wantData.Count, gotData.Count)
+ assert.Equal(t, tt.wantData.Name, gotData.Name)
+ assert.Equal(t, tt.wantData.Description, gotData.Description)
+ if tt.wantData.Count > 0 {
+ assert.NotEmpty(t, gotData.NewsArtList)
+ }
+ })
+ }
+}
+
+func TestNewsArtData_DataSize(t *testing.T) {
+ tests := []struct {
+ name string
+ art NewsArtData
+ want [2]byte
+ }{
+ {
+ name: "empty data",
+ art: NewsArtData{Data: ""},
+ want: [2]byte{0x00, 0x00},
+ },
+ {
+ name: "short data",
+ art: NewsArtData{Data: "hello"},
+ want: [2]byte{0x00, 0x05},
+ },
+ {
+ name: "longer data",
+ art: NewsArtData{Data: "This is a longer test message with more content"},
+ want: [2]byte{0x00, 0x2F}, // 47 bytes
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := tt.art.DataSize()
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+func TestNewsArtListData_Read(t *testing.T) {
+ tests := []struct {
+ name string
+ nald NewsArtListData
+ bufferSize int
+ wantN int
+ wantErr bool
+ }{
+ {
+ name: "empty data",
+ nald: NewsArtListData{
+ ID: [4]byte{0x00, 0x01, 0x02, 0x03},
+ Name: []byte("test"),
+ Description: []byte("desc"),
+ NewsArtList: []byte{},
+ Count: 0,
+ },
+ bufferSize: 100,
+ wantN: 18, // 4 (ID) + 4 (count) + 1 (name len) + 4 (name) + 1 (desc len) + 4 (desc) + 0 (news art list)
+ wantErr: false,
+ },
+ {
+ name: "with article list",
+ nald: NewsArtListData{
+ ID: [4]byte{0x00, 0x01, 0x02, 0x03},
+ Name: []byte("test"),
+ Description: []byte("desc"),
+ NewsArtList: []byte{0x01, 0x02, 0x03},
+ Count: 1,
+ },
+ bufferSize: 100,
+ wantN: 21, // 4 (ID) + 4 (count) + 1 (name len) + 4 (name) + 1 (desc len) + 4 (desc) + 3 (news art list)
+ wantErr: false,
+ },
+ {
+ name: "small buffer",
+ nald: NewsArtListData{
+ ID: [4]byte{0x00, 0x01, 0x02, 0x03},
+ Name: []byte("test"),
+ Description: []byte("desc"),
+ Count: 0,
+ },
+ bufferSize: 5,
+ wantN: 5,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ p := make([]byte, tt.bufferSize)
+ gotN, err := tt.nald.Read(p)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ assert.Equal(t, tt.wantN, gotN)
+ })
+ }
+}
+
+func TestNewsArtList_Read(t *testing.T) {
+ tests := []struct {
+ name string
+ nal NewsArtList
+ bufferSize int
+ wantN int
+ wantErr bool
+ }{
+ {
+ name: "basic article",
+ nal: NewsArtList{
+ ID: [4]byte{0x00, 0x01, 0x02, 0x03},
+ TimeStamp: [8]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
+ ParentID: [4]byte{0x00, 0x00, 0x00, 0x00},
+ Flags: [4]byte{0x00, 0x00, 0x00, 0x00},
+ Title: []byte("Test Title"),
+ Poster: []byte("Test Poster"),
+ ArticleSize: [2]byte{0x00, 0x0A},
+ },
+ bufferSize: 100,
+ wantN: 58, // 4 (ID) + 8 (timestamp) + 4 (parent) + 4 (flags) + 2 (flavor count) + 1 (title len) + 10 (title) + 1 (poster len) + 11 (poster) + 1 (flavor len) + 10 (flavor) + 2 (article size)
+ wantErr: false,
+ },
+ {
+ name: "small buffer",
+ nal: NewsArtList{
+ ID: [4]byte{0x00, 0x01, 0x02, 0x03},
+ TimeStamp: [8]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
+ Title: []byte("Test"),
+ Poster: []byte("Author"),
+ },
+ bufferSize: 10,
+ wantN: 10,
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ p := make([]byte, tt.bufferSize)
+ gotN, err := tt.nal.Read(p)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("Read() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ assert.Equal(t, tt.wantN, gotN)
+ })
+ }
+}
+
+func TestNewsPathScanner(t *testing.T) {
+ tests := []struct {
+ name string
+ data []byte
+ wantAdvance int
+ wantToken []byte
+ wantErr bool
+ }{
+ {
+ name: "insufficient data",
+ data: []byte{0x00, 0x01},
+ wantAdvance: 0,
+ wantToken: nil,
+ wantErr: false,
+ },
+ {
+ name: "valid token",
+ data: []byte{0x00, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74}, // length 4, "test"
+ wantAdvance: 7,
+ wantToken: []byte("test"),
+ wantErr: false,
+ },
+ {
+ name: "zero length token",
+ data: []byte{0x00, 0x01, 0x00},
+ wantAdvance: 3,
+ wantToken: []byte{},
+ wantErr: false,
+ },
+ {
+ name: "single character token",
+ data: []byte{0x00, 0x01, 0x01, 0x61}, // length 1, "a"
+ wantAdvance: 4,
+ wantToken: []byte("a"),
+ wantErr: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotAdvance, gotToken, err := newsPathScanner(tt.data, false)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("newsPathScanner() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ assert.Equal(t, tt.wantAdvance, gotAdvance)
+ assert.Equal(t, tt.wantToken, gotToken)
+ })
+ }
+}