refactor(coderd/healthcheck): make Warnings an object with { Code, Message } (#10950)

- Adds health.Message { code string, mesasge string }
- Refactors existing warnings []string to be of type []health.Message instead
This commit is contained in:
Cian Johnston
2023-11-30 14:49:50 +00:00
committed by GitHub
parent 4f9292859d
commit 07895006d9
18 changed files with 459 additions and 151 deletions

62
coderd/apidoc/docs.go generated
View File

@ -12258,7 +12258,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -12297,7 +12297,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -12348,7 +12348,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -12367,6 +12367,56 @@ const docTemplate = `{
}
}
},
"health.Code": {
"type": "string",
"enum": [
"EUNKNOWN",
"EWP01",
"EWP02",
"EWP03",
"EWP04",
"EDB01",
"EDB02",
"EWS01",
"EWS02",
"EWS03",
"EACS01",
"EACS02",
"EACS03",
"EACS04",
"EDERP01",
"EDERP02"
],
"x-enum-varnames": [
"CodeUnknown",
"CodeProxyUpdate",
"CodeProxyFetch",
"CodeProxyVersionMismatch",
"CodeProxyUnhealthy",
"CodeDatabasePingFailed",
"CodeDatabasePingSlow",
"CodeWebsocketDial",
"CodeWebsocketEcho",
"CodeWebsocketMsg",
"CodeAccessURLNotSet",
"CodeAccessURLInvalid",
"CodeAccessURLFetch",
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy"
]
},
"health.Message": {
"type": "object",
"properties": {
"code": {
"$ref": "#/definitions/health.Code"
},
"message": {
"type": "string"
}
}
},
"health.Severity": {
"type": "string",
"enum": [
@ -12420,7 +12470,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -12465,7 +12515,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -12579,7 +12629,7 @@ const docTemplate = `{
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
},
"workspace_proxies": {

View File

@ -11163,7 +11163,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -11198,7 +11198,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -11245,7 +11245,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -11264,6 +11264,56 @@
}
}
},
"health.Code": {
"type": "string",
"enum": [
"EUNKNOWN",
"EWP01",
"EWP02",
"EWP03",
"EWP04",
"EDB01",
"EDB02",
"EWS01",
"EWS02",
"EWS03",
"EACS01",
"EACS02",
"EACS03",
"EACS04",
"EDERP01",
"EDERP02"
],
"x-enum-varnames": [
"CodeUnknown",
"CodeProxyUpdate",
"CodeProxyFetch",
"CodeProxyVersionMismatch",
"CodeProxyUnhealthy",
"CodeDatabasePingFailed",
"CodeDatabasePingSlow",
"CodeWebsocketDial",
"CodeWebsocketEcho",
"CodeWebsocketMsg",
"CodeAccessURLNotSet",
"CodeAccessURLInvalid",
"CodeAccessURLFetch",
"CodeAccessURLNotOK",
"CodeDERPNodeUsesWebsocket",
"CodeDERPOneNodeUnhealthy"
]
},
"health.Message": {
"type": "object",
"properties": {
"code": {
"$ref": "#/definitions/health.Code"
},
"message": {
"type": "string"
}
}
},
"health.Severity": {
"type": "string",
"enum": ["ok", "warning", "error"],
@ -11305,7 +11355,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -11346,7 +11396,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
}
}
@ -11452,7 +11502,7 @@
"warnings": {
"type": "array",
"items": {
"type": "string"
"$ref": "#/definitions/health.Message"
}
},
"workspace_proxies": {

View File

@ -8,16 +8,15 @@ import (
"time"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
)
// @typescript-generate AccessURLReport
type AccessURLReport struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Dismissed bool `json:"dismissed"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
AccessURL string `json:"access_url"`
Reachable bool `json:"reachable"`
@ -38,11 +37,11 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
defer cancel()
r.Severity = health.SeverityOK
r.Warnings = []string{}
r.Warnings = []health.Message{}
r.Dismissed = opts.Dismissed
if opts.AccessURL == nil {
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLNotSet, "Access URL not set"))
r.Error = health.Errorf(health.CodeAccessURLNotSet, "Access URL not set")
r.Severity = health.SeverityError
return
}
@ -54,21 +53,21 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
accessURL, err := opts.AccessURL.Parse("/healthz")
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLInvalid, "parse healthz endpoint: %s", err))
r.Error = health.Errorf(health.CodeAccessURLInvalid, "parse healthz endpoint: %s", err)
r.Severity = health.SeverityError
return
}
req, err := http.NewRequestWithContext(ctx, "GET", accessURL.String(), nil)
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "create healthz request: %s", err))
r.Error = health.Errorf(health.CodeAccessURLFetch, "create healthz request: %s", err)
r.Severity = health.SeverityError
return
}
res, err := opts.Client.Do(req)
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "get healthz endpoint: %s", err))
r.Error = health.Errorf(health.CodeAccessURLFetch, "get healthz endpoint: %s", err)
r.Severity = health.SeverityError
return
}
@ -76,7 +75,7 @@ func (r *AccessURLReport) Run(ctx context.Context, opts *AccessURLReportOptions)
body, err := io.ReadAll(res.Body)
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeAccessURLFetch, "read healthz response: %s", err))
r.Error = health.Errorf(health.CodeAccessURLFetch, "read healthz response: %s", err)
r.Severity = health.SeverityError
return
}

View File

@ -131,7 +131,7 @@ func TestAccessURL(t *testing.T) {
assert.Equal(t, string(resp), report.HealthzResponse)
assert.Nil(t, report.Error)
if assert.NotEmpty(t, report.Warnings) {
assert.Contains(t, report.Warnings[0], health.CodeAccessURLNotOK)
assert.Equal(t, report.Warnings[0].Code, health.CodeAccessURLNotOK)
}
})

View File

@ -4,11 +4,10 @@ import (
"context"
"time"
"golang.org/x/exp/slices"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
"golang.org/x/exp/slices"
)
const (
@ -18,10 +17,10 @@ const (
// @typescript-generate DatabaseReport
type DatabaseReport struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Dismissed bool `json:"dismissed"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Reachable bool `json:"reachable"`
Latency string `json:"latency"`
@ -38,7 +37,7 @@ type DatabaseReportOptions struct {
}
func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) {
r.Warnings = []string{}
r.Warnings = []health.Message{}
r.Severity = health.SeverityOK
r.Dismissed = opts.Dismissed
@ -55,7 +54,7 @@ func (r *DatabaseReport) Run(ctx context.Context, opts *DatabaseReportOptions) {
for i := 0; i < pingCount; i++ {
pong, err := opts.DB.Ping(ctx)
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeDatabasePingFailed, "ping database: %s", err))
r.Error = health.Errorf(health.CodeDatabasePingFailed, "ping database: %s", err)
r.Severity = health.SeverityError
return

View File

@ -143,7 +143,7 @@ func TestDatabase(t *testing.T) {
assert.Equal(t, time.Second.Milliseconds(), report.ThresholdMS)
assert.Nil(t, report.Error)
if assert.NotEmpty(t, report.Warnings) {
assert.Contains(t, report.Warnings[0], health.CodeDatabasePingSlow)
assert.Equal(t, report.Warnings[0].Code, health.CodeDatabasePingSlow)
}
})
}

