1
0
forked from baron/baron-sso

userfront&backend test coverage 추가

This commit is contained in:
2026-05-29 18:04:04 +09:00
parent 23cd316c23
commit 4c56c28481
26 changed files with 2405 additions and 260 deletions

View File

@@ -1,6 +1,9 @@
package domain
import "testing"
import (
"reflect"
"testing"
)
func TestHydraClient_HeadlessLoginFlags(t *testing.T) {
t.Run("metadata-backed headless login client is supported", func(t *testing.T) {
@@ -76,3 +79,104 @@ func TestHydraClient_HeadlessLoginFlags(t *testing.T) {
}
})
}
func TestHydraClientHeadlessMetadataAccessors(t *testing.T) {
t.Run("metadata values override inline values", func(t *testing.T) {
metadataJWKS := map[string]any{"keys": []any{"metadata-key"}}
client := HydraClient{
TokenEndpointAuthMethod: "client_secret_post",
JWKSUri: "https://inline.example.com/jwks.json",
JWKS: map[string]any{"keys": []any{"inline-key"}},
Metadata: map[string]any{
MetadataHeadlessTokenEndpointAuthMethod: " private_key_jwt ",
MetadataHeadlessJWKSURI: " https://metadata.example.com/jwks.json ",
MetadataHeadlessJWKS: metadataJWKS,
},
}
if got := client.HeadlessTokenEndpointAuthMethod(); got != "private_key_jwt" {
t.Fatalf("unexpected auth method: %q", got)
}
if got := client.HeadlessJWKSURI(); got != "https://metadata.example.com/jwks.json" {
t.Fatalf("unexpected jwks uri: %q", got)
}
if got := client.HeadlessJWKS(); !reflect.DeepEqual(got, metadataJWKS) {
t.Fatalf("unexpected jwks value: %#v", got)
}
})
t.Run("blank or missing metadata values fall back to inline values", func(t *testing.T) {
inlineJWKS := map[string]any{"keys": []any{"inline-key"}}
client := HydraClient{
TokenEndpointAuthMethod: " private_key_jwt ",
JWKSUri: " https://inline.example.com/jwks.json ",
JWKS: inlineJWKS,
Metadata: map[string]any{
MetadataHeadlessTokenEndpointAuthMethod: " ",
MetadataHeadlessJWKSURI: " ",
MetadataHeadlessJWKS: nil,
},
}
if got := client.HeadlessTokenEndpointAuthMethod(); got != "private_key_jwt" {
t.Fatalf("unexpected auth method: %q", got)
}
if got := client.HeadlessJWKSURI(); got != "https://inline.example.com/jwks.json" {
t.Fatalf("unexpected jwks uri: %q", got)
}
if got := client.HeadlessJWKS(); !reflect.DeepEqual(got, inlineJWKS) {
t.Fatalf("unexpected jwks value: %#v", got)
}
})
}
func TestHydraClientBackchannelLogoutAccessors(t *testing.T) {
t.Run("metadata values override inline values", func(t *testing.T) {
inlineRequired := false
client := HydraClient{
BackChannelLogoutURI: "https://inline.example.com/logout",
BackChannelLogoutSessionRequired: &inlineRequired,
Metadata: map[string]any{
MetadataBackChannelLogoutURI: " https://metadata.example.com/logout ",
MetadataBackChannelLogoutSessionRequired: true,
},
}
if got := client.BackchannelLogoutURI(); got != "https://metadata.example.com/logout" {
t.Fatalf("unexpected logout uri: %q", got)
}
if !client.BackchannelLogoutSessionRequiredValue() {
t.Fatalf("expected metadata session_required value")
}
})
t.Run("blank or missing metadata values fall back to inline values", func(t *testing.T) {
inlineRequired := true
client := HydraClient{
BackChannelLogoutURI: " https://inline.example.com/logout ",
BackChannelLogoutSessionRequired: &inlineRequired,
Metadata: map[string]any{
MetadataBackChannelLogoutURI: " ",
MetadataBackChannelLogoutSessionRequired: "true",
},
}
if got := client.BackchannelLogoutURI(); got != "https://inline.example.com/logout" {
t.Fatalf("unexpected logout uri: %q", got)
}
if !client.BackchannelLogoutSessionRequiredValue() {
t.Fatalf("expected inline session_required value")
}
})
t.Run("missing session required defaults to false", func(t *testing.T) {
client := HydraClient{}
if got := client.BackchannelLogoutURI(); got != "" {
t.Fatalf("unexpected logout uri: %q", got)
}
if client.BackchannelLogoutSessionRequiredValue() {
t.Fatalf("expected default session_required false")
}
})
}

