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) } }