diff --git a/auth/auth.go b/auth/auth.go index cf7417e..3291b27 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -40,11 +40,14 @@ import ( coreerr "dappco.re/go/core/log" ) -// Default durations for challenge and session lifetimes. const ( + // DefaultChallengeTTL is the default lifetime for a generated challenge. + // Usage: pass DefaultChallengeTTL into WithChallengeTTL(...) to keep the package default. DefaultChallengeTTL = 5 * time.Minute - DefaultSessionTTL = 24 * time.Hour - nonceBytes = 32 + // DefaultSessionTTL is the default lifetime for an authenticated session. + // Usage: pass DefaultSessionTTL into WithSessionTTL(...) to keep the package default. + DefaultSessionTTL = 24 * time.Hour + nonceBytes = 32 ) // protectedUsers lists usernames that cannot be deleted. @@ -55,6 +58,7 @@ var protectedUsers = map[string]bool{ } // User represents a registered user with PGP credentials. +// Usage: use User with the other exported helpers in this package. type User struct { PublicKey string `json:"public_key"` KeyID string `json:"key_id"` @@ -65,6 +69,7 @@ type User struct { } // Challenge is a PGP-encrypted nonce sent to a client during authentication. +// Usage: use Challenge with the other exported helpers in this package. type Challenge struct { Nonce []byte `json:"nonce"` Encrypted string `json:"encrypted"` // PGP-encrypted nonce (armored) @@ -72,6 +77,7 @@ type Challenge struct { } // Session represents an authenticated session. +// Usage: use Session with the other exported helpers in this package. type Session struct { Token string `json:"token"` UserID string `json:"user_id"` @@ -80,6 +86,7 @@ type Session struct { // Revocation records the details of a revoked user key. // Stored as JSON in the user's .rev file, replacing the legacy placeholder. +// Usage: use Revocation with the other exported helpers in this package. type Revocation struct { UserID string `json:"user_id"` Reason string `json:"reason"` @@ -87,9 +94,11 @@ type Revocation struct { } // Option configures an Authenticator. +// Usage: use Option with the other exported helpers in this package. type Option func(*Authenticator) // WithChallengeTTL sets the lifetime of a challenge before it expires. +// Usage: pass WithChallengeTTL(...) into the related constructor to adjust the default behaviour. func WithChallengeTTL(d time.Duration) Option { return func(a *Authenticator) { a.challengeTTL = d @@ -97,6 +106,7 @@ func WithChallengeTTL(d time.Duration) Option { } // WithSessionTTL sets the lifetime of a session before it expires. +// Usage: pass WithSessionTTL(...) into the related constructor to adjust the default behaviour. func WithSessionTTL(d time.Duration) Option { return func(a *Authenticator) { a.sessionTTL = d @@ -105,6 +115,7 @@ func WithSessionTTL(d time.Duration) Option { // WithSessionStore sets the SessionStore implementation. // If not provided, an in-memory store is used (sessions lost on restart). +// Usage: pass WithSessionStore(...) into the related constructor to adjust the default behaviour. func WithSessionStore(s SessionStore) Option { return func(a *Authenticator) { a.store = s @@ -120,6 +131,7 @@ func WithSessionStore(s SessionStore) Option { // An optional HardwareKey can be provided via WithHardwareKey for // hardware-backed cryptographic operations (PKCS#11, YubiKey, etc.). // See auth/hardware.go for the interface definition and integration points. +// Usage: create an Authenticator with New(...) and then call Register, Login, or CreateChallenge. type Authenticator struct { medium io.Medium store SessionStore @@ -133,6 +145,7 @@ type Authenticator struct { // New creates an Authenticator that persists user data via the given Medium. // By default, sessions are stored in memory. Use WithSessionStore to provide // a persistent implementation (e.g. SQLiteSessionStore). +// Usage: call New(...) to create a ready-to-use value. func New(m io.Medium, opts ...Option) *Authenticator { a := &Authenticator{ medium: m, @@ -159,6 +172,7 @@ func userPath(userID, ext string) string { // produce a userID, generates a PGP keypair (protected by the given password), // and persists the public key, private key, revocation placeholder, password // hash (Argon2id), and encrypted metadata via the Medium. +// Usage: call Register(...) during the package's normal workflow. func (a *Authenticator) Register(username, password string) (*User, error) { const op = "auth.Register" @@ -237,6 +251,7 @@ func (a *Authenticator) Register(username, password string) (*User, error) { // CreateChallenge generates a cryptographic challenge for the given user. // A random nonce is created and encrypted with the user's PGP public key. // The client must decrypt the nonce and sign it to prove key ownership. +// Usage: call CreateChallenge(...) during the package's normal workflow. func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) { const op = "auth.CreateChallenge" @@ -279,6 +294,7 @@ func (a *Authenticator) CreateChallenge(userID string) (*Challenge, error) { // ValidateResponse verifies a signed nonce from the client. The client must // have decrypted the challenge nonce and signed it with their private key. // On success, a new session is created and returned. +// Usage: call ValidateResponse(...) during the package's normal workflow. func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Session, error) { const op = "auth.ValidateResponse" @@ -313,6 +329,7 @@ func (a *Authenticator) ValidateResponse(userID string, signedNonce []byte) (*Se } // ValidateSession checks whether a token maps to a valid, non-expired session. +// Usage: call ValidateSession(...) during the package's normal workflow. func (a *Authenticator) ValidateSession(token string) (*Session, error) { const op = "auth.ValidateSession" @@ -330,6 +347,7 @@ func (a *Authenticator) ValidateSession(token string) (*Session, error) { } // RefreshSession extends the expiry of an existing valid session. +// Usage: call RefreshSession(...) during the package's normal workflow. func (a *Authenticator) RefreshSession(token string) (*Session, error) { const op = "auth.RefreshSession" @@ -351,6 +369,7 @@ func (a *Authenticator) RefreshSession(token string) (*Session, error) { } // RevokeSession removes a session, invalidating the token immediately. +// Usage: call RevokeSession(...) during the package's normal workflow. func (a *Authenticator) RevokeSession(token string) error { const op = "auth.RevokeSession" @@ -363,6 +382,7 @@ func (a *Authenticator) RevokeSession(token string) error { // DeleteUser removes a user and all associated keys from storage. // The "server" user is protected and cannot be deleted (mirroring the // original TypeScript implementation's safeguard). +// Usage: call DeleteUser(...) during the package's normal workflow. func (a *Authenticator) DeleteUser(userID string) error { const op = "auth.DeleteUser" @@ -403,6 +423,8 @@ func (a *Authenticator) DeleteUser(userID string) error { // - Otherwise, falls back to legacy .lthn file with LTHN hash verification. // On successful legacy login, the password is re-hashed with Argon2id and // a .hash file is written (transparent migration). +// +// Usage: call Login(...) for password-based flows when challenge-response is not required. func (a *Authenticator) Login(userID, password string) (*Session, error) { const op = "auth.Login" @@ -455,6 +477,7 @@ func (a *Authenticator) Login(userID, password string) (*Session, error) { // all existing sessions. The caller must provide the current password // (oldPassword) to decrypt existing metadata and the new password (newPassword) // to protect the new keypair. +// Usage: call RotateKeyPair(...) during the package's normal workflow. func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) (*User, error) { const op = "auth.RotateKeyPair" @@ -539,6 +562,7 @@ func (a *Authenticator) RotateKeyPair(userID, oldPassword, newPassword string) ( // RevokeKey marks a user's key as revoked. It verifies the password first, // writes a JSON revocation record to the .rev file (replacing the placeholder), // and invalidates all sessions for the user. +// Usage: call RevokeKey(...) during the package's normal workflow. func (a *Authenticator) RevokeKey(userID, password, reason string) error { const op = "auth.RevokeKey" @@ -576,6 +600,7 @@ func (a *Authenticator) RevokeKey(userID, password, reason string) error { // IsRevoked checks whether a user's key has been revoked by inspecting the // .rev file. Returns true only if the file contains valid revocation JSON // (not the legacy "REVOCATION_PLACEHOLDER" string). +// Usage: call IsRevoked(...) during the package's normal workflow. func (a *Authenticator) IsRevoked(userID string) bool { content, err := a.medium.Read(userPath(userID, ".rev")) if err != nil { @@ -601,6 +626,7 @@ func (a *Authenticator) IsRevoked(userID string) bool { // WriteChallengeFile writes an encrypted challenge to a file for air-gapped // (courier) transport. The challenge is created and then its encrypted nonce // is written to the specified path on the Medium. +// Usage: call WriteChallengeFile(...) during the package's normal workflow. func (a *Authenticator) WriteChallengeFile(userID, path string) error { const op = "auth.WriteChallengeFile" @@ -625,6 +651,7 @@ func (a *Authenticator) WriteChallengeFile(userID, path string) error { // ReadResponseFile reads a signed response from a file and validates it, // completing the air-gapped authentication flow. The file must contain the // raw PGP signature bytes (armored). +// Usage: call ReadResponseFile(...) during the package's normal workflow. func (a *Authenticator) ReadResponseFile(userID, path string) (*Session, error) { const op = "auth.ReadResponseFile" @@ -698,6 +725,7 @@ func (a *Authenticator) createSession(userID string) (*Session, error) { // StartCleanup runs a background goroutine that periodically removes expired // sessions from the store. It stops when the context is cancelled. +// Usage: call StartCleanup(...) during the package's normal workflow. func (a *Authenticator) StartCleanup(ctx context.Context, interval time.Duration) { go func() { ticker := time.NewTicker(interval) diff --git a/auth/auth_test.go b/auth/auth_test.go index 97a3c17..822a571 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -23,7 +23,7 @@ func newTestAuth(opts ...Option) (*Authenticator, *io.MockMedium) { // --- Register --- -func TestRegister_Good(t *testing.T) { +func TestAuth_Register_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("alice", "hunter2") @@ -48,7 +48,7 @@ func TestRegister_Good(t *testing.T) { assert.False(t, user.Created.IsZero()) } -func TestRegister_Bad(t *testing.T) { +func TestAuth_Register_Bad(t *testing.T) { a, _ := newTestAuth() // Register first time succeeds @@ -61,7 +61,7 @@ func TestRegister_Bad(t *testing.T) { assert.Contains(t, err.Error(), "user already exists") } -func TestRegister_Ugly(t *testing.T) { +func TestAuth_Register_Ugly(t *testing.T) { a, _ := newTestAuth() // Empty username/password should still work (PGP allows it) @@ -72,7 +72,7 @@ func TestRegister_Ugly(t *testing.T) { // --- CreateChallenge --- -func TestCreateChallenge_Good(t *testing.T) { +func TestAuth_CreateChallenge_Good(t *testing.T) { a, _ := newTestAuth() user, err := a.Register("charlie", "pass") @@ -87,7 +87,7 @@ func TestCreateChallenge_Good(t *testing.T) { assert.True(t, challenge.ExpiresAt.After(time.Now())) } -func TestCreateChallenge_Bad(t *testing.T) { +func TestAuth_CreateChallenge_Bad(t *testing.T) { a, _ := newTestAuth() // Challenge for non-existent user @@ -96,7 +96,7 @@ func TestCreateChallenge_Bad(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -func TestCreateChallenge_Ugly(t *testing.T) { +func TestAuth_CreateChallenge_Ugly(t *testing.T) { a, _ := newTestAuth() // Empty userID @@ -106,7 +106,7 @@ func TestCreateChallenge_Ugly(t *testing.T) { // --- ValidateResponse (full challenge-response flow) --- -func TestValidateResponse_Good(t *testing.T) { +func TestAuth_ValidateResponse_Good(t *testing.T) { a, m := newTestAuth() // Register user @@ -140,7 +140,7 @@ func TestValidateResponse_Good(t *testing.T) { assert.True(t, session.ExpiresAt.After(time.Now())) } -func TestValidateResponse_Bad(t *testing.T) { +func TestAuth_ValidateResponse_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("eve", "pass") @@ -153,7 +153,7 @@ func TestValidateResponse_Bad(t *testing.T) { assert.Contains(t, err.Error(), "no pending challenge") } -func TestValidateResponse_Ugly(t *testing.T) { +func TestAuth_ValidateResponse_Ugly(t *testing.T) { a, m := newTestAuth(WithChallengeTTL(1 * time.Millisecond)) _, err := a.Register("frank", "pass") @@ -180,7 +180,7 @@ func TestValidateResponse_Ugly(t *testing.T) { // --- ValidateSession --- -func TestValidateSession_Good(t *testing.T) { +func TestAuth_ValidateSession_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("grace", "pass") @@ -196,7 +196,7 @@ func TestValidateSession_Good(t *testing.T) { assert.Equal(t, userID, validated.UserID) } -func TestValidateSession_Bad(t *testing.T) { +func TestAuth_ValidateSession_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.ValidateSession("nonexistent-token") @@ -204,7 +204,7 @@ func TestValidateSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestValidateSession_Ugly(t *testing.T) { +func TestAuth_ValidateSession_Ugly(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("heidi", "pass") @@ -223,7 +223,7 @@ func TestValidateSession_Ugly(t *testing.T) { // --- RefreshSession --- -func TestRefreshSession_Good(t *testing.T) { +func TestAuth_RefreshSession_Good(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Hour)) _, err := a.Register("ivan", "pass") @@ -243,7 +243,7 @@ func TestRefreshSession_Good(t *testing.T) { assert.True(t, refreshed.ExpiresAt.After(originalExpiry)) } -func TestRefreshSession_Bad(t *testing.T) { +func TestAuth_RefreshSession_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.RefreshSession("nonexistent-token") @@ -251,7 +251,7 @@ func TestRefreshSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestRefreshSession_Ugly(t *testing.T) { +func TestAuth_RefreshSession_Ugly(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("judy", "pass") @@ -270,7 +270,7 @@ func TestRefreshSession_Ugly(t *testing.T) { // --- RevokeSession --- -func TestRevokeSession_Good(t *testing.T) { +func TestAuth_RevokeSession_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("karl", "pass") @@ -288,7 +288,7 @@ func TestRevokeSession_Good(t *testing.T) { assert.Error(t, err) } -func TestRevokeSession_Bad(t *testing.T) { +func TestAuth_RevokeSession_Bad(t *testing.T) { a, _ := newTestAuth() err := a.RevokeSession("nonexistent-token") @@ -296,7 +296,7 @@ func TestRevokeSession_Bad(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestRevokeSession_Ugly(t *testing.T) { +func TestAuth_RevokeSession_Ugly(t *testing.T) { a, _ := newTestAuth() // Revoke empty token @@ -306,7 +306,7 @@ func TestRevokeSession_Ugly(t *testing.T) { // --- DeleteUser --- -func TestDeleteUser_Good(t *testing.T) { +func TestAuth_DeleteUser_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("larry", "pass") @@ -334,7 +334,7 @@ func TestDeleteUser_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestDeleteUser_Bad(t *testing.T) { +func TestAuth_DeleteUser_Bad(t *testing.T) { a, _ := newTestAuth() // Protected user "server" cannot be deleted @@ -343,7 +343,7 @@ func TestDeleteUser_Bad(t *testing.T) { assert.Contains(t, err.Error(), "cannot delete protected user") } -func TestDeleteUser_Ugly(t *testing.T) { +func TestAuth_DeleteUser_Ugly(t *testing.T) { a, _ := newTestAuth() // Non-existent user @@ -354,7 +354,7 @@ func TestDeleteUser_Ugly(t *testing.T) { // --- Login --- -func TestLogin_Good(t *testing.T) { +func TestAuth_Login_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("mallory", "secret") @@ -370,7 +370,7 @@ func TestLogin_Good(t *testing.T) { assert.True(t, session.ExpiresAt.After(time.Now())) } -func TestLogin_Bad(t *testing.T) { +func TestAuth_Login_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("nancy", "correct-password") @@ -383,7 +383,7 @@ func TestLogin_Bad(t *testing.T) { assert.Contains(t, err.Error(), "invalid password") } -func TestLogin_Ugly(t *testing.T) { +func TestAuth_Login_Ugly(t *testing.T) { a, _ := newTestAuth() // Login for non-existent user @@ -394,7 +394,7 @@ func TestLogin_Ugly(t *testing.T) { // --- WriteChallengeFile / ReadResponseFile (Air-Gapped) --- -func TestAirGappedFlow_Good(t *testing.T) { +func TestAuth_AirGappedFlow_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("oscar", "airgap-pass") @@ -439,7 +439,7 @@ func TestAirGappedFlow_Good(t *testing.T) { assert.Equal(t, userID, session.UserID) } -func TestWriteChallengeFile_Bad(t *testing.T) { +func TestAuth_WriteChallengeFile_Bad(t *testing.T) { a, _ := newTestAuth() // Challenge for non-existent user @@ -447,7 +447,7 @@ func TestWriteChallengeFile_Bad(t *testing.T) { assert.Error(t, err) } -func TestReadResponseFile_Bad(t *testing.T) { +func TestAuth_ReadResponseFile_Bad(t *testing.T) { a, _ := newTestAuth() // Response file does not exist @@ -455,7 +455,7 @@ func TestReadResponseFile_Bad(t *testing.T) { assert.Error(t, err) } -func TestReadResponseFile_Ugly(t *testing.T) { +func TestAuth_ReadResponseFile_Ugly(t *testing.T) { a, m := newTestAuth() _, err := a.Register("peggy", "pass") @@ -477,13 +477,13 @@ func TestReadResponseFile_Ugly(t *testing.T) { // --- Options --- -func TestWithChallengeTTL_Good(t *testing.T) { +func TestAuth_WithChallengeTTL_Good(t *testing.T) { ttl := 30 * time.Second a, _ := newTestAuth(WithChallengeTTL(ttl)) assert.Equal(t, ttl, a.challengeTTL) } -func TestWithSessionTTL_Good(t *testing.T) { +func TestAuth_WithSessionTTL_Good(t *testing.T) { ttl := 2 * time.Hour a, _ := newTestAuth(WithSessionTTL(ttl)) assert.Equal(t, ttl, a.sessionTTL) @@ -491,7 +491,7 @@ func TestWithSessionTTL_Good(t *testing.T) { // --- Full Round-Trip (Online Flow) --- -func TestFullRoundTrip_Good(t *testing.T) { +func TestAuth_FullRoundTrip_Good(t *testing.T) { a, m := newTestAuth() // 1. Register @@ -541,7 +541,7 @@ func TestFullRoundTrip_Good(t *testing.T) { // --- Concurrent Access --- -func TestConcurrentSessions_Good(t *testing.T) { +func TestAuth_ConcurrentSessions_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("ruth", "pass") @@ -579,9 +579,9 @@ func TestConcurrentSessions_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestConcurrentSessionCreation_Good verifies that 10 goroutines creating +// TestAuth_ConcurrentSessionCreation_Good verifies that 10 goroutines creating // sessions simultaneously do not produce data races or errors. -func TestConcurrentSessionCreation_Good(t *testing.T) { +func TestAuth_ConcurrentSessionCreation_Good(t *testing.T) { a, _ := newTestAuth() // Register 10 distinct users to avoid contention on a single user record @@ -619,9 +619,9 @@ func TestConcurrentSessionCreation_Good(t *testing.T) { } } -// TestSessionTokenUniqueness_Good generates 1000 session tokens and verifies +// TestAuth_SessionTokenUniqueness_Good generates 1000 session tokens and verifies // no collisions without paying the full login hash-verification cost each time. -func TestSessionTokenUniqueness_Good(t *testing.T) { +func TestAuth_SessionTokenUniqueness_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("uniqueness-test", "pass") @@ -645,9 +645,9 @@ func TestSessionTokenUniqueness_Good(t *testing.T) { assert.Len(t, tokens, n, "all 1000 tokens should be unique") } -// TestChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary. +// TestAuth_ChallengeExpiryBoundary_Ugly tests validation right at the 5-minute boundary. // The challenge should still be valid just before expiry and rejected after. -func TestChallengeExpiryBoundary_Ugly(t *testing.T) { +func TestAuth_ChallengeExpiryBoundary_Ugly(t *testing.T) { // Use a very short TTL to test the boundary without sleeping 5 minutes ttl := 50 * time.Millisecond a, m := newTestAuth(WithChallengeTTL(ttl)) @@ -691,9 +691,9 @@ func TestChallengeExpiryBoundary_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "challenge expired") } -// TestEmptyPasswordRegistration_Good verifies that empty password registration works. +// TestAuth_EmptyPasswordRegistration_Good verifies that empty password registration works. // PGP key is generated unencrypted in this case. -func TestEmptyPasswordRegistration_Good(t *testing.T) { +func TestAuth_EmptyPasswordRegistration_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("no-password-user", "") @@ -730,8 +730,8 @@ func TestEmptyPasswordRegistration_Good(t *testing.T) { assert.NotNil(t, crSession) } -// TestVeryLongUsername_Ugly verifies behaviour with a 10K character username. -func TestVeryLongUsername_Ugly(t *testing.T) { +// TestAuth_VeryLongUsername_Ugly verifies behaviour with a 10K character username. +func TestAuth_VeryLongUsername_Ugly(t *testing.T) { a, _ := newTestAuth() longName := core.NewBuilder() @@ -753,8 +753,8 @@ func TestVeryLongUsername_Ugly(t *testing.T) { assert.NotNil(t, session) } -// TestUnicodeUsernamePassword_Good verifies registration and login with Unicode characters. -func TestUnicodeUsernamePassword_Good(t *testing.T) { +// TestAuth_UnicodeUsernamePassword_Good verifies registration and login with Unicode characters. +func TestAuth_UnicodeUsernamePassword_Good(t *testing.T) { a, _ := newTestAuth() // Japanese + emoji + Chinese + Arabic @@ -777,9 +777,9 @@ func TestUnicodeUsernamePassword_Good(t *testing.T) { assert.Error(t, err) } -// TestAirGappedRoundTrip_Good tests the full air-gapped flow: +// TestAuth_AirGappedRoundTrip_Good tests the full air-gapped flow: // WriteChallengeFile -> client signs offline -> ReadResponseFile -func TestAirGappedRoundTrip_Good(t *testing.T) { +func TestAuth_AirGappedRoundTrip_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("airgap-roundtrip", "courier-pass") @@ -832,8 +832,8 @@ func TestAirGappedRoundTrip_Good(t *testing.T) { assert.Equal(t, session.Token, validated.Token) } -// TestRefreshExpiredSession_Bad verifies that refreshing an already-expired session fails. -func TestRefreshExpiredSession_Bad(t *testing.T) { +// TestAuth_RefreshExpiredSession_Bad verifies that refreshing an already-expired session fails. +func TestAuth_RefreshExpiredSession_Bad(t *testing.T) { a, _ := newTestAuth(WithSessionTTL(1 * time.Millisecond)) _, err := a.Register("expired-refresh", "pass") @@ -859,8 +859,8 @@ func TestRefreshExpiredSession_Bad(t *testing.T) { // --- Phase 2: Password Hash Migration --- -// TestRegisterArgon2id_Good verifies that new registrations use Argon2id format. -func TestRegisterArgon2id_Good(t *testing.T) { +// TestAuth_RegisterArgon2id_Good verifies that new registrations use Argon2id format. +func TestAuth_RegisterArgon2id_Good(t *testing.T) { a, m := newTestAuth() user, err := a.Register("argon2-user", "strong-pass") @@ -881,8 +881,8 @@ func TestRegisterArgon2id_Good(t *testing.T) { assert.True(t, core.HasPrefix(user.PasswordHash, "$argon2id$")) } -// TestLoginArgon2id_Good verifies login works with Argon2id hashed password. -func TestLoginArgon2id_Good(t *testing.T) { +// TestAuth_LoginArgon2id_Good verifies login works with Argon2id hashed password. +func TestAuth_LoginArgon2id_Good(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("login-argon2", "my-password") @@ -895,8 +895,8 @@ func TestLoginArgon2id_Good(t *testing.T) { assert.NotEmpty(t, session.Token) } -// TestLoginArgon2id_Bad verifies wrong password fails with Argon2id hash. -func TestLoginArgon2id_Bad(t *testing.T) { +// TestAuth_LoginArgon2id_Bad verifies wrong password fails with Argon2id hash. +func TestAuth_LoginArgon2id_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("login-argon2-bad", "correct") @@ -908,9 +908,9 @@ func TestLoginArgon2id_Bad(t *testing.T) { assert.Contains(t, err.Error(), "invalid password") } -// TestLegacyLTHNMigration_Good verifies that a user registered with the legacy +// TestAuth_LegacyLTHNMigration_Good verifies that a user registered with the legacy // LTHN hash format is transparently migrated to Argon2id on successful login. -func TestLegacyLTHNMigration_Good(t *testing.T) { +func TestAuth_LegacyLTHNMigration_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -950,8 +950,8 @@ func TestLegacyLTHNMigration_Good(t *testing.T) { assert.NotEmpty(t, session2.Token) } -// TestLegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users. -func TestLegacyLTHNLogin_Bad(t *testing.T) { +// TestAuth_LegacyLTHNLogin_Bad verifies wrong password fails for legacy LTHN users. +func TestAuth_LegacyLTHNLogin_Bad(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -976,9 +976,9 @@ func TestLegacyLTHNLogin_Bad(t *testing.T) { // --- Phase 2: Key Rotation --- -// TestRotateKeyPair_Good verifies the full key rotation flow: +// TestAuth_RotateKeyPair_Good verifies the full key rotation flow: // register -> login -> rotate -> verify old key can't decrypt -> verify new key works -> sessions invalidated. -func TestRotateKeyPair_Good(t *testing.T) { +func TestAuth_RotateKeyPair_Good(t *testing.T) { a, m := newTestAuth() // Register and login @@ -1032,8 +1032,8 @@ func TestRotateKeyPair_Good(t *testing.T) { assert.True(t, core.HasPrefix(meta.PasswordHash, "$argon2id$")) } -// TestRotateKeyPair_Bad verifies that rotation fails with wrong old password. -func TestRotateKeyPair_Bad(t *testing.T) { +// TestAuth_RotateKeyPair_Bad verifies that rotation fails with wrong old password. +func TestAuth_RotateKeyPair_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("rotate-bad", "correct-pass") @@ -1046,8 +1046,8 @@ func TestRotateKeyPair_Bad(t *testing.T) { assert.Contains(t, err.Error(), "failed to decrypt metadata") } -// TestRotateKeyPair_Ugly verifies rotation for non-existent user. -func TestRotateKeyPair_Ugly(t *testing.T) { +// TestAuth_RotateKeyPair_Ugly verifies rotation for non-existent user. +func TestAuth_RotateKeyPair_Ugly(t *testing.T) { a, _ := newTestAuth() _, err := a.RotateKeyPair("nonexistent-user-id", "old", "new") @@ -1055,9 +1055,9 @@ func TestRotateKeyPair_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestRotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key +// TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good verifies old private key // cannot decrypt metadata after rotation. -func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { +func TestAuth_RotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("rotate-crypto", "pass-a") @@ -1081,9 +1081,9 @@ func TestRotateKeyPair_OldKeyCannotDecrypt_Good(t *testing.T) { // --- Phase 2: Key Revocation --- -// TestRevokeKey_Good verifies the full revocation flow: +// TestAuth_RevokeKey_Good verifies the full revocation flow: // register -> login -> revoke -> login fails -> challenge fails -> sessions invalidated. -func TestRevokeKey_Good(t *testing.T) { +func TestAuth_RevokeKey_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("revoke-user", "pass") @@ -1131,8 +1131,8 @@ func TestRevokeKey_Good(t *testing.T) { assert.Error(t, err) } -// TestRevokeKey_Bad verifies revocation fails with wrong password. -func TestRevokeKey_Bad(t *testing.T) { +// TestAuth_RevokeKey_Bad verifies revocation fails with wrong password. +func TestAuth_RevokeKey_Bad(t *testing.T) { a, _ := newTestAuth() _, err := a.Register("revoke-bad", "correct") @@ -1147,8 +1147,8 @@ func TestRevokeKey_Bad(t *testing.T) { assert.False(t, a.IsRevoked(userID)) } -// TestRevokeKey_Ugly verifies revocation for non-existent user. -func TestRevokeKey_Ugly(t *testing.T) { +// TestAuth_RevokeKey_Ugly verifies revocation for non-existent user. +func TestAuth_RevokeKey_Ugly(t *testing.T) { a, _ := newTestAuth() err := a.RevokeKey("nonexistent-user-id", "pass", "reason") @@ -1156,9 +1156,9 @@ func TestRevokeKey_Ugly(t *testing.T) { assert.Contains(t, err.Error(), "user not found") } -// TestIsRevoked_Placeholder_Good verifies that the legacy placeholder is not +// TestAuth_IsRevoked_Placeholder_Good verifies that the legacy placeholder is not // treated as a valid revocation. -func TestIsRevoked_Placeholder_Good(t *testing.T) { +func TestAuth_IsRevoked_Placeholder_Good(t *testing.T) { a, m := newTestAuth() _, err := a.Register("placeholder-user", "pass") @@ -1174,16 +1174,16 @@ func TestIsRevoked_Placeholder_Good(t *testing.T) { assert.False(t, a.IsRevoked(userID)) } -// TestIsRevoked_NoRevFile_Good verifies that a missing .rev file returns false. -func TestIsRevoked_NoRevFile_Good(t *testing.T) { +// TestAuth_IsRevoked_NoRevFile_Good verifies that a missing .rev file returns false. +func TestAuth_IsRevoked_NoRevFile_Good(t *testing.T) { a, _ := newTestAuth() assert.False(t, a.IsRevoked("completely-nonexistent")) } -// TestRevokeKey_LegacyUser_Good verifies revocation works for a legacy user +// TestAuth_RevokeKey_LegacyUser_Good verifies revocation works for a legacy user // with only a .lthn hash file (no .hash file). -func TestRevokeKey_LegacyUser_Good(t *testing.T) { +func TestAuth_RevokeKey_LegacyUser_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) diff --git a/auth/hardware.go b/auth/hardware.go index c5017fd..2af5ac7 100644 --- a/auth/hardware.go +++ b/auth/hardware.go @@ -17,6 +17,7 @@ package auth // similar tamper-resistant devices. // // All methods must be safe for concurrent use. +// Usage: implement HardwareKey and pass it to WithHardwareKey(...) to wire hardware-backed auth into New(...). type HardwareKey interface { // Sign produces a cryptographic signature over the given data using the // hardware-stored private key. The signature format depends on the @@ -44,6 +45,7 @@ type HardwareKey interface { // // This is a forward-looking option — integration points are documented in // auth.go but not yet wired up. +// Usage: pass WithHardwareKey(...) into New(...) to enable a HardwareKey implementation. func WithHardwareKey(hk HardwareKey) Option { return func(a *Authenticator) { a.hardwareKey = hk diff --git a/auth/session_store.go b/auth/session_store.go index f38f266..aed4ffc 100644 --- a/auth/session_store.go +++ b/auth/session_store.go @@ -9,9 +9,11 @@ import ( ) // ErrSessionNotFound is returned when a session token is not found. +// Usage: compare returned errors against ErrSessionNotFound when branching on failures. var ErrSessionNotFound = coreerr.E("auth", "session not found", nil) // SessionStore abstracts session persistence. +// Usage: use SessionStore with the other exported helpers in this package. type SessionStore interface { Get(token string) (*Session, error) Set(session *Session) error @@ -21,12 +23,14 @@ type SessionStore interface { } // MemorySessionStore is an in-memory SessionStore backed by a map. +// Usage: use MemorySessionStore with the other exported helpers in this package. type MemorySessionStore struct { mu sync.RWMutex sessions map[string]*Session } // NewMemorySessionStore creates a new in-memory session store. +// Usage: call NewMemorySessionStore(...) to create a ready-to-use value. func NewMemorySessionStore() *MemorySessionStore { return &MemorySessionStore{ sessions: make(map[string]*Session), @@ -34,6 +38,7 @@ func NewMemorySessionStore() *MemorySessionStore { } // Get retrieves a session by token. +// Usage: call Get(...) during the package's normal workflow. func (m *MemorySessionStore) Get(token string) (*Session, error) { m.mu.RLock() defer m.mu.RUnlock() @@ -49,6 +54,7 @@ func (m *MemorySessionStore) Get(token string) (*Session, error) { } // Set stores a session, keyed by its token. +// Usage: call Set(...) during the package's normal workflow. func (m *MemorySessionStore) Set(session *Session) error { m.mu.Lock() defer m.mu.Unlock() @@ -60,6 +66,7 @@ func (m *MemorySessionStore) Set(session *Session) error { } // Delete removes a session by token. +// Usage: call Delete(...) during the package's normal workflow. func (m *MemorySessionStore) Delete(token string) error { m.mu.Lock() defer m.mu.Unlock() @@ -73,6 +80,7 @@ func (m *MemorySessionStore) Delete(token string) error { } // DeleteByUser removes all sessions belonging to the given user. +// Usage: call DeleteByUser(...) during the package's normal workflow. func (m *MemorySessionStore) DeleteByUser(userID string) error { m.mu.Lock() defer m.mu.Unlock() @@ -84,6 +92,7 @@ func (m *MemorySessionStore) DeleteByUser(userID string) error { } // Cleanup removes all expired sessions and returns the count removed. +// Usage: call Cleanup(...) during the package's normal workflow. func (m *MemorySessionStore) Cleanup() (int, error) { m.mu.Lock() defer m.mu.Unlock() diff --git a/auth/session_store_sqlite.go b/auth/session_store_sqlite.go index 1d61ff2..73993d5 100644 --- a/auth/session_store_sqlite.go +++ b/auth/session_store_sqlite.go @@ -12,6 +12,7 @@ const sessionGroup = "sessions" // SQLiteSessionStore is a SessionStore backed by core/store (SQLite KV). // A mutex serialises all operations because SQLite is single-writer. +// Usage: use SQLiteSessionStore with the other exported helpers in this package. type SQLiteSessionStore struct { mu sync.Mutex store *store.Store @@ -19,6 +20,7 @@ type SQLiteSessionStore struct { // NewSQLiteSessionStore creates a new SQLite-backed session store. // Use ":memory:" for testing or a file path for persistent storage. +// Usage: call NewSQLiteSessionStore(...) to create a ready-to-use value. func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) { s, err := store.New(dbPath) if err != nil { @@ -28,6 +30,7 @@ func NewSQLiteSessionStore(dbPath string) (*SQLiteSessionStore, error) { } // Get retrieves a session by token from SQLite. +// Usage: call Get(...) during the package's normal workflow. func (s *SQLiteSessionStore) Get(token string) (*Session, error) { s.mu.Lock() defer s.mu.Unlock() @@ -50,6 +53,7 @@ func (s *SQLiteSessionStore) Get(token string) (*Session, error) { } // Set stores a session in SQLite, keyed by its token. +// Usage: call Set(...) during the package's normal workflow. func (s *SQLiteSessionStore) Set(session *Session) error { s.mu.Lock() defer s.mu.Unlock() @@ -63,6 +67,7 @@ func (s *SQLiteSessionStore) Set(session *Session) error { } // Delete removes a session by token from SQLite. +// Usage: call Delete(...) during the package's normal workflow. func (s *SQLiteSessionStore) Delete(token string) error { s.mu.Lock() defer s.mu.Unlock() @@ -79,6 +84,7 @@ func (s *SQLiteSessionStore) Delete(token string) error { } // DeleteByUser removes all sessions belonging to the given user. +// Usage: call DeleteByUser(...) during the package's normal workflow. func (s *SQLiteSessionStore) DeleteByUser(userID string) error { s.mu.Lock() defer s.mu.Unlock() @@ -104,6 +110,7 @@ func (s *SQLiteSessionStore) DeleteByUser(userID string) error { } // Cleanup removes all expired sessions and returns the count removed. +// Usage: call Cleanup(...) during the package's normal workflow. func (s *SQLiteSessionStore) Cleanup() (int, error) { s.mu.Lock() defer s.mu.Unlock() @@ -132,6 +139,7 @@ func (s *SQLiteSessionStore) Cleanup() (int, error) { } // Close closes the underlying SQLite store. +// Usage: call Close(...) during the package's normal workflow. func (s *SQLiteSessionStore) Close() error { s.mu.Lock() defer s.mu.Unlock() diff --git a/auth/session_store_test.go b/auth/session_store_test.go index 977e159..896bdbf 100644 --- a/auth/session_store_test.go +++ b/auth/session_store_test.go @@ -16,7 +16,7 @@ import ( // --- MemorySessionStore --- -func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_GetSetDelete_Good(t *testing.T) { store := NewMemorySessionStore() session := &Session{ @@ -44,21 +44,21 @@ func TestMemorySessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStore_GetNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() _, err := store.Get("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_MemorySessionStore_DeleteNotFound_Bad(t *testing.T) { store := NewMemorySessionStore() err := store.Delete("nonexistent-token") assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_DeleteByUser_Good(t *testing.T) { store := NewMemorySessionStore() // Create sessions for two users @@ -94,7 +94,7 @@ func TestMemorySessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestMemorySessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_Cleanup_Good(t *testing.T) { store := NewMemorySessionStore() // Create expired and valid sessions @@ -134,7 +134,7 @@ func TestMemorySessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestMemorySessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_MemorySessionStore_Concurrent_Good(t *testing.T) { store := NewMemorySessionStore() const n = 20 @@ -164,7 +164,7 @@ func TestMemorySessionStore_Concurrent_Good(t *testing.T) { // --- SQLiteSessionStore --- -func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_GetSetDelete_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -194,7 +194,7 @@ func TestSQLiteSessionStore_GetSetDelete_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_GetNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -203,7 +203,7 @@ func TestSQLiteSessionStore_GetNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -212,7 +212,7 @@ func TestSQLiteSessionStore_DeleteNotFound_Bad(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_DeleteByUser_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -250,7 +250,7 @@ func TestSQLiteSessionStore_DeleteByUser_Good(t *testing.T) { assert.Equal(t, "user-b", got.UserID) } -func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Cleanup_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -292,7 +292,7 @@ func TestSQLiteSessionStore_Cleanup_Good(t *testing.T) { assert.ErrorIs(t, err, ErrSessionNotFound) } -func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Persistence_Good(t *testing.T) { dir := t.TempDir() dbPath := core.Path(dir, "sessions.db") @@ -323,7 +323,7 @@ func TestSQLiteSessionStore_Persistence_Good(t *testing.T) { assert.Equal(t, "persist-token", got.Token) } -func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_Concurrent_Good(t *testing.T) { // Use a temp file — :memory: SQLite has concurrency limitations dbPath := core.Path(t.TempDir(), "concurrent.db") store, err := NewSQLiteSessionStore(dbPath) @@ -359,7 +359,7 @@ func TestSQLiteSessionStore_Concurrent_Good(t *testing.T) { // --- Authenticator with SessionStore --- -func TestAuthenticator_WithSessionStore_Good(t *testing.T) { +func TestSessionStore_Authenticator_WithSessionStore_Good(t *testing.T) { sqliteStore, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer sqliteStore.Close() @@ -398,7 +398,7 @@ func TestAuthenticator_WithSessionStore_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestAuthenticator_DefaultStore_Good(t *testing.T) { +func TestSessionStore_Authenticator_DefaultStore_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -407,7 +407,7 @@ func TestAuthenticator_DefaultStore_Good(t *testing.T) { assert.True(t, ok, "default store should be MemorySessionStore") } -func TestAuthenticator_StartCleanup_Good(t *testing.T) { +func TestSessionStore_Authenticator_StartCleanup_Good(t *testing.T) { m := io.NewMockMedium() a := New(m, WithSessionTTL(1*time.Millisecond)) @@ -436,7 +436,7 @@ func TestAuthenticator_StartCleanup_Good(t *testing.T) { assert.Contains(t, err.Error(), "session not found") } -func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) { +func TestSessionStore_Authenticator_StartCleanup_CancelStops_Good(t *testing.T) { m := io.NewMockMedium() a := New(m) @@ -448,7 +448,7 @@ func TestAuthenticator_StartCleanup_CancelStops_Good(t *testing.T) { time.Sleep(50 * time.Millisecond) } -func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_UpdateExisting_Good(t *testing.T) { store, err := NewSQLiteSessionStore(":memory:") require.NoError(t, err) defer store.Close() @@ -476,7 +476,7 @@ func TestSQLiteSessionStore_UpdateExisting_Good(t *testing.T) { "updated session should have later expiry") } -func TestSQLiteSessionStore_TempFile_Good(t *testing.T) { +func TestSessionStore_SQLiteSessionStore_TempFile_Good(t *testing.T) { // Verify we can use a real temp file (not :memory:) tmpFile := core.Path(t.TempDir(), "go-crypt-test-session-store.db") diff --git a/cmd/crypt/cmd.go b/cmd/crypt/cmd.go index 66101cd..c544622 100644 --- a/cmd/crypt/cmd.go +++ b/cmd/crypt/cmd.go @@ -7,6 +7,7 @@ func init() { } // AddCryptCommands registers the 'crypt' command group and all subcommands. +// Usage: call AddCryptCommands(...) during the package's normal workflow. func AddCryptCommands(root *cli.Command) { cryptCmd := &cli.Command{ Use: "crypt", diff --git a/cmd/testcmd/cmd_main.go b/cmd/testcmd/cmd_main.go index 17892a1..0d2950b 100644 --- a/cmd/testcmd/cmd_main.go +++ b/cmd/testcmd/cmd_main.go @@ -56,6 +56,7 @@ func initTestFlags() { } // AddTestCommands registers the 'test' command and all subcommands. +// Usage: call AddTestCommands(...) during the package's normal workflow. func AddTestCommands(root *cli.Command) { initTestFlags() root.AddCommand(testCmd) diff --git a/cmd/testcmd/output_test.go b/cmd/testcmd/output_test.go index 381befd..80ed8b0 100644 --- a/cmd/testcmd/output_test.go +++ b/cmd/testcmd/output_test.go @@ -6,19 +6,19 @@ import ( "github.com/stretchr/testify/assert" ) -func TestShortenPackageName_Good(t *testing.T) { +func TestOutput_ShortenPackageName_Good(t *testing.T) { assert.Equal(t, "pkg/foo", shortenPackageName("dappco.re/go/core/pkg/foo")) assert.Equal(t, "cli-php", shortenPackageName("example.com/org/cli-php")) assert.Equal(t, "bar", shortenPackageName("github.com/other/bar")) } -func TestFormatCoverage_Good(t *testing.T) { +func TestOutput_FormatCoverage_Good(t *testing.T) { assert.Contains(t, formatCoverage(85.0), "85.0%") assert.Contains(t, formatCoverage(65.0), "65.0%") assert.Contains(t, formatCoverage(25.0), "25.0%") } -func TestParseTestOutput_Good(t *testing.T) { +func TestOutput_ParseTestOutput_Good(t *testing.T) { output := `ok dappco.re/go/core/pkg/foo 0.100s coverage: 50.0% of statements FAIL dappco.re/go/core/pkg/bar ? dappco.re/go/core/pkg/baz [no test files] @@ -33,7 +33,7 @@ FAIL dappco.re/go/core/pkg/bar assert.Equal(t, 50.0, results.packages[0].coverage) } -func TestPrintCoverageSummary_Good_LongPackageNames(t *testing.T) { +func TestOutput_PrintCoverageSummary_Good_LongPackageNames(t *testing.T) { // This tests the bug fix for long package names causing negative Repeat count results := testResults{ packages: []packageCoverage{ diff --git a/crypt/chachapoly/chachapoly.go b/crypt/chachapoly/chachapoly.go index 733feaa..af66835 100644 --- a/crypt/chachapoly/chachapoly.go +++ b/crypt/chachapoly/chachapoly.go @@ -11,6 +11,7 @@ import ( ) // Encrypt encrypts data using ChaCha20-Poly1305. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(plaintext []byte, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -26,6 +27,7 @@ func Encrypt(plaintext []byte, key []byte) ([]byte, error) { } // Decrypt decrypts data using ChaCha20-Poly1305. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { diff --git a/crypt/chachapoly/chachapoly_test.go b/crypt/chachapoly/chachapoly_test.go index 8e2c548..2c281db 100644 --- a/crypt/chachapoly/chachapoly_test.go +++ b/crypt/chachapoly/chachapoly_test.go @@ -15,7 +15,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) { return 0, core.NewError("read error") } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestChachapoly_EncryptDecrypt_Good(t *testing.T) { key := make([]byte, 32) for i := range key { key[i] = 1 @@ -31,14 +31,14 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_Encrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size plaintext := []byte("test") _, err := Encrypt(plaintext, key) assert.Error(t, err) } -func TestDecrypt_Bad_WrongKey(t *testing.T) { +func TestChachapoly_Decrypt_Bad_WrongKey(t *testing.T) { key1 := make([]byte, 32) key2 := make([]byte, 32) key2[0] = 1 // Different key @@ -51,7 +51,7 @@ func TestDecrypt_Bad_WrongKey(t *testing.T) { assert.Error(t, err) // Should fail authentication } -func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) { +func TestChachapoly_Decrypt_Bad_TamperedCiphertext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("secret") ciphertext, err := Encrypt(plaintext, key) @@ -64,7 +64,7 @@ func TestDecrypt_Bad_TamperedCiphertext(t *testing.T) { assert.Error(t, err) } -func TestEncrypt_Good_EmptyPlaintext(t *testing.T) { +func TestChachapoly_Encrypt_Good_EmptyPlaintext(t *testing.T) { key := make([]byte, 32) plaintext := []byte("") ciphertext, err := Encrypt(plaintext, key) @@ -76,7 +76,7 @@ func TestEncrypt_Good_EmptyPlaintext(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestDecrypt_Bad_ShortCiphertext(t *testing.T) { +func TestChachapoly_Decrypt_Bad_ShortCiphertext(t *testing.T) { key := make([]byte, 32) shortCiphertext := []byte("short") @@ -85,7 +85,7 @@ func TestDecrypt_Bad_ShortCiphertext(t *testing.T) { assert.Contains(t, err.Error(), "too short") } -func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) { +func TestChachapoly_CiphertextDiffersFromPlaintext_Good(t *testing.T) { key := make([]byte, 32) plaintext := []byte("Hello, world!") ciphertext, err := Encrypt(plaintext, key) @@ -93,7 +93,7 @@ func TestCiphertextDiffersFromPlaintext_Good(t *testing.T) { assert.NotEqual(t, plaintext, ciphertext) } -func TestEncrypt_Bad_NonceError(t *testing.T) { +func TestChachapoly_Encrypt_Bad_NonceError(t *testing.T) { key := make([]byte, 32) plaintext := []byte("test") @@ -106,7 +106,7 @@ func TestEncrypt_Bad_NonceError(t *testing.T) { assert.Error(t, err) } -func TestDecrypt_Bad_InvalidKeySize(t *testing.T) { +func TestChachapoly_Decrypt_Bad_InvalidKeySize(t *testing.T) { key := make([]byte, 16) // Wrong size ciphertext := []byte("test") _, err := Decrypt(ciphertext, key) diff --git a/crypt/checksum.go b/crypt/checksum.go index 80b8af5..621e3ff 100644 --- a/crypt/checksum.go +++ b/crypt/checksum.go @@ -11,6 +11,7 @@ import ( ) // SHA256File computes the SHA-256 checksum of a file and returns it as a hex string. +// Usage: call SHA256File(...) during the package's normal workflow. func SHA256File(path string) (string, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -29,6 +30,7 @@ func SHA256File(path string) (string, error) { } // SHA512File computes the SHA-512 checksum of a file and returns it as a hex string. +// Usage: call SHA512File(...) during the package's normal workflow. func SHA512File(path string) (string, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -47,12 +49,14 @@ func SHA512File(path string) (string, error) { } // SHA256Sum computes the SHA-256 checksum of data and returns it as a hex string. +// Usage: call SHA256Sum(...) during the package's normal workflow. func SHA256Sum(data []byte) string { h := sha256.Sum256(data) return hex.EncodeToString(h[:]) } // SHA512Sum computes the SHA-512 checksum of data and returns it as a hex string. +// Usage: call SHA512Sum(...) during the package's normal workflow. func SHA512Sum(data []byte) string { h := sha512.Sum512(data) return hex.EncodeToString(h[:]) diff --git a/crypt/checksum_test.go b/crypt/checksum_test.go index 03f92bb..4b4a7df 100644 --- a/crypt/checksum_test.go +++ b/crypt/checksum_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSHA256Sum_Good(t *testing.T) { +func TestChecksum_SHA256Sum_Good(t *testing.T) { data := []byte("hello") expected := "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" @@ -16,7 +16,7 @@ func TestSHA256Sum_Good(t *testing.T) { assert.Equal(t, expected, result) } -func TestSHA512Sum_Good(t *testing.T) { +func TestChecksum_SHA512Sum_Good(t *testing.T) { data := []byte("hello") expected := "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043" @@ -26,8 +26,8 @@ func TestSHA512Sum_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestSHA256FileEmpty_Good verifies checksum of an empty file. -func TestSHA256FileEmpty_Good(t *testing.T) { +// TestChecksum_SHA256FileEmpty_Good verifies checksum of an empty file. +func TestChecksum_SHA256FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() emptyFile := core.Path(tmpDir, "empty.bin") writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) @@ -39,8 +39,8 @@ func TestSHA256FileEmpty_Good(t *testing.T) { assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash) } -// TestSHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. -func TestSHA512FileEmpty_Good(t *testing.T) { +// TestChecksum_SHA512FileEmpty_Good verifies SHA-512 checksum of an empty file. +func TestChecksum_SHA512FileEmpty_Good(t *testing.T) { tmpDir := t.TempDir() emptyFile := core.Path(tmpDir, "empty.bin") writeResult := (&core.Fs{}).New("/").WriteMode(emptyFile, "", 0o644) @@ -51,22 +51,22 @@ func TestSHA512FileEmpty_Good(t *testing.T) { assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", hash) } -// TestSHA256FileNonExistent_Bad verifies error on non-existent file. -func TestSHA256FileNonExistent_Bad(t *testing.T) { +// TestChecksum_SHA256FileNonExistent_Bad verifies error on non-existent file. +func TestChecksum_SHA256FileNonExistent_Bad(t *testing.T) { _, err := SHA256File("/nonexistent/path/to/file.bin") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open file") } -// TestSHA512FileNonExistent_Bad verifies error on non-existent file. -func TestSHA512FileNonExistent_Bad(t *testing.T) { +// TestChecksum_SHA512FileNonExistent_Bad verifies error on non-existent file. +func TestChecksum_SHA512FileNonExistent_Bad(t *testing.T) { _, err := SHA512File("/nonexistent/path/to/file.bin") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open file") } -// TestSHA256FileWithContent_Good verifies checksum of a file with known content. -func TestSHA256FileWithContent_Good(t *testing.T) { +// TestChecksum_SHA256FileWithContent_Good verifies checksum of a file with known content. +func TestChecksum_SHA256FileWithContent_Good(t *testing.T) { tmpDir := t.TempDir() testFile := core.Path(tmpDir, "test.txt") writeResult := (&core.Fs{}).New("/").WriteMode(testFile, "hello", 0o644) diff --git a/crypt/crypt.go b/crypt/crypt.go index df18f2f..5cf2a32 100644 --- a/crypt/crypt.go +++ b/crypt/crypt.go @@ -7,6 +7,7 @@ import ( // Encrypt encrypts data with a passphrase using ChaCha20-Poly1305. // A random salt is generated and prepended to the output. // Format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(plaintext, passphrase []byte) ([]byte, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -29,6 +30,7 @@ func Encrypt(plaintext, passphrase []byte) ([]byte, error) { // Decrypt decrypts data encrypted with Encrypt. // Expects format: salt (16 bytes) + nonce (24 bytes) + ciphertext. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(ciphertext, passphrase []byte) ([]byte, error) { if len(ciphertext) < argon2SaltLen { return nil, coreerr.E("crypt.Decrypt", "ciphertext too short", nil) @@ -50,6 +52,7 @@ func Decrypt(ciphertext, passphrase []byte) ([]byte, error) { // EncryptAES encrypts data using AES-256-GCM with a passphrase. // A random salt is generated and prepended to the output. // Format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +// Usage: call EncryptAES(...) during the package's normal workflow. func EncryptAES(plaintext, passphrase []byte) ([]byte, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -71,6 +74,7 @@ func EncryptAES(plaintext, passphrase []byte) ([]byte, error) { // DecryptAES decrypts data encrypted with EncryptAES. // Expects format: salt (16 bytes) + nonce (12 bytes) + ciphertext. +// Usage: call DecryptAES(...) during the package's normal workflow. func DecryptAES(ciphertext, passphrase []byte) ([]byte, error) { if len(ciphertext) < argon2SaltLen { return nil, coreerr.E("crypt.DecryptAES", "ciphertext too short", nil) diff --git a/crypt/crypt_test.go b/crypt/crypt_test.go index 7d5c1c6..d266e8a 100644 --- a/crypt/crypt_test.go +++ b/crypt/crypt_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestEncryptDecrypt_Good(t *testing.T) { +func TestCrypt_EncryptDecrypt_Good(t *testing.T) { plaintext := []byte("hello, world!") passphrase := []byte("correct-horse-battery-staple") @@ -21,7 +21,7 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncryptDecrypt_Bad(t *testing.T) { +func TestCrypt_EncryptDecrypt_Bad(t *testing.T) { plaintext := []byte("secret data") passphrase := []byte("correct-passphrase") wrongPassphrase := []byte("wrong-passphrase") @@ -33,7 +33,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecryptAES_Good(t *testing.T) { +func TestCrypt_EncryptDecryptAES_Good(t *testing.T) { plaintext := []byte("hello, AES world!") passphrase := []byte("my-secure-passphrase") @@ -48,8 +48,8 @@ func TestEncryptDecryptAES_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestWrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data. -func TestWrongPassphraseDecrypt_Bad(t *testing.T) { +// TestCrypt_WrongPassphraseDecrypt_Bad verifies wrong passphrase returns error, not corrupt data. +func TestCrypt_WrongPassphraseDecrypt_Bad(t *testing.T) { plaintext := []byte("sensitive payload") passphrase := []byte("correct-passphrase") wrongPassphrase := []byte("wrong-passphrase") @@ -70,8 +70,8 @@ func TestWrongPassphraseDecrypt_Bad(t *testing.T) { assert.Nil(t, decryptedAES, "wrong passphrase must not return partial data (AES)") } -// TestEmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext. -func TestEmptyPlaintextRoundTrip_Good(t *testing.T) { +// TestCrypt_EmptyPlaintextRoundTrip_Good verifies encrypt/decrypt of empty plaintext. +func TestCrypt_EmptyPlaintextRoundTrip_Good(t *testing.T) { passphrase := []byte("test-passphrase") // ChaCha20 @@ -93,8 +93,8 @@ func TestEmptyPlaintextRoundTrip_Good(t *testing.T) { assert.Empty(t, decryptedAES) } -// TestLargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload. -func TestLargePlaintextRoundTrip_Good(t *testing.T) { +// TestCrypt_LargePlaintextRoundTrip_Good verifies encrypt/decrypt of a 1MB payload. +func TestCrypt_LargePlaintextRoundTrip_Good(t *testing.T) { passphrase := []byte("large-payload-passphrase") plaintext := bytes.Repeat([]byte("X"), 1024*1024) // 1MB @@ -116,8 +116,8 @@ func TestLargePlaintextRoundTrip_Good(t *testing.T) { assert.Equal(t, plaintext, decryptedAES) } -// TestDecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected. -func TestDecryptCiphertextTooShort_Ugly(t *testing.T) { +// TestCrypt_DecryptCiphertextTooShort_Ugly verifies short ciphertext is rejected. +func TestCrypt_DecryptCiphertextTooShort_Ugly(t *testing.T) { _, err := Decrypt([]byte("short"), []byte("pass")) assert.Error(t, err) assert.Contains(t, err.Error(), "too short") diff --git a/crypt/hash.go b/crypt/hash.go index bd08003..7239f28 100644 --- a/crypt/hash.go +++ b/crypt/hash.go @@ -14,6 +14,7 @@ import ( // HashPassword hashes a password using Argon2id with default parameters. // Returns a string in the format: $argon2id$v=19$m=65536,t=3,p=4$$ +// Usage: call HashPassword(...) during the package's normal workflow. func HashPassword(password string) (string, error) { salt, err := generateSalt(argon2SaltLen) if err != nil { @@ -34,6 +35,7 @@ func HashPassword(password string) (string, error) { // VerifyPassword verifies a password against an Argon2id hash string. // The hash must be in the format produced by HashPassword. +// Usage: call VerifyPassword(...) during the package's normal workflow. func VerifyPassword(password, hash string) (bool, error) { parts := core.Split(hash, "$") if len(parts) != 6 { @@ -116,6 +118,7 @@ func parsePrefixedUint32(input, prefix string) (uint32, error) { // HashBcrypt hashes a password using bcrypt with the given cost. // Cost must be between bcrypt.MinCost and bcrypt.MaxCost. +// Usage: call HashBcrypt(...) during the package's normal workflow. func HashBcrypt(password string, cost int) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(password), cost) if err != nil { @@ -125,6 +128,7 @@ func HashBcrypt(password string, cost int) (string, error) { } // VerifyBcrypt verifies a password against a bcrypt hash. +// Usage: call VerifyBcrypt(...) during the package's normal workflow. func VerifyBcrypt(password, hash string) (bool, error) { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) if err == bcrypt.ErrMismatchedHashAndPassword { diff --git a/crypt/hash_test.go b/crypt/hash_test.go index ad308a0..8459390 100644 --- a/crypt/hash_test.go +++ b/crypt/hash_test.go @@ -7,7 +7,7 @@ import ( "golang.org/x/crypto/bcrypt" ) -func TestHashPassword_Good(t *testing.T) { +func TestHash_HashPassword_Good(t *testing.T) { password := "my-secure-password" hash, err := HashPassword(password) @@ -20,7 +20,7 @@ func TestHashPassword_Good(t *testing.T) { assert.True(t, match) } -func TestVerifyPassword_Bad(t *testing.T) { +func TestHash_VerifyPassword_Bad(t *testing.T) { password := "my-secure-password" wrongPassword := "wrong-password" @@ -32,7 +32,7 @@ func TestVerifyPassword_Bad(t *testing.T) { assert.False(t, match) } -func TestHashBcrypt_Good(t *testing.T) { +func TestHash_HashBcrypt_Good(t *testing.T) { password := "bcrypt-test-password" hash, err := HashBcrypt(password, bcrypt.DefaultCost) diff --git a/crypt/hmac.go b/crypt/hmac.go index adb80c2..4c94e6d 100644 --- a/crypt/hmac.go +++ b/crypt/hmac.go @@ -8,6 +8,7 @@ import ( ) // HMACSHA256 computes the HMAC-SHA256 of a message using the given key. +// Usage: call HMACSHA256(...) during the package's normal workflow. func HMACSHA256(message, key []byte) []byte { mac := hmac.New(sha256.New, key) mac.Write(message) @@ -15,6 +16,7 @@ func HMACSHA256(message, key []byte) []byte { } // HMACSHA512 computes the HMAC-SHA512 of a message using the given key. +// Usage: call HMACSHA512(...) during the package's normal workflow. func HMACSHA512(message, key []byte) []byte { mac := hmac.New(sha512.New, key) mac.Write(message) @@ -23,6 +25,7 @@ func HMACSHA512(message, key []byte) []byte { // VerifyHMAC verifies an HMAC using constant-time comparison. // hashFunc should be sha256.New, sha512.New, etc. +// Usage: call VerifyHMAC(...) during the package's normal workflow. func VerifyHMAC(message, key, mac []byte, hashFunc func() hash.Hash) bool { expected := hmac.New(hashFunc, key) expected.Write(message) diff --git a/crypt/hmac_test.go b/crypt/hmac_test.go index 31dc474..3f8efe5 100644 --- a/crypt/hmac_test.go +++ b/crypt/hmac_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHMACSHA256_Good(t *testing.T) { +func TestHMAC_HMACSHA256_Good(t *testing.T) { // RFC 4231 Test Case 2 key := []byte("Jefe") message := []byte("what do ya want for nothing?") @@ -18,7 +18,7 @@ func TestHMACSHA256_Good(t *testing.T) { assert.Equal(t, expected, hex.EncodeToString(mac)) } -func TestVerifyHMAC_Good(t *testing.T) { +func TestHMAC_VerifyHMAC_Good(t *testing.T) { key := []byte("secret-key") message := []byte("test message") @@ -28,7 +28,7 @@ func TestVerifyHMAC_Good(t *testing.T) { assert.True(t, valid) } -func TestVerifyHMAC_Bad(t *testing.T) { +func TestHMAC_VerifyHMAC_Bad(t *testing.T) { key := []byte("secret-key") message := []byte("test message") tampered := []byte("tampered message") diff --git a/crypt/kdf.go b/crypt/kdf.go index c3058e8..3e466a6 100644 --- a/crypt/kdf.go +++ b/crypt/kdf.go @@ -25,12 +25,14 @@ const ( // DeriveKey derives a key from a passphrase using Argon2id with default parameters. // The salt must be argon2SaltLen bytes. keyLen specifies the desired key length. +// Usage: call DeriveKey(...) during the package's normal workflow. func DeriveKey(passphrase, salt []byte, keyLen uint32) []byte { return argon2.IDKey(passphrase, salt, argon2Time, argon2Memory, argon2Parallelism, keyLen) } // DeriveKeyScrypt derives a key from a passphrase using scrypt. // Uses recommended parameters: N=32768, r=8, p=1. +// Usage: call DeriveKeyScrypt(...) during the package's normal workflow. func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) { key, err := scrypt.Key(passphrase, salt, 32768, 8, 1, keyLen) if err != nil { @@ -42,6 +44,7 @@ func DeriveKeyScrypt(passphrase, salt []byte, keyLen int) ([]byte, error) { // HKDF derives a key using HKDF-SHA256. // secret is the input keying material, salt is optional (can be nil), // info is optional context, and keyLen is the desired output length. +// Usage: call HKDF(...) during the package's normal workflow. func HKDF(secret, salt, info []byte, keyLen int) ([]byte, error) { reader := hkdf.New(sha256.New, secret, salt, info) key := make([]byte, keyLen) diff --git a/crypt/kdf_test.go b/crypt/kdf_test.go index 2c2082e..a4ada5b 100644 --- a/crypt/kdf_test.go +++ b/crypt/kdf_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDeriveKey_Good(t *testing.T) { +func TestKDF_DeriveKey_Good(t *testing.T) { passphrase := []byte("test-passphrase") salt := []byte("1234567890123456") // 16 bytes @@ -21,7 +21,7 @@ func TestDeriveKey_Good(t *testing.T) { assert.NotEqual(t, key1, key3) } -func TestDeriveKeyScrypt_Good(t *testing.T) { +func TestKDF_DeriveKeyScrypt_Good(t *testing.T) { passphrase := []byte("test-passphrase") salt := []byte("1234567890123456") @@ -35,7 +35,7 @@ func TestDeriveKeyScrypt_Good(t *testing.T) { assert.Equal(t, key, key2) } -func TestHKDF_Good(t *testing.T) { +func TestKDF_HKDF_Good(t *testing.T) { secret := []byte("input-keying-material") salt := []byte("optional-salt") info := []byte("context-info") @@ -57,8 +57,8 @@ func TestHKDF_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestKeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key. -func TestKeyDerivationDeterminism_Good(t *testing.T) { +// TestKDF_KeyDerivationDeterminism_Good verifies same passphrase + salt always yields same key. +func TestKDF_KeyDerivationDeterminism_Good(t *testing.T) { passphrase := []byte("determinism-test-passphrase") salt := []byte("1234567890123456") // 16 bytes @@ -82,8 +82,8 @@ func TestKeyDerivationDeterminism_Good(t *testing.T) { assert.Equal(t, scryptKey1, scryptKey2, "scrypt must also be deterministic") } -// TestHKDFDifferentInfoStrings_Good verifies different info strings produce different keys. -func TestHKDFDifferentInfoStrings_Good(t *testing.T) { +// TestKDF_HKDFDifferentInfoStrings_Good verifies different info strings produce different keys. +func TestKDF_HKDFDifferentInfoStrings_Good(t *testing.T) { secret := []byte("shared-secret-material") salt := []byte("common-salt") @@ -114,8 +114,8 @@ func TestHKDFDifferentInfoStrings_Good(t *testing.T) { } } -// TestHKDFNilSalt_Good verifies HKDF works with nil salt. -func TestHKDFNilSalt_Good(t *testing.T) { +// TestKDF_HKDFNilSalt_Good verifies HKDF works with nil salt. +func TestKDF_HKDFNilSalt_Good(t *testing.T) { secret := []byte("input-keying-material") info := []byte("context") diff --git a/crypt/lthn/lthn.go b/crypt/lthn/lthn.go index a2404f5..d4d24a9 100644 --- a/crypt/lthn/lthn.go +++ b/crypt/lthn/lthn.go @@ -42,11 +42,13 @@ var keyMap = map[rune]rune{ // SetKeyMap replaces the default character substitution map. // Use this to customize the quasi-salt derivation for specific applications. // Changes affect all subsequent Hash and Verify calls. +// Usage: call SetKeyMap(...) during the package's normal workflow. func SetKeyMap(newKeyMap map[rune]rune) { keyMap = newKeyMap } // GetKeyMap returns the current character substitution map. +// Usage: call GetKeyMap(...) during the package's normal workflow. func GetKeyMap() map[rune]rune { return keyMap } @@ -61,6 +63,7 @@ func GetKeyMap() map[rune]rune { // // The same input always produces the same hash, enabling verification // without storing a separate salt value. +// Usage: call Hash(...) when you need a deterministic content-style digest rather than a password hash. func Hash(input string) string { salt := createSalt(input) hash := sha256.Sum256([]byte(input + salt)) @@ -89,6 +92,7 @@ func createSalt(input string) string { // Verify checks if an input string produces the given hash. // Returns true if Hash(input) equals the provided hash value. // Uses constant-time comparison to prevent timing attacks. +// Usage: call Verify(...) during the package's normal workflow. func Verify(input string, hash string) bool { computed := Hash(input) return subtle.ConstantTimeCompare([]byte(computed), []byte(hash)) == 1 diff --git a/crypt/lthn/lthn_test.go b/crypt/lthn/lthn_test.go index 428c0d2..0291379 100644 --- a/crypt/lthn/lthn_test.go +++ b/crypt/lthn/lthn_test.go @@ -7,36 +7,36 @@ import ( "github.com/stretchr/testify/assert" ) -func TestHash_Good(t *testing.T) { +func TestLTHN_Hash_Good(t *testing.T) { hash := Hash("hello") assert.NotEmpty(t, hash) } -func TestVerify_Good(t *testing.T) { +func TestLTHN_Verify_Good(t *testing.T) { hash := Hash("hello") assert.True(t, Verify("hello", hash)) } -func TestVerify_Bad(t *testing.T) { +func TestLTHN_Verify_Bad(t *testing.T) { hash := Hash("hello") assert.False(t, Verify("world", hash)) } -func TestCreateSalt_Good(t *testing.T) { +func TestLTHN_CreateSalt_Good(t *testing.T) { // "hello" reversed: "olleh" -> "0113h" expected := "0113h" actual := createSalt("hello") assert.Equal(t, expected, actual, "Salt should be correctly created for 'hello'") } -func TestCreateSalt_Bad(t *testing.T) { +func TestLTHN_CreateSalt_Bad(t *testing.T) { // Test with an empty string expected := "" actual := createSalt("") assert.Equal(t, expected, actual, "Salt for an empty string should be empty") } -func TestCreateSalt_Ugly(t *testing.T) { +func TestLTHN_CreateSalt_Ugly(t *testing.T) { // Test with characters not in the keyMap input := "world123" // "world123" reversed: "321dlrow" -> "e2ld1r0w" @@ -54,7 +54,7 @@ func TestCreateSalt_Ugly(t *testing.T) { var testKeyMapMu sync.Mutex -func TestSetKeyMap_Good(t *testing.T) { +func TestLTHN_SetKeyMap_Good(t *testing.T) { testKeyMapMu.Lock() originalKeyMap := GetKeyMap() t.Cleanup(func() { diff --git a/crypt/openpgp/service.go b/crypt/openpgp/service.go index 5e1f85f..371dad3 100644 --- a/crypt/openpgp/service.go +++ b/crypt/openpgp/service.go @@ -14,17 +14,20 @@ import ( ) // Service provides OpenPGP cryptographic operations. +// Usage: use Service with the other exported helpers in this package. type Service struct { core *framework.Core } // New creates a new OpenPGP service instance. +// Usage: call New(...) to create a ready-to-use value. func New(c *framework.Core) (any, error) { return &Service{core: c}, nil } // CreateKeyPair generates a new RSA-4096 PGP keypair. // Returns the armored private key string. +// Usage: call CreateKeyPair(...) during the package's normal workflow. func (s *Service) CreateKeyPair(name, passphrase string) (string, error) { config := &packet.Config{ Algorithm: packet.PubKeyAlgoRSA, @@ -100,6 +103,7 @@ func serializeEntity(w goio.Writer, e *openpgp.Entity) error { // EncryptPGP encrypts data for a recipient identified by their public key (armored string in recipientPath). // The encrypted data is written to the provided writer and also returned as an armored string. +// Usage: call EncryptPGP(...) during the package's normal workflow. func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opts ...any) (string, error) { entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(recipientPath)) if err != nil { @@ -135,6 +139,7 @@ func (s *Service) EncryptPGP(writer goio.Writer, recipientPath, data string, opt } // DecryptPGP decrypts a PGP message using the provided armored private key and passphrase. +// Usage: call DecryptPGP(...) during the package's normal workflow. func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any) (string, error) { entityList, err := openpgp.ReadArmoredKeyRing(framework.NewReader(privateKey)) if err != nil { @@ -173,6 +178,7 @@ func (s *Service) DecryptPGP(privateKey, message, passphrase string, opts ...any } // HandleIPCEvents handles PGP-related IPC messages. +// Usage: call HandleIPCEvents(...) during the package's normal workflow. func (s *Service) HandleIPCEvents(c *framework.Core, msg framework.Message) error { switch m := msg.(type) { case map[string]any: diff --git a/crypt/openpgp/service_test.go b/crypt/openpgp/service_test.go index 09044ff..d023033 100644 --- a/crypt/openpgp/service_test.go +++ b/crypt/openpgp/service_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateKeyPair_Good(t *testing.T) { +func TestService_CreateKeyPair_Good(t *testing.T) { c := framework.New() s := &Service{core: c} @@ -19,7 +19,7 @@ func TestCreateKeyPair_Good(t *testing.T) { assert.Contains(t, privKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestService_EncryptDecrypt_Good(t *testing.T) { c := framework.New() s := &Service{core: c} diff --git a/crypt/pgp/pgp.go b/crypt/pgp/pgp.go index 56bfef8..eb09be9 100644 --- a/crypt/pgp/pgp.go +++ b/crypt/pgp/pgp.go @@ -16,6 +16,7 @@ import ( ) // KeyPair holds armored PGP public and private keys. +// Usage: use KeyPair with the other exported helpers in this package. type KeyPair struct { PublicKey string PrivateKey string @@ -24,6 +25,7 @@ type KeyPair struct { // CreateKeyPair generates a new PGP key pair for the given identity. // If password is non-empty, the private key is encrypted with it. // Returns a KeyPair with armored public and private keys. +// Usage: call CreateKeyPair(...) during the package's normal workflow. func CreateKeyPair(name, email, password string) (*KeyPair, error) { const op = "pgp.CreateKeyPair" @@ -116,6 +118,7 @@ func serializeEncryptedEntity(w io.Writer, e *openpgp.Entity) error { // Encrypt encrypts data for the recipient identified by their armored public key. // Returns the encrypted data as armored PGP output. +// Usage: call Encrypt(...) during the package's normal workflow. func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) { const op = "pgp.Encrypt" @@ -149,6 +152,7 @@ func Encrypt(data []byte, publicKeyArmor string) ([]byte, error) { // Decrypt decrypts armored PGP data using the given armored private key. // If the private key is encrypted, the password is used to decrypt it first. +// Usage: call Decrypt(...) during the package's normal workflow. func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) { const op = "pgp.Decrypt" @@ -193,6 +197,7 @@ func Decrypt(data []byte, privateKeyArmor, password string) ([]byte, error) { // Sign creates an armored detached signature for the given data using // the armored private key. If the key is encrypted, the password is used // to decrypt it first. +// Usage: call Sign(...) during the package's normal workflow. func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) { const op = "pgp.Sign" @@ -224,6 +229,7 @@ func Sign(data []byte, privateKeyArmor, password string) ([]byte, error) { // Verify verifies an armored detached signature against the given data // and armored public key. Returns nil if the signature is valid. +// Usage: call Verify(...) during the package's normal workflow. func Verify(data, signature []byte, publicKeyArmor string) error { const op = "pgp.Verify" diff --git a/crypt/pgp/pgp_test.go b/crypt/pgp/pgp_test.go index 4f7edd9..9ab0f5e 100644 --- a/crypt/pgp/pgp_test.go +++ b/crypt/pgp/pgp_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateKeyPair_Good(t *testing.T) { +func TestPGP_CreateKeyPair_Good(t *testing.T) { kp, err := CreateKeyPair("Test User", "test@example.com", "") require.NoError(t, err) require.NotNil(t, kp) @@ -15,7 +15,7 @@ func TestCreateKeyPair_Good(t *testing.T) { assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestCreateKeyPair_Bad(t *testing.T) { +func TestPGP_CreateKeyPair_Bad(t *testing.T) { // Empty name still works (openpgp allows it), but test with password kp, err := CreateKeyPair("Secure User", "secure@example.com", "strong-password") require.NoError(t, err) @@ -24,14 +24,14 @@ func TestCreateKeyPair_Bad(t *testing.T) { assert.Contains(t, kp.PrivateKey, "-----BEGIN PGP PRIVATE KEY BLOCK-----") } -func TestCreateKeyPair_Ugly(t *testing.T) { +func TestPGP_CreateKeyPair_Ugly(t *testing.T) { // Minimal identity kp, err := CreateKeyPair("", "", "") require.NoError(t, err) require.NotNil(t, kp) } -func TestEncryptDecrypt_Good(t *testing.T) { +func TestPGP_EncryptDecrypt_Good(t *testing.T) { kp, err := CreateKeyPair("Test User", "test@example.com", "") require.NoError(t, err) @@ -46,7 +46,7 @@ func TestEncryptDecrypt_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestEncryptDecrypt_Bad(t *testing.T) { +func TestPGP_EncryptDecrypt_Bad(t *testing.T) { kp1, err := CreateKeyPair("User One", "one@example.com", "") require.NoError(t, err) kp2, err := CreateKeyPair("User Two", "two@example.com", "") @@ -61,7 +61,7 @@ func TestEncryptDecrypt_Bad(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecrypt_Ugly(t *testing.T) { +func TestPGP_EncryptDecrypt_Ugly(t *testing.T) { // Invalid public key for encryption _, err := Encrypt([]byte("data"), "not-a-pgp-key") assert.Error(t, err) @@ -71,7 +71,7 @@ func TestEncryptDecrypt_Ugly(t *testing.T) { assert.Error(t, err) } -func TestEncryptDecryptWithPassword_Good(t *testing.T) { +func TestPGP_EncryptDecryptWithPassword_Good(t *testing.T) { password := "my-secret-passphrase" kp, err := CreateKeyPair("Secure User", "secure@example.com", password) require.NoError(t, err) @@ -85,7 +85,7 @@ func TestEncryptDecryptWithPassword_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestSignVerify_Good(t *testing.T) { +func TestPGP_SignVerify_Good(t *testing.T) { kp, err := CreateKeyPair("Signer", "signer@example.com", "") require.NoError(t, err) @@ -99,7 +99,7 @@ func TestSignVerify_Good(t *testing.T) { assert.NoError(t, err) } -func TestSignVerify_Bad(t *testing.T) { +func TestPGP_SignVerify_Bad(t *testing.T) { kp, err := CreateKeyPair("Signer", "signer@example.com", "") require.NoError(t, err) @@ -112,7 +112,7 @@ func TestSignVerify_Bad(t *testing.T) { assert.Error(t, err) } -func TestSignVerify_Ugly(t *testing.T) { +func TestPGP_SignVerify_Ugly(t *testing.T) { // Invalid key for signing _, err := Sign([]byte("data"), "not-a-key", "") assert.Error(t, err) @@ -129,7 +129,7 @@ func TestSignVerify_Ugly(t *testing.T) { assert.Error(t, err) } -func TestSignVerifyWithPassword_Good(t *testing.T) { +func TestPGP_SignVerifyWithPassword_Good(t *testing.T) { password := "signing-password" kp, err := CreateKeyPair("Signer", "signer@example.com", password) require.NoError(t, err) @@ -142,7 +142,7 @@ func TestSignVerifyWithPassword_Good(t *testing.T) { assert.NoError(t, err) } -func TestFullRoundTrip_Good(t *testing.T) { +func TestPGP_FullRoundTrip_Good(t *testing.T) { // Generate keys, encrypt, decrypt, sign, and verify - full round trip kp, err := CreateKeyPair("Full Test", "full@example.com", "") require.NoError(t, err) diff --git a/crypt/rsa/rsa.go b/crypt/rsa/rsa.go index a6534e9..2cec9cf 100644 --- a/crypt/rsa/rsa.go +++ b/crypt/rsa/rsa.go @@ -12,14 +12,17 @@ import ( ) // Service provides RSA functionality. +// Usage: use Service with the other exported helpers in this package. type Service struct{} // NewService creates and returns a new Service instance for performing RSA-related operations. +// Usage: call NewService(...) to create a ready-to-use value. func NewService() *Service { return &Service{} } // GenerateKeyPair creates a new RSA key pair. +// Usage: call GenerateKeyPair(...) during the package's normal workflow. func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err error) { const op = "rsa.GenerateKeyPair" @@ -50,6 +53,7 @@ func (s *Service) GenerateKeyPair(bits int) (publicKey, privateKey []byte, err e } // Encrypt encrypts data with a public key. +// Usage: call Encrypt(...) during the package's normal workflow. func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) { const op = "rsa.Encrypt" @@ -77,6 +81,7 @@ func (s *Service) Encrypt(publicKey, data, label []byte) ([]byte, error) { } // Decrypt decrypts data with a private key. +// Usage: call Decrypt(...) during the package's normal workflow. func (s *Service) Decrypt(privateKey, ciphertext, label []byte) ([]byte, error) { const op = "rsa.Decrypt" diff --git a/crypt/rsa/rsa_test.go b/crypt/rsa/rsa_test.go index 8ee7460..5604fb6 100644 --- a/crypt/rsa/rsa_test.go +++ b/crypt/rsa/rsa_test.go @@ -19,7 +19,7 @@ func (r *mockReader) Read(p []byte) (n int, err error) { return 0, core.NewError("read error") } -func TestRSA_Good(t *testing.T) { +func TestRSA_RSA_Good(t *testing.T) { s := NewService() // Generate a new key pair @@ -37,7 +37,7 @@ func TestRSA_Good(t *testing.T) { assert.Equal(t, message, plaintext) } -func TestRSA_Bad(t *testing.T) { +func TestRSA_RSA_Bad(t *testing.T) { s := NewService() // Decrypt with wrong key @@ -56,7 +56,7 @@ func TestRSA_Bad(t *testing.T) { assert.Error(t, err) } -func TestRSA_Ugly(t *testing.T) { +func TestRSA_RSA_Ugly(t *testing.T) { s := NewService() // Malformed keys and messages diff --git a/crypt/symmetric.go b/crypt/symmetric.go index 65fe5f1..f0995c2 100644 --- a/crypt/symmetric.go +++ b/crypt/symmetric.go @@ -13,6 +13,7 @@ import ( // ChaCha20Encrypt encrypts plaintext using ChaCha20-Poly1305. // The key must be 32 bytes. The nonce is randomly generated and prepended // to the ciphertext. +// Usage: call ChaCha20Encrypt(...) during the package's normal workflow. func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -30,6 +31,7 @@ func ChaCha20Encrypt(plaintext, key []byte) ([]byte, error) { // ChaCha20Decrypt decrypts ciphertext encrypted with ChaCha20Encrypt. // The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +// Usage: call ChaCha20Decrypt(...) during the package's normal workflow. func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) { aead, err := chacha20poly1305.NewX(key) if err != nil { @@ -53,6 +55,7 @@ func ChaCha20Decrypt(ciphertext, key []byte) ([]byte, error) { // AESGCMEncrypt encrypts plaintext using AES-256-GCM. // The key must be 32 bytes. The nonce is randomly generated and prepended // to the ciphertext. +// Usage: call AESGCMEncrypt(...) during the package's normal workflow. func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { @@ -75,6 +78,7 @@ func AESGCMEncrypt(plaintext, key []byte) ([]byte, error) { // AESGCMDecrypt decrypts ciphertext encrypted with AESGCMEncrypt. // The key must be 32 bytes. Expects the nonce prepended to the ciphertext. +// Usage: call AESGCMDecrypt(...) during the package's normal workflow. func AESGCMDecrypt(ciphertext, key []byte) ([]byte, error) { block, err := aes.NewCipher(key) if err != nil { diff --git a/crypt/symmetric_test.go b/crypt/symmetric_test.go index d767e48..46985f8 100644 --- a/crypt/symmetric_test.go +++ b/crypt/symmetric_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestChaCha20_Good(t *testing.T) { +func TestSymmetric_ChaCha20_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -23,7 +23,7 @@ func TestChaCha20_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -func TestChaCha20_Bad(t *testing.T) { +func TestSymmetric_ChaCha20_Bad(t *testing.T) { key := make([]byte, 32) wrongKey := make([]byte, 32) _, _ = rand.Read(key) @@ -38,7 +38,7 @@ func TestChaCha20_Bad(t *testing.T) { assert.Error(t, err) } -func TestAESGCM_Good(t *testing.T) { +func TestSymmetric_AESGCM_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -56,8 +56,8 @@ func TestAESGCM_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestAESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data. -func TestAESGCM_Bad_WrongKey(t *testing.T) { +// TestSymmetric_AESGCM_Bad_WrongKey verifies wrong key returns error, not corrupt data. +func TestSymmetric_AESGCM_Bad_WrongKey(t *testing.T) { key := make([]byte, 32) wrongKey := make([]byte, 32) _, _ = rand.Read(key) @@ -72,8 +72,8 @@ func TestAESGCM_Bad_WrongKey(t *testing.T) { assert.Nil(t, decrypted, "wrong key must not return partial data") } -// TestChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level. -func TestChaCha20EmptyPlaintext_Good(t *testing.T) { +// TestSymmetric_ChaCha20EmptyPlaintext_Good verifies empty plaintext round-trip at low level. +func TestSymmetric_ChaCha20EmptyPlaintext_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -87,8 +87,8 @@ func TestChaCha20EmptyPlaintext_Good(t *testing.T) { assert.Empty(t, decrypted) } -// TestAESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level. -func TestAESGCMEmptyPlaintext_Good(t *testing.T) { +// TestSymmetric_AESGCMEmptyPlaintext_Good verifies empty plaintext round-trip at low level. +func TestSymmetric_AESGCMEmptyPlaintext_Good(t *testing.T) { key := make([]byte, 32) _, err := rand.Read(key) assert.NoError(t, err) @@ -102,8 +102,8 @@ func TestAESGCMEmptyPlaintext_Good(t *testing.T) { assert.Empty(t, decrypted) } -// TestChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip. -func TestChaCha20LargePayload_Good(t *testing.T) { +// TestSymmetric_ChaCha20LargePayload_Good verifies 1MB encrypt/decrypt round-trip. +func TestSymmetric_ChaCha20LargePayload_Good(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) @@ -120,8 +120,8 @@ func TestChaCha20LargePayload_Good(t *testing.T) { assert.Equal(t, plaintext, decrypted) } -// TestAESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip. -func TestAESGCMLargePayload_Good(t *testing.T) { +// TestSymmetric_AESGCMLargePayload_Good verifies 1MB encrypt/decrypt round-trip. +func TestSymmetric_AESGCMLargePayload_Good(t *testing.T) { key := make([]byte, 32) _, _ = rand.Read(key) diff --git a/trust/approval.go b/trust/approval.go index e1f0495..f1dd09d 100644 --- a/trust/approval.go +++ b/trust/approval.go @@ -10,18 +10,23 @@ import ( ) // ApprovalStatus represents the state of an approval request. +// Usage: use ApprovalStatus with the other exported helpers in this package. type ApprovalStatus int const ( // ApprovalPending means the request is awaiting review. + // Usage: compare or pass ApprovalPending when using the related package APIs. ApprovalPending ApprovalStatus = iota // ApprovalApproved means the request was approved. + // Usage: compare or pass ApprovalApproved when using the related package APIs. ApprovalApproved // ApprovalDenied means the request was denied. + // Usage: compare or pass ApprovalDenied when using the related package APIs. ApprovalDenied ) // String returns the human-readable name of the approval status. +// Usage: call String(...) during the package's normal workflow. func (s ApprovalStatus) String() string { switch s { case ApprovalPending: @@ -36,6 +41,7 @@ func (s ApprovalStatus) String() string { } // ApprovalRequest represents a queued capability approval request. +// Usage: use ApprovalRequest with the other exported helpers in this package. type ApprovalRequest struct { // ID is the unique identifier for this request. ID string @@ -58,6 +64,7 @@ type ApprovalRequest struct { } // ApprovalQueue manages pending approval requests for NeedsApproval decisions. +// Usage: use ApprovalQueue with the other exported helpers in this package. type ApprovalQueue struct { mu sync.RWMutex requests map[string]*ApprovalRequest @@ -65,6 +72,7 @@ type ApprovalQueue struct { } // NewApprovalQueue creates an empty approval queue. +// Usage: call NewApprovalQueue(...) to create a ready-to-use value. func NewApprovalQueue() *ApprovalQueue { return &ApprovalQueue{ requests: make(map[string]*ApprovalRequest), @@ -73,6 +81,7 @@ func NewApprovalQueue() *ApprovalQueue { // Submit creates a new approval request and returns its ID. // Returns an error if the agent name or capability is empty. +// Usage: call Submit(...) during the package's normal workflow. func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (string, error) { if agent == "" { return "", coreerr.E("trust.ApprovalQueue.Submit", "agent name is required", nil) @@ -101,6 +110,7 @@ func (q *ApprovalQueue) Submit(agent string, cap Capability, repo string) (strin // Approve marks a pending request as approved. Returns an error if the // request is not found or is not in pending status. +// Usage: call Approve(...) during the package's normal workflow. func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) error { q.mu.Lock() defer q.mu.Unlock() @@ -122,6 +132,7 @@ func (q *ApprovalQueue) Approve(id string, reviewedBy string, reason string) err // Deny marks a pending request as denied. Returns an error if the // request is not found or is not in pending status. +// Usage: call Deny(...) during the package's normal workflow. func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error { q.mu.Lock() defer q.mu.Unlock() @@ -142,6 +153,7 @@ func (q *ApprovalQueue) Deny(id string, reviewedBy string, reason string) error } // Get returns the approval request with the given ID, or nil if not found. +// Usage: call Get(...) during the package's normal workflow. func (q *ApprovalQueue) Get(id string) *ApprovalRequest { q.mu.RLock() defer q.mu.RUnlock() @@ -156,6 +168,7 @@ func (q *ApprovalQueue) Get(id string) *ApprovalRequest { } // Pending returns all requests with ApprovalPending status. +// Usage: call Pending(...) during the package's normal workflow. func (q *ApprovalQueue) Pending() []ApprovalRequest { q.mu.RLock() defer q.mu.RUnlock() @@ -170,6 +183,7 @@ func (q *ApprovalQueue) Pending() []ApprovalRequest { } // PendingSeq returns an iterator over all requests with ApprovalPending status. +// Usage: call PendingSeq(...) during the package's normal workflow. func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] { return func(yield func(ApprovalRequest) bool) { q.mu.RLock() @@ -186,6 +200,7 @@ func (q *ApprovalQueue) PendingSeq() iter.Seq[ApprovalRequest] { } // Len returns the total number of requests in the queue. +// Usage: call Len(...) during the package's normal workflow. func (q *ApprovalQueue) Len() int { q.mu.RLock() defer q.mu.RUnlock() diff --git a/trust/approval_test.go b/trust/approval_test.go index 6e3c344..f7bbee8 100644 --- a/trust/approval_test.go +++ b/trust/approval_test.go @@ -11,19 +11,19 @@ import ( // --- ApprovalStatus --- -func TestApprovalStatusString_Good(t *testing.T) { +func TestApproval_ApprovalStatusString_Good(t *testing.T) { assert.Equal(t, "pending", ApprovalPending.String()) assert.Equal(t, "approved", ApprovalApproved.String()) assert.Equal(t, "denied", ApprovalDenied.String()) } -func TestApprovalStatusString_Bad_Unknown(t *testing.T) { +func TestApproval_ApprovalStatusString_Bad_Unknown(t *testing.T) { assert.Contains(t, ApprovalStatus(99).String(), "unknown") } // --- Submit --- -func TestApprovalSubmit_Good(t *testing.T) { +func TestApproval_ApprovalSubmit_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -31,7 +31,7 @@ func TestApprovalSubmit_Good(t *testing.T) { assert.Equal(t, 1, q.Len()) } -func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) { +func TestApproval_ApprovalSubmit_Good_MultipleRequests(t *testing.T) { q := NewApprovalQueue() id1, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -42,7 +42,7 @@ func TestApprovalSubmit_Good_MultipleRequests(t *testing.T) { assert.Equal(t, 2, q.Len()) } -func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) { +func TestApproval_ApprovalSubmit_Good_EmptyRepo(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "") require.NoError(t, err) @@ -53,14 +53,14 @@ func TestApprovalSubmit_Good_EmptyRepo(t *testing.T) { assert.Empty(t, req.Repo) } -func TestApprovalSubmit_Bad_EmptyAgent(t *testing.T) { +func TestApproval_ApprovalSubmit_Bad_EmptyAgent(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("", CapMergePR, "") assert.Error(t, err) assert.Contains(t, err.Error(), "agent name is required") } -func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) { +func TestApproval_ApprovalSubmit_Bad_EmptyCapability(t *testing.T) { q := NewApprovalQueue() _, err := q.Submit("Clotho", "", "") assert.Error(t, err) @@ -69,7 +69,7 @@ func TestApprovalSubmit_Bad_EmptyCapability(t *testing.T) { // --- Get --- -func TestApprovalGet_Good(t *testing.T) { +func TestApproval_ApprovalGet_Good(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -85,7 +85,7 @@ func TestApprovalGet_Good(t *testing.T) { assert.True(t, req.ReviewedAt.IsZero()) } -func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) { +func TestApproval_ApprovalGet_Good_ReturnsSnapshot(t *testing.T) { q := NewApprovalQueue() id, err := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, err) @@ -99,14 +99,14 @@ func TestApprovalGet_Good_ReturnsSnapshot(t *testing.T) { assert.Equal(t, ApprovalPending, original.Status) } -func TestApprovalGet_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalGet_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() assert.Nil(t, q.Get("nonexistent")) } // --- Approve --- -func TestApprovalApprove_Good(t *testing.T) { +func TestApproval_ApprovalApprove_Good(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") @@ -121,14 +121,14 @@ func TestApprovalApprove_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApprovalApprove_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() err := q.Approve("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_AlreadyApproved(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Approve(id, "admin", "")) @@ -138,7 +138,7 @@ func TestApprovalApprove_Bad_AlreadyApproved(t *testing.T) { assert.Contains(t, err.Error(), "already approved") } -func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalApprove_Bad_AlreadyDenied(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "nope")) @@ -150,7 +150,7 @@ func TestApprovalApprove_Bad_AlreadyDenied(t *testing.T) { // --- Deny --- -func TestApprovalDeny_Good(t *testing.T) { +func TestApproval_ApprovalDeny_Good(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") @@ -165,14 +165,14 @@ func TestApprovalDeny_Good(t *testing.T) { assert.False(t, req.ReviewedAt.IsZero()) } -func TestApprovalDeny_Bad_NotFound(t *testing.T) { +func TestApproval_ApprovalDeny_Bad_NotFound(t *testing.T) { q := NewApprovalQueue() err := q.Deny("nonexistent", "admin", "") assert.Error(t, err) assert.Contains(t, err.Error(), "not found") } -func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) { +func TestApproval_ApprovalDeny_Bad_AlreadyDenied(t *testing.T) { q := NewApprovalQueue() id, _ := q.Submit("Clotho", CapMergePR, "host-uk/core") require.NoError(t, q.Deny(id, "admin", "")) @@ -184,7 +184,7 @@ func TestApprovalDeny_Bad_AlreadyDenied(t *testing.T) { // --- Pending --- -func TestApprovalPending_Good(t *testing.T) { +func TestApproval_ApprovalPending_Good(t *testing.T) { q := NewApprovalQueue() q.Submit("Clotho", CapMergePR, "host-uk/core") q.Submit("Hypnos", CapMergePR, "host-uk/docs") @@ -196,12 +196,12 @@ func TestApprovalPending_Good(t *testing.T) { assert.Len(t, pending, 2) } -func TestApprovalPending_Good_Empty(t *testing.T) { +func TestApproval_ApprovalPending_Good_Empty(t *testing.T) { q := NewApprovalQueue() assert.Empty(t, q.Pending()) } -func TestApprovalPendingSeq_Good(t *testing.T) { +func TestApproval_ApprovalPendingSeq_Good(t *testing.T) { q := NewApprovalQueue() q.Submit("Clotho", CapMergePR, "host-uk/core") q.Submit("Hypnos", CapMergePR, "host-uk/docs") @@ -219,7 +219,7 @@ func TestApprovalPendingSeq_Good(t *testing.T) { // --- Concurrent operations --- -func TestApprovalConcurrent_Good(t *testing.T) { +func TestApproval_ApprovalConcurrent_Good(t *testing.T) { q := NewApprovalQueue() const n = 10 @@ -270,7 +270,7 @@ func TestApprovalConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + ApprovalQueue --- -func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflow_Good_EndToEnd(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() @@ -293,7 +293,7 @@ func TestApprovalWorkflow_Good_EndToEnd(t *testing.T) { assert.Equal(t, "Virgil", req.ReviewedBy) } -func TestApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) { +func TestApproval_ApprovalWorkflow_Good_DenyEndToEnd(t *testing.T) { pe := newTestEngine(t) q := NewApprovalQueue() diff --git a/trust/audit.go b/trust/audit.go index 06b9032..3e74f4d 100644 --- a/trust/audit.go +++ b/trust/audit.go @@ -11,6 +11,7 @@ import ( ) // AuditEntry records a single policy evaluation for compliance. +// Usage: use AuditEntry with the other exported helpers in this package. type AuditEntry struct { // Timestamp is when the evaluation occurred. Timestamp time.Time `json:"timestamp"` @@ -27,6 +28,7 @@ type AuditEntry struct { } // MarshalJSON implements custom JSON encoding for Decision. +// Usage: call MarshalJSON(...) during the package's normal workflow. func (d Decision) MarshalJSON() ([]byte, error) { result := core.JSONMarshal(d.String()) if !result.OK { @@ -37,6 +39,7 @@ func (d Decision) MarshalJSON() ([]byte, error) { } // UnmarshalJSON implements custom JSON decoding for Decision. +// Usage: call UnmarshalJSON(...) during the package's normal workflow. func (d *Decision) UnmarshalJSON(data []byte) error { var s string result := core.JSONUnmarshal(data, &s) @@ -58,6 +61,7 @@ func (d *Decision) UnmarshalJSON(data []byte) error { } // AuditLog is an append-only log of policy evaluations. +// Usage: use AuditLog with the other exported helpers in this package. type AuditLog struct { mu sync.Mutex entries []AuditEntry @@ -66,6 +70,7 @@ type AuditLog struct { // NewAuditLog creates an in-memory audit log. If a writer is provided, // each entry is also written as a JSON line to that writer (append-only). +// Usage: call NewAuditLog(...) to create a ready-to-use value. func NewAuditLog(w io.Writer) *AuditLog { return &AuditLog{ writer: w, @@ -73,6 +78,7 @@ func NewAuditLog(w io.Writer) *AuditLog { } // Record appends an evaluation result to the audit log. +// Usage: call Record(...) during the package's normal workflow. func (l *AuditLog) Record(result EvalResult, repo string) error { entry := AuditEntry{ Timestamp: time.Now(), @@ -104,6 +110,7 @@ func (l *AuditLog) Record(result EvalResult, repo string) error { } // Entries returns a snapshot of all audit entries. +// Usage: call Entries(...) during the package's normal workflow. func (l *AuditLog) Entries() []AuditEntry { l.mu.Lock() defer l.mu.Unlock() @@ -114,6 +121,7 @@ func (l *AuditLog) Entries() []AuditEntry { } // EntriesSeq returns an iterator over all audit entries. +// Usage: call EntriesSeq(...) during the package's normal workflow. func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] { return func(yield func(AuditEntry) bool) { l.mu.Lock() @@ -128,6 +136,7 @@ func (l *AuditLog) EntriesSeq() iter.Seq[AuditEntry] { } // Len returns the number of entries in the log. +// Usage: call Len(...) during the package's normal workflow. func (l *AuditLog) Len() int { l.mu.Lock() defer l.mu.Unlock() @@ -135,6 +144,7 @@ func (l *AuditLog) Len() int { } // EntriesFor returns all audit entries for a specific agent. +// Usage: call EntriesFor(...) during the package's normal workflow. func (l *AuditLog) EntriesFor(agent string) []AuditEntry { l.mu.Lock() defer l.mu.Unlock() @@ -149,6 +159,7 @@ func (l *AuditLog) EntriesFor(agent string) []AuditEntry { } // EntriesForSeq returns an iterator over audit entries for a specific agent. +// Usage: call EntriesForSeq(...) during the package's normal workflow. func (l *AuditLog) EntriesForSeq(agent string) iter.Seq[AuditEntry] { return func(yield func(AuditEntry) bool) { l.mu.Lock() diff --git a/trust/audit_test.go b/trust/audit_test.go index 23c9342..583a461 100644 --- a/trust/audit_test.go +++ b/trust/audit_test.go @@ -12,7 +12,7 @@ import ( // --- AuditLog basic --- -func TestAuditRecord_Good(t *testing.T) { +func TestAudit_AuditRecord_Good(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ @@ -26,7 +26,7 @@ func TestAuditRecord_Good(t *testing.T) { assert.Equal(t, 1, log.Len()) } -func TestAuditRecord_Good_EntryFields(t *testing.T) { +func TestAudit_AuditRecord_Good_EntryFields(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ @@ -50,7 +50,7 @@ func TestAuditRecord_Good_EntryFields(t *testing.T) { assert.False(t, e.Timestamp.IsZero()) } -func TestAuditRecord_Good_NoRepo(t *testing.T) { +func TestAudit_AuditRecord_Good_NoRepo(t *testing.T) { log := NewAuditLog(nil) result := EvalResult{ Decision: Allow, @@ -66,7 +66,7 @@ func TestAuditRecord_Good_NoRepo(t *testing.T) { assert.Empty(t, entries[0].Repo) } -func TestAuditEntries_Good_Snapshot(t *testing.T) { +func TestAudit_AuditEntries_Good_Snapshot(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "A", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -78,12 +78,12 @@ func TestAuditEntries_Good_Snapshot(t *testing.T) { assert.Equal(t, "A", log.Entries()[0].Agent) } -func TestAuditEntries_Good_Empty(t *testing.T) { +func TestAudit_AuditEntries_Good_Empty(t *testing.T) { log := NewAuditLog(nil) assert.Empty(t, log.Entries()) } -func TestAuditEntries_Good_AppendOnly(t *testing.T) { +func TestAudit_AuditEntries_Good_AppendOnly(t *testing.T) { log := NewAuditLog(nil) for i := range 5 { @@ -99,7 +99,7 @@ func TestAuditEntries_Good_AppendOnly(t *testing.T) { // --- EntriesFor --- -func TestAuditEntriesFor_Good(t *testing.T) { +func TestAudit_AuditEntriesFor_Good(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -121,7 +121,7 @@ func TestAuditEntriesFor_Good(t *testing.T) { assert.Equal(t, 2, count) } -func TestAuditEntriesSeq_Good(t *testing.T) { +func TestAudit_AuditEntriesSeq_Good(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") log.Record(EvalResult{Agent: "Clotho", Cap: CapCreatePR, Decision: Allow, Reason: "ok"}, "") @@ -133,7 +133,7 @@ func TestAuditEntriesSeq_Good(t *testing.T) { assert.Equal(t, 2, count) } -func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { +func TestAudit_AuditEntriesFor_Bad_NotFound(t *testing.T) { log := NewAuditLog(nil) log.Record(EvalResult{Agent: "Athena", Cap: CapPushRepo, Decision: Allow, Reason: "ok"}, "") @@ -142,7 +142,7 @@ func TestAuditEntriesFor_Bad_NotFound(t *testing.T) { // --- Writer output --- -func TestAuditRecord_Good_WritesToWriter(t *testing.T) { +func TestAudit_AuditRecord_Good_WritesToWriter(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -168,7 +168,7 @@ func TestAuditRecord_Good_WritesToWriter(t *testing.T) { assert.Equal(t, "host-uk/core", entry.Repo) } -func TestAuditRecord_Good_MultipleLines(t *testing.T) { +func TestAudit_AuditRecord_Good_MultipleLines(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -192,7 +192,7 @@ func TestAuditRecord_Good_MultipleLines(t *testing.T) { } } -func TestAuditRecord_Bad_WriterError(t *testing.T) { +func TestAudit_AuditRecord_Bad_WriterError(t *testing.T) { log := NewAuditLog(&failWriter{}) result := EvalResult{ @@ -218,7 +218,7 @@ func (f *failWriter) Write(_ []byte) (int, error) { // --- Decision JSON marshalling --- -func TestDecisionJSON_Good_RoundTrip(t *testing.T) { +func TestAudit_DecisionJSON_Good_RoundTrip(t *testing.T) { decisions := []Decision{Deny, Allow, NeedsApproval} expected := []string{`"deny"`, `"allow"`, `"needs_approval"`} @@ -234,7 +234,7 @@ func TestDecisionJSON_Good_RoundTrip(t *testing.T) { } } -func TestDecisionJSON_Bad_UnknownString(t *testing.T) { +func TestAudit_DecisionJSON_Bad_UnknownString(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`"invalid"`), &d) err, _ := result.Value.(error) @@ -242,7 +242,7 @@ func TestDecisionJSON_Bad_UnknownString(t *testing.T) { assert.Contains(t, err.Error(), "unknown decision") } -func TestDecisionJSON_Bad_NonString(t *testing.T) { +func TestAudit_DecisionJSON_Bad_NonString(t *testing.T) { var d Decision result := core.JSONUnmarshal([]byte(`42`), &d) err, _ := result.Value.(error) @@ -251,7 +251,7 @@ func TestDecisionJSON_Bad_NonString(t *testing.T) { // --- Concurrent audit logging --- -func TestAuditConcurrent_Good(t *testing.T) { +func TestAudit_AuditConcurrent_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) @@ -277,7 +277,7 @@ func TestAuditConcurrent_Good(t *testing.T) { // --- Integration: PolicyEngine + AuditLog --- -func TestAuditPolicyIntegration_Good(t *testing.T) { +func TestAudit_AuditPolicyIntegration_Good(t *testing.T) { buf := core.NewBuilder() log := NewAuditLog(buf) pe := newTestEngine(t) diff --git a/trust/config.go b/trust/config.go index ea2618c..b252fc9 100644 --- a/trust/config.go +++ b/trust/config.go @@ -8,6 +8,7 @@ import ( ) // PolicyConfig is the JSON-serialisable representation of a trust policy. +// Usage: use PolicyConfig with the other exported helpers in this package. type PolicyConfig struct { Tier int `json:"tier"` Allowed []string `json:"allowed"` @@ -16,11 +17,13 @@ type PolicyConfig struct { } // PoliciesConfig is the top-level configuration containing all tier policies. +// Usage: use PoliciesConfig with the other exported helpers in this package. type PoliciesConfig struct { Policies []PolicyConfig `json:"policies"` } // LoadPoliciesFromFile reads a JSON file and returns parsed policies. +// Usage: call LoadPoliciesFromFile(...) during the package's normal workflow. func LoadPoliciesFromFile(path string) ([]Policy, error) { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -31,6 +34,7 @@ func LoadPoliciesFromFile(path string) ([]Policy, error) { } // LoadPolicies reads JSON from a reader and returns parsed policies. +// Usage: call LoadPolicies(...) during the package's normal workflow. func LoadPolicies(r io.Reader) ([]Policy, error) { readResult := core.ReadAll(r) if !readResult.OK { @@ -76,6 +80,7 @@ func convertPolicies(cfg PoliciesConfig) ([]Policy, error) { // ApplyPolicies loads policies from a reader and sets them on the engine, // replacing any existing policies for the same tiers. +// Usage: call ApplyPolicies(...) during the package's normal workflow. func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { policies, err := LoadPolicies(r) if err != nil { @@ -90,6 +95,7 @@ func (pe *PolicyEngine) ApplyPolicies(r io.Reader) error { } // ApplyPoliciesFromFile loads policies from a JSON file and sets them on the engine. +// Usage: call ApplyPoliciesFromFile(...) during the package's normal workflow. func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { openResult := (&core.Fs{}).New("/").Open(path) if !openResult.OK { @@ -100,6 +106,7 @@ func (pe *PolicyEngine) ApplyPoliciesFromFile(path string) error { } // ExportPolicies serialises the current policies as JSON to the given writer. +// Usage: call ExportPolicies(...) during the package's normal workflow. func (pe *PolicyEngine) ExportPolicies(w io.Writer) error { var cfg PoliciesConfig for _, tier := range []Tier{TierUntrusted, TierVerified, TierFull} { diff --git a/trust/config_test.go b/trust/config_test.go index 72825ba..b4a0e01 100644 --- a/trust/config_test.go +++ b/trust/config_test.go @@ -30,13 +30,13 @@ const validPolicyJSON = `{ // --- LoadPolicies --- -func TestLoadPolicies_Good(t *testing.T) { +func TestConfig_LoadPolicies_Good(t *testing.T) { policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) assert.Len(t, policies, 3) } -func TestLoadPolicies_Good_FieldMapping(t *testing.T) { +func TestConfig_LoadPolicies_Good_FieldMapping(t *testing.T) { policies, err := LoadPolicies(core.NewReader(validPolicyJSON)) require.NoError(t, err) @@ -60,33 +60,33 @@ func TestLoadPolicies_Good_FieldMapping(t *testing.T) { assert.Len(t, policies[2].Denied, 2) } -func TestLoadPolicies_Good_EmptyPolicies(t *testing.T) { +func TestConfig_LoadPolicies_Good_EmptyPolicies(t *testing.T) { input := `{"policies": []}` policies, err := LoadPolicies(core.NewReader(input)) require.NoError(t, err) assert.Empty(t, policies) } -func TestLoadPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_LoadPolicies_Bad_InvalidJSON(t *testing.T) { _, err := LoadPolicies(core.NewReader(`{invalid`)) assert.Error(t, err) } -func TestLoadPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_LoadPolicies_Bad_InvalidTier(t *testing.T) { input := `{"policies": [{"tier": 0, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestLoadPolicies_Bad_TierTooHigh(t *testing.T) { +func TestConfig_LoadPolicies_Bad_TierTooHigh(t *testing.T) { input := `{"policies": [{"tier": 99, "allowed": ["repo.push"]}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestLoadPolicies_Bad_UnknownField(t *testing.T) { +func TestConfig_LoadPolicies_Bad_UnknownField(t *testing.T) { input := `{"policies": [{"tier": 1, "allowed": ["repo.push"], "bogus": true}]}` _, err := LoadPolicies(core.NewReader(input)) assert.Error(t, err, "DisallowUnknownFields should reject unknown fields") @@ -94,7 +94,7 @@ func TestLoadPolicies_Bad_UnknownField(t *testing.T) { // --- LoadPoliciesFromFile --- -func TestLoadPoliciesFromFile_Good(t *testing.T) { +func TestConfig_LoadPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() path := core.Path(dir, "policies.json") writePolicyFile(t, path, validPolicyJSON) @@ -104,14 +104,14 @@ func TestLoadPoliciesFromFile_Good(t *testing.T) { assert.Len(t, policies, 3) } -func TestLoadPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_LoadPoliciesFromFile_Bad_NotFound(t *testing.T) { _, err := LoadPoliciesFromFile("/nonexistent/path/policies.json") assert.Error(t, err) } // --- ApplyPolicies --- -func TestApplyPolicies_Good(t *testing.T) { +func TestConfig_ApplyPolicies_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "TestAgent", Tier: TierVerified})) pe := NewPolicyEngine(r) @@ -135,7 +135,7 @@ func TestApplyPolicies_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { +func TestConfig_ApplyPolicies_Bad_InvalidJSON(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -143,7 +143,7 @@ func TestApplyPolicies_Bad_InvalidJSON(t *testing.T) { assert.Error(t, err) } -func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { +func TestConfig_ApplyPolicies_Bad_InvalidTier(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) @@ -154,7 +154,7 @@ func TestApplyPolicies_Bad_InvalidTier(t *testing.T) { // --- ApplyPoliciesFromFile --- -func TestApplyPoliciesFromFile_Good(t *testing.T) { +func TestConfig_ApplyPoliciesFromFile_Good(t *testing.T) { dir := t.TempDir() path := core.Path(dir, "policies.json") writePolicyFile(t, path, validPolicyJSON) @@ -172,7 +172,7 @@ func TestApplyPoliciesFromFile_Good(t *testing.T) { assert.Len(t, p.Allowed, 3) } -func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { +func TestConfig_ApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) err := pe.ApplyPoliciesFromFile("/nonexistent/policies.json") @@ -181,7 +181,7 @@ func TestApplyPoliciesFromFile_Bad_NotFound(t *testing.T) { // --- ExportPolicies --- -func TestExportPolicies_Good(t *testing.T) { +func TestConfig_ExportPolicies_Good(t *testing.T) { r := NewRegistry() pe := NewPolicyEngine(r) // loads defaults @@ -196,7 +196,7 @@ func TestExportPolicies_Good(t *testing.T) { assert.Len(t, cfg.Policies, 3) } -func TestExportPolicies_Good_RoundTrip(t *testing.T) { +func TestConfig_ExportPolicies_Good_RoundTrip(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierFull})) pe := NewPolicyEngine(r) @@ -232,26 +232,26 @@ func writePolicyFile(t *testing.T, path, content string) { // --- Helper conversion --- -func TestToCapabilities_Good(t *testing.T) { +func TestConfig_ToCapabilities_Good(t *testing.T) { caps := toCapabilities([]string{"repo.push", "pr.merge"}) assert.Len(t, caps, 2) assert.Equal(t, CapPushRepo, caps[0]) assert.Equal(t, CapMergePR, caps[1]) } -func TestToCapabilities_Good_Empty(t *testing.T) { +func TestConfig_ToCapabilities_Good_Empty(t *testing.T) { assert.Nil(t, toCapabilities(nil)) assert.Nil(t, toCapabilities([]string{})) } -func TestFromCapabilities_Good(t *testing.T) { +func TestConfig_FromCapabilities_Good(t *testing.T) { ss := fromCapabilities([]Capability{CapPushRepo, CapMergePR}) assert.Len(t, ss, 2) assert.Equal(t, "repo.push", ss[0]) assert.Equal(t, "pr.merge", ss[1]) } -func TestFromCapabilities_Good_Empty(t *testing.T) { +func TestConfig_FromCapabilities_Good_Empty(t *testing.T) { assert.Nil(t, fromCapabilities(nil)) assert.Nil(t, fromCapabilities([]Capability{})) } diff --git a/trust/policy.go b/trust/policy.go index bc72760..c135276 100644 --- a/trust/policy.go +++ b/trust/policy.go @@ -8,6 +8,7 @@ import ( ) // Policy defines the access rules for a given trust tier. +// Usage: use Policy with the other exported helpers in this package. type Policy struct { // Tier is the trust level this policy applies to. Tier Tier @@ -20,24 +21,30 @@ type Policy struct { } // PolicyEngine evaluates capability requests against registered policies. +// Usage: use PolicyEngine with the other exported helpers in this package. type PolicyEngine struct { registry *Registry policies map[Tier]*Policy } // Decision is the result of a policy evaluation. +// Usage: use Decision with the other exported helpers in this package. type Decision int const ( // Deny means the action is not permitted. + // Usage: compare or pass Deny when using the related package APIs. Deny Decision = iota // Allow means the action is permitted. + // Usage: compare or pass Allow when using the related package APIs. Allow // NeedsApproval means the action requires human or higher-tier approval. + // Usage: compare or pass NeedsApproval when using the related package APIs. NeedsApproval ) // String returns the human-readable name of the decision. +// Usage: call String(...) during the package's normal workflow. func (d Decision) String() string { switch d { case Deny: @@ -52,6 +59,7 @@ func (d Decision) String() string { } // EvalResult contains the outcome of a capability evaluation. +// Usage: use EvalResult with the other exported helpers in this package. type EvalResult struct { Decision Decision Agent string @@ -60,6 +68,7 @@ type EvalResult struct { } // NewPolicyEngine creates a policy engine with the given registry and default policies. +// Usage: call NewPolicyEngine(...) to create a ready-to-use value. func NewPolicyEngine(registry *Registry) *PolicyEngine { pe := &PolicyEngine{ registry: registry, @@ -72,6 +81,7 @@ func NewPolicyEngine(registry *Registry) *PolicyEngine { // Evaluate checks whether the named agent can perform the given capability. // If the agent has scoped repos and the capability is repo-scoped, the repo // parameter is checked against the agent's allowed repos. +// Usage: call Evaluate(...) during the package's normal workflow. func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) EvalResult { agent := pe.registry.Get(agentName) if agent == nil { @@ -145,6 +155,7 @@ func (pe *PolicyEngine) Evaluate(agentName string, cap Capability, repo string) } // SetPolicy replaces the policy for a given tier. +// Usage: call SetPolicy(...) during the package's normal workflow. func (pe *PolicyEngine) SetPolicy(p Policy) error { if !p.Tier.Valid() { return coreerr.E("trust.SetPolicy", core.Sprintf("invalid tier %d", p.Tier), nil) @@ -154,6 +165,7 @@ func (pe *PolicyEngine) SetPolicy(p Policy) error { } // GetPolicy returns the policy for a tier, or nil if none is set. +// Usage: call GetPolicy(...) during the package's normal workflow. func (pe *PolicyEngine) GetPolicy(t Tier) *Policy { return pe.policies[t] } diff --git a/trust/policy_test.go b/trust/policy_test.go index c656e89..19ff5f7 100644 --- a/trust/policy_test.go +++ b/trust/policy_test.go @@ -29,19 +29,19 @@ func newTestEngine(t *testing.T) *PolicyEngine { // --- Decision --- -func TestDecisionString_Good(t *testing.T) { +func TestPolicy_DecisionString_Good(t *testing.T) { assert.Equal(t, "deny", Deny.String()) assert.Equal(t, "allow", Allow.String()) assert.Equal(t, "needs_approval", NeedsApproval.String()) } -func TestDecisionString_Bad_Unknown(t *testing.T) { +func TestPolicy_DecisionString_Bad_Unknown(t *testing.T) { assert.Contains(t, Decision(99).String(), "unknown") } // --- Tier 3 (Full Trust) --- -func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier3CanDoAnything(t *testing.T) { pe := newTestEngine(t) caps := []Capability{ @@ -57,56 +57,56 @@ func TestEvaluate_Good_Tier3CanDoAnything(t *testing.T) { // --- Tier 2 (Verified) --- -func TestEvaluate_Good_Tier2CanCreatePR(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanCreatePR(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreatePR, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanPushToScopedRepo(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/core") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2NeedsApprovalToMerge(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapMergePR, "host-uk/core") assert.Equal(t, NeedsApproval, result.Decision) } -func TestEvaluate_Good_Tier2CanCreateIssue(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2CanCreateIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapCreateIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotAccessWorkspace(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapAccessWorkspace, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotModifyFlows(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotModifyFlows(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapModifyFlows, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotRunPrivileged(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2CannotPushToUnscopedRepo(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Clotho", CapPushRepo, "host-uk/secret-repo") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "does not have access") } -func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { pe := newTestEngine(t) // Push without specifying a repo should be denied for scoped agents. result := pe.Evaluate("Clotho", CapPushRepo, "") @@ -115,43 +115,43 @@ func TestEvaluate_Bad_Tier2RepoScopeEmptyRepo(t *testing.T) { // --- Tier 1 (Untrusted) --- -func TestEvaluate_Good_Tier1CanCreatePR(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier1CanCreatePR(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreatePR, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Good_Tier1CanCommentIssue(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier1CanCommentIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCommentIssue, "") assert.Equal(t, Allow, result.Decision) } -func TestEvaluate_Bad_Tier1CannotPush(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotPush(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapPushRepo, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotMerge(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotMerge(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapMergePR, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotCreateIssue(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotCreateIssue(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapCreateIssue, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotReadSecrets(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotReadSecrets(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapReadSecrets, "") assert.Equal(t, Deny, result.Decision) } -func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("BugSETI-001", CapRunPrivileged, "") assert.Equal(t, Deny, result.Decision) @@ -159,14 +159,14 @@ func TestEvaluate_Bad_Tier1CannotRunPrivileged(t *testing.T) { // --- Edge cases --- -func TestEvaluate_Bad_UnknownAgent(t *testing.T) { +func TestPolicy_Evaluate_Bad_UnknownAgent(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Unknown", CapCreatePR, "") assert.Equal(t, Deny, result.Decision) assert.Contains(t, result.Reason, "not registered") } -func TestEvaluate_Good_EvalResultFields(t *testing.T) { +func TestPolicy_Evaluate_Good_EvalResultFields(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", CapPushRepo, "") assert.Equal(t, "Athena", result.Agent) @@ -176,7 +176,7 @@ func TestEvaluate_Good_EvalResultFields(t *testing.T) { // --- SetPolicy --- -func TestSetPolicy_Good(t *testing.T) { +func TestPolicy_SetPolicy_Good(t *testing.T) { pe := newTestEngine(t) err := pe.SetPolicy(Policy{ Tier: TierVerified, @@ -189,64 +189,64 @@ func TestSetPolicy_Good(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestSetPolicy_Bad_InvalidTier(t *testing.T) { +func TestPolicy_SetPolicy_Bad_InvalidTier(t *testing.T) { pe := newTestEngine(t) err := pe.SetPolicy(Policy{Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestGetPolicy_Good(t *testing.T) { +func TestPolicy_GetPolicy_Good(t *testing.T) { pe := newTestEngine(t) p := pe.GetPolicy(TierFull) require.NotNil(t, p) assert.Equal(t, TierFull, p.Tier) } -func TestGetPolicy_Bad_NotFound(t *testing.T) { +func TestPolicy_GetPolicy_Bad_NotFound(t *testing.T) { pe := newTestEngine(t) assert.Nil(t, pe.GetPolicy(Tier(99))) } // --- isRepoScoped / repoAllowed helpers --- -func TestIsRepoScoped_Good(t *testing.T) { +func TestPolicy_IsRepoScoped_Good(t *testing.T) { assert.True(t, isRepoScoped(CapPushRepo)) assert.True(t, isRepoScoped(CapCreatePR)) assert.True(t, isRepoScoped(CapMergePR)) assert.True(t, isRepoScoped(CapReadSecrets)) } -func TestIsRepoScoped_Bad_NotScoped(t *testing.T) { +func TestPolicy_IsRepoScoped_Bad_NotScoped(t *testing.T) { assert.False(t, isRepoScoped(CapRunPrivileged)) assert.False(t, isRepoScoped(CapAccessWorkspace)) assert.False(t, isRepoScoped(CapModifyFlows)) } -func TestRepoAllowed_Good(t *testing.T) { +func TestPolicy_RepoAllowed_Good(t *testing.T) { scoped := []string{"host-uk/core", "host-uk/docs"} assert.True(t, repoAllowed(scoped, "host-uk/core")) assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestRepoAllowed_Bad_NotInScope(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_NotInScope(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "host-uk/secret")) } -func TestRepoAllowed_Bad_EmptyRepo(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_EmptyRepo(t *testing.T) { scoped := []string{"host-uk/core"} assert.False(t, repoAllowed(scoped, "")) } -func TestRepoAllowed_Bad_EmptyScope(t *testing.T) { +func TestPolicy_RepoAllowed_Bad_EmptyScope(t *testing.T) { assert.False(t, repoAllowed(nil, "host-uk/core")) assert.False(t, repoAllowed([]string{}, "host-uk/core")) } // --- Tier 3 ignores repo scoping --- -func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Virgil", @@ -261,7 +261,7 @@ func TestEvaluate_Good_Tier3IgnoresRepoScope(t *testing.T) { // --- Default rate limits --- -func TestDefaultRateLimit_Good(t *testing.T) { +func TestPolicy_DefaultRateLimit_Good(t *testing.T) { assert.Equal(t, 10, defaultRateLimit(TierUntrusted)) assert.Equal(t, 60, defaultRateLimit(TierVerified)) assert.Equal(t, 0, defaultRateLimit(TierFull)) @@ -270,11 +270,11 @@ func TestDefaultRateLimit_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2 +// TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll verifies that a Tier 2 // agent with empty ScopedRepos is treated as "unrestricted" for repo-scoped // capabilities. NOTE: This is a potential security concern documented in // FINDINGS.md — empty ScopedRepos bypasses the repo scope check entirely. -func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { +func TestPolicy_Evaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "Hypnos", @@ -301,9 +301,9 @@ func TestEvaluate_Good_Tier2EmptyScopedReposAllowsAll(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -// TestEvaluate_Bad_CapabilityNotInAnyList verifies that a capability not in +// TestPolicy_Evaluate_Bad_CapabilityNotInAnyList verifies that a capability not in // allowed, denied, or requires_approval lists defaults to deny. -func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) { +func TestPolicy_Evaluate_Bad_CapabilityNotInAnyList(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "TestAgent", @@ -325,9 +325,9 @@ func TestEvaluate_Bad_CapabilityNotInAnyList(t *testing.T) { assert.Contains(t, result.Reason, "not granted") } -// TestEvaluate_Bad_UnknownCapability verifies that a completely invented +// TestPolicy_Evaluate_Bad_UnknownCapability verifies that a completely invented // capability string is denied. -func TestEvaluate_Bad_UnknownCapability(t *testing.T) { +func TestPolicy_Evaluate_Bad_UnknownCapability(t *testing.T) { pe := newTestEngine(t) result := pe.Evaluate("Athena", Capability("nonexistent.capability"), "") @@ -335,9 +335,9 @@ func TestEvaluate_Bad_UnknownCapability(t *testing.T) { assert.Contains(t, result.Reason, "not granted") } -// TestConcurrentEvaluate_Good verifies that concurrent policy evaluations +// TestPolicy_ConcurrentEvaluate_Good verifies that concurrent policy evaluations // with 10 goroutines do not race. -func TestConcurrentEvaluate_Good(t *testing.T) { +func TestPolicy_ConcurrentEvaluate_Good(t *testing.T) { pe := newTestEngine(t) const n = 10 @@ -360,10 +360,10 @@ func TestConcurrentEvaluate_Good(t *testing.T) { wg.Wait() } -// TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that +// TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam verifies that // a scoped agent requesting a repo-scoped capability without specifying // the repo is denied. -func TestEvaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) { +func TestPolicy_Evaluate_Bad_Tier2ScopedReposWithEmptyRepoParam(t *testing.T) { pe := newTestEngine(t) // Clotho has ScopedRepos but passes empty repo diff --git a/trust/scope_test.go b/trust/scope_test.go index db9f758..d2de46d 100644 --- a/trust/scope_test.go +++ b/trust/scope_test.go @@ -9,100 +9,100 @@ import ( // --- matchScope --- -func TestMatchScope_Good_ExactMatch(t *testing.T) { +func TestScope_MatchScope_Good_ExactMatch(t *testing.T) { assert.True(t, matchScope("host-uk/core", "host-uk/core")) } -func TestMatchScope_Good_SingleWildcard(t *testing.T) { +func TestScope_MatchScope_Good_SingleWildcard(t *testing.T) { assert.True(t, matchScope("core/*", "core/php")) assert.True(t, matchScope("core/*", "core/go-crypt")) assert.True(t, matchScope("host-uk/*", "host-uk/core")) } -func TestMatchScope_Good_RecursiveWildcard(t *testing.T) { +func TestScope_MatchScope_Good_RecursiveWildcard(t *testing.T) { assert.True(t, matchScope("core/**", "core/php")) assert.True(t, matchScope("core/**", "core/php/sub")) assert.True(t, matchScope("core/**", "core/a/b/c")) } -func TestMatchScope_Bad_ExactMismatch(t *testing.T) { +func TestScope_MatchScope_Bad_ExactMismatch(t *testing.T) { assert.False(t, matchScope("host-uk/core", "host-uk/docs")) } -func TestMatchScope_Bad_SingleWildcardNoNested(t *testing.T) { +func TestScope_MatchScope_Bad_SingleWildcardNoNested(t *testing.T) { // "core/*" should NOT match "core/php/sub" — only single level. assert.False(t, matchScope("core/*", "core/php/sub")) assert.False(t, matchScope("core/*", "core/a/b")) } -func TestMatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_SingleWildcardNoPrefix(t *testing.T) { // "core/*" should NOT match "other/php". assert.False(t, matchScope("core/*", "other/php")) } -func TestMatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_RecursiveWildcardNoPrefix(t *testing.T) { assert.False(t, matchScope("core/**", "other/php")) } -func TestMatchScope_Bad_EmptyRepo(t *testing.T) { +func TestScope_MatchScope_Bad_EmptyRepo(t *testing.T) { assert.False(t, matchScope("core/*", "")) } -func TestMatchScope_Bad_WildcardInMiddle(t *testing.T) { +func TestScope_MatchScope_Bad_WildcardInMiddle(t *testing.T) { // Wildcard not at the end — should not match. assert.False(t, matchScope("core/*/sub", "core/php/sub")) } -func TestMatchScope_Bad_WildcardOnlyPrefix(t *testing.T) { +func TestScope_MatchScope_Bad_WildcardOnlyPrefix(t *testing.T) { // "core/*" should not match the prefix itself. assert.False(t, matchScope("core/*", "core")) assert.False(t, matchScope("core/*", "core/")) } -func TestMatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) { +func TestScope_MatchScope_Good_RecursiveWildcardSingleLevel(t *testing.T) { // "core/**" should also match single-level children. assert.True(t, matchScope("core/**", "core/php")) } -func TestMatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) { +func TestScope_MatchScope_Bad_RecursiveWildcardPrefixOnly(t *testing.T) { assert.False(t, matchScope("core/**", "core")) assert.False(t, matchScope("core/**", "corefoo")) } // --- repoAllowed with wildcards --- -func TestRepoAllowedWildcard_Good(t *testing.T) { +func TestScope_RepoAllowedWildcard_Good(t *testing.T) { scoped := []string{"core/*", "host-uk/docs"} assert.True(t, repoAllowed(scoped, "core/php")) assert.True(t, repoAllowed(scoped, "core/go-crypt")) assert.True(t, repoAllowed(scoped, "host-uk/docs")) } -func TestRepoAllowedWildcard_Good_Recursive(t *testing.T) { +func TestScope_RepoAllowedWildcard_Good_Recursive(t *testing.T) { scoped := []string{"core/**"} assert.True(t, repoAllowed(scoped, "core/php")) assert.True(t, repoAllowed(scoped, "core/php/sub")) } -func TestRepoAllowedWildcard_Bad_NoMatch(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_NoMatch(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "other/repo")) assert.False(t, repoAllowed(scoped, "core/php/sub")) } -func TestRepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_EmptyRepo(t *testing.T) { scoped := []string{"core/*"} assert.False(t, repoAllowed(scoped, "")) } -func TestRepoAllowedWildcard_Bad_EmptyScope(t *testing.T) { +func TestScope_RepoAllowedWildcard_Bad_EmptyScope(t *testing.T) { assert.False(t, repoAllowed(nil, "core/php")) assert.False(t, repoAllowed([]string{}, "core/php")) } // --- Integration: PolicyEngine with wildcard scopes --- -func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_SingleLevel(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -118,7 +118,7 @@ func TestEvaluateWildcardScope_Good_SingleLevel(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { +func TestScope_EvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -132,7 +132,7 @@ func TestEvaluateWildcardScope_Bad_OutOfScope(t *testing.T) { assert.Contains(t, result.Reason, "does not have access") } -func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { +func TestScope_EvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "WildAgent", @@ -145,7 +145,7 @@ func TestEvaluateWildcardScope_Bad_NestedNotAllowedBySingleStar(t *testing.T) { assert.Equal(t, Deny, result.Decision) } -func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "DeepAgent", @@ -158,7 +158,7 @@ func TestEvaluateWildcardScope_Good_RecursiveAllowsNested(t *testing.T) { assert.Equal(t, Allow, result.Decision) } -func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "MixedAgent", @@ -180,7 +180,7 @@ func TestEvaluateWildcardScope_Good_MixedExactAndWildcard(t *testing.T) { assert.Equal(t, Deny, result.Decision) } -func TestEvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) { +func TestScope_EvaluateWildcardScope_Good_ReadSecretsScoped(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{ Name: "ScopedSecrets", diff --git a/trust/trust.go b/trust/trust.go index 8cc205f..6ed588e 100644 --- a/trust/trust.go +++ b/trust/trust.go @@ -20,18 +20,23 @@ import ( ) // Tier represents an agent's trust level in the system. +// Usage: use Tier with the other exported helpers in this package. type Tier int const ( // TierUntrusted is for external/community agents with minimal access. + // Usage: compare or pass TierUntrusted when using the related package APIs. TierUntrusted Tier = 1 // TierVerified is for partner agents with scoped access. + // Usage: compare or pass TierVerified when using the related package APIs. TierVerified Tier = 2 // TierFull is for internal agents with full access. + // Usage: compare or pass TierFull when using the related package APIs. TierFull Tier = 3 ) // String returns the human-readable name of the tier. +// Usage: call String(...) during the package's normal workflow. func (t Tier) String() string { switch t { case TierUntrusted: @@ -46,26 +51,47 @@ func (t Tier) String() string { } // Valid returns true if the tier is a recognised trust level. +// Usage: call Valid(...) during the package's normal workflow. func (t Tier) Valid() bool { return t >= TierUntrusted && t <= TierFull } // Capability represents a specific action an agent can perform. +// Usage: use Capability with the other exported helpers in this package. type Capability string const ( - CapPushRepo Capability = "repo.push" - CapMergePR Capability = "pr.merge" - CapCreatePR Capability = "pr.create" - CapCreateIssue Capability = "issue.create" - CapCommentIssue Capability = "issue.comment" - CapReadSecrets Capability = "secrets.read" - CapRunPrivileged Capability = "cmd.privileged" + // CapPushRepo allows pushing commits to a repository. + // Usage: pass CapPushRepo to PolicyEngine.Evaluate or include it in a Policy. + CapPushRepo Capability = "repo.push" + // CapMergePR allows merging a pull request. + // Usage: pass CapMergePR to PolicyEngine.Evaluate or include it in a Policy. + CapMergePR Capability = "pr.merge" + // CapCreatePR allows creating a pull request. + // Usage: pass CapCreatePR to PolicyEngine.Evaluate or include it in a Policy. + CapCreatePR Capability = "pr.create" + // CapCreateIssue allows creating an issue. + // Usage: pass CapCreateIssue to PolicyEngine.Evaluate or include it in a Policy. + CapCreateIssue Capability = "issue.create" + // CapCommentIssue allows commenting on an issue. + // Usage: pass CapCommentIssue to PolicyEngine.Evaluate or include it in a Policy. + CapCommentIssue Capability = "issue.comment" + // CapReadSecrets allows reading secret material. + // Usage: pass CapReadSecrets to PolicyEngine.Evaluate or include it in a Policy. + CapReadSecrets Capability = "secrets.read" + // CapRunPrivileged allows running privileged commands. + // Usage: pass CapRunPrivileged to PolicyEngine.Evaluate or include it in a Policy. + CapRunPrivileged Capability = "cmd.privileged" + // CapAccessWorkspace allows accessing the workspace filesystem. + // Usage: pass CapAccessWorkspace to PolicyEngine.Evaluate or include it in a Policy. CapAccessWorkspace Capability = "workspace.access" - CapModifyFlows Capability = "flows.modify" + // CapModifyFlows allows modifying workflow definitions. + // Usage: pass CapModifyFlows to PolicyEngine.Evaluate or include it in a Policy. + CapModifyFlows Capability = "flows.modify" ) // Agent represents an agent identity in the trust system. +// Usage: use Agent with the other exported helpers in this package. type Agent struct { // Name is the unique identifier for the agent (e.g., "Athena", "Clotho"). Name string @@ -83,12 +109,14 @@ type Agent struct { } // Registry manages agent identities and their trust tiers. +// Usage: use Registry with the other exported helpers in this package. type Registry struct { mu sync.RWMutex agents map[string]*Agent } // NewRegistry creates an empty agent registry. +// Usage: call NewRegistry(...) to create a ready-to-use value. func NewRegistry() *Registry { return &Registry{ agents: make(map[string]*Agent), @@ -97,6 +125,7 @@ func NewRegistry() *Registry { // Register adds or updates an agent in the registry. // Returns an error if the agent name is empty or the tier is invalid. +// Usage: call Register(...) during the package's normal workflow. func (r *Registry) Register(agent Agent) error { if agent.Name == "" { return coreerr.E("trust.Register", "agent name is required", nil) @@ -118,6 +147,7 @@ func (r *Registry) Register(agent Agent) error { } // Get returns the agent with the given name, or nil if not found. +// Usage: call Get(...) during the package's normal workflow. func (r *Registry) Get(name string) *Agent { r.mu.RLock() defer r.mu.RUnlock() @@ -125,6 +155,7 @@ func (r *Registry) Get(name string) *Agent { } // Remove deletes an agent from the registry. +// Usage: call Remove(...) during the package's normal workflow. func (r *Registry) Remove(name string) bool { r.mu.Lock() defer r.mu.Unlock() @@ -136,6 +167,7 @@ func (r *Registry) Remove(name string) bool { } // List returns all registered agents. The returned slice is a snapshot. +// Usage: call List(...) during the package's normal workflow. func (r *Registry) List() []Agent { r.mu.RLock() defer r.mu.RUnlock() @@ -147,6 +179,7 @@ func (r *Registry) List() []Agent { } // ListSeq returns an iterator over all registered agents. +// Usage: call ListSeq(...) during the package's normal workflow. func (r *Registry) ListSeq() iter.Seq[Agent] { return func(yield func(Agent) bool) { r.mu.RLock() @@ -160,6 +193,7 @@ func (r *Registry) ListSeq() iter.Seq[Agent] { } // Len returns the number of registered agents. +// Usage: call Len(...) during the package's normal workflow. func (r *Registry) Len() int { r.mu.RLock() defer r.mu.RUnlock() diff --git a/trust/trust_test.go b/trust/trust_test.go index 303facc..46b5d05 100644 --- a/trust/trust_test.go +++ b/trust/trust_test.go @@ -12,23 +12,23 @@ import ( // --- Tier --- -func TestTierString_Good(t *testing.T) { +func TestTrust_TierString_Good(t *testing.T) { assert.Equal(t, "untrusted", TierUntrusted.String()) assert.Equal(t, "verified", TierVerified.String()) assert.Equal(t, "full", TierFull.String()) } -func TestTierString_Bad_Unknown(t *testing.T) { +func TestTrust_TierString_Bad_Unknown(t *testing.T) { assert.Contains(t, Tier(99).String(), "unknown") } -func TestTierValid_Good(t *testing.T) { +func TestTrust_TierValid_Good(t *testing.T) { assert.True(t, TierUntrusted.Valid()) assert.True(t, TierVerified.Valid()) assert.True(t, TierFull.Valid()) } -func TestTierValid_Bad(t *testing.T) { +func TestTrust_TierValid_Bad(t *testing.T) { assert.False(t, Tier(0).Valid()) assert.False(t, Tier(4).Valid()) assert.False(t, Tier(-1).Valid()) @@ -36,14 +36,14 @@ func TestTierValid_Bad(t *testing.T) { // --- Registry --- -func TestRegistryRegister_Good(t *testing.T) { +func TestTrust_RegistryRegister_Good(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Athena", Tier: TierFull}) require.NoError(t, err) assert.Equal(t, 1, r.Len()) } -func TestRegistryRegister_Good_SetsDefaults(t *testing.T) { +func TestTrust_RegistryRegister_Good_SetsDefaults(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Athena", Tier: TierFull}) require.NoError(t, err) @@ -54,7 +54,7 @@ func TestRegistryRegister_Good_SetsDefaults(t *testing.T) { assert.False(t, a.CreatedAt.IsZero()) } -func TestRegistryRegister_Good_TierDefaults(t *testing.T) { +func TestTrust_RegistryRegister_Good_TierDefaults(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "A", Tier: TierUntrusted})) require.NoError(t, r.Register(Agent{Name: "B", Tier: TierVerified})) @@ -65,14 +65,14 @@ func TestRegistryRegister_Good_TierDefaults(t *testing.T) { assert.Equal(t, 0, r.Get("C").RateLimit) } -func TestRegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) { +func TestTrust_RegistryRegister_Good_PreservesExplicitRateLimit(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Custom", Tier: TierVerified, RateLimit: 30}) require.NoError(t, err) assert.Equal(t, 30, r.Get("Custom").RateLimit) } -func TestRegistryRegister_Good_Update(t *testing.T) { +func TestTrust_RegistryRegister_Good_Update(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierVerified})) require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) @@ -81,21 +81,21 @@ func TestRegistryRegister_Good_Update(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } -func TestRegistryRegister_Bad_EmptyName(t *testing.T) { +func TestTrust_RegistryRegister_Bad_EmptyName(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Tier: TierFull}) assert.Error(t, err) assert.Contains(t, err.Error(), "name is required") } -func TestRegistryRegister_Bad_InvalidTier(t *testing.T) { +func TestTrust_RegistryRegister_Bad_InvalidTier(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "Bad", Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -func TestRegistryGet_Good(t *testing.T) { +func TestTrust_RegistryGet_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) a := r.Get("Athena") @@ -103,24 +103,24 @@ func TestRegistryGet_Good(t *testing.T) { assert.Equal(t, "Athena", a.Name) } -func TestRegistryGet_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryGet_Bad_NotFound(t *testing.T) { r := NewRegistry() assert.Nil(t, r.Get("nonexistent")) } -func TestRegistryRemove_Good(t *testing.T) { +func TestTrust_RegistryRemove_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) assert.True(t, r.Remove("Athena")) assert.Equal(t, 0, r.Len()) } -func TestRegistryRemove_Bad_NotFound(t *testing.T) { +func TestTrust_RegistryRemove_Bad_NotFound(t *testing.T) { r := NewRegistry() assert.False(t, r.Remove("nonexistent")) } -func TestRegistryList_Good(t *testing.T) { +func TestTrust_RegistryList_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified})) @@ -136,12 +136,12 @@ func TestRegistryList_Good(t *testing.T) { assert.True(t, names["Clotho"]) } -func TestRegistryList_Good_Empty(t *testing.T) { +func TestTrust_RegistryList_Good_Empty(t *testing.T) { r := NewRegistry() assert.Empty(t, r.List()) } -func TestRegistryList_Good_Snapshot(t *testing.T) { +func TestTrust_RegistryList_Good_Snapshot(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) agents := r.List() @@ -151,7 +151,7 @@ func TestRegistryList_Good_Snapshot(t *testing.T) { assert.Equal(t, TierFull, r.Get("Athena").Tier) } -func TestRegistryListSeq_Good(t *testing.T) { +func TestTrust_RegistryListSeq_Good(t *testing.T) { r := NewRegistry() require.NoError(t, r.Register(Agent{Name: "Athena", Tier: TierFull})) require.NoError(t, r.Register(Agent{Name: "Clotho", Tier: TierVerified})) @@ -169,7 +169,7 @@ func TestRegistryListSeq_Good(t *testing.T) { // --- Agent --- -func TestAgentTokenExpiry_Good(t *testing.T) { +func TestTrust_AgentTokenExpiry_Good(t *testing.T) { agent := Agent{ Name: "Test", Tier: TierVerified, @@ -183,9 +183,9 @@ func TestAgentTokenExpiry_Good(t *testing.T) { // --- Phase 0 Additions --- -// TestConcurrentRegistryOperations_Good verifies that Register/Get/Remove +// TestTrust_ConcurrentRegistryOperations_Good verifies that Register/Get/Remove // from 10 goroutines do not race. -func TestConcurrentRegistryOperations_Good(t *testing.T) { +func TestTrust_ConcurrentRegistryOperations_Good(t *testing.T) { r := NewRegistry() const n = 10 @@ -224,24 +224,24 @@ func TestConcurrentRegistryOperations_Good(t *testing.T) { // No panic or data race = success (run with -race flag) } -// TestRegisterTierZero_Bad verifies that Tier 0 is rejected. -func TestRegisterTierZero_Bad(t *testing.T) { +// TestTrust_RegisterTierZero_Bad verifies that Tier 0 is rejected. +func TestTrust_RegisterTierZero_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "InvalidTierAgent", Tier: Tier(0)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -// TestRegisterNegativeTier_Bad verifies that negative tiers are rejected. -func TestRegisterNegativeTier_Bad(t *testing.T) { +// TestTrust_RegisterNegativeTier_Bad verifies that negative tiers are rejected. +func TestTrust_RegisterNegativeTier_Bad(t *testing.T) { r := NewRegistry() err := r.Register(Agent{Name: "NegativeTier", Tier: Tier(-1)}) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid tier") } -// TestTokenExpiryBoundary_Good verifies token expiry checking. -func TestTokenExpiryBoundary_Good(t *testing.T) { +// TestTrust_TokenExpiryBoundary_Good verifies token expiry checking. +func TestTrust_TokenExpiryBoundary_Good(t *testing.T) { // Token that expires in the future — should be valid futureAgent := Agent{ Name: "FutureAgent", @@ -256,8 +256,8 @@ func TestTokenExpiryBoundary_Good(t *testing.T) { "token should now be expired") } -// TestTokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour. -func TestTokenExpiryZeroValue_Ugly(t *testing.T) { +// TestTrust_TokenExpiryZeroValue_Ugly verifies zero-value TokenExpiresAt behaviour. +func TestTrust_TokenExpiryZeroValue_Ugly(t *testing.T) { agent := Agent{ Name: "ZeroExpiry", Tier: TierVerified, @@ -274,8 +274,8 @@ func TestTokenExpiryZeroValue_Ugly(t *testing.T) { "zero-value token expiry should be in the past") } -// TestConcurrentListDuringMutations_Good verifies List is safe during writes. -func TestConcurrentListDuringMutations_Good(t *testing.T) { +// TestTrust_ConcurrentListDuringMutations_Good verifies List is safe during writes. +func TestTrust_ConcurrentListDuringMutations_Good(t *testing.T) { r := NewRegistry() // Pre-populate