Skip to content

Commit 1607ebe

Browse files
fix: copy internal test helpers to OSS repo (#423)
Updates copybara config to sync `internal` packages I broke the test runs on the OSS version of the repo. First failed run [here](https://github.com/render-oss/cli/actions/runs/25500050945/job/74830134541). 1f1445c introduced an `internal` package for test helpers. Tests rely on this package, but it is not synced by copybara into the OSS repo, so `go test` fails In our case, `internal` is not for private packages that should not be OSS. It's just using go's functionality to let us prevent users from importing test helpers in production code. GitOrigin-RevId: a21168bcb5074e74ed5881e1a74d486246944d38
1 parent b3be02e commit 1607ebe

3 files changed

Lines changed: 379 additions & 0 deletions

File tree

copy.bara.sky

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ DESTINATION_FILES = [
88
"bin/**",
99
"cmd/**",
1010
"e2e/**",
11+
"internal/**",
1112
"pkg/**",
1213
".gitignore",
1314
".golangci.yml",

internal/fakes/renderapi/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package renderapi provides a fake Render API HTTP server for use in
2+
// command-level integration tests.
3+
package renderapi

internal/fakes/renderapi/server.go

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
package renderapi
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/render-oss/cli/pkg/client"
13+
"github.com/rs/xid"
14+
)
15+
16+
// writeJSON encodes v as JSON and writes it with the given status code.
17+
// Returns HTTP 500 if encoding fails (shouldn't happen with these types).
18+
func writeJSON(w http.ResponseWriter, status int, v any) {
19+
data, err := json.Marshal(v)
20+
if err != nil {
21+
http.Error(w, "fake server: encode error: "+err.Error(), http.StatusInternalServerError)
22+
return
23+
}
24+
w.Header().Set("Content-Type", "application/json")
25+
w.WriteHeader(status)
26+
_, _ = w.Write(data)
27+
}
28+
29+
// RecordedRequest captures a single HTTP request made to the fake server.
30+
type RecordedRequest struct {
31+
Method string
32+
URI string
33+
}
34+
35+
// Resource is a generic in-memory store for any fake API resource type.
36+
type Resource[T any] struct {
37+
Instances []T
38+
}
39+
40+
// Add appends item to the store and returns it.
41+
func (r *Resource[T]) Add(item T) T {
42+
r.Instances = append(r.Instances, item)
43+
return item
44+
}
45+
46+
// KVResource holds key-value store state and error injection for the fake server.
47+
// Tests can assert against Instances.
48+
type KVResource struct {
49+
Resource[*client.KeyValueDetail]
50+
// errorQueue holds HTTP status codes to return on successive create operations, drained in order.
51+
errorQueue []int
52+
}
53+
54+
// RespondWith queues an HTTP status code to return on the next create operation.
55+
// Use this to simulate API errors; the queue is drained in FIFO order.
56+
func (kv *KVResource) RespondWith(status int) {
57+
kv.errorQueue = append(kv.errorQueue, status)
58+
}
59+
60+
func (kv *KVResource) nextError() (int, bool) {
61+
if len(kv.errorQueue) == 0 {
62+
return 0, false
63+
}
64+
status := kv.errorQueue[0]
65+
kv.errorQueue = kv.errorQueue[1:]
66+
return status, true
67+
}
68+
69+
// NewOwner returns an Owner with sensible defaults for any zero-value fields.
70+
func NewOwner(o client.Owner) client.Owner {
71+
if o.Id == "" {
72+
o.Id = "tea-" + xid.New().String()
73+
}
74+
if o.Name == "" {
75+
o.Name = "My Team"
76+
}
77+
if o.Email == "" {
78+
o.Email = "team@example.com"
79+
}
80+
return o
81+
}
82+
83+
// NewProject returns a Project with sensible defaults for any zero-value fields.
84+
func NewProject(p client.Project) client.Project {
85+
if p.Id == "" {
86+
p.Id = "prj-" + xid.New().String()
87+
}
88+
if p.Name == "" {
89+
p.Name = "My Project"
90+
}
91+
return p
92+
}
93+
94+
// NewEnvironment returns an Environment with sensible defaults for any zero-value fields.
95+
func NewEnvironment(e client.Environment) client.Environment {
96+
if e.Id == "" {
97+
e.Id = "env-" + xid.New().String()
98+
}
99+
if e.Name == "" {
100+
e.Name = "My Environment"
101+
}
102+
return e
103+
}
104+
105+
// NewKV returns a KeyValueDetail with sensible defaults for any zero-value fields.
106+
func NewKV(kv *client.KeyValueDetail) *client.KeyValueDetail {
107+
if kv == nil {
108+
kv = &client.KeyValueDetail{}
109+
}
110+
if kv.Id == "" {
111+
kv.Id = fmt.Sprintf("kv-%s", xid.New().String())
112+
}
113+
if kv.Name == "" {
114+
kv.Name = "my-kv"
115+
}
116+
if kv.Region == "" {
117+
kv.Region = client.Oregon
118+
}
119+
if kv.Status == "" {
120+
kv.Status = client.DatabaseStatusAvailable
121+
}
122+
if kv.IpAllowList == nil {
123+
kv.IpAllowList = []client.CidrBlockAndDescription{}
124+
}
125+
if kv.CreatedAt.IsZero() || kv.UpdatedAt.IsZero() {
126+
now := time.Now()
127+
if kv.CreatedAt.IsZero() {
128+
kv.CreatedAt = now
129+
}
130+
if kv.UpdatedAt.IsZero() {
131+
kv.UpdatedAt = now
132+
}
133+
}
134+
return kv
135+
}
136+
137+
// Server is a fake Render API HTTP server for command-level tests.
138+
// All HTTP plumbing is internal — tests seed state via Add() methods and assert against resource Instances.
139+
type Server struct {
140+
server *httptest.Server
141+
Requests []RecordedRequest
142+
Owners *Resource[client.Owner]
143+
Projects *Resource[client.Project]
144+
Environments *Resource[client.Environment]
145+
KV *KVResource
146+
}
147+
148+
// URL returns the base URL of the fake server.
149+
func (s *Server) URL() string {
150+
return s.server.URL
151+
}
152+
153+
// HasRequest returns true if any recorded request matches the given method and URI substring.
154+
func (s *Server) HasRequest(method, uriSubstring string) bool {
155+
for _, r := range s.Requests {
156+
if r.Method == method && strings.Contains(r.URI, uriSubstring) {
157+
return true
158+
}
159+
}
160+
return false
161+
}
162+
163+
// NewServer starts a fake Render API server covering all routes used by cmd-level tests.
164+
// The server is closed automatically when t completes. Seed state via server.Owners.Add(), etc.
165+
func NewServer(t *testing.T) *Server {
166+
t.Helper()
167+
168+
s := &Server{
169+
Owners: &Resource[client.Owner]{},
170+
Projects: &Resource[client.Project]{},
171+
Environments: &Resource[client.Environment]{},
172+
KV: &KVResource{},
173+
}
174+
175+
mux := http.NewServeMux()
176+
177+
record := func(r *http.Request) {
178+
s.Requests = append(s.Requests, RecordedRequest{Method: r.Method, URI: r.URL.RequestURI()})
179+
}
180+
181+
// GET /owners — list workspaces (supports ?name= filter)
182+
mux.HandleFunc("/owners", func(w http.ResponseWriter, r *http.Request) {
183+
record(r)
184+
if r.Method != http.MethodGet {
185+
w.WriteHeader(http.StatusMethodNotAllowed)
186+
return
187+
}
188+
result := s.Owners.Instances
189+
if name := r.URL.Query().Get("name"); name != "" {
190+
var filtered []client.Owner
191+
for _, o := range s.Owners.Instances {
192+
if o.Name == name {
193+
filtered = append(filtered, o)
194+
}
195+
}
196+
result = filtered
197+
}
198+
wrapped := make([]client.OwnerWithCursor, len(result))
199+
for i := range result {
200+
o := result[i]
201+
wrapped[i] = client.OwnerWithCursor{Owner: &o}
202+
}
203+
writeJSON(w, http.StatusOK, wrapped)
204+
})
205+
206+
// GET /owners/{id} — retrieve workspace by ID
207+
mux.HandleFunc("/owners/", func(w http.ResponseWriter, r *http.Request) {
208+
record(r)
209+
if r.Method != http.MethodGet {
210+
w.WriteHeader(http.StatusMethodNotAllowed)
211+
return
212+
}
213+
id := strings.TrimPrefix(r.URL.Path, "/owners/")
214+
for _, o := range s.Owners.Instances {
215+
if o.Id == id {
216+
writeJSON(w, http.StatusOK, o)
217+
return
218+
}
219+
}
220+
w.WriteHeader(http.StatusNotFound)
221+
})
222+
223+
// GET /projects — list projects (supports ?name= filter)
224+
mux.HandleFunc("/projects", func(w http.ResponseWriter, r *http.Request) {
225+
record(r)
226+
if r.Method != http.MethodGet {
227+
w.WriteHeader(http.StatusMethodNotAllowed)
228+
return
229+
}
230+
result := s.Projects.Instances
231+
if name := r.URL.Query().Get("name"); name != "" {
232+
var filtered []client.Project
233+
for _, p := range s.Projects.Instances {
234+
if p.Name == name {
235+
filtered = append(filtered, p)
236+
}
237+
}
238+
result = filtered
239+
}
240+
wrapped := make([]client.ProjectWithCursor, len(result))
241+
for i := range result {
242+
wrapped[i] = client.ProjectWithCursor{Project: result[i]}
243+
}
244+
writeJSON(w, http.StatusOK, wrapped)
245+
})
246+
247+
// GET /projects/{id} — retrieve project by ID
248+
mux.HandleFunc("/projects/", func(w http.ResponseWriter, r *http.Request) {
249+
record(r)
250+
if r.Method != http.MethodGet {
251+
w.WriteHeader(http.StatusMethodNotAllowed)
252+
return
253+
}
254+
id := strings.TrimPrefix(r.URL.Path, "/projects/")
255+
for _, p := range s.Projects.Instances {
256+
if p.Id == id {
257+
writeJSON(w, http.StatusOK, p)
258+
return
259+
}
260+
}
261+
w.WriteHeader(http.StatusNotFound)
262+
})
263+
264+
// GET /environments — list environments (supports ?projectId= and ?name= filters)
265+
mux.HandleFunc("/environments", func(w http.ResponseWriter, r *http.Request) {
266+
record(r)
267+
if r.Method != http.MethodGet {
268+
w.WriteHeader(http.StatusMethodNotAllowed)
269+
return
270+
}
271+
result := s.Environments.Instances
272+
if projectIDs := r.URL.Query()["projectId"]; len(projectIDs) > 0 {
273+
var filtered []client.Environment
274+
for _, e := range result {
275+
for _, pid := range projectIDs {
276+
if e.ProjectId == pid {
277+
filtered = append(filtered, e)
278+
}
279+
}
280+
}
281+
result = filtered
282+
}
283+
if name := r.URL.Query().Get("name"); name != "" {
284+
var filtered []client.Environment
285+
for _, e := range result {
286+
if e.Name == name {
287+
filtered = append(filtered, e)
288+
}
289+
}
290+
result = filtered
291+
}
292+
wrapped := make([]client.EnvironmentWithCursor, len(result))
293+
for i := range result {
294+
wrapped[i] = client.EnvironmentWithCursor{Environment: result[i]}
295+
}
296+
writeJSON(w, http.StatusOK, wrapped)
297+
})
298+
299+
// GET /environments/{id} — retrieve environment by ID
300+
mux.HandleFunc("/environments/", func(w http.ResponseWriter, r *http.Request) {
301+
record(r)
302+
if r.Method != http.MethodGet {
303+
w.WriteHeader(http.StatusMethodNotAllowed)
304+
return
305+
}
306+
id := strings.TrimPrefix(r.URL.Path, "/environments/")
307+
for _, e := range s.Environments.Instances {
308+
if e.Id == id {
309+
writeJSON(w, http.StatusOK, e)
310+
return
311+
}
312+
}
313+
w.WriteHeader(http.StatusNotFound)
314+
})
315+
316+
// POST /key-value — create KV store
317+
mux.HandleFunc("/key-value", func(w http.ResponseWriter, r *http.Request) {
318+
record(r)
319+
if r.Method != http.MethodPost {
320+
w.WriteHeader(http.StatusMethodNotAllowed)
321+
return
322+
}
323+
var body client.CreateKeyValueJSONRequestBody
324+
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
325+
w.WriteHeader(http.StatusBadRequest)
326+
return
327+
}
328+
if status, hasError := s.KV.nextError(); hasError {
329+
w.WriteHeader(status)
330+
return
331+
}
332+
333+
var owner client.Owner
334+
for _, o := range s.Owners.Instances {
335+
if o.Id == body.OwnerId {
336+
owner = o
337+
break
338+
}
339+
}
340+
341+
region := client.Oregon
342+
if body.Region != nil {
343+
region = client.Region(*body.Region)
344+
}
345+
346+
var maxmemoryPolicy *string
347+
if body.MaxmemoryPolicy != nil {
348+
mp := string(*body.MaxmemoryPolicy)
349+
maxmemoryPolicy = &mp
350+
}
351+
352+
kv := &client.KeyValueDetail{
353+
Id: fmt.Sprintf("kv-%s", xid.New().String()),
354+
Name: body.Name,
355+
Plan: body.Plan,
356+
Region: region,
357+
Owner: owner,
358+
Status: client.DatabaseStatusAvailable,
359+
EnvironmentId: body.EnvironmentId,
360+
IpAllowList: []client.CidrBlockAndDescription{},
361+
CreatedAt: time.Now(),
362+
UpdatedAt: time.Now(),
363+
}
364+
if maxmemoryPolicy != nil {
365+
kv.Options = client.KeyValueOptions{MaxmemoryPolicy: maxmemoryPolicy}
366+
}
367+
s.KV.Instances = append(s.KV.Instances, kv)
368+
369+
writeJSON(w, http.StatusCreated, kv)
370+
})
371+
372+
s.server = httptest.NewServer(mux)
373+
t.Cleanup(s.server.Close)
374+
return s
375+
}

0 commit comments

Comments
 (0)