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 }