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

+188
View File
@@ -0,0 +1,188 @@
package store
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
"testing"
"time"
)
func TestEnroll(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
if err := s.AddEnrollmentToken(ctx, "sltn_geheim", "t1", clock.now().Add(time.Hour)); err != nil {
t.Fatalf("AddEnrollmentToken: %v", err)
}
n := sampleNode("n1", "key1")
n.TenantID = "manipuliert" // muss durch den Mandanten des Tokens ersetzt werden
node, err := s.Enroll(ctx, "sltn_geheim", n)
if err != nil {
t.Fatalf("Enroll: %v", err)
}
if node.TenantID != "t1" {
t.Errorf("TenantID = %q, erwartet t1 (aus dem Token)", node.TenantID)
}
if got, err := s.GetNodeByPublicKey(ctx, "key1"); err != nil || got.ID != "n1" {
t.Errorf("Knoten nicht angelegt: %+v, %v", got, err)
}
}
func TestAddEnrollmentToken_StoresOnlyHash(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
if err := s.AddEnrollmentToken(ctx, "sltn_geheim", "t1", clock.now().Add(time.Hour)); err != nil {
t.Fatal(err)
}
var stored string
if err := s.db.QueryRowContext(ctx, "SELECT token_hash FROM enrollment_tokens").Scan(&stored); err != nil {
t.Fatal(err)
}
sum := sha256.Sum256([]byte("sltn_geheim"))
if stored != hex.EncodeToString(sum[:]) {
t.Errorf("gespeichert %q, erwartet SHA-256-Hex des Tokens", stored)
}
}
func TestAddEnrollmentToken_Errors(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
later := clock.now().Add(time.Hour)
if err := s.AddEnrollmentToken(ctx, "tok", "fehlt", later); !errors.Is(err, ErrNotFound) {
t.Errorf("unbekannter Mandant: erwartet ErrNotFound, bekommen %v", err)
}
if err := s.AddEnrollmentToken(ctx, "", "t1", later); !errors.Is(err, ErrInvalid) {
t.Errorf("leerer Token: erwartet ErrInvalid, bekommen %v", err)
}
if err := s.AddEnrollmentToken(ctx, "tok", "t1", clock.now()); !errors.Is(err, ErrInvalid) {
t.Errorf("Ablauf nicht in der Zukunft: erwartet ErrInvalid, bekommen %v", err)
}
if err := s.AddEnrollmentToken(ctx, "tok", "t1", later); err != nil {
t.Fatal(err)
}
if err := s.AddEnrollmentToken(ctx, "tok", "t1", later); !errors.Is(err, ErrExists) {
t.Errorf("doppelter Token: erwartet ErrExists, bekommen %v", err)
}
}
func TestEnroll_TokenOnlyOnce(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
s.AddEnrollmentToken(ctx, "tok", "t1", clock.now().Add(time.Hour))
if _, err := s.Enroll(ctx, "tok", sampleNode("n1", "k1")); err != nil {
t.Fatal(err)
}
if _, err := s.Enroll(ctx, "tok", sampleNode("n2", "k2")); !errors.Is(err, ErrTokenUsed) {
t.Errorf("erwartet ErrTokenUsed, bekommen %v", err)
}
if _, err := s.GetNode(ctx, "n2"); !errors.Is(err, ErrNotFound) {
t.Errorf("zweiter Knoten wurde trotzdem angelegt")
}
}
func TestEnroll_Expired(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
s.AddEnrollmentToken(ctx, "tok", "t1", clock.now().Add(time.Hour))
clock.advance(time.Hour) // Ablaufzeitpunkt selbst gilt schon als abgelaufen
if _, err := s.Enroll(ctx, "tok", sampleNode("n1", "k1")); !errors.Is(err, ErrTokenExpired) {
t.Errorf("erwartet ErrTokenExpired, bekommen %v", err)
}
if _, err := s.GetNode(ctx, "n1"); !errors.Is(err, ErrNotFound) {
t.Errorf("Knoten trotz abgelaufenem Token angelegt")
}
}
func TestEnroll_UnknownToken(t *testing.T) {
s, _ := openTest(t)
if _, err := s.Enroll(ctx, "gibts-nicht", sampleNode("n1", "k1")); !errors.Is(err, ErrNotFound) {
t.Errorf("erwartet ErrNotFound, bekommen %v", err)
}
}
// Schlägt das Anlegen des Knotens fehl, darf der Token nicht verbraucht sein.
func TestEnroll_RollbackKeepsToken(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
mustNode(t, s, "vorhanden", "t1", "k-belegt")
s.AddEnrollmentToken(ctx, "tok", "t1", clock.now().Add(time.Hour))
if _, err := s.Enroll(ctx, "tok", sampleNode("n1", "k-belegt")); !errors.Is(err, ErrExists) {
t.Fatalf("doppelter Key: erwartet ErrExists, bekommen %v", err)
}
invalid := sampleNode("n1", "k-neu")
invalid.Hostname = ""
if _, err := s.Enroll(ctx, "tok", invalid); !errors.Is(err, ErrInvalid) {
t.Fatalf("ungültiger Knoten: erwartet ErrInvalid, bekommen %v", err)
}
if _, err := s.Enroll(ctx, "tok", sampleNode("n1", "k-neu")); err != nil {
t.Errorf("Token nach fehlgeschlagenem Enroll nicht mehr nutzbar: %v", err)
}
}
func TestEnroll_ConcurrentSingleUse(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
s.AddEnrollmentToken(ctx, "tok", "t1", clock.now().Add(time.Hour))
const workers = 10
errs := make([]error, workers)
var wg sync.WaitGroup
for i := range workers {
wg.Go(func() {
_, errs[i] = s.Enroll(ctx, "tok", sampleNode(fmt.Sprintf("n%d", i), fmt.Sprintf("k%d", i)))
})
}
wg.Wait()
ok, used := 0, 0
for _, err := range errs {
switch {
case err == nil:
ok++
case errors.Is(err, ErrTokenUsed):
used++
default:
t.Errorf("unerwarteter Fehler: %v", err)
}
}
if ok != 1 || used != workers-1 {
t.Errorf("%d erfolgreich, %d ErrTokenUsed – erwartet genau 1 Erfolg", ok, used)
}
nodes, _ := s.ListNodes(ctx, "t1")
if len(nodes) != 1 {
t.Errorf("%d Knoten angelegt, erwartet 1", len(nodes))
}
}
func TestPurgeEnrollmentTokens(t *testing.T) {
s, clock := openTest(t)
mustTenant(t, s, "t1")
s.AddEnrollmentToken(ctx, "benutzt", "t1", clock.now().Add(time.Hour))
s.AddEnrollmentToken(ctx, "läuft-ab", "t1", clock.now().Add(time.Minute))
s.AddEnrollmentToken(ctx, "gültig", "t1", clock.now().Add(2*time.Hour))
if _, err := s.Enroll(ctx, "benutzt", sampleNode("n1", "k1")); err != nil {
t.Fatal(err)
}
clock.advance(time.Minute)
n, err := s.PurgeEnrollmentTokens(ctx)
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Errorf("%d Tokens gelöscht, erwartet 2", n)
}
if _, err := s.Enroll(ctx, "gültig", sampleNode("n2", "k2")); err != nil {
t.Errorf("gültiger Token wurde gelöscht: %v", err)
}
if _, err := s.Enroll(ctx, "läuft-ab", sampleNode("n3", "k3")); !errors.Is(err, ErrNotFound) {
t.Errorf("abgelaufener Token noch vorhanden: %v", err)
}
}