Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature to mock server APIs in acceptance tests #2226

Merged
merged 35 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
07c2558
Add support for mocking servers in acceptance tests
shreyas-goenka Jan 23, 2025
2bcf43b
-
shreyas-goenka Jan 23, 2025
07e1ba2
-
shreyas-goenka Jan 23, 2025
6b8e1df
-
shreyas-goenka Jan 23, 2025
d9685ab
add unit tests
shreyas-goenka Jan 23, 2025
de326bf
-
shreyas-goenka Jan 23, 2025
09791fc
decouple changes
shreyas-goenka Jan 24, 2025
e22b015
wip finer grained server regex
shreyas-goenka Jan 24, 2025
0d14526
move server to libs
shreyas-goenka Jan 24, 2025
accec71
remove request bits
shreyas-goenka Jan 24, 2025
fffc89c
-
shreyas-goenka Jan 24, 2025
2952a73
rm fn
shreyas-goenka Jan 24, 2025
24be18a
merge
shreyas-goenka Jan 24, 2025
af6ae9c
Merge remote-tracking branch 'origin' into validate-response-body
shreyas-goenka Jan 28, 2025
7c5d65f
allow override test to run in parallel
shreyas-goenka Jan 29, 2025
fadd6ed
-
shreyas-goenka Jan 29, 2025
ba55f5b
merge
shreyas-goenka Jan 29, 2025
39477d5
-
shreyas-goenka Jan 29, 2025
3549c22
-
shreyas-goenka Jan 29, 2025
1f1d705
move config to toml
shreyas-goenka Jan 29, 2025
badfb9c
-
shreyas-goenka Jan 29, 2025
005589a
-
shreyas-goenka Jan 29, 2025
3e7acdb
-
shreyas-goenka Jan 29, 2025
ca17487
-
shreyas-goenka Jan 29, 2025
8505abd
-
shreyas-goenka Jan 29, 2025
8da6c57
address comments
shreyas-goenka Jan 29, 2025
bb87563
inlint append
shreyas-goenka Jan 29, 2025
76ca212
-
shreyas-goenka Jan 29, 2025
b93fa07
Revert "-"
shreyas-goenka Jan 29, 2025
c2eaf1a
-
shreyas-goenka Jan 29, 2025
ef6a673
-
shreyas-goenka Jan 29, 2025
b0628d1
return string directly
shreyas-goenka Jan 29, 2025
15c09c6
Merge remote-tracking branch 'origin' into validate-response-body
shreyas-goenka Jan 29, 2025
6ed7f22
-
shreyas-goenka Jan 29, 2025
0f42155
Merge remote-tracking branch 'origin' into validate-response-body
shreyas-goenka Jan 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/env"
"github.com/databricks/cli/libs/testdiff"
"github.com/databricks/cli/libs/testserver"
"github.com/databricks/databricks-sdk-go"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -109,10 +110,10 @@ func testAccept(t *testing.T, InprocessMode bool, singleTest string) int {
cloudEnv := os.Getenv("CLOUD_ENV")

if cloudEnv == "" {
server := StartServer(t)
AddHandlers(server)
defaultServer := StartServer(t)
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
AddHandlers(defaultServer)
// Redirect API access to local server:
t.Setenv("DATABRICKS_HOST", server.URL)
t.Setenv("DATABRICKS_HOST", defaultServer.URL)
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
t.Setenv("DATABRICKS_TOKEN", "dapi1234")

homeDir := t.TempDir()
Expand Down Expand Up @@ -143,7 +144,7 @@ func testAccept(t *testing.T, InprocessMode bool, singleTest string) int {
for _, dir := range testDirs {
testName := strings.ReplaceAll(dir, "\\", "/")
t.Run(testName, func(t *testing.T) {
if !InprocessMode {
if !InprocessMode && !hasCustomServer(t, dir) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should having custom server disable parallel runs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Originally I did that since I used t.Setenv. I modified the approach to simply set override the command environment variable instead.

t.Parallel()
}

Expand All @@ -154,6 +155,10 @@ func testAccept(t *testing.T, InprocessMode bool, singleTest string) int {
return len(testDirs)
}

func hasCustomServer(t *testing.T, dir string) bool {
return testutil.DetectFile(t, filepath.Join(dir, "server.json"))
}

func getTests(t *testing.T) []string {
testDirs := make([]string, 0, 128)

Expand Down Expand Up @@ -187,6 +192,16 @@ func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsCont
tmpDir = t.TempDir()
}

// If there is a server.json file in the test directory, start a custom server.
// Redirect all API calls to this server.
if hasCustomServer(t, dir) {
server := testserver.NewFromConfig(t, filepath.Join(dir, "server.json"))
t.Setenv("DATABRICKS_HOST", server.URL)
t.Cleanup(func() {
server.Close()
})
}

repls.SetPathWithParents(tmpDir, "$TMPDIR")

scriptContents := readMergedScriptContents(t, dir)
Expand Down
3 changes: 2 additions & 1 deletion acceptance/cmd_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import (
"testing"

"github.com/databricks/cli/internal/testcli"
"github.com/databricks/cli/libs/testserver"
"github.com/stretchr/testify/require"
)

func StartCmdServer(t *testing.T) *TestServer {
func StartCmdServer(t *testing.T) *testserver.Server {
server := StartServer(t)
server.Handle("/", func(r *http.Request) (any, error) {
q := r.URL.Query()
Expand Down
56 changes: 4 additions & 52 deletions acceptance/server_test.go
Original file line number Diff line number Diff line change
@@ -1,73 +1,25 @@
package acceptance_test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/databricks/cli/libs/testserver"
"github.com/databricks/databricks-sdk-go/service/catalog"
"github.com/databricks/databricks-sdk-go/service/compute"
"github.com/databricks/databricks-sdk-go/service/iam"
"github.com/databricks/databricks-sdk-go/service/workspace"
)

type TestServer struct {
*httptest.Server
Mux *http.ServeMux
}

type HandlerFunc func(r *http.Request) (any, error)

func NewTestServer() *TestServer {
mux := http.NewServeMux()
server := httptest.NewServer(mux)

return &TestServer{
Server: server,
Mux: mux,
}
}

func (s *TestServer) Handle(pattern string, handler HandlerFunc) {
s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
resp, err := handler(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")

var respBytes []byte

respString, ok := resp.(string)
if ok {
respBytes = []byte(respString)
} else {
respBytes, err = json.MarshalIndent(resp, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

if _, err := w.Write(respBytes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}

func StartServer(t *testing.T) *TestServer {
server := NewTestServer()
func StartServer(t *testing.T) *testserver.Server {
server := testserver.New(t)
t.Cleanup(func() {
server.Close()
})
return server
}

func AddHandlers(server *TestServer) {
func AddHandlers(server *testserver.Server) {
server.Handle("GET /api/2.0/policies/clusters/list", func(r *http.Request) (any, error) {
return compute.ListPoliciesResponse{
Policies: []compute.Policy{
Expand Down
5 changes: 5 additions & 0 deletions acceptance/workspace/jobs/create/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

>>> $CLI jobs create --json {"name":"abc"}
{
"job_id":1111
}
1 change: 1 addition & 0 deletions acceptance/workspace/jobs/create/script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
trace $CLI jobs create --json '{"name":"abc"}'
11 changes: 11 additions & 0 deletions acceptance/workspace/jobs/create/server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
{
"method": "POST",
"path": "/api/2.1/jobs/create",
"response": {
"body": {
"job_id": 1111
}
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
}
}
]
12 changes: 12 additions & 0 deletions internal/testutil/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ func StatFile(t TestingT, path string) os.FileInfo {
return fi
}

func DetectFile(t TestingT, path string) bool {
_, err := os.Stat(path)
if err == nil {
return true
}
if os.IsNotExist(err) {
return false
}
t.Fatalf("unexpected error: %v", err)
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
return false
}

// AssertFileContents asserts that the file at path has the expected content.
func AssertFileContents(t TestingT, path, expected string) bool {
actual := ReadFile(t, path)
Expand Down
111 changes: 111 additions & 0 deletions libs/testserver/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package testserver

import (
"encoding/json"
"net/http"
"net/http/httptest"

"github.com/databricks/cli/internal/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type Server struct {
*httptest.Server
Mux *http.ServeMux

t testutil.TestingT

// API calls that we expect to be made.
calledPatterns map[string]bool
}

type ApiSpec struct {
Method string `json:"method"`
Path string `json:"path"`
Response struct {
Body json.RawMessage `json:"body"`
} `json:"response"`
}

func New(t testutil.TestingT) *Server {
mux := http.NewServeMux()
server := httptest.NewServer(mux)

return &Server{
Server: server,
Mux: mux,
t: t,
calledPatterns: make(map[string]bool),
}
}

func NewFromConfig(t testutil.TestingT, path string) *Server {
content := testutil.ReadFile(t, path)
var apiSpecs []ApiSpec
err := json.Unmarshal([]byte(content), &apiSpecs)
require.NoError(t, err)

server := New(t)
for _, apiSpec := range apiSpecs {
server.MustHandle(apiSpec)
}

return server
}

type HandlerFunc func(req *http.Request) (resp any, err error)

func (s *Server) MustHandle(apiSpec ApiSpec) {
assert.NotEmpty(s.t, apiSpec.Method)
assert.NotEmpty(s.t, apiSpec.Path)

pattern := apiSpec.Method + " " + apiSpec.Path
s.calledPatterns[pattern] = false

s.Handle(pattern, func(req *http.Request) (any, error) {
// Record the fact that this pattern was called.
s.calledPatterns[pattern] = true

// Return the expected response body.
return apiSpec.Response.Body, nil
})
}

func (s *Server) Close() {
for pattern, called := range s.calledPatterns {
assert.Truef(s.t, called, "expected pattern %s to be called", pattern)
}

s.Server.Close()
}

func (s *Server) Handle(pattern string, handler HandlerFunc) {
shreyas-goenka marked this conversation as resolved.
Show resolved Hide resolved
s.Mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
resp, err := handler(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")

var respBytes []byte

respString, ok := resp.(string)
if ok {
respBytes = []byte(respString)
} else {
respBytes, err = json.MarshalIndent(resp, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

if _, err := w.Write(respBytes); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
Loading