test: improve test coverage for mhfpacket, channelserver, and server packages

Add comprehensive tests for:
- Pure time functions in channelserver (sys_time_test.go)
- Stage-related packet parsing (msg_sys_stage_test.go)
- Acquire packet family parsing (msg_mhf_acquire_test.go)
- Extended mhfpacket tests for login, logout, and stage packets
- Entrance server makeHeader structure and checksum tests
- SignV2 server request/response JSON structure tests
This commit is contained in:
Houmgaor
2026-02-01 23:28:19 +01:00
parent f83761c0b1
commit db3e0bccc7
6 changed files with 1715 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
package channelserver
import (
"testing"
"time"
)
func TestTimeAdjusted(t *testing.T) {
result := TimeAdjusted()
// Should return a time in UTC+9 timezone
_, offset := result.Zone()
expectedOffset := 9 * 60 * 60 // 9 hours in seconds
if offset != expectedOffset {
t.Errorf("TimeAdjusted() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
// The time should be close to current time (within a few seconds)
now := time.Now()
diff := result.Sub(now.In(time.FixedZone("UTC+9", 9*60*60)))
if diff < -time.Second || diff > time.Second {
t.Errorf("TimeAdjusted() time differs from expected by %v", diff)
}
}
func TestTimeMidnight(t *testing.T) {
midnight := TimeMidnight()
// Should be at midnight (hour=0, minute=0, second=0, nanosecond=0)
if midnight.Hour() != 0 {
t.Errorf("TimeMidnight() hour = %d, want 0", midnight.Hour())
}
if midnight.Minute() != 0 {
t.Errorf("TimeMidnight() minute = %d, want 0", midnight.Minute())
}
if midnight.Second() != 0 {
t.Errorf("TimeMidnight() second = %d, want 0", midnight.Second())
}
if midnight.Nanosecond() != 0 {
t.Errorf("TimeMidnight() nanosecond = %d, want 0", midnight.Nanosecond())
}
// Should be in UTC+9 timezone
_, offset := midnight.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("TimeMidnight() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
}
func TestTimeWeekStart(t *testing.T) {
weekStart := TimeWeekStart()
// Should be on Monday (weekday = 1)
if weekStart.Weekday() != time.Monday {
t.Errorf("TimeWeekStart() weekday = %v, want Monday", weekStart.Weekday())
}
// Should be at midnight
if weekStart.Hour() != 0 || weekStart.Minute() != 0 || weekStart.Second() != 0 {
t.Errorf("TimeWeekStart() should be at midnight, got %02d:%02d:%02d",
weekStart.Hour(), weekStart.Minute(), weekStart.Second())
}
// Should be in UTC+9 timezone
_, offset := weekStart.Zone()
expectedOffset := 9 * 60 * 60
if offset != expectedOffset {
t.Errorf("TimeWeekStart() zone offset = %d, want %d (UTC+9)", offset, expectedOffset)
}
// Week start should be before or equal to current midnight
midnight := TimeMidnight()
if weekStart.After(midnight) {
t.Errorf("TimeWeekStart() %v should be <= current midnight %v", weekStart, midnight)
}
}
func TestTimeWeekNext(t *testing.T) {
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// TimeWeekNext should be exactly 7 days after TimeWeekStart
expectedNext := weekStart.Add(time.Hour * 24 * 7)
if !weekNext.Equal(expectedNext) {
t.Errorf("TimeWeekNext() = %v, want %v (7 days after WeekStart)", weekNext, expectedNext)
}
// Should also be on Monday
if weekNext.Weekday() != time.Monday {
t.Errorf("TimeWeekNext() weekday = %v, want Monday", weekNext.Weekday())
}
// Should be at midnight
if weekNext.Hour() != 0 || weekNext.Minute() != 0 || weekNext.Second() != 0 {
t.Errorf("TimeWeekNext() should be at midnight, got %02d:%02d:%02d",
weekNext.Hour(), weekNext.Minute(), weekNext.Second())
}
// Should be in the future relative to week start
if !weekNext.After(weekStart) {
t.Errorf("TimeWeekNext() %v should be after TimeWeekStart() %v", weekNext, weekStart)
}
}
func TestTimeWeekStartSundayEdge(t *testing.T) {
// When today is Sunday, the calculation should go back to last Monday
// This is tested indirectly by verifying the weekday is always Monday
weekStart := TimeWeekStart()
// Regardless of what day it is now, week start should be Monday
if weekStart.Weekday() != time.Monday {
t.Errorf("TimeWeekStart() on any day should return Monday, got %v", weekStart.Weekday())
}
}
func TestTimeMidnightSameDay(t *testing.T) {
adjusted := TimeAdjusted()
midnight := TimeMidnight()
// Midnight should be on the same day (year, month, day)
if midnight.Year() != adjusted.Year() ||
midnight.Month() != adjusted.Month() ||
midnight.Day() != adjusted.Day() {
t.Errorf("TimeMidnight() date = %v, want same day as TimeAdjusted() %v",
midnight.Format("2006-01-02"), adjusted.Format("2006-01-02"))
}
}
func TestTimeWeekDuration(t *testing.T) {
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// Duration between week boundaries should be exactly 7 days
duration := weekNext.Sub(weekStart)
expectedDuration := time.Hour * 24 * 7
if duration != expectedDuration {
t.Errorf("Duration between WeekStart and WeekNext = %v, want %v", duration, expectedDuration)
}
}
func TestTimeZoneConsistency(t *testing.T) {
adjusted := TimeAdjusted()
midnight := TimeMidnight()
weekStart := TimeWeekStart()
weekNext := TimeWeekNext()
// All times should be in the same timezone (UTC+9)
times := []struct {
name string
time time.Time
}{
{"TimeAdjusted", adjusted},
{"TimeMidnight", midnight},
{"TimeWeekStart", weekStart},
{"TimeWeekNext", weekNext},
}
expectedOffset := 9 * 60 * 60
for _, tt := range times {
_, offset := tt.time.Zone()
if offset != expectedOffset {
t.Errorf("%s() zone offset = %d, want %d (UTC+9)", tt.name, offset, expectedOffset)
}
}
}

View File

@@ -203,3 +203,199 @@ func TestMakeHeaderDataIntegrity(t *testing.T) {
})
}
}
// TestMakeHeaderStructure verifies the internal structure of makeHeader output
func TestMakeHeaderStructure(t *testing.T) {
tests := []struct {
name string
data []byte
respType string
entryCount uint16
key byte
}{
{"SV2 response", []byte{0x01, 0x02, 0x03, 0x04}, "SV2", 5, 0x00},
{"SVR response", []byte{0xAA, 0xBB}, "SVR", 10, 0x10},
{"USR response", []byte{0x00}, "USR", 1, 0xFF},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := makeHeader(tt.data, tt.respType, tt.entryCount, tt.key)
// Result should not be empty
if len(result) == 0 {
t.Fatal("makeHeader returned empty result")
}
// First byte should be the key
if result[0] != tt.key {
t.Errorf("first byte = 0x%X, want 0x%X", result[0], tt.key)
}
// Decrypt the rest
encrypted := result[1:]
decrypted := DecryptBin8(encrypted, tt.key)
// First 3 bytes should be respType
if len(decrypted) < 3 {
t.Fatal("decrypted data too short for respType")
}
if string(decrypted[:3]) != tt.respType {
t.Errorf("respType = %s, want %s", string(decrypted[:3]), tt.respType)
}
// Next 2 bytes should be entry count (big endian)
if len(decrypted) < 5 {
t.Fatal("decrypted data too short for entry count")
}
gotCount := uint16(decrypted[3])<<8 | uint16(decrypted[4])
if gotCount != tt.entryCount {
t.Errorf("entryCount = %d, want %d", gotCount, tt.entryCount)
}
// Next 2 bytes should be data length (big endian)
if len(decrypted) < 7 {
t.Fatal("decrypted data too short for data length")
}
gotLen := uint16(decrypted[5])<<8 | uint16(decrypted[6])
if gotLen != uint16(len(tt.data)) {
t.Errorf("dataLen = %d, want %d", gotLen, len(tt.data))
}
})
}
}
// TestMakeHeaderChecksum verifies that checksum is correctly calculated
func TestMakeHeaderChecksum(t *testing.T) {
data := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
key := byte(0x00)
result := makeHeader(data, "SV2", 1, key)
// Decrypt
decrypted := DecryptBin8(result[1:], key)
// After respType(3) + entryCount(2) + dataLen(2) = 7 bytes
// Next 4 bytes should be checksum
if len(decrypted) < 11 {
t.Fatal("decrypted data too short for checksum")
}
expectedChecksum := CalcSum32(data)
gotChecksum := uint32(decrypted[7])<<24 | uint32(decrypted[8])<<16 | uint32(decrypted[9])<<8 | uint32(decrypted[10])
if gotChecksum != expectedChecksum {
t.Errorf("checksum = 0x%X, want 0x%X", gotChecksum, expectedChecksum)
}
}
// TestMakeHeaderDataPreservation verifies original data is preserved in output
func TestMakeHeaderDataPreservation(t *testing.T) {
originalData := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE}
key := byte(0x00)
result := makeHeader(originalData, "SV2", 1, key)
// Decrypt
decrypted := DecryptBin8(result[1:], key)
// Header: respType(3) + entryCount(2) + dataLen(2) + checksum(4) = 11 bytes
// Data starts at offset 11
if len(decrypted) < 11+len(originalData) {
t.Fatalf("decrypted data too short: got %d, want at least %d", len(decrypted), 11+len(originalData))
}
recoveredData := decrypted[11 : 11+len(originalData)]
if !bytes.Equal(recoveredData, originalData) {
t.Errorf("recovered data = %X, want %X", recoveredData, originalData)
}
}
// TestMakeHeaderEmptyDataNoChecksum verifies empty data doesn't include checksum
func TestMakeHeaderEmptyDataNoChecksum(t *testing.T) {
result := makeHeader([]byte{}, "SV2", 0, 0x00)
// Decrypt
decrypted := DecryptBin8(result[1:], 0x00)
// Header without data: respType(3) + entryCount(2) + dataLen(2) = 7 bytes
// No checksum for empty data
if len(decrypted) != 7 {
t.Errorf("decrypted length = %d, want 7 (no checksum for empty data)", len(decrypted))
}
// Verify data length is 0
gotLen := uint16(decrypted[5])<<8 | uint16(decrypted[6])
if gotLen != 0 {
t.Errorf("dataLen = %d, want 0", gotLen)
}
}
// TestMakeHeaderKeyVariation verifies different keys produce different output
func TestMakeHeaderKeyVariation(t *testing.T) {
data := []byte{0x01, 0x02, 0x03}
result1 := makeHeader(data, "SV2", 1, 0x00)
result2 := makeHeader(data, "SV2", 1, 0x55)
result3 := makeHeader(data, "SV2", 1, 0xAA)
// All results should have different first bytes (the key)
if result1[0] == result2[0] || result2[0] == result3[0] {
t.Error("different keys should produce different first bytes")
}
// Encrypted portions should also differ
if bytes.Equal(result1[1:], result2[1:]) {
t.Error("different keys should produce different encrypted data")
}
if bytes.Equal(result2[1:], result3[1:]) {
t.Error("different keys should produce different encrypted data")
}
}
// TestCalcSum32EdgeCases tests edge cases for the checksum function
func TestCalcSum32EdgeCases(t *testing.T) {
tests := []struct {
name string
data []byte
}{
{"single byte", []byte{0x00}},
{"all zeros", make([]byte, 10)},
{"all ones", bytes.Repeat([]byte{0xFF}, 10)},
{"alternating", []byte{0xAA, 0x55, 0xAA, 0x55}},
{"sequential", []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Should not panic
result := CalcSum32(tt.data)
// Result should be deterministic
result2 := CalcSum32(tt.data)
if result != result2 {
t.Errorf("CalcSum32 not deterministic: got %X and %X", result, result2)
}
})
}
}
// TestCalcSum32Uniqueness verifies different inputs produce different checksums
func TestCalcSum32Uniqueness(t *testing.T) {
inputs := [][]byte{
{0x01},
{0x02},
{0x01, 0x02},
{0x02, 0x01},
{0x01, 0x02, 0x03},
}
checksums := make(map[uint32]int)
for i, input := range inputs {
sum := CalcSum32(input)
if prevIdx, exists := checksums[sum]; exists {
t.Errorf("collision: input %d and %d both produce checksum 0x%X", prevIdx, i, sum)
}
checksums[sum] = i
}
}

