|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "os" |
| 6 | + "os/exec" |
| 7 | + "path/filepath" |
| 8 | + "testing" |
| 9 | + |
| 10 | + agenttesting "github.com/codefly-dev/core/agents/testing" |
| 11 | + agentv0 "github.com/codefly-dev/core/generated/go/codefly/services/agent/v0" |
| 12 | + builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0" |
| 13 | + "github.com/stretchr/testify/require" |
| 14 | + "gopkg.in/yaml.v3" |
| 15 | +) |
| 16 | + |
| 17 | +// TestSettingsParseConnectEndpoint proves the setting reads at go-grpc's |
| 18 | +// spelling, defaults to off, and is refused without a gRPC server to transcode |
| 19 | +// onto. |
| 20 | +func TestSettingsParseConnectEndpoint(t *testing.T) { |
| 21 | + var on Settings |
| 22 | + require.NoError(t, yaml.Unmarshal([]byte(` |
| 23 | +grpc-server: |
| 24 | + enabled: true |
| 25 | +connect-endpoint: true |
| 26 | +`), &on)) |
| 27 | + require.True(t, on.ConnectEndpoint) |
| 28 | + require.NoError(t, on.connectSettingError()) |
| 29 | + |
| 30 | + var alone Settings |
| 31 | + require.NoError(t, yaml.Unmarshal([]byte(`connect-endpoint: true`), &alone)) |
| 32 | + require.ErrorContains(t, alone.connectSettingError(), "grpc-server.enabled") |
| 33 | + |
| 34 | + var absent Settings |
| 35 | + require.NoError(t, yaml.Unmarshal([]byte(`python-version: "3.12"`), &absent)) |
| 36 | + require.False(t, absent.ConnectEndpoint) |
| 37 | +} |
| 38 | + |
| 39 | +// TestSyncWritesTheConnectFace proves the face lands where the app imports it, |
| 40 | +// byte for byte, replacing a hand edit. |
| 41 | +func TestSyncWritesTheConnectFace(t *testing.T) { |
| 42 | + builder := NewBuilder(NewService()) |
| 43 | + builder.Location = t.TempDir() |
| 44 | + builder.FastAPI.Settings.GRPCServer.Enabled = true |
| 45 | + builder.FastAPI.Settings.ConnectEndpoint = true |
| 46 | + |
| 47 | + target := filepath.Join(builder.Location, connectFacePath) |
| 48 | + require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755)) |
| 49 | + require.NoError(t, os.WriteFile(target, []byte("# edited by hand\n"), 0o644)) |
| 50 | + |
| 51 | + require.NoError(t, builder.writeConnectFace()) |
| 52 | + written, err := os.ReadFile(target) |
| 53 | + require.NoError(t, err) |
| 54 | + require.Equal(t, connectFace, written) |
| 55 | + require.Contains(t, string(written), "Do not edit this file!") |
| 56 | + |
| 57 | + builder.FastAPI.Settings.GRPCServer.Enabled = false |
| 58 | + require.Error(t, builder.writeConnectFace()) |
| 59 | +} |
| 60 | + |
| 61 | +// TestDeploymentRendersConnectPort proves the Connect container and Service |
| 62 | +// ports appear only when the service opts in, declared as HTTP/1.1. |
| 63 | +func TestDeploymentRendersConnectPort(t *testing.T) { |
| 64 | + enabled := agenttesting.AssertKustomizeTemplates(t, deploymentFS, deploymentTestParameters(t, Parameters{GRPCEnabled: true, GRPCPort: 9090, ConnectEnabled: true, ConnectPort: 8081})) |
| 65 | + svc, err := os.ReadFile(filepath.Join(enabled, "base", "service.yaml")) |
| 66 | + require.NoError(t, err) |
| 67 | + require.Contains(t, string(svc), "name: connect-port") |
| 68 | + require.Contains(t, string(svc), "appProtocol: http\n") |
| 69 | + require.Contains(t, string(svc), "port: 8081") |
| 70 | + deployment, err := os.ReadFile(filepath.Join(enabled, "base", "deployment.yaml")) |
| 71 | + require.NoError(t, err) |
| 72 | + require.Contains(t, string(deployment), "containerPort: 8081") |
| 73 | + |
| 74 | + disabled := agenttesting.AssertKustomizeTemplates(t, deploymentFS, deploymentTestParameters(t, Parameters{GRPCEnabled: true, GRPCPort: 9090})) |
| 75 | + svc, err = os.ReadFile(filepath.Join(disabled, "base", "service.yaml")) |
| 76 | + require.NoError(t, err) |
| 77 | + require.NotContains(t, string(svc), "connect-port") |
| 78 | +} |
| 79 | + |
| 80 | +// TestConnectFaceServesEveryProtocol runs the face's own suite |
| 81 | +// (testdata/connect) against a real grpc.aio server: Connect unary and server |
| 82 | +// streams in both codecs, gRPC-Web, gRPC relayed on the same port, metadata |
| 83 | +// both ways, the service's interceptors on every protocol, deadlines and the |
| 84 | +// HTTP refusals. The Python pins are the ones a service's lock resolves today. |
| 85 | +func TestConnectFaceServesEveryProtocol(t *testing.T) { |
| 86 | + if _, err := exec.LookPath("uv"); err != nil { |
| 87 | + t.Skip("uv is not installed: the Connect face's Python suite cannot run") |
| 88 | + } |
| 89 | + cmd := exec.Command("uv", "run", "--no-project", "--python", "3.12", |
| 90 | + "--with", "grpcio==1.84.0", "--with", "protobuf==6.33.6", "--with", "h11==0.16.0", |
| 91 | + "--with", "httpx==0.28.1", "--with", "pytest==8.4.2", "--with", "pytest-asyncio==1.2.0", |
| 92 | + "pytest", "-q", "-p", "no:cacheprovider", |
| 93 | + "-o", "asyncio_mode=auto", "-o", "asyncio_default_fixture_loop_scope=function", |
| 94 | + filepath.Join("testdata", "connect")) |
| 95 | + cmd.Env = append(os.Environ(), "PYTHONPATH="+filepath.Join("templates", "connect"), "PYTHONDONTWRITEBYTECODE=1") |
| 96 | + out, err := cmd.CombinedOutput() |
| 97 | + require.NoError(t, err, "%s", out) |
| 98 | +} |
| 99 | + |
| 100 | +// TestBuilderLoadReadsTheAgentsSettings is the regression guard for a loaded |
| 101 | +// (not created) service: the builder must see grpc-server and |
| 102 | +// connect-endpoint, or Sync writes neither the gRPC stubs nor the Connect face |
| 103 | +// and Deploy renders neither port. |
| 104 | +func TestBuilderLoadReadsTheAgentsSettings(t *testing.T) { |
| 105 | + created, _ := createServiceForTest(t, map[string]*agentv0.Answer{ |
| 106 | + HotReload: confirmAnswer(false), |
| 107 | + PublicEndpoint: confirmAnswer(false), |
| 108 | + GRPCServer: confirmAnswer(true), |
| 109 | + }) |
| 110 | + ctx := context.Background() |
| 111 | + manifest := filepath.Join(created.Location, "service.codefly.yaml") |
| 112 | + content, err := os.ReadFile(manifest) |
| 113 | + require.NoError(t, err) |
| 114 | + var document map[string]any |
| 115 | + require.NoError(t, yaml.Unmarshal(content, &document)) |
| 116 | + spec, _ := document["spec"].(map[string]any) |
| 117 | + require.Equal(t, true, spec["grpc-server"].(map[string]any)["enabled"]) |
| 118 | + spec["connect-endpoint"] = true |
| 119 | + content, err = yaml.Marshal(document) |
| 120 | + require.NoError(t, err) |
| 121 | + require.NoError(t, os.WriteFile(manifest, content, 0o644)) |
| 122 | + |
| 123 | + identity, err := created.Identity.Proto() |
| 124 | + require.NoError(t, err) |
| 125 | + loaded := NewBuilder(NewService()) |
| 126 | + resp, err := loaded.Load(ctx, &builderv0.LoadRequest{Identity: identity}) |
| 127 | + require.NoError(t, err) |
| 128 | + require.NotEqual(t, builderv0.LoadStatus_ERROR, resp.GetState().GetState(), resp.GetState().GetMessage()) |
| 129 | + require.True(t, loaded.FastAPI.Settings.GRPCServer.Enabled) |
| 130 | + require.True(t, loaded.FastAPI.Settings.ConnectEndpoint) |
| 131 | + require.Equal(t, filepath.Join(created.Location, "code"), loaded.Service.SourceLocation) |
| 132 | +} |
0 commit comments