Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,16 @@ check-breaking-changes:
test/validate-generation-breaking-changes.sh
.PHONY: check-breaking-changes

VENDOR_GOFLAGS = $(strip $(GOFLAGS) -mod=vendor)

.PHONY: generate
generate: imports
hack/update-codegen.sh
hack/generate-ci-op-reference.sh
GOFLAGS="$(VENDOR_GOFLAGS)" hack/update-codegen.sh
GOFLAGS="$(VENDOR_GOFLAGS)" hack/generate-ci-op-reference.sh

.PHONY: imports
imports:
go run ./vendor/github.com/openshift-eng/openshift-goimports/ -m github.com/openshift/ci-tools
GOFLAGS="$(VENDOR_GOFLAGS)" go run ./vendor/github.com/openshift-eng/openshift-goimports/ -m github.com/openshift/ci-tools

.PHONY: verify-gen
verify-gen: generate cmd/pod-scaler/frontend/dist/dummy cmd/repo-init/frontend/dist/dummy # we need the dummy file to exist so there's no diff on it
Expand Down
20 changes: 10 additions & 10 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/openshift/ci-tools

go 1.25.5
go 1.25.8

replace github.com/docker/distribution => github.com/docker/distribution v2.7.1+incompatible

Expand Down Expand Up @@ -140,7 +140,7 @@ require (
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/lint v0.0.0-20241112194109-818c5a804067 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/time v0.12.0
golang.org/x/tools v0.38.0
Expand Down Expand Up @@ -186,11 +186,11 @@ require (
github.com/openshift/library-go v0.0.0-20240207105404-126b47137408
github.com/ovn-org/ovn-kubernetes/go-controller v0.0.0-20240710195803-425a328cd172
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
gopkg.in/evanphx/json-patch.v5 v5.9.0
k8s.io/metrics v0.32.0
sigs.k8s.io/boskos v0.0.0-20240624145324-1e4de26c366a
sigs.k8s.io/prow v0.0.0-20260227184331-937f24a5dcd2
sigs.k8s.io/prow v0.0.0-20260316161740-26fa34da6010
)

require (
Expand All @@ -204,10 +204,10 @@ require (
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/vektah/gqlparser/v2 v2.5.14 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
Expand Down Expand Up @@ -325,9 +325,9 @@ require (
github.com/zeebo/xxh3 v1.0.2 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
Expand Down
36 changes: 18 additions & 18 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1072,8 +1072,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
Expand Down Expand Up @@ -1130,26 +1130,26 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
Expand Down Expand Up @@ -1427,8 +1427,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
Expand Down Expand Up @@ -1789,8 +1789,8 @@ sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNza
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/prow v0.0.0-20260227184331-937f24a5dcd2 h1:WmO1hZGFozg+27fZJe2Jw/LapRzfOPHtjIVGYcZCKdQ=
sigs.k8s.io/prow v0.0.0-20260227184331-937f24a5dcd2/go.mod h1:mp/tLCcvJyJAIk8+QL7zyS2SzI28HDP0LPV91Z73pGU=
sigs.k8s.io/prow v0.0.0-20260316161740-26fa34da6010 h1:dUw+ojFRll9yUCVoX3LNEB9Nezz9l4wZUHRdsoPWdCw=
sigs.k8s.io/prow v0.0.0-20260316161740-26fa34da6010/go.mod h1:LkFUWvukcR88tO0JBhMpKFh0rnzBkWa3fbM37/1eBjU=
sigs.k8s.io/secrets-store-csi-driver v1.4.7 h1:AyuwmPTW2GoPD2RjyVD3OrH1J9cdPZx+0h2qJvzbGXs=
sigs.k8s.io/secrets-store-csi-driver v1.4.7/go.mod h1:0/wMVOv8qLx7YNVMGU+Sh7S4D6TH6GhyEpouo28OTUU=
sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
Expand Down
108 changes: 108 additions & 0 deletions pkg/jira/customfield.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package jira

import (
"context"
"errors"
"maps"
"sync"

jiraapi "github.com/andygrunwald/go-jira"

jirautil "sigs.k8s.io/prow/pkg/jira"
)

// CustomFieldResolver resolves field names to IDs; fetches the field list once and caches.
// Use SetFallbackIDs to supply name→ID when API lookup is empty (e.g. instance-specific IDs).
type CustomFieldResolver struct {
client *jiraapi.Client
mu sync.RWMutex
byName map[string]string
loaded bool
fallbackIDs map[string]string // optional: field name -> custom field ID
}

func NewCustomFieldResolver(client *jiraapi.Client) *CustomFieldResolver {
return &CustomFieldResolver{
client: client,
byName: make(map[string]string),
fallbackIDs: make(map[string]string),
}
}

// SetFallbackIDs sets the optional name→custom field ID map (e.g. "QA Contact" -> "customfield_12316243").
// Safe to call concurrently; replaces the previous map with a copy.
func (r *CustomFieldResolver) SetFallbackIDs(ids map[string]string) {
r.mu.Lock()
defer r.mu.Unlock()
if ids == nil {
r.fallbackIDs = make(map[string]string)
return
}
r.fallbackIDs = maps.Clone(ids)
}

func (r *CustomFieldResolver) loadFields(ctx context.Context) error {
r.mu.RLock()
if r.loaded {
r.mu.RUnlock()
return nil
}
r.mu.RUnlock()

r.mu.Lock()
defer r.mu.Unlock()
if r.loaded {
return nil
}
if r.client == nil {
return errors.New("jira client is nil")
}
fields, resp, err := r.client.Field.GetListWithContext(ctx)
if err != nil {
return jirautil.HandleJiraError(resp, err)
}
for _, f := range fields {
r.byName[f.Name] = f.ID
}
r.loaded = true
return nil
}

// FieldID returns the field ID for name, or "" if not found.
// If name lookup is empty and SetFallbackIDs supplied that name, that ID is returned.
func (r *CustomFieldResolver) FieldID(ctx context.Context, fieldName string) (string, error) {
if err := r.loadFields(ctx); err != nil {
return "", err
}
r.mu.RLock()
id := r.byName[fieldName]
if id == "" {
id = r.fallbackIDs[fieldName]
}
r.mu.RUnlock()
return id, nil
}

// Value returns the custom field value for the issue by field name.
func (r *CustomFieldResolver) Value(ctx context.Context, issue *jiraapi.Issue, fieldName string) (any, error) {
if issue == nil || issue.Fields == nil {
return nil, nil
}
id, err := r.FieldID(ctx, fieldName)
if err != nil || id == "" {
return nil, err
}
return ValueByID(issue, id), nil
}

// ValueByID returns the custom field value for the issue by raw field ID (e.g. customfield_12345).
// Use when name-based resolution is not available or as a temp override.
func ValueByID(issue *jiraapi.Issue, fieldID string) any {
if issue == nil || issue.Fields == nil || issue.Fields.Unknowns == nil || fieldID == "" {
return nil
}
if v, ok := issue.Fields.Unknowns[fieldID]; ok {
return v
}
return nil
}
134 changes: 134 additions & 0 deletions pkg/jira/customfield_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package jira

import (
"context"
"reflect"
"testing"

jiraapi "github.com/andygrunwald/go-jira"
)

func TestValueByID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
issue *jiraapi.Issue
fieldID string
want any
}{
{
name: "nil issue",
issue: nil,
fieldID: "customfield_123",
want: nil,
},
{
name: "nil Fields",
issue: &jiraapi.Issue{},
fieldID: "customfield_123",
want: nil,
},
{
name: "nil Unknowns",
issue: &jiraapi.Issue{Fields: &jiraapi.IssueFields{}},
fieldID: "customfield_123",
want: nil,
},
{
name: "empty fieldID",
issue: &jiraapi.Issue{
Fields: &jiraapi.IssueFields{Unknowns: map[string]interface{}{"customfield_123": "val"}},
},
fieldID: "",
want: nil,
},
{
name: "found",
issue: &jiraapi.Issue{
Fields: &jiraapi.IssueFields{
Unknowns: map[string]interface{}{"customfield_123": "qa-user"},
},
},
fieldID: "customfield_123",
want: "qa-user",
},
{
name: "not found",
issue: &jiraapi.Issue{
Fields: &jiraapi.IssueFields{
Unknowns: map[string]interface{}{"customfield_999": "other"},
},
},
fieldID: "customfield_123",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValueByID(tt.issue, tt.fieldID)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ValueByID() = %v, want %v", got, tt.want)
}
})
}
}

