mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
- Fix duplicate test declarations across coverage2, misc, mercenary, event files - Fix signserver TestHandlePacketDELETE timeout (multi-write pipe deadlock) - Fix entranceserver build error (invalid -1 for uint16 port) - Add handlers_reserve_test.go covering all 56 reserve handler stubs - Add handlers_coverage3_test.go with 115+ handler tests - Add handlers_register_test.go with 55 Raviente register/load tests - Add handlers_coverage_test.go, signserver, entranceserver, usercheck tests
1231 lines
30 KiB
Go
1231 lines
30 KiB
Go
package signserver
|
|
|
|
import (
|
|
"bytes"
|
|
"database/sql"
|
|
"io"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/config"
|
|
"erupe-ce/network"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/jmoiron/sqlx"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// mockConn implements net.Conn for testing
|
|
type mockConn struct {
|
|
readBuf *bytes.Buffer
|
|
writeBuf *bytes.Buffer
|
|
closed bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newMockConn() *mockConn {
|
|
return &mockConn{
|
|
readBuf: new(bytes.Buffer),
|
|
writeBuf: new(bytes.Buffer),
|
|
}
|
|
}
|
|
|
|
func (m *mockConn) Read(b []byte) (n int, err error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.closed {
|
|
return 0, io.EOF
|
|
}
|
|
return m.readBuf.Read(b)
|
|
}
|
|
|
|
func (m *mockConn) Write(b []byte) (n int, err error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.closed {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return m.writeBuf.Write(b)
|
|
}
|
|
|
|
func (m *mockConn) Close() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.closed = true
|
|
return nil
|
|
}
|
|
|
|
func (m *mockConn) LocalAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 53312}
|
|
}
|
|
|
|
func (m *mockConn) RemoteAddr() net.Addr {
|
|
return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
|
|
}
|
|
|
|
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
|
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
func TestSessionStruct(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
conn := newMockConn()
|
|
|
|
s := &Session{
|
|
logger: logger,
|
|
server: nil,
|
|
rawConn: conn,
|
|
cryptConn: network.NewCryptConn(conn),
|
|
}
|
|
|
|
if s.logger != logger {
|
|
t.Error("Session logger not set correctly")
|
|
}
|
|
if s.rawConn != conn {
|
|
t.Error("Session rawConn not set correctly")
|
|
}
|
|
if s.cryptConn == nil {
|
|
t.Error("Session cryptConn should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestSessionStructDefaults(t *testing.T) {
|
|
s := &Session{}
|
|
|
|
if s.logger != nil {
|
|
t.Error("Default Session logger should be nil")
|
|
}
|
|
if s.server != nil {
|
|
t.Error("Default Session server should be nil")
|
|
}
|
|
if s.rawConn != nil {
|
|
t.Error("Default Session rawConn should be nil")
|
|
}
|
|
if s.cryptConn != nil {
|
|
t.Error("Default Session cryptConn should be nil")
|
|
}
|
|
}
|
|
|
|
func TestSessionMutex(t *testing.T) {
|
|
s := &Session{}
|
|
|
|
// Test that we can lock and unlock
|
|
s.Lock()
|
|
s.Unlock()
|
|
|
|
// Test concurrent access
|
|
done := make(chan bool)
|
|
go func() {
|
|
s.Lock()
|
|
time.Sleep(10 * time.Millisecond)
|
|
s.Unlock()
|
|
done <- true
|
|
}()
|
|
|
|
// Small delay to ensure goroutine starts
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
// This should block until the goroutine releases the lock
|
|
s.Lock()
|
|
s.Unlock()
|
|
|
|
<-done
|
|
}
|
|
|
|
func TestHandlePacketUnknownRequest(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
conn := newMockConn()
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: conn,
|
|
cryptConn: network.NewCryptConn(conn),
|
|
}
|
|
|
|
// Create a packet with unknown request type
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("UNKNOWN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("data"))
|
|
|
|
err := session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandlePacketEmptyRequest(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
conn := newMockConn()
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: conn,
|
|
cryptConn: network.NewCryptConn(conn),
|
|
}
|
|
|
|
// Create a packet with empty request type (just null terminator)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte(""))
|
|
|
|
err := session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error for empty request: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandlePacketWithDevModeLogging(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: true,
|
|
DevModeOptions: config.DevModeOptions{
|
|
LogInboundMessages: true,
|
|
},
|
|
}
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
conn := newMockConn()
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: conn,
|
|
cryptConn: network.NewCryptConn(conn),
|
|
}
|
|
|
|
// Create a packet with unknown request type
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("TEST:100"))
|
|
|
|
err := session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() with dev mode returned error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHandlePacketRequestTypes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
reqType string
|
|
}{
|
|
{"unknown", "UNKNOWN:100"},
|
|
{"invalid", "INVALID"},
|
|
{"empty_version", "TEST:"},
|
|
{"no_version", "NOVERSION"},
|
|
{"special_chars", "TEST@#$:100"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{DevMode: false}
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
conn := newMockConn()
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: conn,
|
|
cryptConn: network.NewCryptConn(conn),
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte(tt.reqType))
|
|
|
|
err := session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket(%s) returned error: %v", tt.reqType, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMockConnImplementsNetConn(t *testing.T) {
|
|
var _ net.Conn = (*mockConn)(nil)
|
|
}
|
|
|
|
func TestMockConnReadWrite(t *testing.T) {
|
|
conn := newMockConn()
|
|
|
|
// Write some data to the read buffer (simulating incoming data)
|
|
testData := []byte("hello")
|
|
conn.readBuf.Write(testData)
|
|
|
|
// Read it back
|
|
buf := make([]byte, len(testData))
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Errorf("Read() error: %v", err)
|
|
}
|
|
if n != len(testData) {
|
|
t.Errorf("Read() n = %d, want %d", n, len(testData))
|
|
}
|
|
if !bytes.Equal(buf, testData) {
|
|
t.Errorf("Read() data = %v, want %v", buf, testData)
|
|
}
|
|
|
|
// Write data
|
|
outData := []byte("world")
|
|
n, err = conn.Write(outData)
|
|
if err != nil {
|
|
t.Errorf("Write() error: %v", err)
|
|
}
|
|
if n != len(outData) {
|
|
t.Errorf("Write() n = %d, want %d", n, len(outData))
|
|
}
|
|
if !bytes.Equal(conn.writeBuf.Bytes(), outData) {
|
|
t.Errorf("Write() buffer = %v, want %v", conn.writeBuf.Bytes(), outData)
|
|
}
|
|
}
|
|
|
|
func TestMockConnClose(t *testing.T) {
|
|
conn := newMockConn()
|
|
|
|
err := conn.Close()
|
|
if err != nil {
|
|
t.Errorf("Close() error: %v", err)
|
|
}
|
|
|
|
if !conn.closed {
|
|
t.Error("conn.closed should be true after Close()")
|
|
}
|
|
|
|
// Read after close should return EOF
|
|
buf := make([]byte, 10)
|
|
_, err = conn.Read(buf)
|
|
if err != io.EOF {
|
|
t.Errorf("Read() after close should return EOF, got: %v", err)
|
|
}
|
|
|
|
// Write after close should return error
|
|
_, err = conn.Write([]byte("test"))
|
|
if err != io.ErrClosedPipe {
|
|
t.Errorf("Write() after close should return ErrClosedPipe, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMockConnAddresses(t *testing.T) {
|
|
conn := newMockConn()
|
|
|
|
local := conn.LocalAddr()
|
|
if local == nil {
|
|
t.Error("LocalAddr() should not be nil")
|
|
}
|
|
if local.String() != "127.0.0.1:53312" {
|
|
t.Errorf("LocalAddr() = %s, want 127.0.0.1:53312", local.String())
|
|
}
|
|
|
|
remote := conn.RemoteAddr()
|
|
if remote == nil {
|
|
t.Error("RemoteAddr() should not be nil")
|
|
}
|
|
if remote.String() != "127.0.0.1:12345" {
|
|
t.Errorf("RemoteAddr() = %s, want 127.0.0.1:12345", remote.String())
|
|
}
|
|
}
|
|
|
|
func TestMockConnDeadlines(t *testing.T) {
|
|
conn := newMockConn()
|
|
deadline := time.Now().Add(time.Second)
|
|
|
|
if err := conn.SetDeadline(deadline); err != nil {
|
|
t.Errorf("SetDeadline() error: %v", err)
|
|
}
|
|
if err := conn.SetReadDeadline(deadline); err != nil {
|
|
t.Errorf("SetReadDeadline() error: %v", err)
|
|
}
|
|
if err := conn.SetWriteDeadline(deadline); err != nil {
|
|
t.Errorf("SetWriteDeadline() error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionWithCryptConn(t *testing.T) {
|
|
conn := newMockConn()
|
|
cryptConn := network.NewCryptConn(conn)
|
|
|
|
if cryptConn == nil {
|
|
t.Fatal("NewCryptConn() returned nil")
|
|
}
|
|
|
|
session := &Session{
|
|
rawConn: conn,
|
|
cryptConn: cryptConn,
|
|
}
|
|
|
|
if session.cryptConn != cryptConn {
|
|
t.Error("Session cryptConn not set correctly")
|
|
}
|
|
}
|
|
|
|
// Note: Tests for DSGN:100, DLTSKEYSIGN:100, and DELETE:100 request types
|
|
// require a database connection. These are integration tests that should be
|
|
// run with a test database. The handlePacket method routes to these handlers
|
|
// which immediately access the database.
|
|
|
|
func TestSessionWorkWithDevModeLogging(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: true,
|
|
DevModeOptions: config.DevModeOptions{
|
|
LogInboundMessages: true,
|
|
},
|
|
}
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
// Use net.Pipe for bidirectional communication
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Close client side to cause read error
|
|
clientConn.Close()
|
|
|
|
// work() should exit gracefully on read error
|
|
session.work()
|
|
}
|
|
|
|
func TestSessionWorkWithEmptyRead(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Close client side immediately to cause read failure
|
|
clientConn.Close()
|
|
|
|
// work() should handle the read error gracefully
|
|
session.work()
|
|
}
|
|
|
|
// TestHandlePacketDSGNRequest tests the DSGN:100 path with a mocked database.
|
|
func TestHandlePacketDSGNRequest(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
// Use net.Pipe for bidirectional communication
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Create a DSGN:100 packet with username "testuser" and password "testpass"
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user not found, auto-create off
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
// Read the response in a goroutine
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
// Allow response to be sent
|
|
time.Sleep(50 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDLTSKEYSIGN tests the DLTSKEYSIGN:100 path (falls through to DSGN:100)
|
|
func TestHandlePacketDLTSKEYSIGN(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Create a DLTSKEYSIGN:100 packet
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DLTSKEYSIGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user not found
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDELETE tests the DELETE:100 path
|
|
func TestHandlePacketDELETE(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Create a DELETE:100 packet
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DELETE:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("login-token-abc"))
|
|
bf.WriteUint32(123) // characterID
|
|
bf.WriteUint32(456) // login_token_number
|
|
|
|
// Mock DB: Token verification
|
|
mock.ExpectQuery("SELECT count\\(\\*\\) FROM sign_sessions WHERE token = \\$1").
|
|
WithArgs("login-token-abc").
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
|
|
|
|
// Check if new character
|
|
mock.ExpectQuery("SELECT is_new_character FROM characters WHERE id = \\$1").
|
|
WithArgs(123).
|
|
WillReturnRows(sqlmock.NewRows([]string{"is_new_character"}).AddRow(false))
|
|
|
|
// Soft delete
|
|
mock.ExpectExec("UPDATE characters SET deleted = true WHERE id = \\$1").
|
|
WithArgs(123).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// Read all response data in a goroutine (SendPacket writes header + encrypted data)
|
|
done := make(chan []byte, 1)
|
|
go func() {
|
|
var all []byte
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, readErr := clientConn.Read(buf)
|
|
if n > 0 {
|
|
all = append(all, buf[:n]...)
|
|
}
|
|
if readErr != nil {
|
|
break
|
|
}
|
|
}
|
|
done <- all
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
// Close server side so the reader goroutine finishes
|
|
serverConn.Close()
|
|
|
|
select {
|
|
case <-done:
|
|
// Response received successfully
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("timed out waiting for response")
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNWithAutoCreate tests DSGN:100 with auto-create account enabled
|
|
func TestHandlePacketDSGNWithAutoCreate(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: true,
|
|
DevModeOptions: config.DevModeOptions{
|
|
AutoCreateAccount: true,
|
|
},
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("newuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("newpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user not found
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("newuser").
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
// Auto-create: insert user
|
|
mock.ExpectExec("INSERT INTO users \\(username, password, return_expires\\) VALUES \\(\\$1, \\$2, \\$3\\)").
|
|
WithArgs("newuser", sqlmock.AnyArg(), sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Auto-create: get user ID
|
|
mock.ExpectQuery("SELECT id FROM users WHERE username = \\$1").
|
|
WithArgs("newuser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
|
|
|
// Auto-create: insert character
|
|
mock.ExpectExec("INSERT INTO characters").
|
|
WithArgs(1, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Now get new user ID for makeSignInResp
|
|
mock.ExpectQuery("SELECT id FROM users WHERE username = \\$1").
|
|
WithArgs("newuser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
|
|
|
// makeSignInResp calls getReturnExpiry
|
|
mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(time.Now()))
|
|
|
|
// getReturnExpiry: get return_expires
|
|
mock.ExpectQuery("SELECT return_expires FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"return_expires"}).AddRow(time.Now().Add(time.Hour * 24 * 30)))
|
|
|
|
// getReturnExpiry: update last_login
|
|
mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2").
|
|
WithArgs(sqlmock.AnyArg(), 1).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// getCharactersForUser
|
|
mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id ASC").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "is_female", "is_new_character", "name", "unk_desc_string", "hrp", "gr", "weapon_type", "last_login"}))
|
|
|
|
// registerToken
|
|
mock.ExpectExec("INSERT INTO sign_sessions \\(user_id, token\\) VALUES \\(\\$1, \\$2\\)").
|
|
WithArgs(1, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// getLastCID
|
|
mock.ExpectQuery("SELECT last_character FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_character"}).AddRow(0))
|
|
|
|
// getUserRights
|
|
mock.ExpectQuery("SELECT rights FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"rights"}).AddRow(2))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNWithValidPassword tests DSGN:100 with correct password
|
|
func TestHandlePacketDSGNWithValidPassword(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Generate a bcrypt hash for "testpass"
|
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("testpass"), bcrypt.MinCost)
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("existinguser"))
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user found with correct password
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("existinguser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "password"}).AddRow(1, string(hashedPassword)))
|
|
|
|
// makeSignInResp calls getReturnExpiry
|
|
mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(time.Now()))
|
|
|
|
// getReturnExpiry: get return_expires
|
|
mock.ExpectQuery("SELECT return_expires FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"return_expires"}).AddRow(time.Now().Add(time.Hour * 24 * 30)))
|
|
|
|
// getReturnExpiry: update last_login
|
|
mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2").
|
|
WithArgs(sqlmock.AnyArg(), 1).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// getCharactersForUser
|
|
mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id ASC").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "is_female", "is_new_character", "name", "unk_desc_string", "hrp", "gr", "weapon_type", "last_login"}))
|
|
|
|
// registerToken
|
|
mock.ExpectExec("INSERT INTO sign_sessions \\(user_id, token\\) VALUES \\(\\$1, \\$2\\)").
|
|
WithArgs(1, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// getLastCID
|
|
mock.ExpectQuery("SELECT last_character FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_character"}).AddRow(0))
|
|
|
|
// getUserRights
|
|
mock.ExpectQuery("SELECT rights FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"rights"}).AddRow(2))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNWrongPassword tests DSGN:100 with wrong password
|
|
func TestHandlePacketDSGNWrongPassword(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Generate a bcrypt hash for "correctpass"
|
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("correctpass"), bcrypt.MinCost)
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("wrongpass")) // Wrong password
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user found but password will not match
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "password"}).AddRow(1, string(hashedPassword)))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNWithDBError tests DSGN:100 with a database error
|
|
func TestHandlePacketDSGNWithDBError(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: generic error
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnError(sql.ErrConnDone)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNNewCharaRequest tests DSGN:100 with the '+' suffix for new character
|
|
func TestHandlePacketDSGNNewCharaRequest(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: false,
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
// Generate a bcrypt hash for "testpass"
|
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("testpass"), bcrypt.MinCost)
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser+")) // '+' suffix means new character request
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user found
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "password"}).AddRow(1, string(hashedPassword)))
|
|
|
|
// newUserChara: get user ID
|
|
mock.ExpectQuery("SELECT id FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1))
|
|
|
|
// newUserChara: check existing new chars
|
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM characters WHERE user_id = \\$1 AND is_new_character = true").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
|
|
|
// newUserChara: insert character
|
|
mock.ExpectExec("INSERT INTO characters").
|
|
WithArgs(1, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// makeSignInResp calls
|
|
mock.ExpectQuery("SELECT COALESCE\\(last_login, now\\(\\)\\) FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_login"}).AddRow(time.Now()))
|
|
|
|
mock.ExpectQuery("SELECT return_expires FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"return_expires"}).AddRow(time.Now().Add(time.Hour * 24 * 30)))
|
|
|
|
mock.ExpectExec("UPDATE users SET last_login=\\$1 WHERE id=\\$2").
|
|
WithArgs(sqlmock.AnyArg(), 1).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
mock.ExpectQuery("SELECT id, is_female, is_new_character, name, unk_desc_string, hrp, gr, weapon_type, last_login FROM characters WHERE user_id = \\$1 AND deleted = false ORDER BY id ASC").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "is_female", "is_new_character", "name", "unk_desc_string", "hrp", "gr", "weapon_type", "last_login"}))
|
|
|
|
mock.ExpectExec("INSERT INTO sign_sessions \\(user_id, token\\) VALUES \\(\\$1, \\$2\\)").
|
|
WithArgs(1, sqlmock.AnyArg()).
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
mock.ExpectQuery("SELECT last_character FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"last_character"}).AddRow(0))
|
|
|
|
mock.ExpectQuery("SELECT rights FROM users WHERE id=\\$1").
|
|
WithArgs(1).
|
|
WillReturnRows(sqlmock.NewRows([]string{"rights"}).AddRow(2))
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestHandlePacketDSGNWithDevModeOutboundLogging tests dev mode outbound logging
|
|
func TestHandlePacketDSGNWithDevModeOutboundLogging(t *testing.T) {
|
|
logger := zap.NewNop()
|
|
erupeConfig := &config.Config{
|
|
DevMode: true,
|
|
DevModeOptions: config.DevModeOptions{
|
|
LogOutboundMessages: true,
|
|
},
|
|
}
|
|
|
|
db, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("failed to create sqlmock: %v", err)
|
|
}
|
|
sqlxDB := sqlx.NewDb(db, "sqlmock")
|
|
|
|
server := &Server{
|
|
logger: logger,
|
|
erupeConfig: erupeConfig,
|
|
db: sqlxDB,
|
|
}
|
|
|
|
clientConn, serverConn := net.Pipe()
|
|
defer clientConn.Close()
|
|
defer serverConn.Close()
|
|
|
|
session := &Session{
|
|
logger: logger,
|
|
server: server,
|
|
rawConn: serverConn,
|
|
cryptConn: network.NewCryptConn(serverConn),
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteNullTerminatedBytes([]byte("DSGN:100"))
|
|
bf.WriteNullTerminatedBytes([]byte("testuser"))
|
|
bf.WriteNullTerminatedBytes([]byte("testpass"))
|
|
bf.WriteNullTerminatedBytes([]byte("unk"))
|
|
|
|
// Mock DB: user not found, dev mode but no auto create
|
|
mock.ExpectQuery("SELECT id, password FROM users WHERE username = \\$1").
|
|
WithArgs("testuser").
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
_, err := clientConn.Read(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = session.handlePacket(bf.Data())
|
|
if err != nil {
|
|
t.Errorf("handlePacket() returned error: %v", err)
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
clientConn.Close()
|
|
<-done
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("unfulfilled expectations: %v", err)
|
|
}
|
|
}
|