Files
zeekay 3ccbe432ae luxfi/esign v0.3: Hanzo Base as the default backend store
Make Hanzo Base — per-tenant SQLite + extension runtime (HIP-0105,
HIP-0107, HIP-0302) — the canonical production backend. MemStore stays
for tests and ephemeral local dev.

  * pkg/basestore — BaseStore implements api.Store on a core.App.
    Single esign_envelopes collection holds the canonical JSON
    envelope as the body field; state + doc_hash + version +
    updated_at are mirrored as indexed columns for cheap dashboard
    queries, but the JSON blob is the source of truth so envelope
    schema changes never require a SQLite migration.
    ensureCollection is idempotent.
    Indexes: (state, updated_at DESC), (doc_hash).

  * pkg/basestore/store_test.go — round-trip, upsert, missing-id,
    empty-id rejection. Uses t.TempDir() so each test isolates its
    own SQLite file.

  * pkg/api/handlers.go — bump default envelope ID generator from
    "env_<8 hex>" to "esign<16 hex>" (21 chars, ^[a-z0-9]+$) to
    match Base's default record-id format and clear the 15-char
    minimum. ID stays deterministic from the document so identical
    bytes get the same envelope ID.

  * cmd/esignd — default to BaseStore against `--data-dir esign_data`.
    Pass `--memory` for the in-process MemStore (ephemeral, tests).
    buildStore handles bootstrap + cleanup so the rest of main stays
    a clean wire-up.

Test summary: 4 packages, 26 tests, all green.
2026-06-07 11:43:52 -07:00

116 lines
3.3 KiB
Go

// Copyright 2026 Lux Industries Inc. and the luxfi/esign authors.
// Licensed under the Apache License, Version 2.0 (see LICENSE).
package basestore
import (
"context"
"crypto/sha256"
"testing"
"time"
"github.com/hanzoai/base"
"github.com/luxfi/esign/pkg/envelope"
)
// testApp boots a fresh Base instance against a t.TempDir so the
// per-test SQLite file is isolated and torn down by the test framework.
func testApp(t *testing.T) *base.Base {
t.Helper()
dir := t.TempDir()
app := base.NewWithConfig(base.Config{DefaultDataDir: dir})
if err := app.Bootstrap(); err != nil {
t.Fatalf("Bootstrap: %v", err)
}
t.Cleanup(func() { _ = app.ResetBootstrapState() })
return app
}
// Round-trip through Base: write an envelope, read it back, fields match.
func TestBaseStore_RoundTrip(t *testing.T) {
app := testApp(t)
store, err := NewBaseStore(app)
if err != nil {
t.Fatalf("NewBaseStore: %v", err)
}
now := time.Unix(1_700_000_000, 0).UTC()
doc := []byte("subscription agreement v1\n")
docHash := sha256.Sum256(doc)
want := &envelope.Envelope{
ID: "esignroundtrip001a",
Version: 1,
Document: doc,
DocHash: docHash,
Signers: []envelope.SignerSlot{{SignerHash: [32]byte{1}, Method: envelope.MethodEd25519, Required: true}},
State: envelope.StateDraft,
CreatedAt: now,
UpdatedAt: now,
}
ctx := context.Background()
if err := store.Put(ctx, want); err != nil {
t.Fatalf("Put: %v", err)
}
got, err := store.Get(ctx, want.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.ID != want.ID || got.State != want.State || got.Version != want.Version ||
got.DocHash != want.DocHash {
t.Fatalf("envelope roundtrip mismatch:\ngot: %+v\nwant: %+v", got, want)
}
if len(got.Signers) != 1 || got.Signers[0].SignerHash != want.Signers[0].SignerHash {
t.Errorf("signers lost in roundtrip: %+v", got.Signers)
}
}
// Put updates an existing record instead of creating a duplicate.
func TestBaseStore_Upsert(t *testing.T) {
app := testApp(t)
store, _ := NewBaseStore(app)
ctx := context.Background()
doc := []byte("a")
docHash := sha256.Sum256(doc)
e := &envelope.Envelope{
ID: "esignupserttest01", Version: 1, Document: doc, DocHash: docHash,
State: envelope.StateDraft, UpdatedAt: time.Now().UTC(),
}
if err := store.Put(ctx, e); err != nil {
t.Fatalf("Put #1: %v", err)
}
e.Version = 2
e.State = envelope.StateSent
e.UpdatedAt = time.Now().UTC()
if err := store.Put(ctx, e); err != nil {
t.Fatalf("Put #2: %v", err)
}
got, err := store.Get(ctx, e.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.Version != 2 || got.State != envelope.StateSent {
t.Errorf("upsert lost: got version=%d state=%s", got.Version, got.State)
}
}
// Get on a missing ID returns an error rather than a zero-value envelope.
func TestBaseStore_GetMissing(t *testing.T) {
app := testApp(t)
store, _ := NewBaseStore(app)
if _, err := store.Get(context.Background(), "no-such-id"); err == nil {
t.Fatal("Get(missing) returned nil error")
}
}
// Empty ID is rejected at Put rather than silently dropping the record.
func TestBaseStore_RejectsEmptyID(t *testing.T) {
app := testApp(t)
store, _ := NewBaseStore(app)
if err := store.Put(context.Background(), &envelope.Envelope{}); err == nil {
t.Fatal("Put(empty ID) returned nil error")
}
}