Compare commits

..

1 Commits

Author SHA1 Message Date
Gaurav Tewari
0940db0875 fix(uplot): stop the time-scale trim hiding data on short windows
Assisted-by: Claude Opus 5
2026-08-12 16:24:06 +05:30
10 changed files with 249 additions and 181 deletions

View File

@@ -27,6 +27,7 @@ export interface BaseConfigBuilderProps {
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
useExactTimeRange?: boolean;
stepInterval?: number;
isLogScale?: boolean;
yAxisUnit?: string;
@@ -46,6 +47,7 @@ export function buildBaseConfig({
thresholds,
minTimeScale,
maxTimeScale,
useExactTimeRange,
stepInterval,
isLogScale,
yAxisUnit,
@@ -88,6 +90,7 @@ export function buildBaseConfig({
time: true,
min: minTimeScale,
max: maxTimeScale,
useExactTimeRange,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic

View File

@@ -42,6 +42,7 @@ export class UPlotScaleBuilder extends ConfigBuilder<
logBase = 10,
padMinBy = 0,
padMaxBy = 0.05,
useExactTimeRange = false,
} = this.props;
// Special handling for time scales (X axis)
@@ -58,14 +59,20 @@ export class UPlotScaleBuilder extends ConfigBuilder<
// Align max time to "endTime - 1 minute", rounded down to minute precision
// This matches legacy getXAxisScale behavior and avoids empty space at the right edge
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
const currentDate = new Date(oneMinuteAgoTimestamp);
if (!useExactTimeRange) {
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
const currentDate = new Date(oneMinuteAgoTimestamp);
currentDate.setSeconds(0);
currentDate.setMilliseconds(0);
currentDate.setSeconds(0);
currentDate.setMilliseconds(0);
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
maxTime = unixTimestampSeconds;
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
// Trimming past min inverts the range, which uPlot draws as an empty plot.
if (unixTimestampSeconds > minTime) {
maxTime = unixTimestampSeconds;
}
}
return {
[scaleKey]: {

View File

@@ -79,6 +79,44 @@ describe('UPlotScaleBuilder', () => {
expect(resolvedMax).toBe(expectedMax);
});
it('plots min/max as given when useExactTimeRange is set', () => {
const min = 1_700_000_000;
const max = 1_700_000_630;
const builder = new UPlotScaleBuilder(
createScaleProps({
scaleKey: 'x',
time: true,
min,
max,
useExactTimeRange: true,
}),
);
const config = builder.getConfig();
expect(config.x.range).toStrictEqual([min, max]);
});
it('keeps the requested end when the window is shorter than the trim', () => {
// 23 second window: trimming a minute off the end would put max before min.
const min = 1_786_527_160;
const max = 1_786_527_183;
const builder = new UPlotScaleBuilder(
createScaleProps({
scaleKey: 'x',
time: true,
min,
max,
}),
);
const config = builder.getConfig();
expect(config.x.range).toStrictEqual([min, max]);
});
it('falls back to getFallbackMinMaxTimeStamp when time scale has no min/max', () => {
getFallbackMinMaxSpy.mockReturnValue({
fallbackMin: 100,

View File

@@ -97,6 +97,8 @@ export interface ScaleProps {
auto?: boolean;
logBase?: uPlot.Scale.LogBase;
distribution?: DistributionType;
/** Plots a time scale's `min`/`max` as given, skipping the trim below. */
useExactTimeRange?: boolean;
}
export enum DisconnectedValuesMode {

View File

@@ -223,7 +223,9 @@ func TestEmailNotifyWithErrors(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
title string
@@ -286,7 +288,10 @@ func TestEmailNotifyWithErrors(t *testing.T) {
},
} {
t.Run(tc.title, func(t *testing.T) {
require.NotEmpty(t, tc.errMsg, "please define the expected error message")
if len(tc.errMsg) == 0 {
t.Fatal("please define the expected error message")
return
}
emailCfg := &config.EmailConfig{
Smarthost: c.Smarthost,
@@ -304,15 +309,15 @@ func TestEmailNotifyWithErrors(t *testing.T) {
_, retry, err := notifyEmail(t, emailCfg, c.Server)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
assert.False(t, retry)
require.Contains(t, err.Error(), tc.errMsg)
require.False(t, retry)
e, err := c.Server.getLastEmail(t)
require.NoError(t, err)
if tc.hasEmail {
assert.NotNil(t, e)
require.NotNil(t, e)
} else {
assert.Nil(t, e)
require.Nil(t, e)
}
})
}
@@ -326,7 +331,9 @@ func TestEmailNotifyWithDoneContext(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -343,7 +350,7 @@ func TestEmailNotifyWithDoneContext(t *testing.T) {
c.Server,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "establish connection to server")
require.Contains(t, err.Error(), "establish connection to server")
}
// TestEmailNotifyWithoutAuthentication sends an email to an instance of
@@ -356,7 +363,9 @@ func TestEmailNotifyWithoutAuthentication(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
mail, _, err := notifyEmail(
t,
@@ -381,7 +390,7 @@ func TestEmailNotifyWithoutAuthentication(t *testing.T) {
}
headers = append(headers, k)
}
assert.True(t, foundMsgID, "Couldn't find 'message-id' in %v", headers)
require.True(t, foundMsgID, "Couldn't find 'message-id' in %v", headers)
}
// TestEmailNotifyWithSTARTTLS connects to the server, upgrades the connection
@@ -397,7 +406,9 @@ func TestEmailNotifyWithSTARTTLS(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
trueVar := true
_, _, err = notifyEmail(
@@ -426,7 +437,9 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
td := t.TempDir()
fileWithCorrectPassword, err := os.CreateTemp(td, "smtp-password-correct")
@@ -570,13 +583,13 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
e, retry, err := notifyEmail(t, emailCfg, c.Server)
if len(tc.errMsg) > 0 {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
assert.Equal(t, tc.retry, retry)
require.Contains(t, err.Error(), tc.errMsg)
require.Equal(t, tc.retry, retry)
return
}
require.NoError(t, err)
assert.Equal(t, "1 firing alert(s)", e.Subject)
require.Equal(t, "1 firing alert(s)", e.Subject)
getAddresses := func(addresses []map[string]string) []string {
res := make([]string, 0, len(addresses))
@@ -587,21 +600,19 @@ func TestEmailNotifyWithAuthentication(t *testing.T) {
}
to := getAddresses(e.To)
from := getAddresses(e.From)
assert.Equal(t, strings.Split(emailCfg.To, ","), to)
assert.Equal(t, strings.Split(emailCfg.From, ","), from)
require.Equal(t, strings.Split(emailCfg.To, ","), to)
require.Equal(t, strings.Split(emailCfg.From, ","), from)
if len(emailCfg.HTML) > 0 {
require.NotNil(t, e.HTML)
assert.Equal(t, emailCfg.HTML, *e.HTML)
require.Equal(t, emailCfg.HTML, *e.HTML)
} else {
assert.Nil(t, e.HTML)
require.Nil(t, e.HTML)
}
if len(emailCfg.Text) > 0 {
require.NotNil(t, e.Text)
assert.Equal(t, emailCfg.Text, *e.Text)
require.Equal(t, emailCfg.Text, *e.Text)
} else {
assert.Nil(t, e.Text)
require.Nil(t, e.Text)
}
})
}
@@ -613,7 +624,7 @@ func TestEmailConfigNoAuthMechs(t *testing.T) {
}
_, err := email.auth("")
require.Error(t, err)
assert.Equal(t, "unknown auth mechanism: ", err.Error())
require.Equal(t, "unknown auth mechanism: ", err.Error())
}
func TestEmailConfigMissingAuthParam(t *testing.T) {
@@ -623,19 +634,19 @@ func TestEmailConfigMissingAuthParam(t *testing.T) {
}
_, err := email.auth("CRAM-MD5")
require.Error(t, err)
assert.Equal(t, "missing secret for CRAM-MD5 auth mechanism", err.Error())
require.Equal(t, "missing secret for CRAM-MD5 auth mechanism", err.Error())
_, err = email.auth("PLAIN")
require.Error(t, err)
assert.Equal(t, "missing password for PLAIN auth mechanism", err.Error())
require.Equal(t, "missing password for PLAIN auth mechanism", err.Error())
_, err = email.auth("LOGIN")
require.Error(t, err)
assert.Equal(t, "missing password for LOGIN auth mechanism", err.Error())
require.Equal(t, "missing password for LOGIN auth mechanism", err.Error())
_, err = email.auth("PLAIN LOGIN")
require.Error(t, err)
assert.Equal(t, "missing password for PLAIN auth mechanism\nmissing password for LOGIN auth mechanism", err.Error())
require.Equal(t, "missing password for PLAIN auth mechanism\nmissing password for LOGIN auth mechanism", err.Error())
}
func TestEmailNoUsernameCustomError(t *testing.T) {
@@ -644,7 +655,7 @@ func TestEmailNoUsernameCustomError(t *testing.T) {
}
a, err := email.auth("CRAM-MD5")
require.ErrorIs(t, err, errNoAuthUsernameConfigured)
assert.Nil(t, a)
require.Nil(t, a)
}
// TestEmailRejected simulates the failure of an otherwise valid message submission which fails at a later point than
@@ -709,7 +720,7 @@ func TestEmailRejected(t *testing.T) {
// Send the alert to mock SMTP server.
retry, err := e.Notify(context.Background(), firingAlert)
require.ErrorContains(t, err, "501 5.5.4 Rejected!")
assert.True(t, retry)
require.True(t, retry)
require.NoError(t, srv.Shutdown(ctx))
require.Eventuallyf(t, func() bool {
@@ -778,7 +789,9 @@ func TestEmailNotifyWithThreading(t *testing.T) {
}
c, err := loadEmailTestConfiguration(cfgFile)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
@@ -823,22 +836,22 @@ func TestEmailNotifyWithThreading(t *testing.T) {
referencesValue := mail.Headers["references"]
inReplyToValue := mail.Headers["in-reply-to"]
assert.NotEmpty(t, referencesValue, "References header not found in %v", mail.Headers)
assert.NotEmpty(t, inReplyToValue, "In-Reply-To header not found in %v", mail.Headers)
require.NotEmpty(t, referencesValue, "References header not found in %v", mail.Headers)
require.NotEmpty(t, inReplyToValue, "In-Reply-To header not found in %v", mail.Headers)
assert.Equal(t, referencesValue, inReplyToValue, "References and In-Reply-To should match")
require.Equal(t, referencesValue, inReplyToValue, "References and In-Reply-To should match")
// Verify the format: <alert-HASH-DATE@alertmanager>
assert.Contains(t, referencesValue, "<alert-")
assert.Contains(t, referencesValue, "@alertmanager>")
require.Contains(t, referencesValue, "<alert-")
require.Contains(t, referencesValue, "@alertmanager>")
if tc.wantDatePart {
today := time.Now().Format("2006-01-02")
assert.Contains(t, referencesValue, today, "threading header should contain today's date")
require.Contains(t, referencesValue, today, "threading header should contain today's date")
} else {
// With thread_by_date: none, there should be no date
// (empty string between hash and @).
assert.Contains(t, referencesValue, "-@alertmanager>", "threading header should have empty date part")
require.Contains(t, referencesValue, "-@alertmanager>", "threading header should have empty date part")
}
})
}
@@ -891,14 +904,14 @@ func TestEmailGetPassword(t *testing.T) {
require.Error(t, err)
if errors.Asc(err, errors.CodeInternal) {
_, _, errMsg, _, _, _ := errors.Unwrapb(err)
assert.Contains(t, errMsg, tc.errMsg)
require.Contains(t, errMsg, tc.errMsg)
} else {
assert.Contains(t, err.Error(), tc.errMsg)
require.Contains(t, err.Error(), tc.errMsg)
}
assert.Empty(t, password)
require.Empty(t, password)
} else {
require.NoError(t, err)
assert.Equal(t, "secret", password)
require.Equal(t, "secret", password)
}
})
}
@@ -949,11 +962,11 @@ func TestEmailGetSecret(t *testing.T) {
secret, err := email.getAuthSecret()
if len(tc.errMsg) > 0 {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
assert.Empty(t, secret)
require.Contains(t, err.Error(), tc.errMsg)
require.Empty(t, secret)
} else {
require.NoError(t, err)
assert.Equal(t, "secret", secret)
require.Equal(t, "secret", secret)
}
})
}
@@ -1019,7 +1032,7 @@ func TestEmailImplicitTLS(t *testing.T) {
useImplicitTLS = cfg.Smarthost.Port == "465"
}
assert.Equal(t, tt.expectImplicit, useImplicitTLS,
require.Equal(t, tt.expectImplicit, useImplicitTLS,
"Expected useImplicitTLS=%v for port=%s with forceImplicitTLS=%v",
tt.expectImplicit, tt.port, tt.forceImplicitTLS)
})
@@ -1061,8 +1074,8 @@ func TestPrepareContent(t *testing.T) {
ctx := context.Background()
subject, htmlBody, err := n.prepareContent(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "subj", subject)
assert.Equal(t, "<div><p>line one</p>\n</div><div><p>line two</p>\n</div>", htmlBody)
require.Equal(t, "subj", subject)
require.Equal(t, "<div><p>line one</p>\n</div><div><p>line two</p>\n</div>", htmlBody)
})
t.Run("custom title template; default body HTML template", func(t *testing.T) {
@@ -1090,8 +1103,8 @@ func TestPrepareContent(t *testing.T) {
ctx := context.Background()
subject, htmlBody, err := n.prepareContent(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "Status: firing", htmlBody)
assert.Equal(t, "fixed from firing", subject)
require.Equal(t, "Status: firing", htmlBody)
require.Equal(t, "fixed from firing", subject)
})
t.Run("default template without HTML", func(t *testing.T) {
@@ -1112,8 +1125,8 @@ func TestPrepareContent(t *testing.T) {
ctx := context.Background()
subject, htmlBody, err := n.prepareContent(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "", htmlBody)
assert.Equal(t, "the email subject", subject)
require.Equal(t, "", htmlBody)
require.Equal(t, "the email subject", subject)
})
t.Run("custom title template; custom body template", func(t *testing.T) {
@@ -1147,11 +1160,11 @@ func TestPrepareContent(t *testing.T) {
ctx := context.Background()
subject, htmlBody, err := n.prepareContent(ctx, alerts)
require.NoError(t, err)
assert.Contains(t, htmlBody, "<!DOCTYPE html>")
assert.Contains(t, htmlBody, "<p>line two</p>")
assert.NotContains(t, htmlBody, "Well, what are you?")
assert.Equal(t, "fixed from firing", subject)
assert.NotContains(t, subject, "subject")
require.Contains(t, htmlBody, "<!DOCTYPE html>")
require.Contains(t, htmlBody, "<p>line two</p>")
require.NotContains(t, htmlBody, "Well, what are you?")
require.Equal(t, subject, "fixed from firing")
require.NotContains(t, subject, "subject")
})
}

View File

@@ -22,7 +22,6 @@ import (
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
@@ -55,7 +54,7 @@ func TestMSTeamsV2Retry(t *testing.T) {
for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "retry - error on status %d", statusCode)
require.Equal(t, expected, actual, "retry - error on status %d", statusCode)
}
}
@@ -111,7 +110,7 @@ func TestNotifier_Notify_WithReason(t *testing.T) {
} else {
var reasonError *notify.ErrorWithReason
require.ErrorAs(t, err, &reasonError)
assert.Equal(t, tt.expectedReason, reasonError.Reason)
require.Equal(t, tt.expectedReason, reasonError.Reason)
}
})
}
@@ -189,9 +188,9 @@ func TestMSTeamsV2Templating(t *testing.T) {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.errMsg)
require.Contains(t, err.Error(), tc.errMsg)
}
assert.Equal(t, tc.retry, ok)
require.Equal(t, tc.retry, ok)
})
}
}
@@ -251,14 +250,14 @@ func TestPrepareContent(t *testing.T) {
}
blocks, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
require.Len(t, blocks, 2)
require.NotEmpty(t, blocks)
// First block should be the title with color (firing = red)
assert.Equal(t, "Bolder", blocks[0].Weight)
assert.Equal(t, colorRed, blocks[0].Color)
require.Equal(t, "Bolder", blocks[0].Weight)
require.Equal(t, colorRed, blocks[0].Color)
// verify title text
assert.Equal(t, "Alertname: test", blocks[0].Text)
require.Equal(t, "Alertname: test", blocks[0].Text)
// verify body text
assert.Equal(t, "Firing alert: test", blocks[1].Text)
require.Equal(t, "Firing alert: test", blocks[1].Text)
})
t.Run("custom template - per-alert color", func(t *testing.T) {
@@ -306,15 +305,16 @@ func TestPrepareContent(t *testing.T) {
}
blocks, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
require.NotEmpty(t, blocks)
// total 3 blocks: title and 2 body blocks
require.Len(t, blocks, 3)
require.True(t, len(blocks) == 3)
// First block: title color is overall color of the alerts
assert.Equal(t, colorRed, blocks[0].Color)
require.Equal(t, colorRed, blocks[0].Color)
// verify title text
assert.Equal(t, "Custom Title", blocks[0].Text)
require.Equal(t, "Custom Title", blocks[0].Text)
// Body blocks should have per-alert color
assert.Equal(t, colorRed, blocks[1].Color) // firing
assert.Equal(t, colorGreen, blocks[2].Color) // resolved
require.Equal(t, colorRed, blocks[1].Color) // firing
require.Equal(t, colorGreen, blocks[2].Color) // resolved
})
}

