Some checks failed
Go / build-and-release (push) Has been cancelled
Introduce comprehensive tests for policy validation logic, including owner and policy admin scenarios. Update `HandlePolicyConfigUpdate` to differentiate permissions for owners and policy admins, enforcing stricter field restrictions and validation flows.
691 lines
17 KiB
Go
691 lines
17 KiB
Go
package policy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
)
|
|
|
|
// TestValidateOwnerPolicyUpdate tests owner-specific validation
|
|
func TestValidateOwnerPolicyUpdate(t *testing.T) {
|
|
// Create a base policy
|
|
basePolicy := &P{
|
|
DefaultPolicy: "allow",
|
|
Owners: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
|
PolicyAdmins: []string{"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
newPolicy string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid owner update with non-empty owners",
|
|
newPolicy: `{
|
|
"default_policy": "deny",
|
|
"owners": ["cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"],
|
|
"policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid - empty owners list",
|
|
newPolicy: `{
|
|
"default_policy": "deny",
|
|
"owners": [],
|
|
"policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "owners list cannot be empty",
|
|
},
|
|
{
|
|
name: "invalid - missing owners field",
|
|
newPolicy: `{
|
|
"default_policy": "deny",
|
|
"policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "owners list cannot be empty",
|
|
},
|
|
{
|
|
name: "invalid - bad owner pubkey format",
|
|
newPolicy: `{
|
|
"default_policy": "deny",
|
|
"owners": ["not-a-valid-pubkey"]
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "invalid owner pubkey",
|
|
},
|
|
{
|
|
name: "valid - owner can add multiple owners",
|
|
newPolicy: `{
|
|
"default_policy": "deny",
|
|
"owners": [
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
|
]
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := basePolicy.ValidateOwnerPolicyUpdate([]byte(tt.newPolicy))
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error containing %q, got nil", tt.errorMsg)
|
|
} else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestValidatePolicyAdminUpdate tests policy admin validation
|
|
func TestValidatePolicyAdminUpdate(t *testing.T) {
|
|
// Create a base policy with known owners and admins
|
|
ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
adminPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
allowedPubkey := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
|
|
|
baseJSON := `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`
|
|
|
|
basePolicy := &P{}
|
|
if err := json.Unmarshal([]byte(baseJSON), basePolicy); err != nil {
|
|
t.Fatalf("failed to create base policy: %v", err)
|
|
}
|
|
|
|
adminPubkeyBin := make([]byte, 32)
|
|
for i := range adminPubkeyBin {
|
|
adminPubkeyBin[i] = 0xbb
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
newPolicy string
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid - policy admin can extend write_allow",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `", "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid - policy admin can add to kind whitelist",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7, 30023]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid - policy admin can increase size limit",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 20000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot modify owners",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot modify the 'owners' field",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot modify policy_admins",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot modify the 'policy_admins' field",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot remove from kind whitelist",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot remove kind 7 from whitelist",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot remove from write_allow",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": [],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot remove pubkey",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot reduce size limit",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"size_limit": 5000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot reduce size_limit",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot remove rule",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot remove rule for kind 1",
|
|
},
|
|
{
|
|
name: "valid - policy admin can add blacklist entries for non-admin users",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7],
|
|
"blacklist": [4]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"write_deny": ["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot blacklist owner in write_deny",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"write_deny": ["` + ownerPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot blacklist owner",
|
|
},
|
|
{
|
|
name: "invalid - policy admin cannot blacklist other policy admin",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"write_deny": ["` + adminPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: true,
|
|
errorMsg: "cannot blacklist policy admin",
|
|
},
|
|
{
|
|
name: "valid - policy admin can blacklist whitelisted non-admin user",
|
|
newPolicy: `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"],
|
|
"policy_admins": ["` + adminPubkey + `"],
|
|
"kind": {
|
|
"whitelist": [1, 3, 7]
|
|
},
|
|
"rules": {
|
|
"1": {
|
|
"description": "Text notes",
|
|
"write_allow": ["` + allowedPubkey + `"],
|
|
"write_deny": ["` + allowedPubkey + `"],
|
|
"size_limit": 10000
|
|
}
|
|
}
|
|
}`,
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := basePolicy.ValidatePolicyAdminUpdate([]byte(tt.newPolicy), adminPubkeyBin)
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error containing %q, got nil", tt.errorMsg)
|
|
} else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsOwner tests the IsOwner method
|
|
func TestIsOwner(t *testing.T) {
|
|
ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
nonOwnerPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
|
|
_ = nonOwnerPubkey // Silence unused variable warning
|
|
|
|
policyJSON := `{
|
|
"default_policy": "allow",
|
|
"owners": ["` + ownerPubkey + `"]
|
|
}`
|
|
|
|
policy, err := New([]byte(policyJSON))
|
|
if err != nil {
|
|
t.Fatalf("failed to create policy: %v", err)
|
|
}
|
|
|
|
// Create binary pubkeys
|
|
ownerBin := make([]byte, 32)
|
|
for i := range ownerBin {
|
|
ownerBin[i] = 0xaa
|
|
}
|
|
|
|
nonOwnerBin := make([]byte, 32)
|
|
for i := range nonOwnerBin {
|
|
nonOwnerBin[i] = 0xbb
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
pubkey []byte
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "owner is recognized",
|
|
pubkey: ownerBin,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "non-owner is not recognized",
|
|
pubkey: nonOwnerBin,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "nil pubkey returns false",
|
|
pubkey: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty pubkey returns false",
|
|
pubkey: []byte{},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := policy.IsOwner(tt.pubkey)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStringSliceEqual tests the helper function
|
|
func TestStringSliceEqual(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
a []string
|
|
b []string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "equal slices same order",
|
|
a: []string{"a", "b", "c"},
|
|
b: []string{"a", "b", "c"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "equal slices different order",
|
|
a: []string{"a", "b", "c"},
|
|
b: []string{"c", "a", "b"},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "different lengths",
|
|
a: []string{"a", "b"},
|
|
b: []string{"a", "b", "c"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "different contents",
|
|
a: []string{"a", "b", "c"},
|
|
b: []string{"a", "b", "d"},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "empty slices",
|
|
a: []string{},
|
|
b: []string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "nil slices",
|
|
a: nil,
|
|
b: nil,
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "nil vs empty",
|
|
a: nil,
|
|
b: []string{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "duplicates in both",
|
|
a: []string{"a", "a", "b"},
|
|
b: []string{"a", "b", "a"},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := stringSliceEqual(tt.a, tt.b)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPolicyAdminContributionValidation tests the contribution validation
|
|
func TestPolicyAdminContributionValidation(t *testing.T) {
|
|
ownerPubkey := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
adminPubkey := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
|
|
ownerPolicy := &P{
|
|
DefaultPolicy: "allow",
|
|
Owners: []string{ownerPubkey},
|
|
PolicyAdmins: []string{adminPubkey},
|
|
Kind: Kinds{
|
|
Whitelist: []int{1, 3, 7},
|
|
},
|
|
rules: map[int]Rule{
|
|
1: {
|
|
Description: "Text notes",
|
|
SizeLimit: ptr(int64(10000)),
|
|
},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
contribution *PolicyAdminContribution
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "valid - add kinds to whitelist",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
KindWhitelistAdd: []int{30023},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid - add to blacklist",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
KindBlacklistAdd: []int{4},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid - extend existing rule with larger limit",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
RulesExtend: map[int]RuleExtension{
|
|
1: {
|
|
SizeLimitOverride: ptr(int64(20000)),
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid - extend non-existent rule",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
RulesExtend: map[int]RuleExtension{
|
|
999: {
|
|
SizeLimitOverride: ptr(int64(20000)),
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorMsg: "cannot extend rule for kind 999",
|
|
},
|
|
{
|
|
name: "invalid - size limit override smaller than owner's",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
RulesExtend: map[int]RuleExtension{
|
|
1: {
|
|
SizeLimitOverride: ptr(int64(5000)),
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorMsg: "size_limit_override for kind 1 must be >=",
|
|
},
|
|
{
|
|
name: "valid - add new rule for undefined kind",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
RulesAdd: map[int]Rule{
|
|
30023: {
|
|
Description: "Long-form content",
|
|
SizeLimit: ptr(int64(100000)),
|
|
},
|
|
},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid - add rule for already-defined kind",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: adminPubkey,
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
RulesAdd: map[int]Rule{
|
|
1: {
|
|
Description: "Trying to override",
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
errorMsg: "cannot add rule for kind 1: already defined",
|
|
},
|
|
{
|
|
name: "invalid - bad pubkey length in extension",
|
|
contribution: &PolicyAdminContribution{
|
|
AdminPubkey: "short",
|
|
CreatedAt: 1234567890,
|
|
EventID: "event123",
|
|
},
|
|
expectError: true,
|
|
errorMsg: "invalid admin pubkey length",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := ValidatePolicyAdminContribution(ownerPolicy, tt.contribution, nil)
|
|
if tt.expectError {
|
|
if err == nil {
|
|
t.Errorf("expected error containing %q, got nil", tt.errorMsg)
|
|
} else if tt.errorMsg != "" && !containsSubstring(err.Error(), tt.errorMsg) {
|
|
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function for generic pointer
|
|
func ptr[T any](v T) *T {
|
|
return &v
|
|
}
|