Files
2026-07-14 05:49:15 +00:00

131 lines
4.0 KiB
Go

package fixture
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"io"
"net"
"net/http"
"strings"
"testing"
"time"
)
func TestFixture_VerifiesLocalhostTLS(t *testing.T) {
// Given
fixture, err := NewLocalTLSFixture(time.Now())
requireNoFixtureError(t, err)
address, closeServer := startLocalTLSServer(t, fixture)
defer closeServer()
client := localTLSClient(fixture.ClientConfig("localhost"), address)
// When
response, err := client.Get("https://localhost:" + portOf(t, address) + "/ready")
requireNoFixtureError(t, err)
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
requireNoFixtureError(t, err)
// Then
if response.StatusCode != http.StatusOK || string(body) != "tls-ready" {
t.Fatalf("TLS response = %d %q", response.StatusCode, body)
}
if fixture.ClientConfig("localhost").InsecureSkipVerify {
t.Fatal("TLS fixture disabled certificate verification")
}
if len(fixture.CAPEM()) == 0 || len(fixture.CertificatePEM()) == 0 || len(fixture.PrivateKeyPEM()) == 0 {
t.Fatal("TLS fixture did not expose certificate material")
}
assertLocalCertificateProperties(t, fixture)
}
func assertLocalCertificateProperties(t *testing.T, fixture LocalTLSFixture) {
t.Helper()
caBlock, _ := pem.Decode(fixture.CAPEM())
if caBlock == nil {
t.Fatal("fixture CA PEM is invalid")
}
caCertificate, err := x509.ParseCertificate(caBlock.Bytes)
requireNoFixtureError(t, err)
if !caCertificate.IsCA || !caCertificate.BasicConstraintsValid || caCertificate.KeyUsage&x509.KeyUsageCertSign == 0 {
t.Fatalf("fixture CA constraints are invalid: %+v", caCertificate)
}
leafBlock, _ := pem.Decode(fixture.CertificatePEM())
if leafBlock == nil {
t.Fatal("fixture leaf PEM is invalid")
}
leafCertificate, err := x509.ParseCertificate(leafBlock.Bytes)
requireNoFixtureError(t, err)
if err := leafCertificate.VerifyHostname("localhost"); err != nil {
t.Fatalf("verify localhost SAN: %v", err)
}
if err := leafCertificate.VerifyHostname("127.0.0.1"); err != nil {
t.Fatalf("verify loopback SAN: %v", err)
}
}
func TestFixture_RejectsTLSNameMismatch(t *testing.T) {
// Given
fixture, err := NewLocalTLSFixture(time.Now())
requireNoFixtureError(t, err)
address, closeServer := startLocalTLSServer(t, fixture)
defer closeServer()
client := localTLSClient(fixture.ClientConfig("wronghost.invalid"), address)
// When
_, err = client.Get("https://wronghost.invalid:" + portOf(t, address) + "/ready")
// Then
var hostnameError x509.HostnameError
if !errors.As(err, &hostnameError) {
t.Fatalf("TLS mismatch error = %v", err)
}
}
func startLocalTLSServer(t *testing.T, fixture LocalTLSFixture) (string, func()) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
requireNoFixtureError(t, err)
tlsListener := fixture.Listener(listener)
server := &http.Server{Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/ready" {
http.NotFound(writer, request)
return
}
_, _ = io.WriteString(writer, "tls-ready")
})}
serveDone := make(chan error, 1)
go func() { serveDone <- server.Serve(tlsListener) }()
closeServer := func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
requireNoFixtureError(t, server.Shutdown(ctx))
serveErr := <-serveDone
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
t.Fatalf("serve TLS: %v", serveErr)
}
}
return listener.Addr().String(), closeServer
}
func localTLSClient(config *tls.Config, address string) *http.Client {
transport := &http.Transport{
TLSClientConfig: config,
DialContext: func(ctx context.Context, network, _ string) (net.Conn, error) {
var dialer net.Dialer
return dialer.DialContext(ctx, network, address)
},
}
return &http.Client{Transport: transport, Timeout: 2 * time.Second}
}
func portOf(t *testing.T, address string) string {
t.Helper()
_, port, err := net.SplitHostPort(address)
requireNoFixtureError(t, err)
return strings.TrimSpace(port)
}