Files
2026-07-20 21:41:51 +02:00

206 lines
7.7 KiB
Go

package platform
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/b1tsblog/license-platform/pkg/licensekit"
)
func testServer(t *testing.T, store *Store) *Server {
t.Helper()
master := base64.RawStdEncoding.EncodeToString(bytes.Repeat([]byte{7}, 32))
server, err := New(Config{Brand: "Test Platform", PublicURL: "https://licenses.example.test", MasterKey: master, BootstrapUsername: "admin", BootstrapPassword: "a-very-long-test-password", AdminAPIToken: "api-secret-0123456789-0123456789-ab", SessionTTL: time.Hour}, store, slog.New(slog.NewTextHandler(io.Discard, nil)))
if err != nil {
t.Fatal(err)
}
return server
}
func TestPasswordHash(t *testing.T) {
hash, err := HashPassword("a-very-long-password")
if err != nil {
t.Fatal(err)
}
if !VerifyPassword(hash, "a-very-long-password") || VerifyPassword(hash, "wrong-password") {
t.Fatal("password verification mismatch")
}
}
func TestKeysAreWriteOnceAndEncrypted(t *testing.T) {
path := t.TempDir() + "/platform.json"
store, err := OpenStore(path)
if err != nil {
t.Fatal(err)
}
server := testServer(t, store)
if err := server.generateKeys("issuer", "lease"); err != nil {
t.Fatal(err)
}
if err := server.generateKeys("issuer-2", "lease-2"); err != ErrKeysLocked {
t.Fatalf("expected ErrKeysLocked, got %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "private") && strings.Contains(string(data), "MC4CAQ") {
t.Fatal("data store appears to contain a plaintext private key")
}
keys, ok := store.KeySet()
if !ok || !strings.HasPrefix(keys.IssuerPrivateCipher, "v1.") || !strings.HasPrefix(keys.LeasePrivateCipher, "v1.") {
t.Fatal("private keys are not encrypted")
}
}
func TestIssueValidateAndRevoke(t *testing.T) {
store, _ := OpenStore("")
server := testServer(t, store)
if err := server.generateKeys("issuer", "lease"); err != nil {
t.Fatal(err)
}
admin, ok := store.FindUserByUsername("admin")
if !ok {
t.Fatal("bootstrap admin missing")
}
record, token, err := server.issueLicense(admin, IssueInput{Customer: "ACME", Product: "product", Edition: "pro", Features: []string{"feature"}, Domains: []string{"*"}, Mode: "hybrid", Days: 30, LeaseMinutes: 60, GraceHours: 72})
if err != nil {
t.Fatal(err)
}
trust, _ := server.trustStore()
verified, err := licensekit.VerifyLicense(trust, token, time.Now().UTC())
if err != nil {
t.Fatal(err)
}
if verified.Claims.Verification.ServerURL != "https://licenses.example.test" {
t.Fatalf("unexpected embedded server URL %q", verified.Claims.Verification.ServerURL)
}
response, err := server.validateLicense(validateRequest{Token: token, Product: "product", BaseURL: "https://app.example.test"})
if err != nil || !response.Valid || response.LeaseToken == "" {
t.Fatalf("validation failed: %#v %v", response, err)
}
if err := store.SetRevoked(record.LicenseID, true, "payment"); err != nil {
t.Fatal(err)
}
if _, err := server.validateLicense(validateRequest{Token: token, Product: "product", BaseURL: "https://app.example.test"}); err == nil {
t.Fatal("revoked license was accepted")
}
}
func TestManagementAndValidationAPI(t *testing.T) {
store, _ := OpenStore("")
server := testServer(t, store)
if err := server.generateKeys("issuer", "lease"); err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(server.Handler())
defer ts.Close()
issue := IssueInput{Customer: "API Customer", Product: "api-product", Edition: "team", Domains: []string{"*"}, Mode: "hybrid", Days: 10, LeaseMinutes: 30, GraceHours: 24}
body, _ := json.Marshal(issue)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, ts.URL+"/api/v1/licenses", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer api-secret-0123456789-0123456789-ab")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
var created LicenseView
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated || created.Token == "" {
t.Fatalf("issue API failed: %d %#v", resp.StatusCode, created)
}
validation, _ := json.Marshal(validateRequest{Token: created.Token, Product: "api-product", BaseURL: "https://customer.example.test"})
resp, err = http.Post(ts.URL+"/api/v1/licenses/validate", "application/json", bytes.NewReader(validation))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
payload, _ := io.ReadAll(resp.Body)
t.Fatalf("validation API failed: %d %s", resp.StatusCode, payload)
}
}
func TestResellerCustomerIsolation(t *testing.T) {
store, _ := OpenStore("")
server := testServer(t, store)
if err := server.generateKeys("issuer", "lease"); err != nil {
t.Fatal(err)
}
hash, _ := HashPassword("a-very-long-customer-password")
resellerA := User{ID: "reseller_a", Username: "reseller.a", DisplayName: "Reseller A", Role: RoleReseller, PasswordHash: hash, Active: true}
resellerB := User{ID: "reseller_b", Username: "reseller.b", DisplayName: "Reseller B", Role: RoleReseller, PasswordHash: hash, Active: true}
customer := User{ID: "customer_a", Username: "customer.a", DisplayName: "Customer A", Role: RoleCustomer, ParentUserID: resellerA.ID, PasswordHash: hash, Active: true}
for _, user := range []User{resellerA, resellerB, customer} {
if err := store.CreateUser(user); err != nil {
t.Fatal(err)
}
}
input := IssueInput{CustomerUserID: customer.ID, Customer: customer.DisplayName, Product: "product", Edition: "pro", Domains: []string{"*"}, Mode: "hybrid", Days: 30, LeaseMinutes: 60, GraceHours: 24}
if _, _, err := server.issueLicense(resellerB, input); err == nil {
t.Fatal("unassigned reseller issued a license for another reseller's customer")
}
record, _, err := server.issueLicense(resellerA, input)
if err != nil {
t.Fatal(err)
}
if got := store.ListLicensesFor(resellerB); len(got) != 0 {
t.Fatalf("reseller B can see %d foreign licenses", len(got))
}
if got := store.ListLicensesFor(customer); len(got) != 1 || got[0].LicenseID != record.LicenseID {
t.Fatalf("customer portal did not receive its license: %#v", got)
}
}
func TestLegacyRegistrationCompatibility(t *testing.T) {
store, _ := OpenStore("")
server := testServer(t, store)
if err := server.generateKeys("issuer", "lease"); err != nil {
t.Fatal(err)
}
keys, _ := store.KeySet()
encoded, err := server.vault.Decrypt(keys.IssuerPrivateCipher)
if err != nil {
t.Fatal(err)
}
privateKey, err := licensekit.DecodePrivateKey(encoded)
if err != nil {
t.Fatal(err)
}
now := time.Now().UTC()
token, err := licensekit.SignLicense(privateKey, keys.IssuerKeyID, licensekit.Claims{Version: 1, LicenseID: "legacy-license", Issuer: "legacy", Customer: "Legacy Customer", Product: "legacy-product", Edition: "pro", Domains: []string{"*"}, IssuedAt: now.Unix(), ExpiresAt: now.Add(24 * time.Hour).Unix(), Verification: licensekit.VerificationPolicy{Mode: licensekit.ModeHybrid, ServerURL: "https://licenses.example.test"}})
if err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(server.Handler())
defer ts.Close()
body, _ := json.Marshal(map[string]string{"token": token})
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/admin/licenses", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer api-secret-0123456789-0123456789-ab")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("legacy register status %d", resp.StatusCode)
}
if _, ok := store.GetLicense("legacy-license"); !ok {
t.Fatal("legacy license was not imported")
}
}