View File

@@ -347,3 +347,374 @@ func TestServerConfig(t *testing.T) {
t.Error("Config.Logger should be nil when not set")
}
}
// Note: Tests that require database operations are skipped when no DB is available.
// The following tests validate the structure and JSON handling of endpoints.
// TestLoginRequestStructure tests that login request JSON structure is correct
func TestLoginRequestStructure(t *testing.T) {
// Test JSON marshaling/unmarshaling of request structure
reqData := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "testuser",
Password: "testpass",
}
data, err := json.Marshal(reqData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Username != reqData.Username {
t.Errorf("Username = %s, want %s", decoded.Username, reqData.Username)
}
if decoded.Password != reqData.Password {
t.Errorf("Password = %s, want %s", decoded.Password, reqData.Password)
}
}
// TestRegisterRequestStructure tests that register request JSON structure is correct
func TestRegisterRequestStructure(t *testing.T) {
reqData := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
Username: "newuser",
Password: "newpass",
}
data, err := json.Marshal(reqData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Username != reqData.Username {
t.Errorf("Username = %s, want %s", decoded.Username, reqData.Username)
}
if decoded.Password != reqData.Password {
t.Errorf("Password = %s, want %s", decoded.Password, reqData.Password)
}
}
// TestCreateCharacterRequestStructure tests that create character request JSON structure is correct
func TestCreateCharacterRequestStructure(t *testing.T) {
reqData := struct {
Token string `json:"token"`
}{
Token: "test-token-12345",
}
data, err := json.Marshal(reqData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Token string `json:"token"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Token != reqData.Token {
t.Errorf("Token = %s, want %s", decoded.Token, reqData.Token)
}
}
// TestDeleteCharacterRequestStructure tests that delete character request JSON structure is correct
func TestDeleteCharacterRequestStructure(t *testing.T) {
reqData := struct {
Token string `json:"token"`
CharID int `json:"id"`
}{
Token: "test-token",
CharID: 12345,
}
data, err := json.Marshal(reqData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Token string `json:"token"`
CharID int `json:"id"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Token != reqData.Token {
t.Errorf("Token = %s, want %s", decoded.Token, reqData.Token)
}
if decoded.CharID != reqData.CharID {
t.Errorf("CharID = %d, want %d", decoded.CharID, reqData.CharID)
}
}
// TestLoginResponseStructure tests the login response JSON structure
func TestLoginResponseStructure(t *testing.T) {
respData := struct {
Token string `json:"token"`
Characters []Character `json:"characters"`
}{
Token: "login-token-abc123",
Characters: []Character{
{ID: 1, Name: "Hunter1", IsFemale: false, Weapon: 3, HR: 100, GR: 10},
{ID: 2, Name: "Hunter2", IsFemale: true, Weapon: 7, HR: 200, GR: 20},
},
}
data, err := json.Marshal(respData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Token string `json:"token"`
Characters []Character `json:"characters"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Token != respData.Token {
t.Errorf("Token = %s, want %s", decoded.Token, respData.Token)
}
if len(decoded.Characters) != len(respData.Characters) {
t.Errorf("Characters count = %d, want %d", len(decoded.Characters), len(respData.Characters))
}
}
// TestRegisterResponseStructure tests the register response JSON structure
func TestRegisterResponseStructure(t *testing.T) {
respData := struct {
Token string `json:"token"`
}{
Token: "register-token-xyz789",
}
data, err := json.Marshal(respData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
Token string `json:"token"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Token != respData.Token {
t.Errorf("Token = %s, want %s", decoded.Token, respData.Token)
}
}
// TestCreateCharacterResponseStructure tests the create character response JSON structure
func TestCreateCharacterResponseStructure(t *testing.T) {
respData := struct {
CharID int `json:"id"`
}{
CharID: 42,
}
data, err := json.Marshal(respData)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded struct {
CharID int `json:"id"`
}
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.CharID != respData.CharID {
t.Errorf("CharID = %d, want %d", decoded.CharID, respData.CharID)
}
}
// TestLauncherContentType tests that Launcher sets correct content type
func TestLauncherContentType(t *testing.T) {
s := mockServer()
req := httptest.NewRequest("GET", "/launcher", nil)
w := httptest.NewRecorder()
s.Launcher(w, req)
// Note: The handler sets header after WriteHeader, so we check response body is JSON
resp := w.Result()
var data map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
t.Errorf("Launcher() response is not valid JSON: %v", err)
}
}
// TestLauncherMessageDates tests that launcher message dates are valid timestamps
func TestLauncherMessageDates(t *testing.T) {
s := mockServer()
req := httptest.NewRequest("GET", "/launcher", nil)
w := httptest.NewRecorder()
s.Launcher(w, req)
var data struct {
Important []LauncherMessage `json:"important"`
Normal []LauncherMessage `json:"normal"`
}
json.NewDecoder(w.Result().Body).Decode(&data)
// All dates should be positive unix timestamps
for _, msg := range data.Important {
if msg.Date <= 0 {
t.Errorf("Important message date should be positive, got %d", msg.Date)
}
}
for _, msg := range data.Normal {
if msg.Date <= 0 {
t.Errorf("Normal message date should be positive, got %d", msg.Date)
}
}
}
// TestLauncherMessageLinks tests that launcher message links are valid URLs
func TestLauncherMessageLinks(t *testing.T) {
s := mockServer()
req := httptest.NewRequest("GET", "/launcher", nil)
w := httptest.NewRecorder()
s.Launcher(w, req)
var data struct {
Important []LauncherMessage `json:"important"`
Normal []LauncherMessage `json:"normal"`
}
json.NewDecoder(w.Result().Body).Decode(&data)
// All links should start with http:// or https://
for _, msg := range data.Important {
if len(msg.Link) < 7 || (msg.Link[:7] != "http://" && msg.Link[:8] != "https://") {
t.Errorf("Important message link should be a URL, got %q", msg.Link)
}
}
for _, msg := range data.Normal {
if len(msg.Link) < 7 || (msg.Link[:7] != "http://" && msg.Link[:8] != "https://") {
t.Errorf("Normal message link should be a URL, got %q", msg.Link)
}
}
}
// TestCharacterStructJSONMarshal tests Character struct marshals correctly
func TestCharacterStructJSONMarshal(t *testing.T) {
char := Character{
ID: 42,
Name: "TestHunter",
IsFemale: true,
Weapon: 7,
HR: 999,
GR: 100,
LastLogin: 1609459200,
}
data, err := json.Marshal(char)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded Character
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.ID != char.ID {
t.Errorf("ID = %d, want %d", decoded.ID, char.ID)
}
if decoded.Name != char.Name {
t.Errorf("Name = %s, want %s", decoded.Name, char.Name)
}
if decoded.IsFemale != char.IsFemale {
t.Errorf("IsFemale = %v, want %v", decoded.IsFemale, char.IsFemale)
}
if decoded.Weapon != char.Weapon {
t.Errorf("Weapon = %d, want %d", decoded.Weapon, char.Weapon)
}
if decoded.HR != char.HR {
t.Errorf("HR = %d, want %d", decoded.HR, char.HR)
}
if decoded.GR != char.GR {
t.Errorf("GR = %d, want %d", decoded.GR, char.GR)
}
if decoded.LastLogin != char.LastLogin {
t.Errorf("LastLogin = %d, want %d", decoded.LastLogin, char.LastLogin)
}
}
// TestLauncherMessageJSONMarshal tests LauncherMessage struct marshals correctly
func TestLauncherMessageJSONMarshal(t *testing.T) {
msg := LauncherMessage{
Message: "Test Announcement",
Date: 1609459200,
Link: "https://example.com/news",
}
data, err := json.Marshal(msg)
if err != nil {
t.Fatalf("json.Marshal() error = %v", err)
}
var decoded LauncherMessage
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("json.Unmarshal() error = %v", err)
}
if decoded.Message != msg.Message {
t.Errorf("Message = %s, want %s", decoded.Message, msg.Message)
}
if decoded.Date != msg.Date {
t.Errorf("Date = %d, want %d", decoded.Date, msg.Date)
}
if decoded.Link != msg.Link {
t.Errorf("Link = %s, want %s", decoded.Link, msg.Link)
}
}
// TestEndpointHTTPMethods tests that endpoints respond to correct HTTP methods
func TestEndpointHTTPMethods(t *testing.T) {
s := mockServer()
// Launcher should respond to GET
t.Run("Launcher GET", func(t *testing.T) {
req := httptest.NewRequest("GET", "/launcher", nil)
w := httptest.NewRecorder()
s.Launcher(w, req)
if w.Result().StatusCode != http.StatusOK {
t.Errorf("Launcher() GET status = %d, want %d", w.Result().StatusCode, http.StatusOK)
}
})
// Note: Login, Register, CreateCharacter, DeleteCharacter require database
// and cannot be tested without mocking the database connection
}