mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-08-19 11:26:25 +00:00
233 lines
6.6 KiB
Go
233 lines
6.6 KiB
Go
package dto
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin/binding"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTokenDurationValidation(t *testing.T) {
|
|
type input struct {
|
|
Duration int64 `binding:"omitempty,token_duration"`
|
|
}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
value int64
|
|
wantErr bool
|
|
}{
|
|
{name: "omitted (default)"},
|
|
{name: "negative", value: -1, wantErr: true},
|
|
{name: "minimum", value: 1},
|
|
{name: "custom duration", value: 90},
|
|
{name: "maximum", value: 365 * 24 * 60},
|
|
{name: "above maximum", value: 365*24*60 + 1, wantErr: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
err := binding.Validator.ValidateStruct(input{Duration: test.value})
|
|
if test.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppConfigJSONValidation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
valid bool
|
|
}{
|
|
{
|
|
name: "valid string array",
|
|
valid: validateJSONStringArray(`["group-id"]`),
|
|
},
|
|
{
|
|
name: "valid custom claims",
|
|
valid: validateJSONCustomClaims(`[{"key":"role","value":"user"}]`),
|
|
},
|
|
{
|
|
name: "valid CIMD URL allowlist",
|
|
valid: validateCIMDURLAllowlist(`["https://app.example.com/**"]`),
|
|
},
|
|
{
|
|
name: "custom claims object instead of array",
|
|
valid: !validateJSONCustomClaims(`{"key":"role","value":"user"}`),
|
|
},
|
|
{
|
|
name: "custom claim missing key",
|
|
valid: !validateJSONCustomClaims(`[{"value":"user"}]`),
|
|
},
|
|
{
|
|
name: "non-string group ID",
|
|
valid: !validateJSONStringArray(`[42]`),
|
|
},
|
|
{
|
|
name: "null array",
|
|
valid: !validateJSONCustomClaims(`null`),
|
|
},
|
|
{
|
|
name: "unsafe CIMD pattern",
|
|
valid: !validateCIMDURLAllowlist(`["data:text/html,test"]`),
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert.True(t, test.valid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAppConfigValueTypeValidation(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
valid bool
|
|
}{
|
|
{name: "true boolean", valid: validateBooleanString("true")},
|
|
{name: "false boolean", valid: validateBooleanString("false")},
|
|
{name: "non-boolean word", valid: !validateBooleanString("hello")},
|
|
{name: "numeric boolean", valid: !validateBooleanString("1")},
|
|
{name: "integer", valid: validateIntegerString("60")},
|
|
{name: "negative integer", valid: validateIntegerString("-1")},
|
|
{name: "decimal", valid: !validateIntegerString("1.5")},
|
|
{name: "non-integer word", valid: !validateIntegerString("hello")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
assert.True(t, test.valid)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateUsername(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid simple", "user123", true},
|
|
{"valid with dot", "user.name", true},
|
|
{"valid with underscore", "user_name", true},
|
|
{"valid with hyphen", "user-name", true},
|
|
{"valid with at", "user@name", true},
|
|
{"starts with symbol", ".username", false},
|
|
{"ends with non-alphanumeric", "username-", false},
|
|
{"contains space", "user name", false},
|
|
{"valid single char", "a", true},
|
|
{"empty", "", false},
|
|
{"only special chars", "-._@", false},
|
|
{"valid long", "a1234567890_b.c-d@e", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ValidateUsername(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateClientID(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid simple", "client123", true},
|
|
{"valid with dot", "client.id", true},
|
|
{"valid with underscore", "client_id", true},
|
|
{"valid with hyphen", "client-id", true},
|
|
{"valid with all", "client.id-123_abc", true},
|
|
{"contains space", "client id", false},
|
|
{"contains at", "client@id", false},
|
|
{"empty", "", false},
|
|
{"only special chars", "-._", true},
|
|
{"invalid char", "client!id", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ValidateClientID(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateResourceURI(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid https URI", "https://api.example.com", true},
|
|
{"valid custom scheme URI", "api://PocketID", true},
|
|
{"valid URN", "urn:my-app", true},
|
|
{"valid query component", "https://api.example.com?tenant=1", true},
|
|
{"invalid relative path", "/foo", false},
|
|
{"invalid plain string", "foo", false},
|
|
{"invalid fragment", "https://api.example.com#tokens", false},
|
|
{"invalid empty fragment", "https://api.example.com#", false},
|
|
{"invalid unescaped path space", "https://api.example.com/a b", false},
|
|
{"invalid unescaped opaque space", "urn:my app", false},
|
|
{"invalid malformed URI", "http://[::1", false},
|
|
{"invalid javascript scheme", "javascript:alert(1)", false},
|
|
{"invalid data scheme", "data:text/html,<script>alert(1)</script>", false},
|
|
{"empty", "", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ValidateResourceURI(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateCallbackURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid https URL", "https://example.com/callback", true},
|
|
{"valid loopback URL", "http://127.0.0.1:49813/callback", true},
|
|
{"empty scheme", "//127.0.0.1:49813/callback", true},
|
|
{"valid custom scheme", "pocketid://callback", true},
|
|
{"invalid malformed URL", "http://[::1", false},
|
|
{"invalid missing scheme separator", "://example.com/callback", false},
|
|
{"rejects javascript scheme", "javascript:alert(1)", false},
|
|
{"rejects mixed case javascript scheme", "JavaScript:alert(1)", false},
|
|
{"rejects data scheme", "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
|
{"rejects mixed case data scheme", "DaTa:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ValidateCallbackURL(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateCallbackURLPattern(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected bool
|
|
}{
|
|
{"valid exact URL", "https://example.com/callback", true},
|
|
{"valid wildcard URL", "https://*.example.com/callback", true},
|
|
{"valid custom scheme", "pocketid://callback", true},
|
|
{"valid global wildcard", "*", true},
|
|
{"invalid relative URL", "/callback", false},
|
|
{"invalid malformed URL", "http://[::1", false},
|
|
{"rejects javascript scheme", "javascript:alert(1)", false},
|
|
{"rejects data scheme", "data:text/html;base64,PGgxPkhlbGxvPC9oMT4=", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, ValidateCallbackURLPattern(tt.input))
|
|
})
|
|
}
|
|
}
|