feat: backend service

This commit is contained in:
Ilia Denisov
2026-05-06 10:14:55 +03:00
committed by GitHub
parent 3e2622757e
commit f446c6a2ac
1486 changed files with 49720 additions and 266401 deletions
+91 -78
View File
@@ -4,18 +4,16 @@ import (
"context"
"errors"
"fmt"
"maps"
"os"
"os/signal"
"syscall"
"galaxy/gateway/internal/adminapi"
"galaxy/gateway/internal/app"
"galaxy/gateway/internal/authn"
"galaxy/gateway/authn"
"galaxy/gateway/internal/backendclient"
"galaxy/gateway/internal/config"
"galaxy/gateway/internal/downstream"
"galaxy/gateway/internal/downstream/lobbyservice"
"galaxy/gateway/internal/downstream/userservice"
"galaxy/gateway/internal/events"
"galaxy/gateway/internal/grpcapi"
"galaxy/gateway/internal/logging"
@@ -60,16 +58,29 @@ func run(ctx context.Context) (err error) {
return fmt.Errorf("build gateway telemetry: %w", err)
}
publicRESTDeps, closePublicRESTDeps, err := newPublicRESTDependencies(cfg, logger, telemetryRuntime)
backend, err := backendclient.NewClient(backendclient.Config{
HTTPBaseURL: cfg.Backend.HTTPBaseURL,
GRPCPushURL: cfg.Backend.GRPCPushURL,
GatewayClientID: cfg.Backend.GatewayClientID,
HTTPTimeout: cfg.Backend.HTTPTimeout,
PushReconnectBaseBackoff: cfg.Backend.PushReconnectBaseBackoff,
PushReconnectMaxBackoff: cfg.Backend.PushReconnectMaxBackoff,
})
if err != nil {
_ = telemetryRuntime.Shutdown(context.Background())
_ = logging.Sync(logger)
return err
return fmt.Errorf("build backend client: %w", err)
}
grpcDeps, components, cleanup, err := newAuthenticatedGRPCDependencies(ctx, cfg, logger, telemetryRuntime)
publicRESTDeps := restapi.ServerDependencies{
Logger: logger,
Telemetry: telemetryRuntime,
AuthService: authServiceAdapter{rest: backend.REST()},
}
grpcDeps, components, cleanup, err := newAuthenticatedGRPCDependencies(ctx, cfg, logger, telemetryRuntime, backend)
if err != nil {
_ = closePublicRESTDeps()
_ = backend.Close()
_ = telemetryRuntime.Shutdown(context.Background())
_ = logging.Sync(logger)
return err
@@ -80,8 +91,8 @@ func run(ctx context.Context) (err error) {
err = errors.Join(
err,
closePublicRESTDeps(),
cleanup(),
backend.Close(),
telemetryRuntime.Shutdown(shutdownCtx),
logging.Sync(logger),
)
@@ -103,6 +114,8 @@ func run(ctx context.Context) (err error) {
zap.String("public_http_addr", cfg.PublicHTTP.Addr),
zap.String("authenticated_grpc_addr", cfg.AuthenticatedGRPC.Addr),
zap.String("admin_http_addr", cfg.AdminHTTP.Addr),
zap.String("backend_http_url", cfg.Backend.HTTPBaseURL),
zap.String("backend_grpc_push_url", cfg.Backend.GRPCPushURL),
)
application := app.New(cfg, applicationComponents...)
@@ -111,26 +124,7 @@ func run(ctx context.Context) (err error) {
return err
}
func newPublicRESTDependencies(cfg config.Config, logger *zap.Logger, telemetryRuntime *telemetry.Runtime) (restapi.ServerDependencies, func() error, error) {
deps := restapi.ServerDependencies{
Logger: logger,
Telemetry: telemetryRuntime,
}
if cfg.AuthService.BaseURL == "" {
return deps, errNoopClose, nil
}
authService, err := restapi.NewHTTPAuthServiceClient(cfg.AuthService.BaseURL)
if err != nil {
return restapi.ServerDependencies{}, nil, fmt.Errorf("build public REST dependencies: auth service client: %w", err)
}
deps.AuthService = authService
return deps, authService.Close, nil
}
func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, logger *zap.Logger, telemetryRuntime *telemetry.Runtime) (grpcapi.ServerDependencies, []app.Component, func() error, error) {
func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, logger *zap.Logger, telemetryRuntime *telemetry.Runtime, backend *backendclient.Client) (grpcapi.ServerDependencies, []app.Component, func() error, error) {
responseSigner, err := authn.LoadEd25519ResponseSignerFromPEMFile(cfg.ResponseSigner.PrivateKeyPEMPath)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, fmt.Errorf("build authenticated grpc dependencies: load response signer: %w", err)
@@ -159,7 +153,7 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
)
}
fallbackSessionCache, err := session.NewRedisCache(redisClient, cfg.SessionCacheRedis)
sessionCache, err := session.NewBackendCache(backend.REST())
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
@@ -175,59 +169,25 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
)
}
localSessionCache := session.NewMemoryCache()
sessionCache, err := session.NewReadThroughCache(localSessionCache, fallbackSessionCache)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeRedisClient(),
)
}
pushHub := push.NewHubWithObserver(0, telemetry.NewPushObserver(telemetryRuntime))
sessionSubscriber, err := events.NewRedisSessionSubscriberWithObservability(redisClient, cfg.SessionCacheRedis, cfg.SessionEventsRedis, localSessionCache, pushHub, logger, telemetryRuntime)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeRedisClient(),
)
}
clientEventSubscriber, err := events.NewRedisClientEventSubscriberWithObservability(redisClient, cfg.SessionCacheRedis, cfg.ClientEventsRedis, pushHub, logger, telemetryRuntime)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: %w", err),
closeRedisClient(),
)
}
userRoutes, closeUserServiceRoutes, err := userservice.NewRoutes(cfg.UserService.BaseURL)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: user service routes: %w", err),
closeRedisClient(),
)
}
lobbyRoutes, closeLobbyServiceRoutes, err := lobbyservice.NewRoutes(cfg.LobbyService.BaseURL)
if err != nil {
return grpcapi.ServerDependencies{}, nil, nil, errors.Join(
fmt.Errorf("build authenticated grpc dependencies: lobby service routes: %w", err),
closeUserServiceRoutes(),
closeRedisClient(),
)
}
dispatcher := events.NewDispatcher(pushHub, pushHub, logger, telemetryRuntime)
pushClient := backend.Push().
WithLogger(logger).
WithHandler(dispatcher)
userRoutes := backendclient.UserRoutes(backend.REST())
lobbyRoutes := backendclient.LobbyRoutes(backend.REST())
allRoutes := make(map[string]downstream.Client, len(userRoutes)+len(lobbyRoutes))
maps.Copy(allRoutes, userRoutes)
maps.Copy(allRoutes, lobbyRoutes)
for k, v := range userRoutes {
allRoutes[k] = v
}
for k, v := range lobbyRoutes {
allRoutes[k] = v
}
cleanup := func() error {
return errors.Join(
closeLobbyServiceRoutes(),
closeUserServiceRoutes(),
closeRedisClient(),
)
return closeRedisClient()
}
return grpcapi.ServerDependencies{
@@ -239,5 +199,58 @@ func newAuthenticatedGRPCDependencies(ctx context.Context, cfg config.Config, lo
Logger: logger,
Telemetry: telemetryRuntime,
PushHub: pushHub,
}, []app.Component{sessionSubscriber, clientEventSubscriber}, cleanup, nil
}, []app.Component{pushClient}, cleanup, nil
}
// authServiceAdapter adapts backendclient.RESTClient to the
// restapi.AuthServiceClient interface so the public REST handlers can stay
// unchanged. The two surfaces share the same JSON wire shape; only the Go
// type names differ.
type authServiceAdapter struct {
rest *backendclient.RESTClient
}
func (a authServiceAdapter) SendEmailCode(ctx context.Context, input restapi.SendEmailCodeInput) (restapi.SendEmailCodeResult, error) {
if a.rest == nil {
return restapi.SendEmailCodeResult{}, errors.New("auth service adapter: nil backend client")
}
out, err := a.rest.SendEmailCode(ctx, backendclient.SendEmailCodeInput{
Email: input.Email,
PreferredLanguage: input.PreferredLanguage,
})
if err != nil {
return restapi.SendEmailCodeResult{}, mapAuthError(err)
}
return restapi.SendEmailCodeResult{ChallengeID: out.ChallengeID}, nil
}
func (a authServiceAdapter) ConfirmEmailCode(ctx context.Context, input restapi.ConfirmEmailCodeInput) (restapi.ConfirmEmailCodeResult, error) {
if a.rest == nil {
return restapi.ConfirmEmailCodeResult{}, errors.New("auth service adapter: nil backend client")
}
out, err := a.rest.ConfirmEmailCode(ctx, backendclient.ConfirmEmailCodeInput{
ChallengeID: input.ChallengeID,
Code: input.Code,
ClientPublicKey: input.ClientPublicKey,
TimeZone: input.TimeZone,
})
if err != nil {
return restapi.ConfirmEmailCodeResult{}, mapAuthError(err)
}
return restapi.ConfirmEmailCodeResult{DeviceSessionID: out.DeviceSessionID}, nil
}
func mapAuthError(err error) error {
var ae *backendclient.AuthError
if errors.As(err, &ae) {
return &restapi.AuthServiceError{
StatusCode: ae.StatusCode,
Code: ae.Code,
Message: ae.Message,
}
}
return err
}
var _ restapi.AuthServiceClient = authServiceAdapter{}
var _ = errNoopClose
+125 -266
View File
@@ -7,14 +7,13 @@ import (
"crypto/x509"
"encoding/pem"
"net"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"galaxy/gateway/internal/backendclient"
"galaxy/gateway/internal/config"
"galaxy/gateway/internal/restapi"
"galaxy/redisconn"
"github.com/alicebob/miniredis/v2"
@@ -33,284 +32,145 @@ func testRedisConn(masterAddr string, opTimeout time.Duration) redisconn.Config
return cfg
}
func TestNewPublicRESTDependencies(t *testing.T) {
t.Parallel()
authServer := httptest.NewServer(nil)
defer authServer.Close()
tests := []struct {
name string
cfg config.Config
assert func(*testing.T, restapi.ServerDependencies)
wantErr string
}{
{
name: "default unavailable auth service when base url is empty",
cfg: config.Config{},
assert: func(t *testing.T, deps restapi.ServerDependencies) {
t.Helper()
assert.Nil(t, deps.AuthService)
},
},
{
name: "real auth service client when base url is configured",
cfg: config.Config{
AuthService: config.AuthServiceConfig{
BaseURL: authServer.URL,
},
},
assert: func(t *testing.T, deps restapi.ServerDependencies) {
t.Helper()
require.NotNil(t, deps.AuthService)
_, ok := deps.AuthService.(*restapi.HTTPAuthServiceClient)
assert.True(t, ok)
},
},
{
name: "invalid auth service base url fails fast",
cfg: config.Config{
AuthService: config.AuthServiceConfig{
BaseURL: "/relative",
},
},
wantErr: "auth service client",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
deps, cleanup, err := newPublicRESTDependencies(tt.cfg, zap.NewNop(), nil)
if tt.wantErr != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
require.NoError(t, err)
require.NotNil(t, cleanup)
tt.assert(t, deps)
assert.NoError(t, cleanup())
})
func newTestBackendConfig() config.BackendConfig {
return config.BackendConfig{
HTTPBaseURL: "http://127.0.0.1:8080",
GRPCPushURL: "127.0.0.1:8081",
GatewayClientID: "gw-test",
HTTPTimeout: 250 * time.Millisecond,
PushReconnectBaseBackoff: 100 * time.Millisecond,
PushReconnectMaxBackoff: time.Second,
}
}
func TestNewAuthenticatedGRPCDependencies(t *testing.T) {
func newTestBackendClient(t *testing.T) *backendclient.Client {
t.Helper()
cfg := newTestBackendConfig()
client, err := backendclient.NewClient(backendclient.Config{
HTTPBaseURL: cfg.HTTPBaseURL,
GRPCPushURL: cfg.GRPCPushURL,
GatewayClientID: cfg.GatewayClientID,
HTTPTimeout: cfg.HTTPTimeout,
PushReconnectBaseBackoff: cfg.PushReconnectBaseBackoff,
PushReconnectMaxBackoff: cfg.PushReconnectMaxBackoff,
})
require.NoError(t, err)
t.Cleanup(func() { _ = client.Close() })
return client
}
func TestNewAuthenticatedGRPCDependenciesSuccess(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
backend := newTestBackendClient(t)
tests := []struct {
name string
cfg config.Config
wantErr string
}{
{
name: "success",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
cfg := config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
{
name: "invalid session cache key prefix",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "redis key prefix must not be empty",
},
{
name: "startup ping failure",
cfg: config.Config{
Redis: testRedisConn(unusedTCPAddr(t), 100*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 100 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 100 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "ping redis",
},
{
name: "invalid replay config",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "replay key prefix must not be empty",
},
{
name: "invalid client event config",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
},
wantErr: "client event subscriber: stream must not be empty",
},
{
name: "missing response signer path",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
},
wantErr: "load response signer",
},
{
name: "invalid response signer pem",
cfg: config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
SessionCacheRedis: config.SessionCacheRedisConfig{
KeyPrefix: "gateway:session:",
LookupTimeout: 250 * time.Millisecond,
},
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
SessionEventsRedis: config.SessionEventsRedisConfig{
Stream: "gateway:session_events",
ReadBlockTimeout: time.Second,
},
ClientEventsRedis: config.ClientEventsRedisConfig{
Stream: "gateway:client_events",
ReadBlockTimeout: time.Second,
},
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: writeInvalidPEMFile(t),
},
},
wantErr: "response signer private key",
Backend: newTestBackendConfig(),
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
}
for _, tt := range tests {
tt := tt
deps, components, cleanup, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
require.NoError(t, err)
require.NotNil(t, deps.SessionCache)
require.NotNil(t, deps.ReplayStore)
require.NotNil(t, deps.ResponseSigner)
require.NotNil(t, deps.Router)
require.NotNil(t, deps.Service)
require.Len(t, components, 1)
require.NotNil(t, cleanup)
assert.NoError(t, cleanup())
}
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
func TestNewAuthenticatedGRPCDependenciesPingFailure(t *testing.T) {
t.Parallel()
deps, components, cleanup, err := newAuthenticatedGRPCDependencies(context.Background(), tt.cfg, zap.NewNop(), nil)
if tt.wantErr != "" {
require.Error(t, err)
assert.ErrorContains(t, err, tt.wantErr)
return
}
responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
backend := newTestBackendClient(t)
require.NoError(t, err)
require.NotNil(t, deps.SessionCache)
require.NotNil(t, deps.ReplayStore)
require.NotNil(t, deps.ResponseSigner)
require.NotNil(t, deps.Router)
require.NotNil(t, deps.Service)
require.Len(t, components, 2)
require.NotNil(t, cleanup)
assert.NoError(t, cleanup())
})
cfg := config.Config{
Redis: testRedisConn(unusedTCPAddr(t), 100*time.Millisecond),
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 100 * time.Millisecond,
},
Backend: newTestBackendConfig(),
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
}
_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
require.Error(t, err)
assert.ErrorContains(t, err, "ping redis")
}
func TestNewAuthenticatedGRPCDependenciesInvalidReplayConfig(t *testing.T) {
t.Parallel()
server := miniredis.RunT(t)
responseSignerPEMPath := writeTestResponseSignerPEMFile(t)
backend := newTestBackendClient(t)
cfg := config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
ReplayRedis: config.ReplayRedisConfig{
ReserveTimeout: 250 * time.Millisecond,
},
Backend: newTestBackendConfig(),
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: responseSignerPEMPath,
},
}
_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
require.Error(t, err)
assert.ErrorContains(t, err, "replay key prefix must not be empty")
}
func TestNewAuthenticatedGRPCDependenciesMissingResponseSigner(t *testing.T) {
t.Parallel()
backend := newTestBackendClient(t)
cfg := config.Config{
Backend: newTestBackendConfig(),
}
_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
require.Error(t, err)
assert.ErrorContains(t, err, "load response signer")
}
func TestNewAuthenticatedGRPCDependenciesInvalidResponseSignerPEM(t *testing.T) {
t.Parallel()
backend := newTestBackendClient(t)
server := miniredis.RunT(t)
cfg := config.Config{
Redis: testRedisConn(server.Addr(), 250*time.Millisecond),
ReplayRedis: config.ReplayRedisConfig{
KeyPrefix: "gateway:replay:",
ReserveTimeout: 250 * time.Millisecond,
},
Backend: newTestBackendConfig(),
ResponseSigner: config.ResponseSignerConfig{
PrivateKeyPEMPath: writeInvalidPEMFile(t),
},
}
_, _, _, err := newAuthenticatedGRPCDependencies(context.Background(), cfg, zap.NewNop(), nil, backend)
require.Error(t, err)
assert.ErrorContains(t, err, "response signer private key")
}
func unusedTCPAddr(t *testing.T) string {
@@ -348,8 +208,7 @@ func writeInvalidPEMFile(t *testing.T) string {
t.Helper()
path := filepath.Join(t.TempDir(), "invalid-response-signer.pem")
err := os.WriteFile(path, []byte("not a valid pem"), 0o600)
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, []byte("not a valid pem"), 0o600))
return path
}