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") } }