package providerapi import ( "context" "errors" "fmt" "runtime" "strings" "sync" "testing" "time" "github.com/proxy-pool/proxy-pool/internal/config" controllerProvider "github.com/proxy-pool/proxy-pool/internal/controller/provider" proxyDomain "github.com/proxy-pool/proxy-pool/internal/domain/proxy" "github.com/proxy-pool/proxy-pool/internal/platform/credentials" ) var _ controllerProvider.Parser = (*TemplateParser)(nil) func newTemplateParser( upstreamID string, upstream config.Upstream, stores ...credentials.Store, ) (*TemplateParser, error) { var store credentials.Store if len(stores) > 0 { store = stores[0] } return NewTemplateParser(upstreamID, upstream, store) } func TestTemplateParserFormattingDoesNotExposeTemplateCredentials(t *testing.T) { t.Parallel() parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `http://response-user:response-secret@192.0.2.10:8080`}, ProxyAuth: config.ProxyAuth{Type: "static", Username: "static-user", Password: "static-secret"}, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } formatted := fmt.Sprintf("%v %+v %#v", parser, parser, parser) for _, secret := range []string{"response-secret", "static-secret", "response-user"} { if strings.Contains(formatted, secret) { t.Fatalf("formatted parser contains %q: %s", secret, formatted) } } } func TestTemplateParserExecutesWhitelistedRegexAndBuildsProxy(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{$value := regexFind "[0-9.]+:[0-9]+" .}}{{if $value}}{{printf "http://%s\n" $value}}{{end}}`}, Pool: config.Pool{MaxSize: 10}, Capacity: config.Capacity{MaxConcurrencyPerProxy: 7}, Fetch: config.Fetch{ MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), []byte("address=192.0.2.10:8080")) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 { t.Fatalf("proxy count = %d, want 1", len(proxies)) } got := proxies[0] if got.Scheme != proxyDomain.SchemeHTTP || got.Host != "192.0.2.10" || got.Port != 8080 { t.Fatalf("proxy endpoint = %s://%s:%d, want http://192.0.2.10:8080", got.Scheme, got.Host, got.Port) } if got.SourceUpstream != "provider-a" || got.MaxConcurrency != 7 || got.State != proxyDomain.StateFetched { t.Fatalf("proxy metadata = %+v", got) } } func TestTemplateParserRejectsOversizedInput(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{.}}`}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{ MaxResponseBytes: 4, TemplateTimeout: config.Duration(time.Second), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), []byte("12345")) if !errors.Is(err, ErrTemplateInputTooLarge) { t.Fatalf("Parse() error = %v, want ErrTemplateInputTooLarge", err) } } func TestTemplateParserStopsOversizedOutputDuringExecution(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `0123456789`}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{ MaxResponseBytes: 8, TemplateTimeout: config.Duration(time.Second), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), []byte("x")) if !errors.Is(err, ErrTemplateOutputTooLarge) { t.Fatalf("Parse() error = %v, want ErrTemplateOutputTooLarge", err) } } func TestTemplateParserRejectsTooManyCandidates(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: "http://192.0.2.1:8001\nhttp://192.0.2.2:8002\n"}, Pool: config.Pool{MaxSize: 1}, Fetch: config.Fetch{ MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), nil) if !errors.Is(err, ErrTooManyCandidates) { t.Fatalf("Parse() error = %v, want ErrTooManyCandidates", err) } } func TestTemplateParserRegexFindAllCountSurvivesMaxCandidateBoundary(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{range regexFindAll "a" . 1}}http://192.0.2.10:8080 {{end}}`}, Pool: config.Pool{MaxSize: int(^uint(0) >> 1)}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), []byte("aaa")) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 { t.Fatalf("proxy count = %d, want regex count limit 1", len(proxies)) } } func TestTemplateParserEnforcesExecutionTimeout(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{$matches := regexFindAll "a" . -1}}{{range $matches}}http://192.0.2.10:8080 {{end}}`}, Pool: config.Pool{MaxSize: 20_000}, Fetch: config.Fetch{ MaxResponseBytes: 1 << 20, TemplateTimeout: config.Duration(time.Nanosecond), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), []byte(strings.Repeat("a", 10_000))) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Parse() error = %v, want context.DeadlineExceeded", err) } } func TestTemplateParserTimedOutExecutionsRemainBounded(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{$matches := regexFindAll "a" . -1}}{{range $matches}}{{printf "%s" .}}{{end}}`}, Pool: config.Pool{MaxSize: 20_000}, Fetch: config.Fetch{ MaxResponseBytes: 1 << 20, TemplateTimeout: config.Duration(time.Nanosecond), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } baseline := runtime.NumGoroutine() for range 20 { _, err := parser.Parse(context.Background(), []byte(strings.Repeat("a", 10_000))) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("Parse() error = %v, want context.DeadlineExceeded", err) } } deadline := time.Now().Add(2 * time.Second) for runtime.NumGoroutine() > baseline+4 && time.Now().Before(deadline) { runtime.Gosched() time.Sleep(time.Millisecond) } if got := runtime.NumGoroutine(); got > baseline+4 { t.Fatalf("goroutines after timed-out executions = %d, baseline = %d", got, baseline) } } func TestTemplateParserRejectsRecursiveTemplates(t *testing.T) { _, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: ` {{define "first"}}{{template "second"}}{{end}} {{define "second"}}{{template "first"}}{{end}} {{template "first"}}`}, Pool: config.Pool{MaxSize: 10}, }) if !errors.Is(err, ErrRecursiveTemplate) { t.Fatalf("newTemplateParser() error = %v, want ErrRecursiveTemplate", err) } } func TestTemplateParserRejectsNonEmptyAllInvalidOutput(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `not-a-proxy`}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{ MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second), }, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), nil) if !errors.Is(err, ErrInvalidProxyOutput) { t.Fatalf("Parse() error = %v, want ErrInvalidProxyOutput", err) } } func TestTemplateParserAcceptsIPv6AndFiltersUnsafeEndpoints(t *testing.T) { parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: strings.Join([]string{ "http://[2001:db8::1]:8080", "http://192.0.2.1:8080/path", "http://192.0.2.2:8080?token=secret", "http://192.0.2.3:0", "http://192.0.2.4:65536", "socks5://192.0.2.5:1080", }, "\n")}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 || proxies[0].Host != "2001:db8::1" || proxies[0].Port != 8080 { t.Fatalf("proxies = %+v, want only IPv6 endpoint", proxies) } } func TestTemplateParserDoesNotOverrideStaticProxyAuthFromResponse(t *testing.T) { store, err := credentials.NewMemoryStore(10) if err != nil { t.Fatalf("NewMemoryStore(): %v", err) } parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `http://response-user:response-pass@192.0.2.10:8080`}, ProxyAuth: config.ProxyAuth{Type: "static", Username: "configured-user", Password: "configured-pass"}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 || proxies[0].Username != "configured-user" { t.Fatalf("proxies = %+v, want configured static username", proxies) } if strings.Contains(proxies[0].SecretRef, "configured-pass") || strings.Contains(proxies[0].SecretRef, "response-pass") { t.Fatalf("SecretRef contains plaintext password: %q", proxies[0].SecretRef) } if proxies[0].SecretRef == "" || proxies[0].CredentialVersion == "" { t.Fatalf("proxy credential reference is incomplete: %+v", proxies[0]) } value, err := store.Resolve(context.Background(), credentials.Reference{ SecretRef: proxies[0].SecretRef, CredentialVersion: proxies[0].CredentialVersion, }) if err != nil { t.Fatalf("Resolve(): %v", err) } if value.Username != "configured-user" || value.Password != "configured-pass" { t.Fatalf("resolved static credentials = %v, want configured credentials", value) } } func TestTemplateParserRetainsDistinctEndpointsSharingStaticCredentials(t *testing.T) { store, err := credentials.NewMemoryStore(1) if err != nil { t.Fatalf("NewMemoryStore(): %v", err) } parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: strings.Join([]string{ "http://192.0.2.10:8080", "http://192.0.2.11:8080", }, "\n")}, ProxyAuth: config.ProxyAuth{Type: "static", Username: "configured-user", Password: "configured-pass"}, Pool: config.Pool{MaxSize: 2}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("NewTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 2 { t.Fatalf("proxy count = %d, want both static-auth endpoints", len(proxies)) } if proxies[0].SecretRef == "" || proxies[0].SecretRef != proxies[1].SecretRef { t.Fatalf("static credential references = %q and %q, want same opaque reference", proxies[0].SecretRef, proxies[1].SecretRef) } } func TestTemplateParserStoresResponseCredentialsByOpaqueReference(t *testing.T) { store, err := credentials.NewMemoryStore(10) if err != nil { t.Fatalf("NewMemoryStore(): %v", err) } parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `http://response-user:response-secret@192.0.2.10:8080`}, ProxyAuth: config.ProxyAuth{Type: "response"}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 { t.Fatalf("proxy count = %d, want 1", len(proxies)) } got := proxies[0] if got.Username != "response-user" || got.SecretRef == "" || got.CredentialVersion == "" { t.Fatalf("proxy credentials metadata = %+v", got) } formatted := fmt.Sprintf("%v %+v %#v", got, got, got) if strings.Contains(formatted, "response-secret") || strings.Contains(got.SecretRef, "response-secret") { t.Fatalf("proxy formatting or reference contains password: %s", formatted) } value, err := store.Resolve(context.Background(), credentials.Reference{ SecretRef: got.SecretRef, CredentialVersion: got.CredentialVersion, }) if err != nil { t.Fatalf("Resolve(): %v", err) } if value.Username != "response-user" || value.Password != "response-secret" { t.Fatalf("resolved response credentials = %v, want response credentials", value) } } func TestTemplateParserKeepsDistinctAccountsForSameEndpointResolvable(t *testing.T) { store, err := credentials.NewMemoryStore(2) if err != nil { t.Fatalf("NewMemoryStore(): %v", err) } parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: strings.Join([]string{ "http://alice:alice-secret@192.0.2.10:8080", "http://bob:bob-secret@192.0.2.10:8080", }, "\n")}, ProxyAuth: config.ProxyAuth{Type: "response"}, Pool: config.Pool{MaxSize: 2}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("NewTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 2 { t.Fatalf("proxy count = %d, want 2", len(proxies)) } for _, candidate := range proxies { value, err := store.Resolve(context.Background(), credentials.Reference{ SecretRef: candidate.SecretRef, CredentialVersion: candidate.CredentialVersion, }) if err != nil { t.Fatalf("Resolve(%s): %v", candidate.Username, err) } if value.Username != candidate.Username { t.Fatalf("resolved username = %q, want %q", value.Username, candidate.Username) } } } func TestTemplateParserKeepsLatestCredentialVersionWithinOneResponse(t *testing.T) { store, err := credentials.NewMemoryStore(1) if err != nil { t.Fatalf("NewMemoryStore(): %v", err) } parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: strings.Join([]string{ "http://alice:old-secret@192.0.2.10:8080", "http://alice:new-secret@192.0.2.10:8080", }, "\n")}, ProxyAuth: config.ProxyAuth{Type: "response"}, Pool: config.Pool{MaxSize: 2}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("NewTemplateParser(): %v", err) } proxies, err := parser.Parse(context.Background(), nil) if err != nil { t.Fatalf("Parse(): %v", err) } if len(proxies) != 1 { t.Fatalf("proxy count = %d, want latest credential only", len(proxies)) } value, err := store.Resolve(context.Background(), credentials.Reference{ SecretRef: proxies[0].SecretRef, CredentialVersion: proxies[0].CredentialVersion, }) if err != nil { t.Fatalf("Resolve(): %v", err) } if value.Password != "new-secret" { t.Fatalf("resolved latest password mismatch") } } func TestTemplateParserRedactsCredentialStoreErrors(t *testing.T) { const password = "provider-password" storeErr := errors.New("store failed for " + password) store := &failingCredentialStore{err: storeErr} parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `http://user:` + password + `@192.0.2.10:8080`}, ProxyAuth: config.ProxyAuth{Type: "response"}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }, store) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), nil) if !errors.Is(err, storeErr) { t.Fatalf("Parse() error = %v, want wrapped store error", err) } if strings.Contains(err.Error(), password) { t.Fatalf("Parse() error leaked password: %v", err) } } func TestTemplateParserRejectsMissingStoreAndEmptyCredentialReference(t *testing.T) { upstream := config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `http://user:password@192.0.2.10:8080`}, ProxyAuth: config.ProxyAuth{Type: "response"}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, } parser, err := newTemplateParser("provider-a", upstream) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } if _, err := parser.Parse(context.Background(), nil); !errors.Is(err, ErrCredentialStoreRequired) { t.Fatalf("Parse() error = %v, want ErrCredentialStoreRequired", err) } parser, err = newTemplateParser("provider-a", upstream, emptyReferenceCredentialStore{}) if err != nil { t.Fatalf("newTemplateParser(with store): %v", err) } if _, err := parser.Parse(context.Background(), nil); !errors.Is(err, credentials.ErrInvalidReference) { t.Fatalf("Parse() error = %v, want credentials.ErrInvalidReference", err) } } type failingCredentialStore struct{ err error } func (s *failingCredentialStore) Put(context.Context, string, credentials.Value) (credentials.Reference, error) { return credentials.Reference{}, s.err } func (s *failingCredentialStore) Resolve(context.Context, credentials.Reference) (credentials.Value, error) { return credentials.Value{}, s.err } type emptyReferenceCredentialStore struct{} func (emptyReferenceCredentialStore) Put(context.Context, string, credentials.Value) (credentials.Reference, error) { return credentials.Reference{}, nil } func (emptyReferenceCredentialStore) Resolve(context.Context, credentials.Reference) (credentials.Value, error) { return credentials.Value{}, credentials.ErrCredentialMissing } func TestTemplateParserRejectsFunctionsOutsideWhitelist(t *testing.T) { tests := []string{ `{{env "SECRET_TOKEN"}}`, `{{readFile "credentials.txt"}}`, `{{httpGet "https://example.invalid"}}`, `{{exec "command"}}`, } for _, source := range tests { if _, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: source}, Pool: config.Pool{MaxSize: 10}, }); err == nil { t.Fatalf("newTemplateParser(%q) error = nil, want unknown function error", source) } } } func TestTemplateParserRedactsInvalidRegexPattern(t *testing.T) { const secretPattern = "(?P" parser, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: `{{regexFind . "value"}}`}, Pool: config.Pool{MaxSize: 10}, Fetch: config.Fetch{MaxResponseBytes: 1024, TemplateTimeout: config.Duration(time.Second)}, }) if err != nil { t.Fatalf("newTemplateParser(): %v", err) } _, err = parser.Parse(context.Background(), []byte(secretPattern)) if err == nil { t.Fatal("Parse() error = nil, want invalid regex error") } if strings.Contains(err.Error(), secretPattern) || strings.Contains(err.Error(), "secret-token") { t.Fatalf("Parse() error leaked regex pattern: %v", err) } } func TestTemplateParserRejectsExcessiveASTNesting(t *testing.T) { const nesting = 40 source := strings.Repeat("{{range .}}", nesting) + strings.Repeat("{{end}}", nesting) _, err := newTemplateParser("provider-a", config.Upstream{ Provider: config.Provider{Protocols: []string{"http"}}, API: config.ProviderAPI{Template: source}, Pool: config.Pool{MaxSize: 10}, }) if !errors.Is(err, ErrTemplateTooComplex) { t.Fatalf("newTemplateParser() error = %v, want ErrTemplateTooComplex", err) } } func TestExecutionLimiterRetainsSlotUntilTimedOutExecutionExits(t *testing.T) { limiter := newExecutionLimiter(1) started := make(chan struct{}) release := make(chan struct{}) firstDone := make(chan error, 1) firstCtx, cancelFirst := context.WithCancel(context.Background()) go func() { firstDone <- limiter.Run(firstCtx, func() error { close(started) <-release return nil }) }() <-started cancelFirst() if err := <-firstDone; !errors.Is(err, context.Canceled) { t.Fatalf("first Run() error = %v, want context.Canceled", err) } secondStarted := make(chan struct{}) secondCtx, cancelSecond := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancelSecond() err := limiter.Run(secondCtx, func() error { close(secondStarted) return nil }) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("second Run() error = %v, want context.DeadlineExceeded", err) } select { case <-secondStarted: t.Fatal("second execution started while timed-out execution retained the only slot") default: } close(release) deadline := time.Now().Add(time.Second) for limiter.InFlight() != 0 && time.Now().Before(deadline) { time.Sleep(time.Millisecond) } if got := limiter.InFlight(); got != 0 { t.Fatalf("in-flight executions = %d, want 0 after execution exit", got) } } func TestExecutionLimiterIsSafeForConcurrentInspection(t *testing.T) { limiter := newExecutionLimiter(2) var wait sync.WaitGroup for range 10 { wait.Add(1) go func() { defer wait.Done() _ = limiter.InFlight() }() } wait.Wait() } func TestExecutionLimiterDoesNotStartWithCanceledContext(t *testing.T) { limiter := newExecutionLimiter(1) ctx, cancel := context.WithCancel(context.Background()) cancel() for range 20 { started := make(chan struct{}, 1) release := make(chan struct{}) err := limiter.Run(ctx, func() error { started <- struct{}{} <-release return nil }) if !errors.Is(err, context.Canceled) { t.Fatalf("Run() error = %v, want context.Canceled", err) } select { case <-started: close(release) t.Fatal("execution started with an already canceled context") case <-time.After(5 * time.Millisecond): close(release) } } } func TestExecutionLimiterCapsConfiguredConcurrency(t *testing.T) { limiter := newExecutionLimiter(maxTemplateExecutions + 1) if got := cap(limiter.slots); got != maxTemplateExecutions { t.Fatalf("execution slot capacity = %d, want hard cap %d", got, maxTemplateExecutions) } }