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
This commit is contained in:
parent
99ee8758cc
commit
e49882b8a8
9 changed files with 1280 additions and 0 deletions
133
internal/opnsense/integration_test.go
Normal file
133
internal/opnsense/integration_test.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
//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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue