mirror of
https://github.com/luxfi/esign.git
synced 2026-10-30 16:58:18 +00:00
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.
116 lines
3.3 KiB
Go
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")
|
|
}
|
|
}
|