generated from coulomb/repo-seed
Some checks failed
Build and Publish Container Image / build-and-push (push) Has been cancelled
331 lines
10 KiB
Go
331 lines
10 KiB
Go
package privacyidea_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"keycape/internal/adapters/privacyidea"
|
|
"keycape/internal/domain"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mock HTTP client
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// mockHTTPClient implements privacyidea.HTTPClient for test injection.
|
|
type mockHTTPClient struct {
|
|
doFn func(req *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
|
if m.doFn != nil {
|
|
return m.doFn(req)
|
|
}
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader("{}")),
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// testConfig returns a minimal Config suitable for tests.
|
|
func testConfig() privacyidea.Config {
|
|
return privacyidea.Config{
|
|
BaseURL: "https://privacyidea.local",
|
|
AdminToken: "service-jwt",
|
|
Realm: "netkingdom",
|
|
}
|
|
}
|
|
|
|
// jsonResponse returns a *http.Response with a JSON body and status 200.
|
|
func jsonResponse(body string) *http.Response {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(body)),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
}
|
|
}
|
|
|
|
// tokenListResponse builds a privacyIDEA /token/ JSON response.
|
|
func tokenListResponse(tokens []map[string]interface{}) string {
|
|
tokenJSON := "["
|
|
for i, t := range tokens {
|
|
if i > 0 {
|
|
tokenJSON += ","
|
|
}
|
|
active, _ := t["active"].(bool)
|
|
tokenJSON += fmt.Sprintf(`{"serial":"TOK%d","active":%v}`, i, active)
|
|
}
|
|
tokenJSON += "]"
|
|
return fmt.Sprintf(`{"result":{"status":true,"value":{"tokens":%s}}}`, tokenJSON)
|
|
}
|
|
|
|
// validateResponse builds a privacyIDEA /validate/check JSON response.
|
|
func validateResponse(success bool) string {
|
|
return fmt.Sprintf(`{"result":{"status":true,"value":%v}}`, success)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CheckMFARequired — tokens present
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCheckMFARequired_ActiveTokenPresent_ReturnsTrue(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(req *http.Request) (*http.Response, error) {
|
|
if req.Method != http.MethodGet {
|
|
t.Errorf("expected GET, got %s", req.Method)
|
|
}
|
|
if !strings.Contains(req.URL.String(), "alice") {
|
|
t.Errorf("expected user in URL, got: %s", req.URL)
|
|
}
|
|
return jsonResponse(tokenListResponse([]map[string]interface{}{
|
|
{"active": true},
|
|
})), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
required, err := adapter.CheckMFARequired(context.Background(), "alice")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !required {
|
|
t.Error("expected MFA required=true when active token present")
|
|
}
|
|
}
|
|
|
|
func TestCheckMFARequired_RequireForAll_ReturnsTrueWithoutTokenList(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
t.Fatal("token-list endpoint must not be called when RequireForAll is enabled")
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
cfg := testConfig()
|
|
cfg.RequireForAll = true
|
|
adapter := privacyidea.New(cfg, client)
|
|
|
|
required, err := adapter.CheckMFARequired(context.Background(), "alice")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !required {
|
|
t.Error("expected MFA required=true when RequireForAll is enabled")
|
|
}
|
|
}
|
|
|
|
func TestCheckMFARequired_InactiveTokenOnly_ReturnsFalse(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return jsonResponse(tokenListResponse([]map[string]interface{}{
|
|
{"active": false},
|
|
})), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
required, err := adapter.CheckMFARequired(context.Background(), "bob")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if required {
|
|
t.Error("expected MFA required=false when only inactive tokens present")
|
|
}
|
|
}
|
|
|
|
func TestCheckMFARequired_NoTokens_ReturnsFalse(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return jsonResponse(tokenListResponse(nil)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
required, err := adapter.CheckMFARequired(context.Background(), "charlie")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if required {
|
|
t.Error("expected MFA required=false when no tokens")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CheckMFARequired — error cases (fail closed)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCheckMFARequired_HTTPError_ReturnsError(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return nil, fmt.Errorf("connection refused")
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
_, err := adapter.CheckMFARequired(context.Background(), "alice")
|
|
if err == nil {
|
|
t.Fatal("expected error on HTTP failure, got nil (must fail closed)")
|
|
}
|
|
}
|
|
|
|
func TestCheckMFARequired_Non200Status_ReturnsError(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusInternalServerError,
|
|
Body: io.NopCloser(strings.NewReader(`{"result":{"status":false}}`)),
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
_, err := adapter.CheckMFARequired(context.Background(), "alice")
|
|
if err == nil {
|
|
t.Fatal("expected error on non-200 status, got nil (must fail closed)")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CheckMFARequired — Authorization header
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestCheckMFARequired_SendsAdminToken(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(req *http.Request) (*http.Response, error) {
|
|
auth := req.Header.Get("Authorization")
|
|
if !strings.HasPrefix(auth, "Bearer ") {
|
|
t.Errorf("expected Bearer token in Authorization, got %q", auth)
|
|
}
|
|
if !strings.Contains(auth, "service-jwt") {
|
|
t.Errorf("expected admin token in Authorization header, got %q", auth)
|
|
}
|
|
return jsonResponse(tokenListResponse(nil)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
_, _ = adapter.CheckMFARequired(context.Background(), "alice")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateMFAToken — success
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestValidateMFAToken_ValidOTP_ReturnsNil(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(req *http.Request) (*http.Response, error) {
|
|
if req.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", req.Method)
|
|
}
|
|
body, _ := io.ReadAll(req.Body)
|
|
bodyStr := string(body)
|
|
if !strings.Contains(bodyStr, "alice") {
|
|
t.Errorf("expected user in POST body, got: %s", bodyStr)
|
|
}
|
|
if !strings.Contains(bodyStr, "123456") {
|
|
t.Errorf("expected OTP in POST body, got: %s", bodyStr)
|
|
}
|
|
return jsonResponse(validateResponse(true)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
err := adapter.ValidateMFAToken(context.Background(), "alice", "123456")
|
|
if err != nil {
|
|
t.Errorf("expected nil error for valid OTP, got %v", err)
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateMFAToken — failure
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestValidateMFAToken_InvalidOTP_ReturnsErrMFAFailed(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return jsonResponse(validateResponse(false)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
err := adapter.ValidateMFAToken(context.Background(), "alice", "wrong")
|
|
if err == nil {
|
|
t.Fatal("expected ErrMFAFailed, got nil")
|
|
}
|
|
if err != domain.ErrMFAFailed {
|
|
t.Errorf("expected domain.ErrMFAFailed, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateMFAToken_HTTPError_ReturnsError(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return nil, fmt.Errorf("network failure")
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
err := adapter.ValidateMFAToken(context.Background(), "alice", "123456")
|
|
if err == nil {
|
|
t.Fatal("expected error on HTTP failure, got nil (must fail closed)")
|
|
}
|
|
}
|
|
|
|
func TestValidateMFAToken_Non200Status_ReturnsError(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(_ *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusBadGateway,
|
|
Body: io.NopCloser(strings.NewReader(`{}`)),
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
err := adapter.ValidateMFAToken(context.Background(), "alice", "123456")
|
|
if err == nil {
|
|
t.Fatal("expected error on non-200 status, got nil (must fail closed)")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ValidateMFAToken — realm is included in request
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestValidateMFAToken_IncludesRealm(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(req *http.Request) (*http.Response, error) {
|
|
body, _ := io.ReadAll(req.Body)
|
|
if !strings.Contains(string(body), "netkingdom") {
|
|
t.Errorf("expected realm in POST body, got: %s", body)
|
|
}
|
|
return jsonResponse(validateResponse(true)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
_ = adapter.ValidateMFAToken(context.Background(), "alice", "000000")
|
|
}
|
|
|
|
func TestCheckMFARequired_IncludesRealm(t *testing.T) {
|
|
client := &mockHTTPClient{
|
|
doFn: func(req *http.Request) (*http.Response, error) {
|
|
if !strings.Contains(req.URL.String(), "netkingdom") {
|
|
t.Errorf("expected realm in request URL, got: %s", req.URL)
|
|
}
|
|
return jsonResponse(tokenListResponse(nil)), nil
|
|
},
|
|
}
|
|
|
|
adapter := privacyidea.New(testConfig(), client)
|
|
_, _ = adapter.CheckMFARequired(context.Background(), "alice")
|
|
}
|