* Copyright (c) KylinSoft Co., Ltd. 2027.All rights reserved.
* ha-api licensed under the Mulan Permissive Software License, Version 2.
* See LICENSE file for more details.
* Author: xuxiaojuan <xuxiaojuan@kylinos.cn>
* Date: Wed July 8 13:56:40 2026 +0800
*/
package models
import (
"errors"
"strings"
"testing"
"gitee.com/openeuler/ha-api/utils"
"gitee.com/openeuler/ha-api/validations"
"github.com/stretchr/testify/assert"
)
func ruleMockCmd(t *testing.T, fn func(string) ([]byte, error)) {
t.Helper()
orig := utils.RunCommand
utils.RunCommand = fn
t.Cleanup(func() { utils.RunCommand = orig })
}
func TestRulesGet_Success(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
xml := `<constraints>
<rsc_location id="location-dummy-node1" rsc="dummy" node="node1" score="INFINITY">
<rule id="location-dummy-node1-rule" score="INFINITY">
<expression id="expr-1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
<rsc_location id="location-vip-node2" rsc="vip" node="node2" score="100">
<rule id="location-vip-node2-rule" score="100">
<expression id="expr-2" attribute="#uname" operation="eq" value="node2"/>
</rule>
</rsc_location>
</constraints>`
return []byte(xml), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Len(t, result.Data, 1)
assert.Equal(t, "dummy", result.Data[0].Rsc)
assert.Equal(t, "location-dummy-node1-rule", result.Data[0].RuleId)
assert.Equal(t, "INFINITY", result.Data[0].Score)
assert.Equal(t, "#uname", result.Data[0].Attribute)
assert.Equal(t, "eq", result.Data[0].Operation)
assert.Equal(t, "node1", result.Data[0].Value)
}
func TestRulesGet_MultipleRulesForSameResource(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
xml := `<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="rule-1" score="100">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
<rsc_location id="loc-2" rsc="dummy">
<rule id="rule-2" score="-INFINITY">
<expression id="e2" attribute="#uname" operation="eq" value="node2"/>
</rule>
</rsc_location>
</constraints>`
return []byte(xml), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Len(t, result.Data, 2)
assert.Equal(t, "rule-1", result.Data[0].RuleId)
assert.Equal(t, "100", result.Data[0].Score)
assert.Equal(t, "rule-2", result.Data[1].RuleId)
assert.Equal(t, "-INFINITY", result.Data[1].Score)
}
func TestRulesGet_NoMatchingResource(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
xml := `<constraints>
<rsc_location id="loc-1" rsc="vip">
<rule id="rule-vip" score="INFINITY">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
</constraints>`
return []byte(xml), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Empty(t, result.Data)
}
func TestRulesGet_EmptyConstraints(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte(`<constraints></constraints>`), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Empty(t, result.Data)
}
func TestRulesGet_CommandError(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte("Error: cibadmin failed"), errors.New("command failed")
})
result := RulesGet("dummy")
assert.False(t, result.Action)
assert.NotEmpty(t, result.Error)
}
func TestRulesGet_XMLParseError(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte(`<invalid xml><<<`), nil
})
result := RulesGet("dummy")
assert.False(t, result.Action)
assert.NotEmpty(t, result.Error)
}
func TestRulesGet_RuleWithoutRuleElement(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
xml := `<constraints>
<rsc_location id="loc-1" rsc="dummy" node="node1" score="INFINITY"/>
</constraints>`
return []byte(xml), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Empty(t, result.Data)
}
func TestRulesGet_RuleWithNilExpression(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
xml := `<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="rule-no-expr" score="INFINITY"/>
</rsc_location>
</constraints>`
return []byte(xml), nil
})
result := RulesGet("dummy")
assert.True(t, result.Action)
assert.Len(t, result.Data, 1)
assert.Equal(t, "rule-no-expr", result.Data[0].RuleId)
assert.Equal(t, "INFINITY", result.Data[0].Score)
assert.Empty(t, result.Data[0].Attribute)
assert.Empty(t, result.Data[0].Operation)
assert.Empty(t, result.Data[0].Value)
}
func TestRulesDelete_Success(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte(""), nil
})
result := RulesDelete(&validations.DeleteRuleS{RuleIDs: []string{"rule-1", "rule-2"}})
assert.True(t, result.Action)
assert.Equal(t, "Delete rule success", result.Info)
assert.Empty(t, result.Error)
}
func TestRulesDelete_SingleRule(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
assert.Contains(t, cmd, "pcs constraint rule delete")
return []byte(""), nil
})
result := RulesDelete(&validations.DeleteRuleS{RuleIDs: []string{"rule-1"}})
assert.True(t, result.Action)
}
func TestRulesDelete_PartialFailure(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "rule-bad") {
return []byte("Error: rule not found"), errors.New("not found")
}
return []byte(""), nil
})
result := RulesDelete(&validations.DeleteRuleS{RuleIDs: []string{"rule-good", "rule-bad"}})
assert.False(t, result.Action)
assert.Len(t, result.Error, 1)
assert.Equal(t, "rule-bad", result.Error[0]["id"])
}
func TestRulesDelete_AllFail(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte("Error: rule not found"), errors.New("not found")
})
result := RulesDelete(&validations.DeleteRuleS{RuleIDs: []string{"rule-1", "rule-2"}})
assert.False(t, result.Action)
assert.Len(t, result.Error, 2)
}
func TestRulesDelete_EmptyList(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
t.Fatal("should not call RunCommand for empty list")
return nil, nil
})
result := RulesDelete(&validations.DeleteRuleS{RuleIDs: []string{}})
assert.True(t, result.Action)
assert.Equal(t, "Delete rule success", result.Info)
}
func TestRuleAdd_WithRuleID(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
assert.Contains(t, cmd, "pcs constraint location")
assert.Contains(t, cmd, "score=")
assert.Contains(t, cmd, "id=")
return []byte(""), nil
})
result := RuleAdd(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.True(t, result.Action)
assert.Equal(t, "Add rule success", result.Info)
}
func TestRuleAdd_WithoutRuleID(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
assert.Contains(t, cmd, "pcs constraint location")
assert.Contains(t, cmd, "score=")
assert.NotContains(t, cmd, "id=")
return []byte(""), nil
})
result := RuleAdd(&validations.RuleS{
Rsc: "dummy", Score: "100",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.True(t, result.Action)
assert.Equal(t, "Add rule success", result.Info)
}
func TestRuleAdd_EmptyValue(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
assert.Contains(t, cmd, "'defined' '#uname'")
return []byte(""), nil
})
result := RuleAdd(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY",
Attribute: "#uname", Operation: "defined", Value: "",
})
assert.True(t, result.Action)
assert.Equal(t, "Add rule success", result.Info)
}
func TestRuleAdd_CommandFailure(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte("Error: duplicate constraint"), errors.New("duplicate")
})
result := RuleAdd(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.False(t, result.Action)
assert.NotEmpty(t, result.Error)
}
func TestRuleUpdate_Success(t *testing.T) {
callCount := 0
ruleMockCmd(t, func(cmd string) ([]byte, error) {
callCount++
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="my-rule" score="100">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
</constraints>`), nil
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "200", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node2",
})
assert.True(t, result.Action)
assert.Equal(t, "Update rule success", result.Info)
assert.Equal(t, 3, callCount)
}
func TestRuleUpdate_QueryCommandFails(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte("Error: cibadmin failed"), errors.New("cibadmin error")
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.False(t, result.Action)
assert.NotEmpty(t, result.Error)
}
func TestRuleUpdate_XMLParseError(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
return []byte(`<invalid xml><<<`), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.False(t, result.Action)
assert.NotEmpty(t, result.Error)
}
func TestRuleUpdate_DeleteFails(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="my-rule" score="100">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
</constraints>`), nil
}
if strings.Contains(cmd, "delete") {
return []byte("Error: rule not found"), errors.New("not found")
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "200", RuleID: "nonexistent-rule",
Attribute: "#uname", Operation: "eq", Value: "node2",
})
assert.False(t, result.Action)
assert.Contains(t, result.Error, "not found")
}
func TestRuleUpdate_AddNewRuleFails_TriggersRecovery(t *testing.T) {
var executedCmds []string
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="my-rule" score="100">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
</constraints>`), nil
}
executedCmds = append(executedCmds, cmd)
if strings.Contains(cmd, "delete") {
return []byte(""), nil
}
if strings.Contains(cmd, "node2") {
return []byte("Error: duplicate"), errors.New("duplicate")
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "200", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node2",
})
assert.False(t, result.Action)
assert.Contains(t, result.Error, "duplicate constraint")
assert.Len(t, executedCmds, 3)
assert.Contains(t, executedCmds[0], "rule delete")
assert.Contains(t, executedCmds[1], "score='200'")
assert.Contains(t, executedCmds[1], "'node2'")
assert.Contains(t, executedCmds[2], "'dummy'")
assert.Contains(t, executedCmds[2], "score='100'")
assert.Contains(t, executedCmds[2], "'node1'")
}
func TestRuleUpdate_RuleIDNotFoundInXML(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="other-rule" score="100">
<expression id="e1" attribute="#uname" operation="eq" value="node1"/>
</rule>
</rsc_location>
</constraints>`), nil
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "200", RuleID: "nonexistent-rule",
Attribute: "#uname", Operation: "eq", Value: "node2",
})
assert.True(t, result.Action)
}
func TestRuleUpdate_RuleWithNilExpression(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints>
<rsc_location id="loc-1" rsc="dummy">
<rule id="my-rule" score="100"/>
</rsc_location>
</constraints>`), nil
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "200", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node2",
})
assert.True(t, result.Action)
assert.Equal(t, "Update rule success", result.Info)
}
func TestRuleUpdate_EmptyConstraints(t *testing.T) {
ruleMockCmd(t, func(cmd string) ([]byte, error) {
if strings.Contains(cmd, "cibadmin") {
return []byte(`<constraints></constraints>`), nil
}
if strings.Contains(cmd, "delete") {
return []byte("Error: not found"), errors.New("not found")
}
return []byte(""), nil
})
result := RuleUpdate(&validations.RuleS{
Rsc: "dummy", Score: "INFINITY", RuleID: "my-rule",
Attribute: "#uname", Operation: "eq", Value: "node1",
})
assert.False(t, result.Action)
}