(ctx context.Context, tc *models.TestCase, testSetID string, logger *zap.Logger, cfg SimulationConfig)
| 559 | } |
| 560 | |
| 561 | func SimulateGRPC(ctx context.Context, tc *models.TestCase, testSetID string, logger *zap.Logger, cfg SimulationConfig) (*models.GrpcResp, error) { |
| 562 | if strings.Contains(tc.HTTPReq.URL, "%7B") { // case in which URL string has encoded template placeholders |
| 563 | decoded, err := url.QueryUnescape(tc.HTTPReq.URL) |
| 564 | if err == nil { |
| 565 | tc.HTTPReq.URL = decoded |
| 566 | } |
| 567 | } |
| 568 | // Render any template values in the test case before simulation |
| 569 | templateData := buildTemplateDataSnapshot() |
| 570 | if len(templateData) > 0 { |
| 571 | testCaseBytes, err := json.Marshal(tc) |
| 572 | if err != nil { |
| 573 | utils.LogError(logger, err, "failed to marshal the testcase for templating") |
| 574 | return nil, err |
| 575 | } |
| 576 | |
| 577 | // Render only real Keploy placeholders ({{ .x }}, {{ string .y }}, etc.), |
| 578 | // ignoring LaTeX/HTML like {{\pi}}. |
| 579 | renderedStr, rerr := utils.RenderTemplatesInString(logger, string(testCaseBytes), templateData) |
| 580 | if rerr != nil { |
| 581 | logger.Debug("template rendering had recoverable errors", zap.Error(rerr)) |
| 582 | } |
| 583 | |
| 584 | err = json.Unmarshal([]byte(renderedStr), &tc) |
| 585 | if err != nil { |
| 586 | utils.LogError(logger, err, "failed to unmarshal the rendered testcase") |
| 587 | return nil, err |
| 588 | } |
| 589 | } |
| 590 | |
| 591 | grpcReq := tc.GrpcReq |
| 592 | |
| 593 | logger.Info("starting test for", zap.String("test case", models.HighlightString(tc.Name)), zap.String("test set", models.HighlightString(testSetID))) |
| 594 | |
| 595 | // Extract target address from headers |
| 596 | authority, ok := grpcReq.Headers.PseudoHeaders[":authority"] |
| 597 | if !ok { |
| 598 | return nil, fmt.Errorf("missing :authority header") |
| 599 | } |
| 600 | |
| 601 | // Determine which port to use for test execution. |
| 602 | var err error |
| 603 | authority, err = ResolveTestTarget(authority, cfg.URLReplacements, cfg.PortMappings, cfg.ConfigHost, tc.AppPort, cfg.ConfigPort, false, logger) |
| 604 | if err != nil { |
| 605 | return nil, err |
| 606 | } |
| 607 | |
| 608 | // Extract method path |
| 609 | path, ok := grpcReq.Headers.PseudoHeaders[":path"] |
| 610 | if !ok { |
| 611 | return nil, fmt.Errorf("missing :path header") |
| 612 | } |
| 613 | |
| 614 | // Create a TCP connection, retrying a pure connection-refused. A slow-starting |
| 615 | // gRPC app (especially a docker app under CI load) can briefly refuse |
| 616 | // connections while still coming up; a refused dial sent zero bytes and |
| 617 | // consumed zero mocks, so the re-dial is safe. Mirrors the HTTP replay path's |
| 618 | // doRequestWithConnRefusedRetry — a genuinely-down app still fails fast after |
no test coverage detected