diff --git a/backend/internal/middleware/license_guard_test.go b/backend/internal/middleware/license_guard_test.go new file mode 100644 index 0000000..32a4f23 --- /dev/null +++ b/backend/internal/middleware/license_guard_test.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestCalcLicensePhase(t *testing.T) { + now := time.Now() + + // nil = perpetual → normal + assert.Equal(t, PhaseNormal, CalcLicensePhase(nil)) + + // future expiry → normal + future := now.Add(10 * 24 * time.Hour) + assert.Equal(t, PhaseNormal, CalcLicensePhase(&future)) + + // just expired (1h ago) → grace + grace := now.Add(-1 * time.Hour) + assert.Equal(t, PhaseGrace, CalcLicensePhase(&grace)) + + // expired 6 days ago → grace (boundary) + grace6d := now.Add(-6 * 24 * time.Hour) + assert.Equal(t, PhaseGrace, CalcLicensePhase(&grace6d)) + + // expired 8 days ago → readonly + readonly := now.Add(-8 * 24 * time.Hour) + assert.Equal(t, PhaseReadOnly, CalcLicensePhase(&readonly)) + + // expired 14 days ago → readonly (boundary) + readonly14d := now.Add(-14 * 24 * time.Hour) + assert.Equal(t, PhaseReadOnly, CalcLicensePhase(&readonly14d)) + + // expired 16 days ago → locked + locked := now.Add(-16 * 24 * time.Hour) + assert.Equal(t, PhaseLocked, CalcLicensePhase(&locked)) +} diff --git a/backend/internal/util/license_key_test.go b/backend/internal/util/license_key_test.go new file mode 100644 index 0000000..f8e3801 --- /dev/null +++ b/backend/internal/util/license_key_test.go @@ -0,0 +1,75 @@ +package util + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLicenseKeyRoundTrip(t *testing.T) { + priv, pub, err := GenerateEd25519KeyPair() + require.NoError(t, err) + + exp := time.Now().Add(30 * 24 * time.Hour).Unix() + payload := LicensePayload{ + ShopID: 42, + LicenseID: 7, + Type: "annual", + IssuedAt: time.Now().Unix(), + ExpiresAt: &exp, + MaxDevices: 3, + } + + token, err := IssueLicenseToken(payload, priv) + require.NoError(t, err) + assert.NotEmpty(t, token) + + got, err := VerifyLicenseToken(token, pub) + require.NoError(t, err) + assert.Equal(t, payload.ShopID, got.ShopID) + assert.Equal(t, payload.Type, got.Type) + assert.Equal(t, payload.MaxDevices, got.MaxDevices) + assert.Equal(t, *payload.ExpiresAt, *got.ExpiresAt) +} + +func TestVerifyLicenseToken_TamperedPayload(t *testing.T) { + priv, pub, err := GenerateEd25519KeyPair() + require.NoError(t, err) + + exp := time.Now().Add(30 * 24 * time.Hour).Unix() + token, err := IssueLicenseToken(LicensePayload{ + ShopID: 1, Type: "trial", IssuedAt: time.Now().Unix(), ExpiresAt: &exp, MaxDevices: 1, + }, priv) + require.NoError(t, err) + + // Flip the last byte of the signature to simulate tampering + tampered := token[:len(token)-2] + "XX" + _, err = VerifyLicenseToken(tampered, pub) + assert.Error(t, err, "tampered token must be rejected") +} + +func TestVerifyLicenseToken_WrongKey(t *testing.T) { + priv, _, err := GenerateEd25519KeyPair() + require.NoError(t, err) + _, otherPub, err := GenerateEd25519KeyPair() + require.NoError(t, err) + + exp := time.Now().Add(30 * 24 * time.Hour).Unix() + token, err := IssueLicenseToken(LicensePayload{ + ShopID: 1, Type: "trial", IssuedAt: time.Now().Unix(), ExpiresAt: &exp, MaxDevices: 1, + }, priv) + require.NoError(t, err) + + _, err = VerifyLicenseToken(token, otherPub) + assert.ErrorIs(t, err, ErrInvalidLicenseSignature) +} + +func TestVerifyLicenseToken_InvalidFormat(t *testing.T) { + _, pub, err := GenerateEd25519KeyPair() + require.NoError(t, err) + + _, err = VerifyLicenseToken("not-a-valid-token", pub) + assert.ErrorIs(t, err, ErrInvalidLicenseToken) +}