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