View File

@@ -21,7 +21,6 @@ import (
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
@@ -50,7 +49,7 @@ func TestOpsGenieRetry(t *testing.T) {
retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)
for statusCode, expected := range test.RetryTests(retryCodes) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "error on status %d", statusCode)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
}
@@ -104,7 +103,9 @@ func TestGettingOpsGegineApikeyFromFile(t *testing.T) {
func TestOpsGenie(t *testing.T) {
u, err := url.Parse("https://opsgenie/api")
require.NoError(t, err)
if err != nil {
t.Fatalf("failed to parse URL: %v", err)
}
logger := promslog.NewNopLogger()
tmpl := test.CreateTmpl(t)
@@ -235,10 +236,10 @@ func TestOpsGenie(t *testing.T) {
req, retry, err := notifier.createRequests(ctx, alert1)
require.NoError(t, err)
require.Len(t, req, 1)
assert.True(t, retry)
assert.Equal(t, expectedURL, req[0].URL)
assert.Equal(t, "GenieKey http://am", req[0].Header.Get("Authorization"))
assert.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0]))
require.True(t, retry)
require.Equal(t, expectedURL, req[0].URL)
require.Equal(t, "GenieKey http://am", req[0].Header.Get("Authorization"))
require.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0]))
// Fully defined alert.
alert2 := &types.Alert{
@@ -265,15 +266,15 @@ func TestOpsGenie(t *testing.T) {
}
req, retry, err = notifier.createRequests(ctx, alert2)
require.NoError(t, err)
assert.True(t, retry)
require.True(t, retry)
require.Len(t, req, 1)
assert.Equal(t, tc.expectedBody, readBody(t, req[0]))
require.Equal(t, tc.expectedBody, readBody(t, req[0]))
// Broken API Key Template.
tc.cfg.APIKey = "{{ kaput "
_, _, err = notifier.createRequests(ctx, alert2)
require.Error(t, err)
assert.Equal(t, "template: :1: function \"kaput\" not defined", err.Error())
require.Equal(t, "template: :1: function \"kaput\" not defined", err.Error())
})
}
}
@@ -306,7 +307,7 @@ func TestOpsGenieWithUpdate(t *testing.T) {
require.NoError(t, err)
requests, retry, err := notifierWithUpdate.createRequests(ctx, alert)
require.NoError(t, err)
assert.True(t, retry)
require.True(t, retry)
require.Len(t, requests, 3)
body0 := readBody(t, requests[0])
@@ -315,13 +316,13 @@ func TestOpsGenieWithUpdate(t *testing.T) {
key, _ := notify.ExtractGroupKey(ctx)
alias := key.Hash()
assert.Equal(t, "https://test-opsgenie-url/v2/alerts", requests[0].URL.String())
assert.NotEmpty(t, body0)
require.Equal(t, "https://test-opsgenie-url/v2/alerts", requests[0].URL.String())
require.NotEmpty(t, body0)
assert.Equal(t, requests[1].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/message?identifierType=alias", alias))
assert.JSONEq(t, `{"message":"new message"}`, body1)
assert.Equal(t, requests[2].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/description?identifierType=alias", alias))
assert.JSONEq(t, `{"description":"new description"}`, body2)
require.Equal(t, requests[1].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/message?identifierType=alias", alias))
require.JSONEq(t, `{"message":"new message"}`, body1)
require.Equal(t, requests[2].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/description?identifierType=alias", alias))
require.JSONEq(t, `{"description":"new description"}`, body2)
}
func TestOpsGenieApiKeyFile(t *testing.T) {
@@ -340,8 +341,7 @@ func TestOpsGenieApiKeyFile(t *testing.T) {
require.NoError(t, err)
requests, _, err := notifierWithUpdate.createRequests(ctx)
require.NoError(t, err)
require.Len(t, requests, 1)
assert.Equal(t, "GenieKey my_secret_api_key", requests[0].Header.Get("Authorization"))
require.Equal(t, "GenieKey my_secret_api_key", requests[0].Header.Get("Authorization"))
}
func TestPrepareContent(t *testing.T) {
@@ -377,8 +377,8 @@ func TestPrepareContent(t *testing.T) {
title, desc, prepErr := notifier.prepareContent(ctx, alerts)
require.NoError(t, prepErr)
assert.Equal(t, "Firing alert: test", title)
assert.Equal(t, "Check runbook for more details", desc)
require.Equal(t, "Firing alert: test", title)
require.Equal(t, "Check runbook for more details", desc)
})
t.Run("custom template", func(t *testing.T) {
@@ -431,9 +431,9 @@ func TestPrepareContent(t *testing.T) {
title, desc, err := notifier.prepareContent(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "High request throughput for payment", title)
require.Equal(t, "High request throughput for payment", title)
// Each alert body wrapped in <div>, separated by <hr>
assert.Equal(t, "<div><p>Alert firing in NS: potter-the-harry</p>\n</div><hr><div><p>Alert firing in NS: smart-the-rat</p>\n</div>", desc)
require.Equal(t, "<div><p>Alert firing in NS: potter-the-harry</p>\n</div><hr><div><p>Alert firing in NS: smart-the-rat</p>\n</div>", desc)
})
}

View File

@@ -25,7 +25,6 @@ import (
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
@@ -55,7 +54,7 @@ func TestPagerDutyRetryV1(t *testing.T) {
retryCodes := append(test.DefaultRetryCodes(), http.StatusForbidden)
for statusCode, expected := range test.RetryTests(retryCodes) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "retryv1 - error on status %d", statusCode)
require.Equal(t, expected, actual, "retryv1 - error on status %d", statusCode)
}
}
@@ -75,7 +74,7 @@ func TestPagerDutyRetryV2(t *testing.T) {
retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)
for statusCode, expected := range test.RetryTests(retryCodes) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "retryv2 - error on status %d", statusCode)
require.Equal(t, expected, actual, "retryv2 - error on status %d", statusCode)
}
}
@@ -350,12 +349,12 @@ func TestPagerDutyTemplating(t *testing.T) {
require.Error(t, err)
if errors.Asc(err, errors.CodeInternal) {
_, _, errMsg, _, _, _ := errors.Unwrapb(err)
assert.Contains(t, errMsg, tc.errMsg)
require.Contains(t, errMsg, tc.errMsg)
} else {
assert.Contains(t, err.Error(), tc.errMsg)
require.Contains(t, err.Error(), tc.errMsg)
}
}
assert.Equal(t, tc.retry, ok)
require.Equal(t, tc.retry, ok)
})
}
}
@@ -394,7 +393,7 @@ func TestErrDetails(t *testing.T) {
} {
t.Run("", func(t *testing.T) {
err := errDetails(tc.status, tc.body)
assert.Contains(t, err, tc.exp)
require.Contains(t, err, tc.exp)
})
}
}
@@ -428,7 +427,7 @@ func TestEventSizeEnforcement(t *testing.T) {
encodedV1, err := notifierV1.encodeMessage(context.Background(), msgV1)
require.NoError(t, err)
assert.Contains(t, encodedV1.String(), `"details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
require.Contains(t, encodedV1.String(), `"details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
// V2 Messages
msgV2 := &pagerDutyMessage{
@@ -452,7 +451,7 @@ func TestEventSizeEnforcement(t *testing.T) {
encodedV2, err := notifierV2.encodeMessage(context.Background(), msgV2)
require.NoError(t, err)
assert.Contains(t, encodedV2.String(), `"custom_details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
require.Contains(t, encodedV2.String(), `"custom_details":{"error":"Custom details have been removed because the original event exceeds the maximum size of 512KB"}`)
}
func TestPagerDutyEmptySrcHref(t *testing.T) {
@@ -544,9 +543,8 @@ func TestPagerDutyEmptySrcHref(t *testing.T) {
}
}
// Handler runs on the server's goroutine — require is illegal here.
assert.Equal(t, expectedImages, event.Images)
assert.Equal(t, expectedLinks, event.Links)
require.Equal(t, expectedImages, event.Images)
require.Equal(t, expectedLinks, event.Links)
},
))
defer server.Close()
@@ -646,7 +644,7 @@ func TestPagerDutyTimeout(t *testing.T) {
},
}
_, err = pd.Notify(ctx, alert)
assert.Equal(t, tt.wantErr, err != nil)
require.Equal(t, tt.wantErr, err != nil)
})
}
}
@@ -901,12 +899,11 @@ func TestRenderDetails(t *testing.T) {
tmpl: test.CreateTmpl(t),
}
got, err := n.renderDetails(tt.args.data)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
if (err != nil) != tt.wantErr {
t.Errorf("renderDetails() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
require.Equal(t, tt.want, got)
})
}
}
@@ -947,7 +944,7 @@ func TestPrepareContent(t *testing.T) {
title, err := notifier.prepareTitle(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "HighCPU for Payment service (FIRING)", title)
require.Equal(t, "HighCPU for Payment service (FIRING)", title)
})
t.Run("custom template uses $variable annotation for title", func(t *testing.T) {
@@ -983,6 +980,6 @@ func TestPrepareContent(t *testing.T) {
title, err := notifier.prepareTitle(ctx, alerts)
require.NoError(t, err)
assert.Equal(t, "HighCPU on api-server is in resolved state", title)
require.Equal(t, "HighCPU on api-server is in resolved state", title)
})
}

View File

@@ -23,7 +23,6 @@ import (
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/config"
@@ -51,7 +50,7 @@ func TestSlackRetry(t *testing.T) {
for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "error on status %d", statusCode)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
}
@@ -233,15 +232,15 @@ func TestNotifier_Notify_WithReason(t *testing.T) {
},
}
retry, err := notifier.Notify(ctx, alert1)
assert.Equal(t, tt.expectedRetry, retry)
require.Equal(t, tt.expectedRetry, retry)
if tt.noError {
require.NoError(t, err)
} else {
var reasonError *notify.ErrorWithReason
require.ErrorAs(t, err, &reasonError)
assert.Equal(t, tt.expectedReason, reasonError.Reason)
assert.Contains(t, err.Error(), tt.expectedErr)
assert.Contains(t, err.Error(), "channelname")
require.Equal(t, tt.expectedReason, reasonError.Reason)
require.Contains(t, err.Error(), tt.expectedErr)
require.Contains(t, err.Error(), "channelname")
}
})
}
@@ -297,7 +296,7 @@ func TestSlackTimeout(t *testing.T) {
},
}
_, err = notifier.Notify(ctx, alert)
assert.Equal(t, tt.wantErr, err != nil)
require.Equal(t, tt.wantErr, err != nil)
})
}
}
@@ -351,14 +350,14 @@ func TestPrepareContent(t *testing.T) {
require.NoError(t, err)
require.Len(t, atts, 1)
assert.Equal(t, "HighCPU (FIRING)", atts[0].Title)
assert.Equal(t, "Alert: HighCPU - severity critical", atts[0].Text)
require.Equal(t, "HighCPU (FIRING)", atts[0].Title)
require.Equal(t, "Alert: HighCPU - severity critical", atts[0].Text)
// Color is templated — firing alert should be "danger"
assert.Equal(t, "danger", atts[0].Color)
require.Equal(t, "danger", atts[0].Color)
// No BlockKit blocks for default template
assert.Nil(t, atts[0].Blocks)
require.Nil(t, atts[0].Blocks)
// Default markdownIn when config has none
assert.Equal(t, []string{"fallback", "pretext", "text"}, atts[0].MrkdwnIn)
require.Equal(t, []string{"fallback", "pretext", "text"}, atts[0].MrkdwnIn)
})
t.Run("custom template produces 1+N attachments with per-alert color", func(t *testing.T) {
@@ -429,10 +428,10 @@ func TestPrepareContent(t *testing.T) {
require.Len(t, atts, 3)
// First attachment: title-only, no color, no blocks
assert.Equal(t, "[firing] HighCPU — api-server", atts[0].Title)
assert.Empty(t, atts[0].Color)
assert.Nil(t, atts[0].Blocks)
assert.Equal(t, "https://alertmanager.signoz.com", atts[0].TitleLink)
require.Equal(t, "[firing] HighCPU — api-server", atts[0].Title)
require.Empty(t, atts[0].Color)
require.Nil(t, atts[0].Blocks)
require.Equal(t, "https://alertmanager.signoz.com", atts[0].TitleLink)
expectedFiringBody := "*HighCPU*\n\n" +
"*Service:* _api-server_\n*Instance:* _i-0abc123_\n*Region:* _us-east-1_\n*Method:* _GET_\n\n" +
@@ -447,16 +446,16 @@ func TestPrepareContent(t *testing.T) {
"*Status:* resolved | *Severity:* critical\n\n"
// Second attachment: firing alert body rendered as slack mrkdwn text, red color
assert.Nil(t, atts[1].Blocks)
assert.Equal(t, "#FF0000", atts[1].Color)
assert.Equal(t, []string{"text"}, atts[1].MrkdwnIn)
assert.Equal(t, expectedFiringBody, atts[1].Text)
require.Nil(t, atts[1].Blocks)
require.Equal(t, "#FF0000", atts[1].Color)
require.Equal(t, []string{"text"}, atts[1].MrkdwnIn)
require.Equal(t, expectedFiringBody, atts[1].Text)
// Third attachment: resolved alert body rendered as slack mrkdwn text, green color
assert.Nil(t, atts[2].Blocks)
assert.Equal(t, "#00FF00", atts[2].Color)
assert.Equal(t, []string{"text"}, atts[2].MrkdwnIn)
assert.Equal(t, expectedResolvedBody, atts[2].Text)
require.Nil(t, atts[2].Blocks)
require.Equal(t, "#00FF00", atts[2].Color)
require.Equal(t, []string{"text"}, atts[2].MrkdwnIn)
require.Equal(t, expectedResolvedBody, atts[2].Text)
})
t.Run("default template with fields and actions", func(t *testing.T) {
@@ -499,45 +498,49 @@ func TestPrepareContent(t *testing.T) {
// prepareContent does not populate fields/actions — that's done by
// addFieldsAndActions which is called from Notify.
assert.Nil(t, atts[0].Fields)
assert.Nil(t, atts[0].Actions)
require.Nil(t, atts[0].Fields)
require.Nil(t, atts[0].Actions)
// Simulate what Notify does after prepareContent
notifier.addFieldsAndActions(&atts[0], tmplText)
// Verify fields
require.Len(t, atts[0].Fields, 2)
assert.Equal(t, "Severity", atts[0].Fields[0].Title)
assert.Equal(t, "critical", atts[0].Fields[0].Value)
require.NotNil(t, atts[0].Fields[0].Short)
assert.True(t, *atts[0].Fields[0].Short)
assert.Equal(t, "Service", atts[0].Fields[1].Title)
assert.Equal(t, "api-server", atts[0].Fields[1].Value)
require.Equal(t, "Severity", atts[0].Fields[0].Title)
require.Equal(t, "critical", atts[0].Fields[0].Value)
require.True(t, *atts[0].Fields[0].Short)
require.Equal(t, "Service", atts[0].Fields[1].Title)
require.Equal(t, "api-server", atts[0].Fields[1].Value)
// Verify actions
require.Len(t, atts[0].Actions, 1)
assert.Equal(t, "button", atts[0].Actions[0].Type)
assert.Equal(t, "View Alert", atts[0].Actions[0].Text)
assert.Equal(t, "https://alertmanager.signoz.com", atts[0].Actions[0].URL)
require.Equal(t, "button", atts[0].Actions[0].Type)
require.Equal(t, "View Alert", atts[0].Actions[0].Text)
require.Equal(t, "https://alertmanager.signoz.com", atts[0].Actions[0].URL)
})
}
func TestSlackMessageField(t *testing.T) {
// 1. Setup a fake Slack server. The handler runs on the server's
// goroutine, so only assert (never require) is safe here.
// 1. Setup a fake Slack server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
assert.NoError(t, json.NewDecoder(r.Body).Decode(&body))
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatal(err)
}
// 2. VERIFY: Top-level text exists
assert.Equal(t, "My Top Level Message", body["text"])
if body["text"] != "My Top Level Message" {
t.Errorf("Expected top-level 'text' to be 'My Top Level Message', got %v", body["text"])
}
// 3. VERIFY: Old attachments still exist
attachments, ok := body["attachments"].([]any)
if assert.True(t, ok, "expected attachments to exist") && assert.NotEmpty(t, attachments) {
first, ok := attachments[0].(map[string]any)
if assert.True(t, ok, "expected attachment to be an object") {
assert.Equal(t, "Old Attachment Title", first["title"])
if !ok || len(attachments) == 0 {
t.Errorf("Expected attachments to exist")
} else {
first := attachments[0].(map[string]any)
if first["title"] != "Old Attachment Title" {
t.Errorf("Expected attachment title 'Old Attachment Title', got %v", first["title"])
}
}
@@ -558,16 +561,21 @@ func TestSlackMessageField(t *testing.T) {
}
tmpl, err := template.FromGlobs([]string{})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
tmpl.ExternalURL = u
logger := slog.New(slog.DiscardHandler)
notifier, err := New(conf, tmpl, logger, newTestTemplater(tmpl))
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group-key")
_, err = notifier.Notify(ctx)
require.NoError(t, err, "Notify failed")
if _, err := notifier.Notify(ctx); err != nil {
t.Fatal("Notify failed:", err)
}
}

View File

@@ -19,7 +19,6 @@ import (
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/common/model"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/SigNoz/signoz/pkg/alertmanager/alertmanagertemplate"
@@ -40,12 +39,14 @@ func TestWebhookRetry(t *testing.T) {
promslog.NewNopLogger(),
alertmanagertemplate.New(tmpl, slog.Default()),
)
require.NoError(t, err)
if err != nil {
require.NoError(t, err)
}
t.Run("test retry status code", func(t *testing.T) {
for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) {
actual, _ := notifier.retrier.Check(statusCode, nil)
assert.Equal(t, expected, actual, "error on status %d", statusCode)
require.Equal(t, expected, actual, "error on status %d", statusCode)
}
})
@@ -72,8 +73,7 @@ func TestWebhookRetry(t *testing.T) {
} {
t.Run("", func(t *testing.T) {
_, err = notifier.retrier.Check(tc.status, tc.body)
require.Error(t, err)
assert.Equal(t, tc.exp, err.Error())
require.Equal(t, tc.exp, err.Error())
})
}
})
@@ -83,16 +83,16 @@ func TestWebhookTruncateAlerts(t *testing.T) {
alerts := make([]*types.Alert, 10)
truncatedAlerts, numTruncated := truncateAlerts(0, alerts)
assert.Len(t, truncatedAlerts, 10)
assert.EqualValues(t, 0, numTruncated)
require.Len(t, truncatedAlerts, 10)
require.EqualValues(t, 0, numTruncated)
truncatedAlerts, numTruncated = truncateAlerts(4, alerts)
assert.Len(t, truncatedAlerts, 4)
assert.EqualValues(t, 6, numTruncated)
require.Len(t, truncatedAlerts, 4)
require.EqualValues(t, 6, numTruncated)
truncatedAlerts, numTruncated = truncateAlerts(100, alerts)
assert.Len(t, truncatedAlerts, 10)
assert.EqualValues(t, 0, numTruncated)
require.Len(t, truncatedAlerts, 10)
require.EqualValues(t, 0, numTruncated)
}
func TestWebhookRedactedURL(t *testing.T) {
@@ -219,10 +219,10 @@ func TestWebhookURLTemplating(t *testing.T) {
if tc.expectError {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErrMsg)
require.Contains(t, err.Error(), tc.expectedErrMsg)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expectedPath, calledURL)
require.Equal(t, tc.expectedPath, calledURL)
}
})
}