mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
3d05fd3cda
Validate complete bounded admin objects before decoding or applying them. Preserve singleton aliases, unknown fields, case-sensitive role map keys, explicit access clears, saved secrets and existing tenant and one-use token boundaries. Add public producer, persistence and API contract controls. Change-source: pulse-maintainer
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
package api
|
||
|
||
import (
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestDecodeAdminJSONObject(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
body string
|
||
ok bool
|
||
}{
|
||
{"unique", `{"name":"SSO","allowedGroups":["ops"]}`, true},
|
||
{"singleton aliases", `{"NAME":"SSO","allowedGroup\u0073":[],"OIDC":{"CLIENTID":"pulse"}}`, true},
|
||
{"unicode singleton alias", `{"allowedGroupſ":["ops"]}`, true},
|
||
{"explicit clears", `{"allowedGroups":[],"allowedDomains":[],"allowedEmails":[],"groupRoleMappings":{}}`, true},
|
||
{"case sensitive group names", `{"groupRoleMappings":{"Ops":"admin","ops":"viewer"}}`, true},
|
||
{"unknown fields", `{"future":{"allowedGroups":[],"AllowedGroups":["ignored"]},"runtimeManaged":true}`, true},
|
||
{"exact duplicate", `{"allowedGroups":["ops"],"allowedGroups":[]}`, false},
|
||
{"escaped duplicate", `{"allowedGroups":["ops"],"allowedGroup\u0073":[]}`, false},
|
||
{"case folded duplicate", `{"allowedGroups":["ops"],"ALLOWEDGROUPS":[]}`, false},
|
||
{"unicode folded duplicate", `{"allowedGroups":["ops"],"allowedGroupſ":[]}`, false},
|
||
{"nested duplicate", `{"oidc":{"clientSecret":"one","CLIENTSECRET":"two"}}`, false},
|
||
{"merged nested objects", `{"oidc":{"clientId":"one"},"OIDC":{"clientSecret":"two"}}`, false},
|
||
{"group duplicate", `{"groupRoleMappings":{"ops":"viewer","ops":"admin"}}`, false},
|
||
{"escaped group duplicate", `{"groupRoleMappings":{"ops":"viewer","op\u0073":"admin"}}`, false},
|
||
{"wrong field type", `{"allowedGroups":"ops"}`, false},
|
||
{"null root", `null`, false},
|
||
{"array root", `[]`, false},
|
||
{"empty", ``, false},
|
||
{"incomplete", `{"name":"SSO"`, false},
|
||
{"second object", `{"name":"SSO"}{}`, false},
|
||
{"trailing junk", `{"name":"SSO"}x`, false},
|
||
}
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
var provider config.SSOProvider
|
||
err := decodeAdminJSONObject(httptest.NewRecorder(), httptest.NewRequest("POST", "/", strings.NewReader(tt.body)), &provider, 1024)
|
||
if tt.ok {
|
||
require.NoError(t, err)
|
||
} else {
|
||
require.Error(t, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDecodeAdminJSONObjectCompleteBound(t *testing.T) {
|
||
body := `{"name":"SSO"}`
|
||
for _, tt := range []struct {
|
||
name string
|
||
body string
|
||
ok bool
|
||
}{
|
||
{"exact limit", body, true},
|
||
{"whitespace within limit", body + " ", true},
|
||
{"whitespace overflow", body + " ", false},
|
||
{"unread second object", body + " {}", false},
|
||
} {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
var provider config.SSOProvider
|
||
err := decodeAdminJSONObject(httptest.NewRecorder(), httptest.NewRequest("POST", "/", strings.NewReader(tt.body)), &provider, int64(len(body)+1))
|
||
if tt.ok {
|
||
require.NoError(t, err)
|
||
} else {
|
||
require.Error(t, err)
|
||
}
|
||
})
|
||
}
|
||
}
|