144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
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)
|
||
}
|
||
}
|