Files
ice/renomination_test.go
2025-08-25 10:44:44 -04:00

907 lines
25 KiB
Go

// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// +build !js
package ice
import (
"net"
"testing"
"time"
"github.com/pion/ice/v4/internal/fakenet"
"github.com/pion/stun/v3"
"github.com/stretchr/testify/assert"
)
const (
testLocalUfrag = "localufrag"
testLocalPwd = "localpwd"
testRemoteUfrag = "remoteufrag"
testRemotePwd = "remotepwd"
)
// Mock packet conn that captures sent packets.
type mockPacketConnWithCapture struct {
sentPackets [][]byte
sentAddrs []net.Addr
}
func (m *mockPacketConnWithCapture) ReadFrom([]byte) (n int, addr net.Addr, err error) {
return 0, nil, nil
}
func (m *mockPacketConnWithCapture) WriteTo(b []byte, addr net.Addr) (n int, err error) {
// Capture the packet
packet := make([]byte, len(b))
copy(packet, b)
m.sentPackets = append(m.sentPackets, packet)
m.sentAddrs = append(m.sentAddrs, addr)
return len(b), nil
}
func (m *mockPacketConnWithCapture) Close() error { return nil }
func (m *mockPacketConnWithCapture) LocalAddr() net.Addr { return nil }
func (m *mockPacketConnWithCapture) SetDeadline(time.Time) error { return nil }
func (m *mockPacketConnWithCapture) SetReadDeadline(time.Time) error { return nil }
func (m *mockPacketConnWithCapture) SetWriteDeadline(time.Time) error { return nil }
// createRenominationTestAgent creates a test agent with renomination enabled and returns local/remote candidates.
func createRenominationTestAgent(t *testing.T, controlling bool) (*Agent, Candidate, Candidate) {
t.Helper()
agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 1 }))
assert.NoError(t, err)
agent.isControlling.Store(controlling)
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
return agent, local, remote
}
func TestNominationAttribute(t *testing.T) {
t.Run("AddTo and GetFrom", func(t *testing.T) {
m := &stun.Message{}
attr := NominationAttribute{Value: 0x123456}
err := attr.AddTo(m)
assert.NoError(t, err)
var parsed NominationAttribute
err = parsed.GetFrom(m)
assert.NoError(t, err)
assert.Equal(t, uint32(0x123456), parsed.Value)
})
t.Run("24-bit value boundary", func(t *testing.T) {
m := &stun.Message{}
maxValue := uint32((1 << 24) - 1) // 24-bit max value
attr := NominationAttribute{Value: maxValue}
err := attr.AddTo(m)
assert.NoError(t, err)
var parsed NominationAttribute
err = parsed.GetFrom(m)
assert.NoError(t, err)
assert.Equal(t, maxValue, parsed.Value)
})
t.Run("String representation", func(t *testing.T) {
attr := NominationAttribute{Value: 12345}
str := attr.String()
assert.Contains(t, str, "NOMINATION")
assert.Contains(t, str, "12345")
})
t.Run("Nomination helper function", func(t *testing.T) {
attr := Nomination(42)
assert.Equal(t, uint32(42), attr.Value)
})
}
func TestRenominationConfiguration(t *testing.T) {
nominationCounter := uint32(0)
agent, err := NewAgentWithOptions(WithRenomination(func() uint32 {
nominationCounter++
return nominationCounter
}))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
assert.True(t, agent.enableRenomination)
assert.NotNil(t, agent.nominationValueGenerator)
// Test nomination value generation
value1 := agent.nominationValueGenerator()
value2 := agent.nominationValueGenerator()
assert.Equal(t, uint32(1), value1)
assert.Equal(t, uint32(2), value2)
}
func TestControlledSelectorNominationAcceptance(t *testing.T) {
agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator()))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
selector := &controlledSelector{
agent: agent,
log: agent.log,
}
selector.Start()
// First nomination should be accepted
nomination1 := uint32(5)
assert.True(t, selector.shouldAcceptNomination(&nomination1))
// Higher nomination should be accepted
nomination2 := uint32(10)
assert.True(t, selector.shouldAcceptNomination(&nomination2))
// Lower nomination should be rejected
nomination3 := uint32(7)
assert.False(t, selector.shouldAcceptNomination(&nomination3))
// Equal nomination should be rejected
nomination4 := uint32(10)
assert.False(t, selector.shouldAcceptNomination(&nomination4))
// Nil nomination should be accepted (standard ICE)
assert.True(t, selector.shouldAcceptNomination(nil))
}
func TestControlledSelectorNominationDisabled(t *testing.T) {
config := &AgentConfig{
// Renomination disabled by default
}
agent, err := NewAgent(config)
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
selector := &controlledSelector{
agent: agent,
log: agent.log,
}
selector.Start()
// Standard ICE nomination (no value) should be accepted
assert.True(t, selector.shouldAcceptNomination(nil))
// When controlling side uses renomination (sends nomination values),
// controlled side should apply "last nomination wins" regardless of local config
nomination1 := uint32(5)
assert.True(t, selector.shouldAcceptNomination(&nomination1))
nomination2 := uint32(3) // Lower value should be rejected
assert.False(t, selector.shouldAcceptNomination(&nomination2))
nomination3 := uint32(8) // Higher value should be accepted
assert.True(t, selector.shouldAcceptNomination(&nomination3))
}
func TestAgentRenominateCandidate(t *testing.T) {
t.Run("controlling agent can renominate", func(t *testing.T) {
nominationCounter := uint32(0)
agent, err := NewAgentWithOptions(WithRenomination(func() uint32 {
nominationCounter++
return nominationCounter
}))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Set up credentials for STUN authentication
agent.localUfrag = testLocalUfrag
agent.localPwd = testLocalPwd
agent.remoteUfrag = testRemoteUfrag
agent.remotePwd = testRemotePwd
// Set agent as controlling
agent.isControlling.Store(true)
// Create test candidates with mock connection
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
// Mock the connection for the local candidate to avoid nil pointer
mockConn := &fakenet.MockPacketConn{}
local.conn = mockConn
// Add pair to agent
pair := agent.addPair(local, remote)
pair.state = CandidatePairStateSucceeded
// Test renomination
err = agent.RenominateCandidate(local, remote)
assert.NoError(t, err)
})
t.Run("non-controlling agent cannot renominate", func(t *testing.T) {
agent, local, remote := createRenominationTestAgent(t, false)
defer func() {
assert.NoError(t, agent.Close())
}()
err := agent.RenominateCandidate(local, remote)
assert.Error(t, err)
assert.Contains(t, err.Error(), "only controlling agent can renominate")
})
t.Run("renomination when disabled", func(t *testing.T) {
config := &AgentConfig{
// Renomination disabled by default
}
agent, err := NewAgent(config)
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
agent.isControlling.Store(true)
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
err = agent.RenominateCandidate(local, remote)
assert.Error(t, err)
assert.Contains(t, err.Error(), "renomination is not enabled")
})
t.Run("renomination with non-existent candidate pair", func(t *testing.T) {
agent, local, remote := createRenominationTestAgent(t, true)
defer func() {
assert.NoError(t, agent.Close())
}()
// Don't add pair to agent - should fail
err := agent.RenominateCandidate(local, remote)
assert.Error(t, err)
assert.Contains(t, err.Error(), "candidate pair not found")
})
}
func TestSendNominationRequest(t *testing.T) {
t.Run("STUN message contains nomination attribute", func(t *testing.T) {
nominationCounter := uint32(0)
agent, err := NewAgentWithOptions(WithRenomination(func() uint32 {
nominationCounter++
return nominationCounter
}))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Set up credentials for STUN authentication
agent.localUfrag = testLocalUfrag
agent.localPwd = testLocalPwd
agent.remoteUfrag = testRemoteUfrag
agent.remotePwd = testRemotePwd
agent.isControlling.Store(true)
// Create test candidates
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
// Mock connection to capture sent messages
mockConn := &mockPacketConnWithCapture{}
local.conn = mockConn
pair := agent.addPair(local, remote)
pair.state = CandidatePairStateSucceeded
// Test sendNominationRequest directly
nominationValue := uint32(123)
err = agent.sendNominationRequest(pair, nominationValue)
assert.NoError(t, err)
// Verify message was sent
assert.True(t, len(mockConn.sentPackets) > 0)
// Parse the sent STUN message
msg := &stun.Message{}
err = msg.UnmarshalBinary(mockConn.sentPackets[0])
assert.NoError(t, err)
// Verify it's a binding request
assert.True(t, msg.Type.Method == stun.MethodBinding)
assert.True(t, msg.Type.Class == stun.ClassRequest)
// Verify USE-CANDIDATE is present
assert.True(t, msg.Contains(stun.AttrUseCandidate))
// Verify nomination attribute is present
var nomination NominationAttribute
err = nomination.GetFrom(msg)
assert.NoError(t, err)
assert.Equal(t, nominationValue, nomination.Value)
})
t.Run("STUN message without nomination when disabled", func(t *testing.T) {
config := &AgentConfig{
// Renomination disabled by default
}
agent, err := NewAgent(config)
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Set up credentials
agent.localUfrag = testLocalUfrag
agent.localPwd = testLocalPwd
agent.remoteUfrag = testRemoteUfrag
agent.remotePwd = testRemotePwd
agent.isControlling.Store(true)
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
mockConn := &mockPacketConnWithCapture{}
local.conn = mockConn
pair := agent.addPair(local, remote)
// Send nomination with value 0 (should not include nomination attribute)
err = agent.sendNominationRequest(pair, 0)
assert.NoError(t, err)
// Parse the sent message
msg := &stun.Message{}
err = msg.UnmarshalBinary(mockConn.sentPackets[0])
assert.NoError(t, err)
// Verify USE-CANDIDATE is present
assert.True(t, msg.Contains(stun.AttrUseCandidate))
// Verify nomination attribute is NOT present
var nomination NominationAttribute
err = nomination.GetFrom(msg)
assert.Error(t, err) // Should fail since attribute is not present
})
}
func TestRenominationErrorCases(t *testing.T) {
t.Run("getNominationValue with nil generator", func(t *testing.T) {
// Try to create agent with nil generator - should fail
_, err := NewAgentWithOptions(WithRenomination(nil))
assert.ErrorIs(t, err, ErrInvalidNominationValueGenerator)
// Create agent without renomination for testing
agent, err := NewAgentWithOptions()
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Should return 0 when no generator is set
value := agent.getNominationValue()
assert.Equal(t, uint32(0), value)
})
t.Run("STUN message build with invalid attributes", func(t *testing.T) {
agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 1 }))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Set up minimal credentials but missing remote password
agent.localUfrag = "localufrag"
agent.localPwd = "localpwd"
agent.remoteUfrag = "remoteufrag"
// agent.remotePwd = "" // Missing remote password
agent.isControlling.Store(true)
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
mockConn := &mockPacketConnWithCapture{}
local.conn = mockConn
pair := agent.addPair(local, remote)
// This should succeed even with missing remote password
// as the STUN library will still build the message
err = agent.sendNominationRequest(pair, 1)
assert.NoError(t, err)
})
}
func TestNominationValueBoundaries(t *testing.T) {
t.Run("24-bit maximum value", func(t *testing.T) {
maxValue := uint32((1 << 24) - 1) // 0xFFFFFF
attr := NominationAttribute{Value: maxValue}
m := &stun.Message{}
err := attr.AddTo(m)
assert.NoError(t, err)
var parsed NominationAttribute
err = parsed.GetFrom(m)
assert.NoError(t, err)
assert.Equal(t, maxValue, parsed.Value)
})
t.Run("zero nomination value", func(t *testing.T) {
attr := NominationAttribute{Value: 0}
m := &stun.Message{}
err := attr.AddTo(m)
assert.NoError(t, err)
var parsed NominationAttribute
err = parsed.GetFrom(m)
assert.NoError(t, err)
assert.Equal(t, uint32(0), parsed.Value)
})
t.Run("nomination value overflow", func(t *testing.T) {
// Test value larger than 24-bit
overflowValue := uint32(1 << 25) // Larger than 24-bit max
attr := NominationAttribute{Value: overflowValue}
m := &stun.Message{}
err := attr.AddTo(m)
assert.NoError(t, err)
var parsed NominationAttribute
err = parsed.GetFrom(m)
assert.NoError(t, err)
// Should be truncated to 24-bit value
expectedValue := overflowValue & 0xFFFFFF
assert.Equal(t, expectedValue, parsed.Value)
})
t.Run("invalid attribute size", func(t *testing.T) {
m := &stun.Message{}
// Add a nomination attribute with invalid size (too short)
m.Add(DefaultNominationAttribute, []byte{0x01, 0x02}) // Only 2 bytes instead of 4
var nomination NominationAttribute
err := nomination.GetFrom(m)
assert.Error(t, err)
assert.Equal(t, stun.ErrAttributeSizeInvalid, err)
})
t.Run("configurable nomination attribute type", func(t *testing.T) {
// Test with custom attribute type
customAttrType := stun.AttrType(0x0040)
attr := NominationAttribute{Value: 12345}
m := &stun.Message{}
err := attr.AddToWithType(m, customAttrType)
assert.NoError(t, err)
// Try to read with default type - should fail
var parsed1 NominationAttribute
err = parsed1.GetFrom(m)
assert.Error(t, err)
// Read with custom type - should succeed
var parsed2 NominationAttribute
err = parsed2.GetFromWithType(m, customAttrType)
assert.NoError(t, err)
assert.Equal(t, uint32(12345), parsed2.Value)
})
t.Run("NominationSetter with custom attribute type", func(t *testing.T) {
customAttrType := stun.AttrType(0x0050)
setter := NominationSetter{
Value: 98765,
AttrType: customAttrType,
}
m := &stun.Message{}
err := setter.AddTo(m)
assert.NoError(t, err)
// Verify the attribute was added with custom type
var parsed NominationAttribute
err = parsed.GetFromWithType(m, customAttrType)
assert.NoError(t, err)
assert.Equal(t, uint32(98765), parsed.Value)
// Verify it wasn't added with default type
var parsedDefault NominationAttribute
err = parsedDefault.GetFrom(m)
assert.Error(t, err)
})
}
func TestControlledSelectorWithActualSTUNMessages(t *testing.T) {
t.Run("HandleBindingRequest with nomination attribute", func(t *testing.T) {
agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator()))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Set up credentials for STUN
agent.localUfrag = testLocalUfrag
agent.localPwd = testLocalPwd
agent.remoteUfrag = testRemoteUfrag
agent.remotePwd = testRemotePwd
selector := &controlledSelector{
agent: agent,
log: agent.log,
}
selector.Start()
// Create test candidates
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
// Mock connection for response
mockConn := &mockPacketConnWithCapture{}
local.conn = mockConn
// Create STUN binding request with nomination and USE-CANDIDATE
msg, err := stun.Build(
stun.BindingRequest,
stun.TransactionID,
stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag),
UseCandidate(),
Nomination(5), // First nomination value
stun.NewShortTermIntegrity(agent.localPwd),
stun.Fingerprint,
)
assert.NoError(t, err)
// Handle the binding request
selector.HandleBindingRequest(msg, local, remote)
// Verify selector accepted the nomination
assert.NotNil(t, selector.lastNomination)
assert.Equal(t, uint32(5), *selector.lastNomination)
// Create another STUN request with higher nomination value
msg2, err := stun.Build(
stun.BindingRequest,
stun.TransactionID,
stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag),
UseCandidate(),
Nomination(10), // Higher nomination value
stun.NewShortTermIntegrity(agent.localPwd),
stun.Fingerprint,
)
assert.NoError(t, err)
// Handle the second binding request
selector.HandleBindingRequest(msg2, local, remote)
// Should accept higher nomination
assert.Equal(t, uint32(10), *selector.lastNomination)
// Create another STUN request with lower nomination value
msg3, err := stun.Build(
stun.BindingRequest,
stun.TransactionID,
stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag),
UseCandidate(),
Nomination(7), // Lower nomination value
stun.NewShortTermIntegrity(agent.localPwd),
stun.Fingerprint,
)
assert.NoError(t, err)
// Handle the third binding request
selector.HandleBindingRequest(msg3, local, remote)
// Should reject lower nomination (lastNomination should remain 10)
assert.Equal(t, uint32(10), *selector.lastNomination)
})
t.Run("HandleBindingRequest without nomination attribute", func(t *testing.T) {
agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator()))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
agent.localUfrag = testLocalUfrag
agent.localPwd = testLocalPwd
agent.remoteUfrag = testRemoteUfrag
agent.remotePwd = testRemotePwd
selector := &controlledSelector{
agent: agent,
log: agent.log,
}
selector.Start()
local, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 12345,
Component: 1,
})
assert.NoError(t, err)
remote, err := NewCandidateHost(&CandidateHostConfig{
Network: "udp",
Address: "127.0.0.1",
Port: 54321,
Component: 1,
})
assert.NoError(t, err)
mockConn := &mockPacketConnWithCapture{}
local.conn = mockConn
// Create STUN binding request without nomination (standard ICE)
msg, err := stun.Build(
stun.BindingRequest,
stun.TransactionID,
stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag),
UseCandidate(),
// No nomination attribute
stun.NewShortTermIntegrity(agent.localPwd),
stun.Fingerprint,
)
assert.NoError(t, err)
// Handle the binding request
selector.HandleBindingRequest(msg, local, remote)
// Without nomination attribute, lastNomination should remain nil
assert.Nil(t, selector.lastNomination)
})
}
func TestInvalidRenominationConfig(t *testing.T) {
t.Run("nil nomination generator with renomination enabled", func(t *testing.T) {
config := &AgentConfig{}
// Without renomination, agent should work fine
agent, err := NewAgent(config)
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent.Close())
}()
// Agent should be created successfully without renomination
assert.False(t, agent.enableRenomination)
assert.Nil(t, agent.nominationValueGenerator)
// getNominationValue should return 0
value := agent.getNominationValue()
assert.Equal(t, uint32(0), value)
})
t.Run("different generator behaviors", func(t *testing.T) {
// Test constant generator
agent1, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 42 }))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent1.Close())
}()
value1 := agent1.getNominationValue()
value2 := agent1.getNominationValue()
assert.Equal(t, uint32(42), value1)
assert.Equal(t, uint32(42), value2)
// Test incrementing generator
counter := uint32(0)
agent2, err := NewAgentWithOptions(WithRenomination(func() uint32 {
counter++
return counter
}))
assert.NoError(t, err)
defer func() {
assert.NoError(t, agent2.Close())
}()
value3 := agent2.getNominationValue()
value4 := agent2.getNominationValue()
assert.Equal(t, uint32(1), value3)
assert.Equal(t, uint32(2), value4)
})
t.Run("controlled agent handles renomination regardless of local config", func(t *testing.T) {
// Create controlled agent with renomination DISABLED
controlledAgent, err := NewAgent(&AgentConfig{
// Renomination disabled by default // Disabled locally
})
assert.NoError(t, err)
defer func() {
assert.NoError(t, controlledAgent.Close())
}()
// Set up as controlled (non-controlling)
controlledAgent.isControlling.Store(false)
// Create controlled selector to test nomination handling
selector := &controlledSelector{
agent: controlledAgent,
log: controlledAgent.log,
}
// Test 1: Should accept nomination without value (standard ICE)
assert.True(t, selector.shouldAcceptNomination(nil))
// Test 2: Should accept first nomination with value (renomination from controlling side)
value1 := uint32(1)
assert.True(t, selector.shouldAcceptNomination(&value1))
assert.Equal(t, &value1, selector.lastNomination)
// Test 3: Should accept higher nomination value
value2 := uint32(2)
assert.True(t, selector.shouldAcceptNomination(&value2))
assert.Equal(t, &value2, selector.lastNomination)
// Test 4: Should reject lower nomination value
value0 := uint32(0)
assert.False(t, selector.shouldAcceptNomination(&value0))
assert.Equal(t, &value2, selector.lastNomination) // Should remain unchanged
})
}
func TestAgentWithCustomNominationAttribute(t *testing.T) {
t.Run("agent uses custom nomination attribute with option", func(t *testing.T) {
customAttr := uint16(0x0042)
// Create agent with custom nomination attribute using option
agent, err := NewAgentWithOptions(
WithRenomination(func() uint32 { return 100 }),
WithNominationAttribute(customAttr),
)
assert.NoError(t, err)
defer agent.Close() //nolint:errcheck
// Verify the agent has the custom attribute configured
assert.Equal(t, stun.AttrType(customAttr), agent.nominationAttribute)
})
t.Run("agent uses default nomination attribute when not configured", func(t *testing.T) {
// Create agent without custom nomination attribute
agentConfig := &AgentConfig{
NetworkTypes: []NetworkType{NetworkTypeUDP4},
}
agent, err := NewAgent(agentConfig)
assert.NoError(t, err)
defer agent.Close() //nolint:errcheck
// Verify the agent has the default attribute
assert.Equal(t, stun.AttrType(0x0030), agent.nominationAttribute)
})
t.Run("multiple options can be applied", func(t *testing.T) {
customAttr := uint16(0x0055)
// Test that multiple options can be applied
agent, err := NewAgentWithOptions(
WithRenomination(func() uint32 { return 200 }),
WithNominationAttribute(customAttr),
)
assert.NoError(t, err)
defer agent.Close() //nolint:errcheck
assert.Equal(t, stun.AttrType(customAttr), agent.nominationAttribute)
})
t.Run("WithNominationAttribute returns error for invalid value", func(t *testing.T) {
// Test that 0x0000 is rejected as invalid
_, err := NewAgentWithOptions(WithNominationAttribute(0x0000))
assert.ErrorIs(t, err, ErrInvalidNominationAttribute)
})
}