View File

@ -36,10 +36,10 @@ const (
// @typescript-generate Report
type Report struct {
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Dismissed bool `json:"dismissed"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Regions map[int]*RegionReport `json:"regions"`
@ -55,9 +55,9 @@ type RegionReport struct {
mu sync.Mutex
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Region *tailcfg.DERPRegion `json:"region"`
NodeReports []*NodeReport `json:"node_reports"`
@ -70,9 +70,9 @@ type NodeReport struct {
clientCounter int
// Healthy is deprecated and left for backward compatibility purposes, use `Severity` instead.
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []string `json:"warnings"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity" enums:"ok,warning,error"`
Warnings []health.Message `json:"warnings"`
Node *tailcfg.DERPNode `json:"node"`
@ -104,7 +104,7 @@ type ReportOptions struct {
func (r *Report) Run(ctx context.Context, opts *ReportOptions) {
r.Healthy = true
r.Severity = health.SeverityOK
r.Warnings = []string{}
r.Warnings = []health.Message{}
r.Dismissed = opts.Dismissed
r.Regions = map[int]*RegionReport{}
@ -168,7 +168,7 @@ func (r *RegionReport) Run(ctx context.Context) {
r.Healthy = true
r.Severity = health.SeverityOK
r.NodeReports = []*NodeReport{}
r.Warnings = []string{}
r.Warnings = []health.Message{}
wg := &sync.WaitGroup{}
var unhealthyNodes int // atomic.Int64 is not mandatory as we depend on RegionReport mutex.
@ -263,7 +263,7 @@ func (r *NodeReport) Run(ctx context.Context) {
r.Severity = health.SeverityOK
r.ClientLogs = [][]string{}
r.ClientErrs = [][]string{}
r.Warnings = []string{}
r.Warnings = []health.Message{}
wg := &sync.WaitGroup{}

View File

@ -130,7 +130,7 @@ func TestDERP(t *testing.T) {
assert.Equal(t, health.SeverityWarning, report.Severity)
assert.True(t, report.Dismissed)
if assert.NotEmpty(t, report.Warnings) {
assert.Contains(t, report.Warnings[0], health.CodeDERPOneNodeUnhealthy)
assert.Contains(t, report.Warnings[0].Code, health.CodeDERPOneNodeUnhealthy)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)
@ -236,7 +236,7 @@ func TestDERP(t *testing.T) {
assert.True(t, report.Healthy)
assert.Equal(t, health.SeverityWarning, report.Severity)
if assert.NotEmpty(t, report.Warnings) {
assert.Contains(t, report.Warnings[0], health.CodeDERPNodeUsesWebsocket)
assert.Equal(t, report.Warnings[0].Code, health.CodeDERPNodeUsesWebsocket)
}
for _, region := range report.Regions {
assert.True(t, region.Healthy)

View File

@ -3,6 +3,8 @@ package health
import (
"fmt"
"strings"
"github.com/coder/coder/v2/coderd/util/ptr"
)
const (
@ -47,16 +49,34 @@ func (s Severity) Value() int {
return severityRank[s]
}
// @typescript-generate Message
type Message struct {
Code Code `json:"code"`
Message string `json:"message"`
}
func (m Message) String() string {
var sb strings.Builder
_, _ = sb.WriteString(string(m.Code))
_, _ = sb.WriteRune(':')
_, _ = sb.WriteRune(' ')
_, _ = sb.WriteString(m.Message)
return sb.String()
}
// Code is a stable identifier used to link to documentation.
// @typescript-generate Code
type Code string
// Messagef is a convenience function for formatting a healthcheck error message.
func Messagef(code Code, msg string, args ...any) string {
var sb strings.Builder
_, _ = sb.WriteString(string(code))
_, _ = sb.WriteRune(':')
_, _ = sb.WriteRune(' ')
_, _ = sb.WriteString(fmt.Sprintf(msg, args...))
return sb.String()
// Messagef is a convenience function for returning a health.Message
func Messagef(code Code, msg string, args ...any) Message {
return Message{
Code: code,
Message: fmt.Sprintf(msg, args...),
}
}
// Errorf is a convenience function for returning a stringly-typed version of a Message.
func Errorf(code Code, msg string, args ...any) *string {
return ptr.Ref(Messagef(code, msg, args...).String())
}

View File

@ -103,7 +103,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.DERP.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "derp report panic: %s", err))
report.DERP.Error = health.Errorf(health.CodeUnknown, "derp report panic: %s", err)
}
}()
@ -115,7 +115,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.AccessURL.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "access url report panic: %s", err))
report.AccessURL.Error = health.Errorf(health.CodeUnknown, "access url report panic: %s", err)
}
}()
@ -127,7 +127,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.Websocket.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "websocket report panic: %s", err))
report.Websocket.Error = health.Errorf(health.CodeUnknown, "websocket report panic: %s", err)
}
}()
@ -139,7 +139,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.Database.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "database report panic: %s", err))
report.Database.Error = health.Errorf(health.CodeUnknown, "database report panic: %s", err)
}
}()
@ -151,7 +151,7 @@ func Run(ctx context.Context, opts *ReportOptions) *Report {
defer wg.Done()
defer func() {
if err := recover(); err != nil {
report.WorkspaceProxy.Error = ptr.Ref(health.Messagef(health.CodeUnknown, "proxy report panic: %s", err))
report.WorkspaceProxy.Error = health.Errorf(health.CodeUnknown, "proxy report panic: %s", err)
}
}()

View File

@ -107,7 +107,7 @@ func TestHealthcheck(t *testing.T) {
checker: &testChecker{
DERPReport: derphealth.Report{
Healthy: true,
Warnings: []string{"foobar"},
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
Severity: health.SeverityWarning,
},
AccessURLReport: healthcheck.AccessURLReport{
@ -259,7 +259,7 @@ func TestHealthcheck(t *testing.T) {
},
WorkspaceProxyReport: healthcheck.WorkspaceProxyReport{
Healthy: true,
Warnings: []string{"foobar"},
Warnings: []health.Message{{Message: "foobar", Code: "EFOOBAR"}},
Severity: health.SeverityWarning,
},
},

View File

@ -13,7 +13,6 @@ import (
"nhooyr.io/websocket"
"github.com/coder/coder/v2/coderd/healthcheck/health"
"github.com/coder/coder/v2/coderd/util/ptr"
)
// @typescript-generate WebsocketReport
@ -76,7 +75,7 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions)
}
if err != nil {
r.Error = convertError(xerrors.Errorf("websocket dial: %w", err))
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketDial, "websocket dial: %s", err))
r.Error = health.Errorf(health.CodeWebsocketDial, "websocket dial: %s", err)
r.Severity = health.SeverityError
return
}
@ -86,26 +85,26 @@ func (r *WebsocketReport) Run(ctx context.Context, opts *WebsocketReportOptions)
msg := strconv.Itoa(i)
err := c.Write(ctx, websocket.MessageText, []byte(msg))
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "write message: %s", err))
r.Error = health.Errorf(health.CodeWebsocketEcho, "write message: %s", err)
r.Severity = health.SeverityError
return
}
ty, got, err := c.Read(ctx)
if err != nil {
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketEcho, "read message: %s", err))
r.Error = health.Errorf(health.CodeWebsocketEcho, "read message: %s", err)
r.Severity = health.SeverityError
return
}
if ty != websocket.MessageText {
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message type: %v", ty))
r.Error = health.Errorf(health.CodeWebsocketMsg, "received incorrect message type: %v", ty)
r.Severity = health.SeverityError
return
}
if string(got) != msg {
r.Error = ptr.Ref(health.Messagef(health.CodeWebsocketMsg, "received incorrect message: wanted %q, got %q", msg, string(got)))
r.Error = health.Errorf(health.CodeWebsocketMsg, "received incorrect message: wanted %q, got %q", msg, string(got))
r.Severity = health.SeverityError
return
}