View File

@@ -0,0 +1,93 @@
package domain
import (
"encoding/json"
"testing"
)
func TestJSONMapValue(t *testing.T) {
t.Run("nil map returns nil database value", func(t *testing.T) {
var payload JSONMap
value, err := payload.Value()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if value != nil {
t.Fatalf("expected nil value, got %v", value)
}
})
t.Run("map marshals to JSON string", func(t *testing.T) {
payload := JSONMap{"enabled": true, "name": "baron"}
value, err := payload.Value()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
raw, ok := value.(string)
if !ok {
t.Fatalf("expected string value, got %T", value)
}
var decoded map[string]any
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
t.Fatalf("value should be valid json: %v", err)
}
if decoded["enabled"] != true || decoded["name"] != "baron" {
t.Fatalf("unexpected decoded value: %#v", decoded)
}
})
}
func TestJSONMapScan(t *testing.T) {
t.Run("nil value becomes empty map", func(t *testing.T) {
var payload JSONMap
if err := payload.Scan(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if payload == nil || len(payload) != 0 {
t.Fatalf("expected empty map, got %#v", payload)
}
})
t.Run("byte slice value decodes JSON", func(t *testing.T) {
var payload JSONMap
if err := payload.Scan([]byte(`{"count":2,"name":"baron"}`)); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if payload["count"] != float64(2) || payload["name"] != "baron" {
t.Fatalf("unexpected payload: %#v", payload)
}
})
t.Run("string value decodes JSON", func(t *testing.T) {
var payload JSONMap
if err := payload.Scan(`{"active":true}`); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if payload["active"] != true {
t.Fatalf("unexpected payload: %#v", payload)
}
})
t.Run("unsupported value type returns error", func(t *testing.T) {
var payload JSONMap
if err := payload.Scan(42); err == nil {
t.Fatalf("expected unsupported type error")
}
})
t.Run("invalid JSON returns error", func(t *testing.T) {
var payload JSONMap
if err := payload.Scan(`{invalid`); err == nil {
t.Fatalf("expected invalid JSON error")
}
})
}

View File

@@ -0,0 +1,357 @@
package domain
import (
"testing"
"time"
"github.com/google/uuid"
)
func requireGeneratedUUID(t *testing.T, value string) {
t.Helper()
if value == "" {
t.Fatalf("expected generated uuid")
}
if _, err := uuid.Parse(value); err != nil {
t.Fatalf("expected valid uuid, got %q: %v", value, err)
}
}
func TestBeforeCreateGeneratesMissingIDs(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{
name: "api key",
run: func(t *testing.T) {
model := ApiKey{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "client consent",
run: func(t *testing.T) {
model := ClientConsent{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "identity provider config",
run: func(t *testing.T) {
model := IdentityProviderConfig{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "keto outbox",
run: func(t *testing.T) {
model := KetoOutbox{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "tenant",
run: func(t *testing.T) {
model := Tenant{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "tenant domain",
run: func(t *testing.T) {
model := TenantDomain{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "user",
run: func(t *testing.T) {
model := User{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "user group",
run: func(t *testing.T) {
model := UserGroup{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
{
name: "worksmobile resource mapping",
run: func(t *testing.T) {
model := WorksmobileResourceMapping{}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, model.ID)
},
},
}
for _, tc := range tests {
t.Run(tc.name, tc.run)
}
}
func TestBeforeCreatePreservesExistingIDs(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{
name: "api key",
run: func(t *testing.T) {
model := ApiKey{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "client consent",
run: func(t *testing.T) {
model := ClientConsent{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "identity provider config",
run: func(t *testing.T) {
model := IdentityProviderConfig{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "keto outbox",
run: func(t *testing.T) {
model := KetoOutbox{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "tenant",
run: func(t *testing.T) {
model := Tenant{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "tenant domain",
run: func(t *testing.T) {
model := TenantDomain{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "user",
run: func(t *testing.T) {
model := User{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "user group",
run: func(t *testing.T) {
model := UserGroup{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
{
name: "worksmobile resource mapping",
run: func(t *testing.T) {
model := WorksmobileResourceMapping{ID: "existing-id"}
if err := model.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if model.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, tc.run)
}
}
func TestTableNames(t *testing.T) {
tests := []struct {
name string
got string
expected string
}{
{name: "keto outbox", got: (&KetoOutbox{}).TableName(), expected: "keto_outbox"},
{name: "rp usage event", got: (&RPUsageEvent{}).TableName(), expected: "rp_usage_outbox"},
{name: "rp user metadata", got: (RPUserMetadata{}).TableName(), expected: "rp_user_metadata"},
{name: "user group", got: (&UserGroup{}).TableName(), expected: "user_groups"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.got != tc.expected {
t.Fatalf("unexpected table name: got=%s expected=%s", tc.got, tc.expected)
}
})
}
}
func TestTenantIsActive(t *testing.T) {
tests := []struct {
name string
status string
expected bool
}{
{name: "active", status: TenantStatusActive, expected: true},
{name: "pending", status: TenantStatusPending, expected: false},
{name: "suspended", status: TenantStatusSuspended, expected: false},
{name: "deleted", status: TenantStatusDeleted, expected: false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tenant := Tenant{Status: tc.status}
if got := tenant.IsActive(); got != tc.expected {
t.Fatalf("unexpected active state: got=%v expected=%v", got, tc.expected)
}
})
}
}
func TestRPUsageEventBeforeCreateDefaults(t *testing.T) {
event := RPUsageEvent{}
if err := event.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, event.ID)
if event.Status != RPUsageOutboxStatusPending {
t.Fatalf("unexpected status: %s", event.Status)
}
if event.OccurredAt.IsZero() {
t.Fatalf("expected occurred_at default")
}
if event.Payload == nil {
t.Fatalf("expected empty payload default")
}
}
func TestRPUsageEventBeforeCreatePreservesExplicitValues(t *testing.T) {
occurredAt := time.Date(2026, 5, 29, 1, 2, 3, 0, time.UTC)
event := RPUsageEvent{
ID: "existing-id",
Status: RPUsageOutboxStatusProcessing,
OccurredAt: occurredAt,
Payload: JSONMap{"source": "test"},
}
if err := event.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if event.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
if event.Status != RPUsageOutboxStatusProcessing {
t.Fatalf("expected status to be preserved")
}
if !event.OccurredAt.Equal(occurredAt) {
t.Fatalf("expected occurred_at to be preserved")
}
if event.Payload["source"] != "test" {
t.Fatalf("expected payload to be preserved")
}
}
func TestWorksmobileOutboxBeforeCreateDefaults(t *testing.T) {
outbox := WorksmobileOutbox{}
if err := outbox.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
requireGeneratedUUID(t, outbox.ID)
if outbox.Status != WorksmobileOutboxStatusPending {
t.Fatalf("unexpected status: %s", outbox.Status)
}
}
func TestWorksmobileOutboxBeforeCreatePreservesExplicitValues(t *testing.T) {
outbox := WorksmobileOutbox{
ID: "existing-id",
Status: WorksmobileOutboxStatusProcessing,
}
if err := outbox.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if outbox.ID != "existing-id" {
t.Fatalf("expected existing id to be preserved")
}
if outbox.Status != WorksmobileOutboxStatusProcessing {
t.Fatalf("expected status to be preserved")
}
}

View File

@@ -0,0 +1,80 @@
package domain
import (
"encoding/hex"
"testing"
"time"
)
func TestSharedLinkBeforeCreate(t *testing.T) {
t.Run("generates id and token when missing", func(t *testing.T) {
link := SharedLink{}
if err := link.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if link.ID == "" {
t.Fatalf("expected generated id")
}
if len(link.Token) != 64 {
t.Fatalf("expected 64-character token, got %q", link.Token)
}
if _, err := hex.DecodeString(link.Token); err != nil {
t.Fatalf("expected hex token: %v", err)
}
})
t.Run("preserves existing id and token", func(t *testing.T) {
link := SharedLink{
ID: "existing-id",
Token: "existing-token",
}
if err := link.BeforeCreate(nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if link.ID != "existing-id" || link.Token != "existing-token" {
t.Fatalf("expected existing fields to be preserved: %#v", link)
}
})
}
func TestSharedLinkIsValid(t *testing.T) {
future := time.Now().Add(time.Hour)
past := time.Now().Add(-time.Hour)
tests := []struct {
name string
link SharedLink
expected bool
}{
{
name: "active link without expiration is valid",
link: SharedLink{IsActive: true},
expected: true,
},
{
name: "active link with future expiration is valid",
link: SharedLink{IsActive: true, ExpiresAt: &future},
expected: true,
},
{
name: "inactive link is invalid",
link: SharedLink{IsActive: false, ExpiresAt: &future},
expected: false,
},
{
name: "expired link is invalid",
link: SharedLink{IsActive: true, ExpiresAt: &past},
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := tc.link.IsValid(); got != tc.expected {
t.Fatalf("unexpected validity: got=%v expected=%v", got, tc.expected)
}
})
}
}

View File

@@ -1,10 +1,13 @@
package pagination
import (
"encoding/base64"
"testing"
"time"
"github.com/stretchr/testify/require"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type testItem struct {
@@ -53,3 +56,87 @@ func TestSortByKeyDescUsesIDAsTieBreaker(t *testing.T) {
items[2].id,
})
}
func TestEncodeReturnsEmptyForInvalidInput(t *testing.T) {
require.Empty(t, Encode(time.Time{}, "id"))
require.Empty(t, Encode(time.Now(), " "))
}
func TestDecodeRejectsInvalidCursor(t *testing.T) {
t.Run("blank cursor is nil", func(t *testing.T) {
cursor, err := Decode(" ")
require.NoError(t, err)
require.Nil(t, cursor)
})
t.Run("invalid base64 returns error", func(t *testing.T) {
cursor, err := Decode("not base64")
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("invalid json returns error", func(t *testing.T) {
raw := base64.RawURLEncoding.EncodeToString([]byte("{invalid"))
cursor, err := Decode(raw)
require.Error(t, err)
require.Nil(t, cursor)
})
t.Run("missing timestamp returns invalid cursor", func(t *testing.T) {
raw := base64.RawURLEncoding.EncodeToString([]byte(`{"id":"abc"}`))
cursor, err := Decode(raw)
require.EqualError(t, err, "invalid cursor")
require.Nil(t, cursor)
})
t.Run("missing id returns invalid cursor", func(t *testing.T) {
raw := base64.RawURLEncoding.EncodeToString([]byte(`{"timestamp":"2026-05-29T00:00:00Z"}`))
cursor, err := Decode(raw)
require.EqualError(t, err, "invalid cursor")
require.Nil(t, cursor)
})
}
func TestComesAfter(t *testing.T) {
now := time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)
cursor := &Cursor{Timestamp: now, ID: "m"}
require.True(t, ComesAfter(now, "id", nil))
require.True(t, ComesAfter(now.Add(-time.Second), "z", cursor))
require.True(t, ComesAfter(now, "a", cursor))
require.False(t, ComesAfter(now.Add(time.Second), "a", cursor))
require.False(t, ComesAfter(now, "z", cursor))
}
func TestPageByCursorReturnsDecodeError(t *testing.T) {
items := []testItem{{id: "a", createdAt: time.Now()}}
page, nextCursor, err := PageByCursor(items, 10, "not base64", func(item testItem) (time.Time, string) {
return item.createdAt, item.id
})
require.Error(t, err)
require.Nil(t, page)
require.Empty(t, nextCursor)
}
func TestApplyCreatedAtIDCursor(t *testing.T) {
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: "host=localhost user=test dbname=test sslmode=disable",
}), &gorm.Config{
DryRun: true,
DisableAutomaticPing: true,
})
require.NoError(t, err)
require.Same(t, db, ApplyCreatedAtIDCursor(db, nil, "created_at", "id"))
cursor := &Cursor{
Timestamp: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC),
ID: "cursor-id",
}
query := ApplyCreatedAtIDCursor(db.Model(&struct{}{}), cursor, "created_at", "id").Find(&[]struct{}{})
require.Contains(t, query.Statement.SQL.String(), "created_at < $1 OR (created_at = $2 AND id < $3)")
require.Equal(t, []any{cursor.Timestamp, cursor.Timestamp, cursor.ID}, query.Statement.Vars)
}

View File

@@ -58,6 +58,34 @@ func TestErrorWithDetailsResponseShape(t *testing.T) {
}
}
func TestErrorResponseShape(t *testing.T) {
app := fiber.New()
app.Get("/test", func(c *fiber.Ctx) error {
return Error(c, fiber.StatusUnauthorized, "invalid_session", "login required")
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("unexpected status code: %d", resp.StatusCode)
}
body := parseBody(t, resp)
if body["error"] != "login required" {
t.Fatalf("unexpected error value: %v", body["error"])
}
if body["code"] != "invalid_session" {
t.Fatalf("unexpected code value: %v", body["code"])
}
if _, exists := body["details"]; exists {
t.Fatalf("details should be omitted when nil: %#v", body)
}
}
func TestStatusCodeMapping(t *testing.T) {
tests := []struct {
name string

View File

@@ -251,8 +251,8 @@ func TestBuildWorksmobileUserPayloadKeepsFirstAffiliationPrimaryWhenBaronReprese
user,
gpdtdcTenant,
map[string]domain.Tenant{
gpdtdcID: gpdtdcTenant,
firstTenantID: firstTenant,
gpdtdcID: gpdtdcTenant,
firstTenantID: firstTenant,
secondTenantID: secondTenant,
},
nil,

View File

@@ -0,0 +1,27 @@
package utils
import "testing"
func TestParseAuditDetails(t *testing.T) {
t.Run("empty details returns error", func(t *testing.T) {
if _, err := ParseAuditDetails(""); err == nil {
t.Fatalf("expected empty details error")
}
})
t.Run("invalid JSON returns error", func(t *testing.T) {
if _, err := ParseAuditDetails("{invalid"); err == nil {
t.Fatalf("expected invalid JSON error")
}
})
t.Run("valid JSON returns payload", func(t *testing.T) {
payload, err := ParseAuditDetails(`{"actor":"admin","count":2}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if payload["actor"] != "admin" || payload["count"] != float64(2) {
t.Fatalf("unexpected payload: %#v", payload)
}
})
}

View File

@@ -22,3 +22,48 @@ func TestResolveClientIP_PrefersPublicRealIPOverPrivateForwarded(t *testing.T) {
t.Fatalf("expected public real IP, got %q", got)
}
}
func TestResolveClientIP_PrefersPublicRemoteIPWhenHeadersArePrivate(t *testing.T) {
got := ResolveClientIP("10.0.0.2", "192.168.0.10", "203.0.113.8:12345")
if got != "203.0.113.8" {
t.Fatalf("expected public remote IP, got %q", got)
}
}
func TestResolveClientIP_FallsBackToRealIPWhenNoForwardedCandidates(t *testing.T) {
got := ResolveClientIP("invalid", "192.168.0.10", "bad-remote")
if got != "192.168.0.10" {
t.Fatalf("expected normalized real IP, got %q", got)
}
}
func TestResolveClientIP_ReturnsEmptyForInvalidInputs(t *testing.T) {
got := ResolveClientIP("", "bad-real", "bad-remote")
if got != "" {
t.Fatalf("expected empty IP, got %q", got)
}
}
func TestIsPrivateOrReservedIP(t *testing.T) {
tests := []struct {
name string
ip string
expected bool
}{
{name: "invalid", ip: "not-an-ip", expected: false},
{name: "public", ip: "203.0.113.8", expected: false},
{name: "private ipv4", ip: "10.0.0.1", expected: true},
{name: "loopback", ip: "127.0.0.1", expected: true},
{name: "link local", ip: "169.254.1.1", expected: true},
{name: "carrier grade nat", ip: "100.64.0.1", expected: true},
{name: "unique local ipv6", ip: "fc00::1", expected: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := IsPrivateOrReservedIP(tc.ip); got != tc.expected {
t.Fatalf("unexpected private state for %s: got=%v expected=%v", tc.ip, got, tc.expected)
}
})
}
}

View File

@@ -22,6 +22,11 @@ func TestValidatePasswordWithPolicy(t *testing.T) {
assert.NoError(t, err)
})
t.Run("Nil Policy", func(t *testing.T) {
err := ValidatePasswordWithPolicy(nil, "")
assert.NoError(t, err)
})
t.Run("Too Short", func(t *testing.T) {
err := ValidatePasswordWithPolicy(policy, "P123!")
assert.Error(t, err)
@@ -34,11 +39,29 @@ func TestValidatePasswordWithPolicy(t *testing.T) {
assert.Contains(t, err.Error(), "소문자")
})
t.Run("Missing Uppercase", func(t *testing.T) {
err := ValidatePasswordWithPolicy(policy, "pass1234!")
assert.Error(t, err)
assert.Contains(t, err.Error(), "대문자")
})
t.Run("Missing Number", func(t *testing.T) {
err := ValidatePasswordWithPolicy(policy, "Password!")
assert.Error(t, err)
assert.Contains(t, err.Error(), "숫자")
})
t.Run("Missing Symbol", func(t *testing.T) {
err := ValidatePasswordWithPolicy(policy, "Pass1234")
assert.Error(t, err)
assert.Contains(t, err.Error(), "특수문자")
})
t.Run("Missing Minimum Character Types", func(t *testing.T) {
err := ValidatePasswordWithPolicy(&domain.PasswordPolicy{MinLength: 4, MinCharacterTypes: 4}, "abcd")
assert.Error(t, err)
assert.Contains(t, err.Error(), "4가지")
})
}
func TestGeneratePasswordWithPolicy(t *testing.T) {
@@ -55,8 +78,51 @@ func TestGeneratePasswordWithPolicy(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, password, 16)
// Generated password must satisfy the policy
err = ValidatePasswordWithPolicy(policy, password)
assert.NoError(t, err, "Generated password '%s' does not satisfy policy", password)
})
t.Run("Nil Policy Uses Default Length", func(t *testing.T) {
password, err := GeneratePasswordWithPolicy(nil)
assert.NoError(t, err)
assert.Len(t, password, 12)
})
t.Run("Minimum Character Types Adds Optional Categories", func(t *testing.T) {
policy := &domain.PasswordPolicy{
MinLength: 4,
Lowercase: true,
MinCharacterTypes: 4,
}
password, err := GeneratePasswordWithPolicy(policy)
assert.NoError(t, err)
assert.Len(t, password, 4)
assert.NoError(t, ValidatePasswordWithPolicy(policy, password))
})
t.Run("Required Categories Raise Short Minimum Length", func(t *testing.T) {
policy := &domain.PasswordPolicy{
MinLength: 1,
Lowercase: true,
Uppercase: true,
Number: true,
NonAlphanumeric: true,
}
password, err := GeneratePasswordWithPolicy(policy)
assert.NoError(t, err)
assert.Len(t, password, 4)
assert.NoError(t, ValidatePasswordWithPolicy(policy, password))
})
}
func TestPasswordPolicyRandomHelpersRejectInvalidInput(t *testing.T) {
_, err := randomIndex(0)
assert.Error(t, err)
_, err = randomChar("")
assert.Error(t, err)
assert.NoError(t, shuffleRunes([]rune("a")))
}

View File

@@ -2,13 +2,15 @@ package validator
import (
"baron-sso-backend/internal/domain"
"errors"
"net/http"
"testing"
)
// MockProvider는 IdentityProvider 인터페이스를 구현하는 테스트용 구조체입니다.
type MockProvider struct {
Supported []string
Supported []string
MetadataErr error
}
func (m *MockProvider) Name() string {
@@ -16,6 +18,9 @@ func (m *MockProvider) Name() string {
}
func (m *MockProvider) GetMetadata() (*domain.IDPMetadata, error) {
if m.MetadataErr != nil {
return nil, m.MetadataErr
}
return &domain.IDPMetadata{
SupportedFields: m.Supported,
}, nil
@@ -118,3 +123,23 @@ func TestValidateIDPCompatibility(t *testing.T) {
})
}
}
func TestValidateIDPCompatibilityMetadataError(t *testing.T) {
mockIDP := &MockProvider{MetadataErr: errors.New("metadata unavailable")}
err := ValidateIDPCompatibility(domain.BrokerUser{}, mockIDP)
if err == nil {
t.Fatalf("expected metadata error")
}
if got := err.Error(); got != "failed to fetch metadata from IDP MockIDP: metadata unavailable" {
t.Fatalf("unexpected error: %s", got)
}
}
func TestValidateIDPCompatibilityPointerModel(t *testing.T) {
mockIDP := &MockProvider{Supported: []string{"id", "email", "grade", "department"}}
if err := ValidateIDPCompatibility(&domain.BrokerUser{}, mockIDP); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}