func TestValueByID_with_fallback_id(t *testing.T) {
t.Parallel()
// ValueByID is used with a known custom field ID (e.g. customfield_12316243).
// SetFallbackIDs can map names to these IDs when name-based API lookup is empty.
issue := &jiraapi.Issue{
Fields: &jiraapi.IssueFields{
Unknowns: map[string]interface{}{"customfield_12316243": "qa@example.com"},
},
}
got := ValueByID(issue, "customfield_12316243")
if !reflect.DeepEqual(got, "qa@example.com") {
t.Errorf("ValueByID = %v, want qa@example.com", got)
}
}

// TestFieldID_and_Value_use_fallback exercises the fallback path when the field list
// is loaded but the name is not in the API response (e.g. instance-specific custom field).
func TestFieldID_and_Value_use_fallback(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Resolver with no client, already "loaded" with empty byName so we skip API and use fallback.
r := &CustomFieldResolver{
client: nil,
byName: map[string]string{},
loaded: true,
fallbackIDs: map[string]string{"QA Contact": "customfield_12316243"},
}
id, err := r.FieldID(ctx, "QA Contact")
if err != nil {
t.Fatalf("FieldID() error = %v", err)
}
if id != "customfield_12316243" {
t.Errorf("FieldID() = %q, want customfield_12316243", id)
}
issue := &jiraapi.Issue{
Fields: &jiraapi.IssueFields{
Unknowns: map[string]interface{}{"customfield_12316243": "qa@example.com"},
},
}
val, err := r.Value(ctx, issue, "QA Contact")
if err != nil {
t.Fatalf("Value() error = %v", err)
}
if !reflect.DeepEqual(val, "qa@example.com") {
t.Errorf("Value() = %v, want qa@example.com", val)
}
}

// TestFieldID_nil_client_returns_error ensures we return an error instead of panicking.
func TestFieldID_nil_client_returns_error(t *testing.T) {
t.Parallel()
ctx := context.Background()
r := NewCustomFieldResolver(nil)
r.SetFallbackIDs(map[string]string{"QA Contact": "customfield_12316243"})
_, err := r.FieldID(ctx, "QA Contact")
if err == nil {
t.Error("FieldID() expected error when client is nil")
}
}
Loading