View File

@ -16,11 +16,11 @@ import (
// @typescript-generate WorkspaceProxyReport
type WorkspaceProxyReport struct {
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity"`
Warnings []string `json:"warnings"`
Dismissed bool `json:"dismissed"`
Error *string `json:"error"`
Healthy bool `json:"healthy"`
Severity health.Severity `json:"severity"`
Warnings []health.Message `json:"warnings"`
Dismissed bool `json:"dismissed"`
Error *string `json:"error"`
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
}
@ -54,7 +54,7 @@ func (*AGPLWorkspaceProxiesFetchUpdater) Update(context.Context) error {
func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyReportOptions) {
r.Healthy = true
r.Severity = health.SeverityOK
r.Warnings = []string{}
r.Warnings = []health.Message{}
r.Dismissed = opts.Dismissed
if opts.WorkspaceProxiesFetchUpdater == nil {
@ -72,7 +72,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
if err != nil {
r.Healthy = false
r.Severity = health.SeverityError
r.Error = ptr.Ref(health.Messagef(health.CodeProxyFetch, "fetch workspace proxies: %s", err))
r.Error = health.Errorf(health.CodeProxyFetch, "fetch workspace proxies: %s", err)
return
}
@ -104,7 +104,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
case health.SeverityWarning, health.SeverityOK:
r.Warnings = append(r.Warnings, health.Messagef(health.CodeProxyUnhealthy, err))
case health.SeverityError:
r.appendError(health.Messagef(health.CodeProxyUnhealthy, err))
r.appendError(*health.Errorf(health.CodeProxyUnhealthy, err))
}
}
@ -113,7 +113,7 @@ func (r *WorkspaceProxyReport) Run(ctx context.Context, opts *WorkspaceProxyRepo
if vErr := checkVersion(proxy, opts.CurrentVersion); vErr != nil {
r.Healthy = false
r.Severity = health.SeverityError
r.appendError(health.Messagef(health.CodeProxyVersionMismatch, vErr.Error()))
r.appendError(*health.Errorf(health.CodeProxyVersionMismatch, vErr.Error()))
}
}
}

