forgejo-runner-optimiser/internal/receiver/token_test.go
2026-02-13 12:48:57 +01:00

180 lines
5.5 KiB
Go

package receiver
import (
"strconv"
"strings"
"testing"
"time"
)
func TestGenerateToken_Format(t *testing.T) {
token := GenerateToken("key", "org", "repo", "wf", "job")
parts := strings.SplitN(token, ":", 2)
if len(parts) != 2 {
t.Fatalf("token should have format 'timestamp:hmac', got %q", token)
}
if len(parts[1]) != 64 {
t.Errorf("HMAC part length = %d, want 64", len(parts[1]))
}
}
func TestGenerateTokenAt_Deterministic(t *testing.T) {
ts := time.Unix(1700000000, 0)
token1 := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
token2 := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
if token1 != token2 {
t.Errorf("tokens differ: %q vs %q", token1, token2)
}
}
func TestGenerateTokenAt_ScopePinning(t *testing.T) {
ts := time.Unix(1700000000, 0)
base := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
variants := []struct {
name string
org string
repo string
wf string
job string
}{
{"different org", "other-org", "repo", "wf", "job"},
{"different repo", "org", "other-repo", "wf", "job"},
{"different workflow", "org", "repo", "other-wf", "job"},
{"different job", "org", "repo", "wf", "other-job"},
}
for _, v := range variants {
t.Run(v.name, func(t *testing.T) {
token := GenerateTokenAt("key", v.org, v.repo, v.wf, v.job, ts)
if token == base {
t.Errorf("token for %s should differ from base", v.name)
}
})
}
}
func TestGenerateTokenAt_DifferentKeys(t *testing.T) {
ts := time.Unix(1700000000, 0)
token1 := GenerateTokenAt("key-a", "org", "repo", "wf", "job", ts)
token2 := GenerateTokenAt("key-b", "org", "repo", "wf", "job", ts)
if token1 == token2 {
t.Error("different keys should produce different tokens")
}
}
func TestGenerateTokenAt_DifferentTimestamps(t *testing.T) {
ts1 := time.Unix(1700000000, 0)
ts2 := time.Unix(1700000001, 0)
token1 := GenerateTokenAt("key", "org", "repo", "wf", "job", ts1)
token2 := GenerateTokenAt("key", "org", "repo", "wf", "job", ts2)
if token1 == token2 {
t.Error("different timestamps should produce different tokens")
}
}
func TestValidateToken_Correct(t *testing.T) {
ts := time.Now()
token := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
if !ValidateToken("key", token, "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should accept correct token")
}
}
func TestValidateToken_WrongToken(t *testing.T) {
if ValidateToken("key", "12345:deadbeef", "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should reject wrong token")
}
}
func TestValidateToken_WrongScope(t *testing.T) {
ts := time.Now()
token := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
if ValidateToken("key", token, "org", "repo", "wf", "other-job", 5*time.Minute) {
t.Error("ValidateToken should reject token for different scope")
}
}
func TestValidateToken_Expired(t *testing.T) {
ts := time.Now().Add(-10 * time.Minute)
token := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
if ValidateToken("key", token, "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should reject expired token")
}
}
func TestValidateTokenAt_NotExpired(t *testing.T) {
tokenTime := time.Unix(1700000000, 0)
token := GenerateTokenAt("key", "org", "repo", "wf", "job", tokenTime)
// Validate at 4 minutes later (within 5 minute TTL)
now := tokenTime.Add(4 * time.Minute)
if !ValidateTokenAt("key", token, "org", "repo", "wf", "job", 5*time.Minute, now) {
t.Error("ValidateTokenAt should accept token within TTL")
}
}
func TestValidateTokenAt_JustExpired(t *testing.T) {
tokenTime := time.Unix(1700000000, 0)
token := GenerateTokenAt("key", "org", "repo", "wf", "job", tokenTime)
// Validate at 6 minutes later (beyond 5 minute TTL)
now := tokenTime.Add(6 * time.Minute)
if ValidateTokenAt("key", token, "org", "repo", "wf", "job", 5*time.Minute, now) {
t.Error("ValidateTokenAt should reject token beyond TTL")
}
}
func TestValidateToken_InvalidFormat(t *testing.T) {
if ValidateToken("key", "no-colon-here", "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should reject token without colon")
}
if ValidateToken("key", "not-a-number:abc123", "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should reject token with invalid timestamp")
}
}
func TestParseTokenTimestamp(t *testing.T) {
ts := time.Unix(1700000000, 0)
token := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
parsed, err := ParseTokenTimestamp(token)
if err != nil {
t.Fatalf("ParseTokenTimestamp failed: %v", err)
}
if !parsed.Equal(ts) {
t.Errorf("parsed timestamp = %v, want %v", parsed, ts)
}
}
func TestParseTokenTimestamp_Invalid(t *testing.T) {
_, err := ParseTokenTimestamp("no-colon")
if err == nil {
t.Error("ParseTokenTimestamp should fail on missing colon")
}
_, err = ParseTokenTimestamp("not-a-number:abc123")
if err == nil {
t.Error("ParseTokenTimestamp should fail on invalid timestamp")
}
}
func TestValidateToken_TamperedTimestamp(t *testing.T) {
// Generate a valid token
ts := time.Now()
token := GenerateTokenAt("key", "org", "repo", "wf", "job", ts)
parts := strings.SplitN(token, ":", 2)
if len(parts) != 2 {
t.Fatalf("unexpected token format: %q", token)
}
hmacPart := parts[1]
// Tamper with timestamp (e.g., attacker tries to extend token lifetime)
tamperedTimestamp := strconv.FormatInt(time.Now().Add(1*time.Hour).Unix(), 10)
tamperedToken := tamperedTimestamp + ":" + hmacPart
if ValidateToken("key", tamperedToken, "org", "repo", "wf", "job", 5*time.Minute) {
t.Error("ValidateToken should reject token with tampered timestamp")
}
}