github.com/grafana/pyroscope@v1.18.0/pkg/validation/validate_test.go (about)

     1  package validation
     2  
     3  import (
     4  	"testing"
     5  	"time"
     6  
     7  	"github.com/go-kit/log"
     8  	"github.com/prometheus/common/model"
     9  	"github.com/stretchr/testify/require"
    10  
    11  	googlev1 "github.com/grafana/pyroscope/api/gen/proto/go/google/v1"
    12  	typesv1 "github.com/grafana/pyroscope/api/gen/proto/go/types/v1"
    13  	phlaremodel "github.com/grafana/pyroscope/pkg/model"
    14  	"github.com/grafana/pyroscope/pkg/pprof"
    15  )
    16  
    17  func TestValidateLabels(t *testing.T) {
    18  	for _, tt := range []struct {
    19  		name           string
    20  		lbs            []*typesv1.LabelPair
    21  		expectedErr    string
    22  		expectedReason Reason
    23  	}{
    24  		{
    25  			name: "valid labels",
    26  			lbs: []*typesv1.LabelPair{
    27  				{Name: "foo", Value: "bar"},
    28  				{Name: model.MetricNameLabel, Value: "qux"},
    29  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
    30  			},
    31  		},
    32  		{
    33  			name:           "empty labels",
    34  			lbs:            []*typesv1.LabelPair{},
    35  			expectedErr:    `error at least one label pair is required per profile`,
    36  			expectedReason: MissingLabels,
    37  		},
    38  		{
    39  			name: "missing service name",
    40  			lbs: []*typesv1.LabelPair{
    41  				{Name: model.MetricNameLabel, Value: "qux"},
    42  			},
    43  			expectedErr:    `invalid labels '{__name__="qux"}' with error: service name is not provided`,
    44  			expectedReason: MissingLabels,
    45  		},
    46  		{
    47  			name: "max labels",
    48  			lbs: []*typesv1.LabelPair{
    49  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
    50  				{Name: "foo1", Value: "bar"},
    51  				{Name: "foo2", Value: "bar"},
    52  				{Name: "foo3", Value: "bar"},
    53  				{Name: "foo4", Value: "bar"},
    54  			},
    55  			expectedErr:    `profile series '{foo1="bar", foo2="bar", foo3="bar", foo4="bar", service_name="svc"}' has 5 label names; limit 4`,
    56  			expectedReason: MaxLabelNamesPerSeries,
    57  		},
    58  		{
    59  			name: "invalid metric name",
    60  			lbs: []*typesv1.LabelPair{
    61  				{Name: model.MetricNameLabel, Value: "\x80"},
    62  			},
    63  			expectedErr:    `invalid labels '{__name__="\x80"}' with error: invalid metric name`,
    64  			expectedReason: InvalidLabels,
    65  		},
    66  		{
    67  			name: "invalid label value",
    68  			lbs: []*typesv1.LabelPair{
    69  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
    70  				{Name: model.MetricNameLabel, Value: "qux"},
    71  				{Name: "foo", Value: "\xc5"},
    72  			},
    73  			expectedErr:    "invalid labels '{__name__=\"qux\", foo=\"\\xc5\", service_name=\"svc\"}' with error: invalid label value '\xc5'",
    74  			expectedReason: InvalidLabels,
    75  		},
    76  		{
    77  			name: "invalid label name",
    78  			lbs: []*typesv1.LabelPair{
    79  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
    80  				{Name: model.MetricNameLabel, Value: "qux"},
    81  				{Name: "\xc5", Value: "foo"},
    82  			},
    83  			expectedErr:    "invalid labels '{__name__=\"qux\", service_name=\"svc\", \xc5=\"foo\"}' with error: invalid label name '\xc5'",
    84  			expectedReason: InvalidLabels,
    85  		},
    86  		{
    87  			name: "name too long",
    88  			lbs: []*typesv1.LabelPair{
    89  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
    90  				{Name: "foooooooooooooooo", Value: "bar"},
    91  				{Name: model.MetricNameLabel, Value: "qux"},
    92  			},
    93  			expectedReason: LabelNameTooLong,
    94  			expectedErr:    "profile with labels '{__name__=\"qux\", foooooooooooooooo=\"bar\", service_name=\"svc\"}' has label name too long: 'foooooooooooooooo'",
    95  		},
    96  		{
    97  			name: "value too long",
    98  			lbs: []*typesv1.LabelPair{
    99  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
   100  				{Name: "foo", Value: "barrrrrrrrrrrrrrr"},
   101  				{Name: model.MetricNameLabel, Value: "qux"},
   102  			},
   103  			expectedReason: LabelValueTooLong,
   104  			expectedErr:    `profile with labels '{__name__="qux", foo="barrrrrrrrrrrrrrr", service_name="svc"}' has label value too long: 'barrrrrrrrrrrrrrr'`,
   105  		},
   106  
   107  		{
   108  			name: "dupe",
   109  			lbs: []*typesv1.LabelPair{
   110  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
   111  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
   112  				{Name: model.MetricNameLabel, Value: "qux"},
   113  			},
   114  			expectedReason: DuplicateLabelNames,
   115  			expectedErr:    "profile with labels '{__name__=\"qux\", service_name=\"svc\", service_name=\"svc\"}' has duplicate label name: 'service_name'",
   116  		},
   117  
   118  		{
   119  			name: "dupe sanitized",
   120  			lbs: []*typesv1.LabelPair{
   121  				{Name: model.MetricNameLabel, Value: "qux"},
   122  				{Name: "label.name", Value: "foo"},
   123  				{Name: "label.name", Value: "bar"},
   124  				{Name: phlaremodel.LabelNameServiceName, Value: "svc"},
   125  			},
   126  			expectedReason: DuplicateLabelNames,
   127  			expectedErr:    "profile with labels '{__name__=\"qux\", label.name=\"bar\", label_name=\"foo\", service_name=\"svc\"}' has duplicate label name 'label_name' after label name sanitization from 'label.name'",
   128  		},
   129  		{
   130  			name: "duplicates once sanitized with matching values",
   131  			lbs: []*typesv1.LabelPair{
   132  				{Name: model.MetricNameLabel, Value: "qux"},
   133  				{Name: "service.name", Value: "svc0"},
   134  				{Name: "service_abc", Value: "def"},
   135  				{Name: "service_name", Value: "svc0"},
   136  			},
   137  		},
   138  	} {
   139  		t.Run(tt.name, func(t *testing.T) {
   140  			_, err := ValidateLabels(MockLimits{
   141  				MaxLabelNamesPerSeriesValue: 4,
   142  				MaxLabelNameLengthValue:     12,
   143  				MaxLabelValueLengthValue:    10,
   144  			}, "foo", tt.lbs, log.NewNopLogger())
   145  			if tt.expectedErr != "" {
   146  				require.Error(t, err)
   147  				require.Equal(t, tt.expectedErr, err.Error())
   148  				require.Equal(t, tt.expectedReason, ReasonOf(err))
   149  			} else {
   150  				require.NoError(t, err)
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestValidateLabels_SanitizedLabelsReturned(t *testing.T) {
   157  	for _, tt := range []struct {
   158  		name           string
   159  		inputLabels    []*typesv1.LabelPair
   160  		expectedLabels []*typesv1.LabelPair
   161  	}{
   162  		{
   163  			name: "single dotted label is sanitized",
   164  			inputLabels: []*typesv1.LabelPair{
   165  				{Name: model.MetricNameLabel, Value: "cpu"},
   166  				{Name: "service_name", Value: "my-svc"},
   167  				{Name: "label.dot", Value: "val"},
   168  			},
   169  			expectedLabels: []*typesv1.LabelPair{
   170  				{Name: model.MetricNameLabel, Value: "cpu"},
   171  				{Name: "label_dot", Value: "val"},
   172  				{Name: "service_name", Value: "my-svc"},
   173  			},
   174  		},
   175  		{
   176  			name: "dotted label merged with existing underscore label",
   177  			inputLabels: []*typesv1.LabelPair{
   178  				{Name: model.MetricNameLabel, Value: "cpu"},
   179  				{Name: "service.name", Value: "my-svc"},
   180  				{Name: "service_name", Value: "my-svc"},
   181  			},
   182  			expectedLabels: []*typesv1.LabelPair{
   183  				{Name: model.MetricNameLabel, Value: "cpu"},
   184  				{Name: "service_name", Value: "my-svc"},
   185  			},
   186  		},
   187  		{
   188  			name: "multiple dotted labels sanitized",
   189  			inputLabels: []*typesv1.LabelPair{
   190  				{Name: model.MetricNameLabel, Value: "cpu"},
   191  				{Name: "foo.bar", Value: "val1"},
   192  				{Name: "label.dot", Value: "val2"},
   193  				{Name: "service_name", Value: "my-svc"},
   194  			},
   195  			expectedLabels: []*typesv1.LabelPair{
   196  				{Name: model.MetricNameLabel, Value: "cpu"},
   197  				{Name: "foo_bar", Value: "val1"},
   198  				{Name: "label_dot", Value: "val2"},
   199  				{Name: "service_name", Value: "my-svc"},
   200  			},
   201  		},
   202  		{
   203  			name: "labels without dots unchanged",
   204  			inputLabels: []*typesv1.LabelPair{
   205  				{Name: model.MetricNameLabel, Value: "cpu"},
   206  				{Name: "service_name", Value: "my-svc"},
   207  			},
   208  			expectedLabels: []*typesv1.LabelPair{
   209  				{Name: model.MetricNameLabel, Value: "cpu"},
   210  				{Name: "service_name", Value: "my-svc"},
   211  			},
   212  		},
   213  	} {
   214  		t.Run(tt.name, func(t *testing.T) {
   215  			result, err := ValidateLabels(MockLimits{
   216  				MaxLabelNamesPerSeriesValue: 10,
   217  				MaxLabelNameLengthValue:     50,
   218  				MaxLabelValueLengthValue:    50,
   219  			}, "test-tenant", tt.inputLabels, log.NewNopLogger())
   220  
   221  			require.NoError(t, err)
   222  			require.Equal(t, len(tt.expectedLabels), len(result), "unexpected number of labels")
   223  
   224  			for i, expected := range tt.expectedLabels {
   225  				require.Equal(t, expected.Name, result[i].Name, "label name mismatch at index %d", i)
   226  				require.Equal(t, expected.Value, result[i].Value, "label value mismatch at index %d", i)
   227  			}
   228  		})
   229  	}
   230  }
   231  
   232  func Test_ValidateRangeRequest(t *testing.T) {
   233  	now := model.Now()
   234  	for _, tt := range []struct {
   235  		name        string
   236  		in          model.Interval
   237  		expectedErr error
   238  		expected    ValidatedRangeRequest
   239  	}{
   240  		{
   241  			name: "valid",
   242  			in: model.Interval{
   243  				Start: now.Add(-24 * time.Hour),
   244  				End:   now,
   245  			},
   246  			expected: ValidatedRangeRequest{
   247  				Interval: model.Interval{
   248  					Start: now.Add(-24 * time.Hour),
   249  					End:   now,
   250  				},
   251  			},
   252  		},
   253  		{
   254  			name: "empty outside of the lookback",
   255  			in: model.Interval{
   256  				Start: now.Add(-75 * time.Hour),
   257  				End:   now.Add(-73 * time.Hour),
   258  			},
   259  			expected: ValidatedRangeRequest{
   260  				IsEmpty: true,
   261  				Interval: model.Interval{
   262  					Start: now.Add(-75 * time.Hour),
   263  					End:   now.Add(-73 * time.Hour),
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name: "too large range",
   269  			in: model.Interval{
   270  				Start: now.Add(-150 * time.Hour),
   271  				End:   now.Add(time.Hour),
   272  			},
   273  			expected:    ValidatedRangeRequest{},
   274  			expectedErr: NewErrorf(QueryLimit, QueryTooLongErrorMsg, "73h0m0s", "2d"),
   275  		},
   276  		{
   277  			name: "reduced range to the lookback",
   278  			in: model.Interval{
   279  				Start: now.Add(-75 * time.Hour),
   280  				End:   now.Add(-68 * time.Hour),
   281  			},
   282  			expected: ValidatedRangeRequest{
   283  				Interval: model.Interval{
   284  					Start: now.Add(-72 * time.Hour),
   285  					End:   now.Add(-68 * time.Hour),
   286  				},
   287  			},
   288  		},
   289  		{
   290  			name: "empty start",
   291  			in: model.Interval{
   292  				Start: 0,
   293  				End:   now,
   294  			},
   295  			expectedErr: NewErrorf(QueryMissingTimeRange, QueryMissingTimeRangeErrorMsg),
   296  		},
   297  		{
   298  			name: "empty end",
   299  			in: model.Interval{
   300  				Start: now,
   301  				End:   0,
   302  			},
   303  			expectedErr: NewErrorf(QueryMissingTimeRange, QueryMissingTimeRangeErrorMsg),
   304  		},
   305  		{
   306  			name: "empty start and end",
   307  			in: model.Interval{
   308  				Start: 0,
   309  				End:   0,
   310  			},
   311  			expectedErr: NewErrorf(QueryMissingTimeRange, QueryMissingTimeRangeErrorMsg),
   312  		},
   313  		{
   314  			name: "start after end",
   315  			in: model.Interval{
   316  				Start: 1000,
   317  				End:   500,
   318  			},
   319  			expectedErr: NewErrorf(QueryInvalidTimeRange, QueryStartAfterEndErrorMsg),
   320  		},
   321  	} {
   322  		tt := tt
   323  		t.Run(tt.name, func(t *testing.T) {
   324  			actual, err := ValidateRangeRequest(MockLimits{
   325  				MaxQueryLengthValue:   48 * time.Hour,
   326  				MaxQueryLookbackValue: 72 * time.Hour,
   327  			}, []string{"foo"}, tt.in, now)
   328  			require.Equal(t, tt.expectedErr, err)
   329  			require.Equal(t, tt.expected, actual)
   330  		})
   331  	}
   332  }
   333  
   334  func TestValidateProfile(t *testing.T) {
   335  	now := model.TimeFromUnixNano(1_676_635_994_000_000_000)
   336  
   337  	for _, tc := range []struct {
   338  		name        string
   339  		profile     *googlev1.Profile
   340  		size        int
   341  		limits      ProfileValidationLimits
   342  		expectedErr error
   343  		assert      func(t *testing.T, profile *googlev1.Profile)
   344  	}{
   345  		{
   346  			"nil profile",
   347  			nil,
   348  			0,
   349  			MockLimits{},
   350  			NewErrorf(MalformedProfile, "nil profile"),
   351  			nil,
   352  		},
   353  		{
   354  			"empty profile",
   355  			&googlev1.Profile{},
   356  			0,
   357  			MockLimits{},
   358  			NewErrorf(MalformedProfile, "empty profile"),
   359  			nil,
   360  		},
   361  		{
   362  			"empty string table",
   363  			&googlev1.Profile{
   364  				SampleType: []*googlev1.ValueType{{}},
   365  			},
   366  			3,
   367  			MockLimits{
   368  				MaxProfileSizeBytesValue: 100,
   369  			},
   370  			NewErrorf(MalformedProfile, "string 0 should be empty string"),
   371  			nil,
   372  		},
   373  		{
   374  			"too big",
   375  			&googlev1.Profile{
   376  				SampleType: []*googlev1.ValueType{{}},
   377  			},
   378  			3,
   379  			MockLimits{
   380  				MaxProfileSizeBytesValue: 1,
   381  			},
   382  			NewErrorf(ProfileSizeLimit, ProfileTooBigErrorMsg, `{foo="bar"}`, 3, 1),
   383  			nil,
   384  		},
   385  		{
   386  			"too many samples",
   387  			&googlev1.Profile{
   388  				SampleType: []*googlev1.ValueType{{}},
   389  				Sample:     make([]*googlev1.Sample, 3),
   390  			},
   391  			0,
   392  			MockLimits{
   393  				MaxProfileStacktraceSamplesValue: 2,
   394  			},
   395  			NewErrorf(SamplesLimit, ProfileTooManySamplesErrorMsg, `{foo="bar"}`, 3, 2),
   396  			nil,
   397  		},
   398  		{
   399  			"nil sample",
   400  			&googlev1.Profile{
   401  				SampleType: []*googlev1.ValueType{{}},
   402  				Sample:     make([]*googlev1.Sample, 3),
   403  			},
   404  			0,
   405  			MockLimits{
   406  				MaxProfileStacktraceSamplesValue: 100,
   407  			},
   408  			NewErrorf(MalformedProfile, "nil sample"),
   409  			nil,
   410  		},
   411  		{
   412  			"sample value mismatch",
   413  			&googlev1.Profile{
   414  				SampleType: []*googlev1.ValueType{{}},
   415  				Sample:     []*googlev1.Sample{{Value: []int64{1, 2}}},
   416  			},
   417  			0,
   418  			MockLimits{
   419  				MaxProfileStacktraceSamplesValue: 100,
   420  			},
   421  			NewErrorf(MalformedProfile, "sample value length mismatch"),
   422  			nil,
   423  		},
   424  		{
   425  			"too many labels",
   426  			&googlev1.Profile{
   427  				SampleType: []*googlev1.ValueType{{}},
   428  				Sample: []*googlev1.Sample{
   429  					{
   430  						Label: make([]*googlev1.Label, 3),
   431  						Value: []int64{239},
   432  					},
   433  				},
   434  			},
   435  			0,
   436  			MockLimits{
   437  				MaxProfileStacktraceSampleLabelsValue: 2,
   438  			},
   439  			NewErrorf(SampleLabelsLimit, ProfileTooManySampleLabelsErrorMsg, `{foo="bar"}`, 3, 2),
   440  			nil,
   441  		},
   442  		{
   443  			"truncate labels and stacktrace",
   444  			&googlev1.Profile{
   445  				SampleType:  []*googlev1.ValueType{{}},
   446  				StringTable: []string{"", "foo", "/foo/bar"},
   447  				Sample: []*googlev1.Sample{
   448  					{
   449  						LocationId: []uint64{0, 1, 2, 3, 4, 5},
   450  						Value:      []int64{239},
   451  					},
   452  				},
   453  			},
   454  			0,
   455  			MockLimits{
   456  				MaxProfileStacktraceDepthValue:   2,
   457  				MaxProfileSymbolValueLengthValue: 3,
   458  			},
   459  			nil,
   460  			func(t *testing.T, profile *googlev1.Profile) {
   461  				t.Helper()
   462  				require.Equal(t, []string{"", "foo", "bar"}, profile.StringTable)
   463  				require.Equal(t, []uint64{4, 5}, profile.Sample[0].LocationId)
   464  			},
   465  		},
   466  		{
   467  			name: "newer than ingestion window",
   468  			profile: &googlev1.Profile{
   469  				SampleType: []*googlev1.ValueType{{}},
   470  				TimeNanos:  now.Add(1 * time.Hour).UnixNano(),
   471  			},
   472  			limits: MockLimits{
   473  				RejectNewerThanValue: 10 * time.Minute,
   474  			},
   475  			expectedErr: &Error{
   476  				Reason: NotInIngestionWindow,
   477  				msg:    "profile with labels '{foo=\"bar\"}' is outside of ingestion window (profile timestamp: 2023-02-17 13:13:14 +0000 UTC, the ingestion window ends at 2023-02-17 12:23:14 +0000 UTC)",
   478  			},
   479  		},
   480  		{
   481  			name: "older than ingestion window",
   482  			profile: &googlev1.Profile{
   483  				SampleType: []*googlev1.ValueType{{}},
   484  				TimeNanos:  now.Add(-61 * time.Minute).UnixNano(),
   485  			},
   486  			limits: MockLimits{
   487  				RejectOlderThanValue: time.Hour,
   488  			},
   489  			expectedErr: &Error{
   490  				Reason: NotInIngestionWindow,
   491  				msg:    "profile with labels '{foo=\"bar\"}' is outside of ingestion window (profile timestamp: 2023-02-17 11:12:14 +0000 UTC, the ingestion window starts at 2023-02-17 11:13:14 +0000 UTC)",
   492  			},
   493  		},
   494  		{
   495  			name: "just in the ingestion window",
   496  			profile: &googlev1.Profile{
   497  				SampleType:  []*googlev1.ValueType{{}},
   498  				TimeNanos:   now.Add(-1 * time.Minute).UnixNano(),
   499  				StringTable: []string{""},
   500  			},
   501  			limits: MockLimits{
   502  				RejectOlderThanValue: time.Hour,
   503  				RejectNewerThanValue: 10 * time.Minute,
   504  			},
   505  		},
   506  		{
   507  			name: "without timestamp",
   508  			profile: &googlev1.Profile{
   509  				SampleType:  []*googlev1.ValueType{{}},
   510  				StringTable: []string{""},
   511  			},
   512  			limits: MockLimits{
   513  				RejectOlderThanValue: time.Hour,
   514  				RejectNewerThanValue: 10 * time.Minute,
   515  			},
   516  		},
   517  	} {
   518  		tc := tc
   519  		t.Run(tc.name, func(t *testing.T) {
   520  			_, err := ValidateProfile(tc.limits, "foo", pprof.RawFromProto(tc.profile), tc.size, phlaremodel.LabelsFromStrings("foo", "bar"), now)
   521  			if tc.expectedErr != nil {
   522  				require.Error(t, err)
   523  				require.Equal(t, tc.expectedErr, err)
   524  			} else {
   525  				require.NoError(t, err)
   526  			}
   527  
   528  			if tc.assert != nil {
   529  				tc.assert(t, tc.profile)
   530  			}
   531  		})
   532  	}
   533  }
   534  
   535  func TestValidateFlamegraphMaxNodes(t *testing.T) {
   536  	type testCase struct {
   537  		name      string
   538  		maxNodes  int64
   539  		validated int64
   540  		limits    FlameGraphLimits
   541  		err       error
   542  	}
   543  
   544  	testCases := []testCase{
   545  		{
   546  			name:      "default limit",
   547  			maxNodes:  0,
   548  			validated: 10,
   549  			limits: MockLimits{
   550  				MaxFlameGraphNodesDefaultValue: 10,
   551  			},
   552  		},
   553  		{
   554  			name:      "within limit",
   555  			maxNodes:  10,
   556  			validated: 10,
   557  			limits: MockLimits{
   558  				MaxFlameGraphNodesMaxValue: 10,
   559  			},
   560  		},
   561  		{
   562  			name:     "limit exceeded",
   563  			maxNodes: 10,
   564  			limits: MockLimits{
   565  				MaxFlameGraphNodesMaxValue: 5,
   566  			},
   567  			err: &Error{Reason: "flamegraph_limit", msg: "max flamegraph nodes limit 10 is greater than allowed 5"},
   568  		},
   569  		{
   570  			name:      "limit disabled",
   571  			maxNodes:  -1,
   572  			validated: -1,
   573  			limits:    MockLimits{},
   574  		},
   575  		{
   576  			name:     "limit disabled with max set",
   577  			maxNodes: -1,
   578  			limits: MockLimits{
   579  				MaxFlameGraphNodesMaxValue: 5,
   580  			},
   581  			err: &Error{Reason: "flamegraph_limit", msg: "max flamegraph nodes limit must be set (max allowed 5)"},
   582  		},
   583  	}
   584  
   585  	for _, tc := range testCases {
   586  		tc := tc
   587  		t.Run(tc.name, func(t *testing.T) {
   588  			v, err := ValidateMaxNodes(tc.limits, []string{"tenant"}, tc.maxNodes)
   589  			require.Equal(t, tc.err, err)
   590  			require.Equal(t, tc.validated, v)
   591  		})
   592  	}
   593  }
   594  
   595  func Test_SanitizeLegacyLabelName(t *testing.T) {
   596  	tests := []struct {
   597  		Name          string
   598  		LabelName     string
   599  		WantOld       string
   600  		WantSanitized string
   601  		WantOk        bool
   602  	}{
   603  		{
   604  			Name:          "empty string is invalid",
   605  			LabelName:     "",
   606  			WantOld:       "",
   607  			WantSanitized: "",
   608  			WantOk:        false,
   609  		},
   610  		{
   611  			Name:          "valid simple label name",
   612  			LabelName:     "service",
   613  			WantOld:       "service",
   614  			WantSanitized: "service",
   615  			WantOk:        true,
   616  		},
   617  		{
   618  			Name:          "valid label with underscores",
   619  			LabelName:     "service_name",
   620  			WantOld:       "service_name",
   621  			WantSanitized: "service_name",
   622  			WantOk:        true,
   623  		},
   624  		{
   625  			Name:          "valid label with numbers",
   626  			LabelName:     "service123",
   627  			WantOld:       "service123",
   628  			WantSanitized: "service123",
   629  			WantOk:        true,
   630  		},
   631  		{
   632  			Name:          "valid mixed case label",
   633  			LabelName:     "ServiceName",
   634  			WantOld:       "ServiceName",
   635  			WantSanitized: "ServiceName",
   636  			WantOk:        true,
   637  		},
   638  		{
   639  			Name:          "label with dots gets sanitized",
   640  			LabelName:     "service.name",
   641  			WantOld:       "service.name",
   642  			WantSanitized: "service_name",
   643  			WantOk:        true,
   644  		},
   645  		{
   646  			Name:          "label with multiple dots gets sanitized",
   647  			LabelName:     "service.name.type",
   648  			WantOld:       "service.name.type",
   649  			WantSanitized: "service_name_type",
   650  			WantOk:        true,
   651  		},
   652  		{
   653  			Name:          "label starting with number is invalid",
   654  			LabelName:     "123service",
   655  			WantOld:       "123service",
   656  			WantSanitized: "123service",
   657  			WantOk:        false,
   658  		},
   659  		{
   660  			Name:          "label with hyphen is invalid",
   661  			LabelName:     "service-name",
   662  			WantOld:       "service-name",
   663  			WantSanitized: "service-name",
   664  			WantOk:        false,
   665  		},
   666  		{
   667  			Name:          "label with space is invalid",
   668  			LabelName:     "service name",
   669  			WantOld:       "service name",
   670  			WantSanitized: "service name",
   671  			WantOk:        false,
   672  		},
   673  		{
   674  			Name:          "label with special characters is invalid",
   675  			LabelName:     "service@name",
   676  			WantOld:       "service@name",
   677  			WantSanitized: "service@name",
   678  			WantOk:        false,
   679  		},
   680  		{
   681  			Name:          "label with dots and invalid characters is invalid",
   682  			LabelName:     "service.name@host",
   683  			WantOld:       "service.name@host",
   684  			WantSanitized: "service.name@host",
   685  			WantOk:        false,
   686  		},
   687  		{
   688  			Name:          "label starting with underscore",
   689  			LabelName:     "_service",
   690  			WantOld:       "_service",
   691  			WantSanitized: "_service",
   692  			WantOk:        true,
   693  		},
   694  		{
   695  			Name:          "label with only underscores",
   696  			LabelName:     "___",
   697  			WantOld:       "___",
   698  			WantSanitized: "___",
   699  			WantOk:        true,
   700  		},
   701  		{
   702  			Name:          "label ending with dot",
   703  			LabelName:     "service.",
   704  			WantOld:       "service.",
   705  			WantSanitized: "service_",
   706  			WantOk:        true,
   707  		},
   708  		{
   709  			Name:          "label starting with dot gets sanitized",
   710  			LabelName:     ".service",
   711  			WantOld:       ".service",
   712  			WantSanitized: "_service",
   713  			WantOk:        true,
   714  		},
   715  		{
   716  			Name:          "single dot",
   717  			LabelName:     ".",
   718  			WantOld:       ".",
   719  			WantSanitized: "_",
   720  			WantOk:        true,
   721  		},
   722  		{
   723  			Name:          "double dots",
   724  			LabelName:     "..",
   725  			WantOld:       "..",
   726  			WantSanitized: "__",
   727  			WantOk:        true,
   728  		},
   729  		{
   730  			Name:          "double dots with letter at end",
   731  			LabelName:     "..a",
   732  			WantOld:       "..a",
   733  			WantSanitized: "__a",
   734  			WantOk:        true,
   735  		},
   736  		{
   737  			Name:          "letter with double dots at end",
   738  			LabelName:     "a..",
   739  			WantOld:       "a..",
   740  			WantSanitized: "a__",
   741  			WantOk:        true,
   742  		},
   743  		{
   744  			Name:          "letter surrounded by dots",
   745  			LabelName:     ".a.",
   746  			WantOld:       ".a.",
   747  			WantSanitized: "_a_",
   748  			WantOk:        true,
   749  		},
   750  		{
   751  			Name:          "letter surrounded by double dots",
   752  			LabelName:     "..a..",
   753  			WantOld:       "..a..",
   754  			WantSanitized: "__a__",
   755  			WantOk:        true,
   756  		},
   757  		{
   758  			Name:          "letter with dot and number",
   759  			LabelName:     "a.0",
   760  			WantOld:       "a.0",
   761  			WantSanitized: "a_0",
   762  			WantOk:        true,
   763  		},
   764  		{
   765  			Name:          "number with dot is invalid",
   766  			LabelName:     "0.a",
   767  			WantOld:       "0.a",
   768  			WantSanitized: "0.a",
   769  			WantOk:        false,
   770  		},
   771  		{
   772  			Name:          "single underscore",
   773  			LabelName:     "_",
   774  			WantOld:       "_",
   775  			WantSanitized: "_",
   776  			WantOk:        true,
   777  		},
   778  		{
   779  			Name:          "double underscore with letter",
   780  			LabelName:     "__a",
   781  			WantOld:       "__a",
   782  			WantSanitized: "__a",
   783  			WantOk:        true,
   784  		},
   785  		{
   786  			Name:          "letter surrounded by double underscores",
   787  			LabelName:     "__a__",
   788  			WantOld:       "__a__",
   789  			WantSanitized: "__a__",
   790  			WantOk:        true,
   791  		},
   792  		{
   793  			Name:          "unicode characters are invalid",
   794  			LabelName:     "世界",
   795  			WantOld:       "世界",
   796  			WantSanitized: "世界",
   797  			WantOk:        false,
   798  		},
   799  		{
   800  			Name:          "mixed unicode with valid characters is invalid",
   801  			LabelName:     "界世_a",
   802  			WantOld:       "界世_a",
   803  			WantSanitized: "界世_a",
   804  			WantOk:        false,
   805  		},
   806  		{
   807  			Name:          "mixed unicode with underscores is invalid",
   808  			LabelName:     "界世__a",
   809  			WantOld:       "界世__a",
   810  			WantSanitized: "界世__a",
   811  			WantOk:        false,
   812  		},
   813  		{
   814  			Name:          "valid characters with unicode suffix is invalid",
   815  			LabelName:     "a_世界",
   816  			WantOld:       "a_世界",
   817  			WantSanitized: "a_世界",
   818  			WantOk:        false,
   819  		},
   820  	}
   821  
   822  	for _, tt := range tests {
   823  		t.Run(tt.Name, func(t *testing.T) {
   824  			t.Parallel()
   825  
   826  			gotOld, gotSanitized, gotOk := SanitizeLegacyLabelName(tt.LabelName)
   827  			require.Equal(t, tt.WantOld, gotOld)
   828  			require.Equal(t, tt.WantSanitized, gotSanitized)
   829  			require.Equal(t, tt.WantOk, gotOk)
   830  		})
   831  	}
   832  }