View File

@ -2,7 +2,6 @@ package healthcheck_test
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -27,7 +26,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth func(context.Context) error
expectedHealthy bool
expectedError string
expectedWarning string
expectedWarningCode health.Code
expectedSeverity health.Severity
}{
{
@ -103,10 +102,10 @@ func TestWorkspaceProxies(t *testing.T) {
fakeWorkspaceProxy("alpha", false, currentVersion),
fakeWorkspaceProxy("beta", true, currentVersion),
),
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: true,
expectedSeverity: health.SeverityWarning,
expectedWarning: string(health.CodeProxyUnhealthy),
updateProxyHealth: fakeUpdateProxyHealth(nil),
expectedHealthy: true,
expectedSeverity: health.SeverityWarning,
expectedWarningCode: health.CodeProxyUnhealthy,
},
{
name: "Enabled/AllUnhealthy",
@ -163,7 +162,7 @@ func TestWorkspaceProxies(t *testing.T) {
updateProxyHealth: fakeUpdateProxyHealth(assert.AnError),
expectedHealthy: true,
expectedSeverity: health.SeverityWarning,
expectedWarning: string(health.CodeProxyUpdate),
expectedWarningCode: health.CodeProxyUpdate,
},
} {
tt := tt
@ -190,15 +189,15 @@ func TestWorkspaceProxies(t *testing.T) {
} else {
assert.Nil(t, rpt.Error)
}
if tt.expectedWarning != "" && assert.NotEmpty(t, rpt.Warnings) {
if tt.expectedWarningCode != "" && assert.NotEmpty(t, rpt.Warnings) {
var found bool
for _, w := range rpt.Warnings {
if strings.Contains(w, tt.expectedWarning) {
if w.Code == tt.expectedWarningCode {
found = true
break
}
}
assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarning, rpt.Warnings)
assert.True(t, found, "expected warning %s not found in %v", tt.expectedWarningCode, rpt.Warnings)
} else {
assert.Empty(t, rpt.Warnings)
}