Γ–ffentliche Dateiansicht: Raw-Dateien, Tree, Releases und Issues sind ohne Login verfΓΌgbar.
admin/admin_test.go
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package admin

import (
	"os"
	"testing"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

const testSecret = "test-secret-for-unit-tests-only"

func makeToken(role string, exp time.Time) string {
	claims := jwt.MapClaims{
		"issuer":   "api-gateway",
		"username": "admin",
		"role":     role,
		"exp":      exp.Unix(),
	}
	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	s, _ := tok.SignedString([]byte(testSecret))
	return s
}

func TestMain(m *testing.M) {
	os.Setenv("JWTSECRET", testSecret)
	os.Exit(m.Run())
}

// ─── validateToken ────────────────────────────────────────────────────────────

func TestValidateToken_Valid(t *testing.T) {
	tok := makeToken("admin", time.Now().Add(1*time.Hour))
	if !validateToken(tok) {
		t.Error("valid admin token should pass")
	}
}

func TestValidateToken_WrongRole(t *testing.T) {
	tok := makeToken("viewer", time.Now().Add(1*time.Hour))
	if validateToken(tok) {
		t.Error("non-admin role should be rejected")
	}
}

func TestValidateToken_Expired(t *testing.T) {
	tok := makeToken("admin", time.Now().Add(-1*time.Second))
	if validateToken(tok) {
		t.Error("expired token should be rejected")
	}
}

func TestValidateToken_EmptyString(t *testing.T) {
	if validateToken("") {
		t.Error("empty string should be rejected")
	}
}

func TestValidateToken_Garbage(t *testing.T) {
	if validateToken("not.a.jwt") {
		t.Error("garbage token should be rejected")
	}
}

func TestValidateToken_WrongSecret(t *testing.T) {
	claims := jwt.MapClaims{
		"role": "admin",
		"exp":  time.Now().Add(1 * time.Hour).Unix(),
	}
	tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	s, _ := tok.SignedString([]byte("wrong-secret"))
	if validateToken(s) {
		t.Error("token signed with wrong secret should be rejected")
	}
}

func TestValidateToken_AlgNoneAttack(t *testing.T) {
	// Classic JWT "alg=none" attack: craft a token with no signature.
	// A vulnerable validator would accept this; ours must reject it.
	header := "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0"           // {"alg":"none","typ":"JWT"}
	payload := "eyJyb2xlIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTl9" // {"role":"admin","exp":9999999999}
	noneToken := header + "." + payload + "."
	if validateToken(noneToken) {
		t.Error("alg=none token must be rejected")
	}
}

func TestValidateToken_TamperedSignature(t *testing.T) {
	tok := makeToken("admin", time.Now().Add(1*time.Hour))
	// Replace the last character with a different one to ensure the signature is actually changed.
	last := tok[len(tok)-1]
	replacement := byte('X')
	if last == 'X' {
		replacement = 'Y'
	}
	tampered := tok[:len(tok)-1] + string(replacement)
	if validateToken(tampered) {
		t.Error("token with tampered signature must be rejected")
	}
}

// ─── Login rate limiter ───────────────────────────────────────────────────────

func resetLoginAttempts() {
	loginMu.Lock()
	defer loginMu.Unlock()
	loginAttempts = make(map[string]*loginCounter)
}

func TestLoginRateLimit_AllowsUnderLimit(t *testing.T) {
	resetLoginAttempts()
	ip := "1.2.3.4"
	for i := 0; i < maxLoginAttempts; i++ {
		if !checkLoginRateLimit(ip) {
			t.Fatalf("attempt %d should be allowed (limit=%d)", i+1, maxLoginAttempts)
		}
	}
}

func TestLoginRateLimit_BlocksAfterLimit(t *testing.T) {
	resetLoginAttempts()
	ip := "1.2.3.5"
	for i := 0; i < maxLoginAttempts; i++ {
		checkLoginRateLimit(ip)
	}
	if checkLoginRateLimit(ip) {
		t.Errorf("attempt %d should be blocked (limit=%d)", maxLoginAttempts+1, maxLoginAttempts)
	}
}

func TestLoginRateLimit_DifferentIPsIndependent(t *testing.T) {
	resetLoginAttempts()
	ipA := "10.0.0.1"
	ipB := "10.0.0.2"
	// Exhaust ipA
	for i := 0; i < maxLoginAttempts+1; i++ {
		checkLoginRateLimit(ipA)
	}
	// ipB should still be allowed
	if !checkLoginRateLimit(ipB) {
		t.Error("rate limit on one IP should not affect other IPs")
	}
}

func TestLoginRateLimit_ResetsAfterWindow(t *testing.T) {
	resetLoginAttempts()
	ip := "10.0.0.9"
	// Exhaust the limit
	for i := 0; i < maxLoginAttempts+2; i++ {
		checkLoginRateLimit(ip)
	}
	// Manually backdate the reset time so the window has passed
	loginMu.Lock()
	if entry := loginAttempts[ip]; entry != nil {
		entry.resetAt = time.Now().Add(-1 * time.Second)
	}
	loginMu.Unlock()
	if !checkLoginRateLimit(ip) {
		t.Error("should be allowed again after window expires")
	}
}
Sprachen
Go 46%
JavaScript 45%
Markdown 3.3%
HTML 2.5%
YAML 1.7%
JSON 1.1%
Klonen
HTTPS