erste module drin.

zwischenstand: Architektur aktualisiert, TOFU implementiert,
This commit is contained in:
br committed 2026-10-01 22:22:13 +02:00
1 parent 9d5789c5d7
commit c8df9d7651
41 files changed
+6455 -36

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
package store
import (
"context"
"errors"
"path/filepath"
"testing"
"time"
)
var ctx = context.Background()
// fakeClock ist eine steuerbare Uhr für deterministische Zeitstempel.
type fakeClock struct{ t time.Time }
func (c *fakeClock) now() time.Time { return c.t }
func (c *fakeClock) advance(d time.Duration) { c.t = c.t.Add(d) }
// openTest öffnet einen frischen Store in einem temporären Verzeichnis mit
// steuerbarer Uhr.
func openTest(t *testing.T) (*Store, *fakeClock) {
t.Helper()
s, err := Open(ctx, filepath.Join(t.TempDir(), "sultan.db"))
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { s.Close() })
clock := &fakeClock{t: time.Date(2026, 10, 1, 12, 0, 0, 123456789, time.UTC)}
s.now = clock.now
return s, clock
}
func TestOpen_MigratesSchema(t *testing.T) {
s, _ := openTest(t)
v, err := s.SchemaVersion(ctx)
if err != nil {
t.Fatal(err)
}
if v != len(migrations) || v < 1 {
t.Errorf("Schema-Version = %d, erwartet %d", v, len(migrations))
}
}
func TestOpen_ReopenKeepsData(t *testing.T) {
path := filepath.Join(t.TempDir(), "sultan.db")
s, err := Open(ctx, path)
if err != nil {
t.Fatal(err)
}
if _, err := s.CreateTenant(ctx, sampleTenant("t1")); err != nil {
t.Fatal(err)
}
if err := s.Close(); err != nil {
t.Fatal(err)
}
s, err = Open(ctx, path)
if err != nil {
t.Fatalf("erneutes Open: %v", err)
}
defer s.Close()
if _, err := s.GetTenant(ctx, "t1"); err != nil {
t.Errorf("Daten nach erneutem Öffnen weg: %v", err)
}
if v, _ := s.SchemaVersion(ctx); v != len(migrations) {
t.Errorf("Schema-Version nach erneutem Öffnen = %d", v)
}
}
func TestOpen_InvalidPath(t *testing.T) {
_, err := Open(ctx, filepath.Join(t.TempDir(), "gibt", "es", "nicht", "sultan.db"))
if err == nil {
t.Error("erwartet Fehler für nicht existierendes Verzeichnis")
}
}
func TestOpen_RejectsNewerSchema(t *testing.T) {
path := filepath.Join(t.TempDir(), "sultan.db")
s, err := Open(ctx, path)
if err != nil {
t.Fatal(err)
}
if _, err := s.db.ExecContext(ctx, "PRAGMA user_version = 999"); err != nil {
t.Fatal(err)
}
s.Close()
if _, err := Open(ctx, path); !errors.Is(err, ErrSchemaTooNew) {
t.Errorf("erwartet ErrSchemaTooNew, bekommen %v", err)
}
}
func TestOpen_ForeignKeysEnabled(t *testing.T) {
s, _ := openTest(t)
var on int
if err := s.db.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&on); err != nil {
t.Fatal(err)
}
if on != 1 {
t.Error("foreign_keys ist nicht aktiv – ON DELETE CASCADE würde nicht greifen")
}
}
func TestDBTime_Scan(t *testing.T) {
want := time.Date(2026, 12, 15, 10, 30, 0, 987654321, time.UTC)
cet := want.In(time.FixedZone("CET", 3600))
for name, src := range map[string]any{
"Text": "2026-12-15T10:30:00.987654321Z",
"Bytes": []byte("2026-12-15T10:30:00.987654321Z"),
"time.Time (CET)": cet,
} {
t.Run(name, func(t *testing.T) {
var got time.Time
if err := (dbTime{&got}).Scan(src); err != nil {
t.Fatal(err)
}
if !got.Equal(want) || got.Location() != time.UTC {
t.Errorf("got %v, erwartet %v in UTC", got, want)
}
})
}
var got time.Time
if err := (dbTime{&got}).Scan("15.12.2026"); err == nil {
t.Error("ungültiger Text: erwartet Fehler")
}
if err := (dbTime{&got}).Scan(int64(42)); err == nil {
t.Error("unerwarteter Typ: erwartet Fehler")
}
}
// Zeitstempel müssen als Text fester Breite gespeichert werden, sonst
// funktionieren Vergleiche in SQL (z. B. PurgeEnrollmentTokens) nicht.
func TestTimesStoredAsSortableText(t *testing.T) {
s, _ := openTest(t)
mustTenant(t, s, "t1")
var typ, raw string
if err := s.db.QueryRowContext(ctx, "SELECT typeof(created_at), CAST(created_at AS TEXT) FROM tenants").Scan(&typ, &raw); err != nil {
t.Fatal(err)
}
if typ != "text" || raw != "2026-10-01T12:00:00.123456789Z" {
t.Errorf("gespeichert als %s %q", typ, raw)
}
}