opnsense-portal/internal/opnsense/integration_test.go
Carsten Abele e49882b8a8 feat(opnsense,certmatch): read-only Export-Client mit Streaming und CN-Zuordnung
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
2026-08-14 09:15:53 +02:00

133 lines
3.8 KiB
Go

//go:build integration
// Diese Tests laufen nur mit `go test -tags integration ./internal/opnsense/`
// gegen eine echte OPNsense-Testinstanz. Sie bestätigen die in
// docs/opnsense-api.md dokumentierten Annahmen über Feldnamen und Formate.
package opnsense
import (
"context"
"io"
"os"
"testing"
"time"
)
func integrationClient(t *testing.T) *Client {
t.Helper()
base, key, secret := os.Getenv("OPNSENSE_URL"), os.Getenv("OPNSENSE_KEY"), os.Getenv("OPNSENSE_SECRET")
if base == "" || key == "" || secret == "" {
t.Skip("OPNSENSE_URL/OPNSENSE_KEY/OPNSENSE_SECRET nicht gesetzt")
}
c, err := New(Options{BaseURL: base, APIKey: key, APISecret: secret,
CAFile: os.Getenv("OPNSENSE_CA"), Timeout: 20 * time.Second})
if err != nil {
t.Fatal(err)
}
return c
}
// TestIntegrationProviders bestätigt Feldnamen und Map-Form der Provider-Antwort.
func TestIntegrationProviders(t *testing.T) {
ps, err := integrationClient(t).Providers(context.Background())
if err != nil {
t.Fatalf("Providers: %v", err)
}
if len(ps) == 0 {
t.Fatal("keine Provider — mindestens eine OpenVPN-Instanz muss exportierbar sein")
}
for _, p := range ps {
if p.VPNID == "" || p.Name == "" {
t.Errorf("unvollständiger Provider: %+v", p)
}
}
}
// TestIntegrationAccountsExpiryIsParsed bestätigt, dass das Ablaufdatum in
// einem der unterstützten Formate ankommt und nicht stillschweigend leer bleibt.
func TestIntegrationAccountsExpiryIsParsed(t *testing.T) {
c := integrationClient(t)
ps, err := c.Providers(context.Background())
if err != nil {
t.Fatal(err)
}
var seen, withDate int
for _, p := range ps {
accs, err := c.Accounts(context.Background(), p.VPNID)
if err != nil {
t.Fatalf("Accounts(%s): %v", p.VPNID, err)
}
for _, a := range accs {
seen++
if a.CommonName == "" {
t.Errorf("Zertifikat %s ohne CommonName — Feldname prüfen: %+v", a.RefID, a)
}
if !a.ValidTo.IsZero() {
withDate++
}
}
}
if seen == 0 {
t.Skip("keine Zertifikate auf der Testinstanz")
}
if withDate == 0 {
t.Fatalf("kein einziges der %d Zertifikate hat ein auswertbares Ablaufdatum — "+
"flexTimeLayouts bzw. die Feldnamen in rawAccount müssen ergänzt werden", seen)
}
}
// TestIntegrationAccountsRevokedField bestätigt, dass revozierte Zertifikate
// als solche erkennbar sind. Voraussetzung: auf der Testinstanz existiert ein
// revoziertes Zertifikat mit CN aus OPNSENSE_REVOKED_CN.
func TestIntegrationAccountsRevokedField(t *testing.T) {
wantCN := os.Getenv("OPNSENSE_REVOKED_CN")
if wantCN == "" {
t.Skip("OPNSENSE_REVOKED_CN nicht gesetzt")
}
c := integrationClient(t)
ps, err := c.Providers(context.Background())
if err != nil {
t.Fatal(err)
}
for _, p := range ps {
accs, err := c.Accounts(context.Background(), p.VPNID)
if err != nil {
t.Fatalf("Accounts(%s): %v", p.VPNID, err)
}
for _, a := range accs {
if a.CommonName == wantCN {
if !a.Revoked {
t.Fatalf("Zertifikat %q wird nicht als revoziert gemeldet: %+v", wantCN, a)
}
if a.IsUsable(time.Now()) {
t.Fatalf("revoziertes Zertifikat %q gilt als nutzbar", wantCN)
}
return
}
}
}
t.Fatalf("CN %q auf keiner Instanz gefunden", wantCN)
}
// TestIntegrationExportFormats bestätigt, dass beide angebotenen Formate
// nicht-leere Konfigurationen liefern.
func TestIntegrationExportFormats(t *testing.T) {
refID := os.Getenv("OPNSENSE_CERT_REF")
vpnID := os.Getenv("OPNSENSE_VPNID")
if refID == "" || vpnID == "" {
t.Skip("OPNSENSE_CERT_REF/OPNSENSE_VPNID nicht gesetzt")
}
c := integrationClient(t)
for _, format := range []string{FormatOVPN, FormatViscosity} {
res, err := c.Export(context.Background(), vpnID, refID, format)
if err != nil {
t.Errorf("Export(%s): %v", format, err)
continue
}
n, _ := io.Copy(io.Discard, res.Body)
res.Body.Close()
if n == 0 {
t.Errorf("Export(%s) lieferte 0 Bytes", format)
}
}
}