Skip to content

Conversation

@mwbrooks
Copy link
Member

@mwbrooks mwbrooks commented May 21, 2025

Summary

This pull request updates our tests to not require a network connection. Now our tests run ⚑ slightly faster and πŸ”Œ work offline.

A note about the HTTPClient:

  • The HTTPClientOptions defines a wide range of options available.
  • However, only a subset are implemented. For example, retry logic is not implement
  • This is because the implementation was borrowed from package api.
  • I decided to scope this PR to moving existing code around without adding new logic.
  • The hope is that this will allow us to use it across the code base in a less risky way.
  • Then we can improve the HTTPClient with the unsupported options.

A few opportunities for follow-up pull requests:

  1. URLChecker(...) should be updated to use httpClient.Head instead of httpClient.Get
    • This will improve the speed of the URLChecker(...) and prevent a double-download of a template from happening
    • βœ‹πŸ» I can do this after we merge this PR
  2. generateGitZipFileURL(...) should be updated to call URLChecker(...) for custom branches as well
    • This will improve the error message when a custom branch doesn't exist
    • βœ‹πŸ» I can do this after we merge this PR
  3. package update should be updated to use the new HTTPClient interface
    • The HTTPClient interface was borrowed from the package api implementation
    • We should update both package api, package update, and package project to use the new HTTPClient interface
    • βœ‹πŸ» I can do this after we merge this PR
  4. Use HTTPClient interface in other areas of the code base that use a custom http.Client
    • De-dup implementation and use one HTTPClient implementation throughout the code base

Reviewers

Test Offline Mode

# Turn off your network
$ make test | grep FAIL: 
# β†’ Confirm no failures (no output)

Test Updates to generateGitZipFileURL and ``URLChecker`

# Create a regular project
$ lack create asdf/
# β†’ Choose any project
# β†’ Confirm no errors
$ rm -rf asdf/

# Create from a template
$ lack create asdf -t slack-samples/bolt-js-starter-template
# β†’ Confirm no errors
$ rm -rf asdf/

# Create from a template and custom branch
$ lack create asdf -t slack-samples/bolt-js-starter-template --branch zimeg-feat-logger
# β†’ Confirm no errors
$ cat asdf/package.json
# β†’ Confirm "@slack/bolt": "^3.19.0",
# β†’ Confirm "@slack/logger": "^4.0.0",
$ rm -rf asdf/

# Error when create from a template with non-existent branch
$ lack create asdf -t slack-samples/bolt-js-starter-template --branch master
# β†’ Confirm error code "git_clone_error"
$ rm -rf asdf/

Requirements

@mwbrooks mwbrooks added this to the Next Release milestone May 21, 2025
@mwbrooks mwbrooks self-assigned this May 21, 2025
@mwbrooks mwbrooks added code health M-T: Test improvements and anything that improves code health semver:patch Use on pull requests to describe the release version increment labels May 21, 2025
Copy link
Member Author

@mwbrooks mwbrooks left a comment

Choose a reason for hiding this comment

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

Sharing a few learnings and decisions that I made along this flight path ✈️


// URLChecker returns url if its status code is 200, otherwise returns empty string
func URLChecker(httpClient slackdeps.HTTPClient, url string) string {
resp, err := httpClient.Get(url)
Copy link
Member Author

Choose a reason for hiding this comment

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

note: A future PR should update this to .Head. This will require adding Head to our HTTPClient interface as well.

Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks Great thinking! For templates that vendor dependencies I imagine this being an expensive operation and I fear rate limits might've been blocking downloads in CI sometimes as well πŸ˜“

It's so good to know of this follow up! 🧠 ✨

Copy link
Member Author

Choose a reason for hiding this comment

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

I fear rate limits might've been blocking downloads in CI sometimes as well πŸ˜“

🧠 Hopefully this reduces some flakiness!

func Test_URLChecker(t *testing.T) {
url := URLChecker("https://github.com/slack-samples/deno-starter-template")
assert.Equal(t, "https://github.com/slack-samples/deno-starter-template", url, "should return url when url is valid")
tests := map[string]struct {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: Reimplemented the tests as a table test and extended the cover. This function now has 100% test coverage. πŸ’―

Comment on lines 229 to 230
httpClient := slackdeps.NewHTTPClient(slackdeps.HTTPClientOptions{})
zipFileURL := generateGitZipFileURL(httpClient, template.path, gitBranch)
Copy link
Member Author

Choose a reason for hiding this comment

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

note: This is the main code change. We now inject a httpClient and mock it in tests.

We could take this further by defining a clients.HTTPClient instance, but I wanted to keep these changes scoped smaller to allow us to discuss those trade-offs later.

Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks So disciplined but a solid call for now!

I think it makes sense to use a shared http client that might allow specific options in calls, but I agree that discussion can wait.

FWIW this will be so fast to find as reference I hope πŸ” ✨

zipURL = deputil.URLChecker(httpClient, masterURL)
}
} else {
zipURL = zipURL + gitBranch + ".zip"
Copy link
Member Author

Choose a reason for hiding this comment

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

note: Kinda funny that we never apply URLChecker to custom branches. Work for the future!

Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks I was wondering of possible reason for this, but the points around how we can improve error messages for missing URLs outweigh the missing checks here I hope πŸͺ„


url = generateGitZipFileURL("https://github.com/slack-samples/deno-starter-template", "")
assert.Equal(t, "https://github.com/slack-samples/deno-starter-template/archive/refs/heads/main.zip", url, "should return zip download link with main")
tests := map[string]struct {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: Reimplemented the tests as a table test and extended the cover. This function now has 100% test coverage. πŸ’―

Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks Huge unlock for test stabilities all around here πŸ” ✨

While it'd be so surprising to me, I can imagine missing branches might cause some of the past tests to fail πŸ˜‰

Comment on lines 28 to 32
// HTTPClient interface for http.Client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
Get(url string) (*http.Response, error)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

note: I only added what we use today, but this will need to extend to Post and Head as we use it in other areas of the code base.

Comment on lines 45 to 46
// NewHTTPClient returns an http.Client configured with HTTPClientOptions.
func NewHTTPClient(opts HTTPClientOptions) *http.Client {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: This is taken from internal/api/httpclient.go so it's been well used and tested. I only updated the comments and style for declaring the pointer client := &http.Client{}.

Copy link
Member

Choose a reason for hiding this comment

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

I'm excited for the future refactors that bring the http logic out of api πŸ€– ✨

Copy link
Member Author

Choose a reason for hiding this comment

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

Same same same same same!

Comment on lines 56 to 57
// MockHTTPResponse is a helper that returns a mocked http.Response with the provided httpStatus and body.
func MockHTTPResponse(httpStatus int, body string) *http.Response {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: This little helper makes me really happy πŸ˜„

Copy link
Member

Choose a reason for hiding this comment

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

	"net/http/httptest"

This is such an impressive package IMO! I'm glad we're making tests at the right level here πŸŽ‰

resWriter := httptest.NewRecorder()

resWriter.WriteHeader(httpStatus)
_, _ = io.WriteString(resWriter, body)
Copy link
Member Author

Choose a reason for hiding this comment

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

tip: After some searching, I learned that fmt.Printf(resWriter, body) and io.WriteString(resWriter, body) are both acceptable. WriteString is preferred because it's more readable and some packages such as http have optimized implementations of it.

Right now, we use both throughout our code base.

@codecov
Copy link

codecov bot commented May 21, 2025

Codecov Report

Attention: Patch coverage is 93.33333% with 2 lines in your changes missing coverage. Please review.

Project coverage is 63.42%. Comparing base (56d8c20) to head (96adc92).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/pkg/create/create.go 66.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #102      +/-   ##
==========================================
+ Coverage   63.37%   63.42%   +0.04%     
==========================================
  Files         211      212       +1     
  Lines       22284    22308      +24     
==========================================
+ Hits        14122    14148      +26     
  Misses       7071     7071              
+ Partials     1091     1089       -2     

β˜” View full report in Codecov by Sentry.
πŸ“’ Have feedback on the report? Share it here.

πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mwbrooks mwbrooks marked this pull request as ready for review May 21, 2025 17:55
@mwbrooks mwbrooks requested a review from a team as a code owner May 21, 2025 17:55
Copy link
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

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

@mwbrooks Hacking in the cloud can callout such interesting problems! πŸ€– ☁️

All of these changes move the code to such a good place for enhanced tests and all related improvements to follow. I'm glad to find tests running a bit faster and more stable too, and the cases provided are working well during review at this moment πŸ˜‰


// URLChecker returns url if its status code is 200, otherwise returns empty string
func URLChecker(httpClient slackdeps.HTTPClient, url string) string {
resp, err := httpClient.Get(url)
Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks Great thinking! For templates that vendor dependencies I imagine this being an expensive operation and I fear rate limits might've been blocking downloads in CI sometimes as well πŸ˜“

It's so good to know of this follow up! 🧠 ✨

Comment on lines 59 to 64
// Create mocks
httpClientMock := &slackdeps.HTTPClientMock{}
tt.setupHTTPClientMock(httpClientMock)

url = URLChecker("fake_url")
assert.Equal(t, "", url, "should return empty string when url is invalid")
// Execute
url := URLChecker(httpClientMock, tt.url)
Copy link
Member

Choose a reason for hiding this comment

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

The table tests, mock setup, and injected dependencies are so good to see here! 🀩

Copy link
Member Author

Choose a reason for hiding this comment

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

The trio dream team!

// URLChecker returns url if it's status code is 200, otherwise returns empty string
func URLChecker(url string) string {
resp, err := http.Get(url)
"github.com/slackapi/slack-cli/internal/slackdeps"
Copy link
Member

Choose a reason for hiding this comment

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

πŸ“ No blocker but I can imagine we move custom HTTP setups - including this file - and interface implementations into a separate internal package?

Suggested change
"github.com/slackapi/slack-cli/internal/slackdeps"
"github.com/slackapi/slack-cli/internal/slackhttp"

Copy link
Member Author

Choose a reason for hiding this comment

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

I was thinking the same thing. The use of *util or *deps usually smells of opportunity for improvement. This seems like a good step forward.

"github.com/slackapi/slack-cli/internal/slackdeps"
)

// URLChecker returns url if its status code is 200, otherwise returns empty string
Copy link
Member

Choose a reason for hiding this comment

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

πŸ” I'm not sure if this was included in the followup plans, but we might also want to return an error here for continued improvements to error messages?

It's not clear to me if .Get returns separate errors for the specific error cases:

  • Internet broke
  • URL is missing

Copy link
Member Author

Choose a reason for hiding this comment

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

I had the same thought but let it slide. I agree, let's follow-up with returning an error from URLChecker πŸ‘πŸ»

Comment on lines 229 to 230
httpClient := slackdeps.NewHTTPClient(slackdeps.HTTPClientOptions{})
zipFileURL := generateGitZipFileURL(httpClient, template.path, gitBranch)
Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks So disciplined but a solid call for now!

I think it makes sense to use a shared http client that might allow specific options in calls, but I agree that discussion can wait.

FWIW this will be so fast to find as reference I hope πŸ” ✨


url = generateGitZipFileURL("https://github.com/slack-samples/deno-starter-template", "")
assert.Equal(t, "https://github.com/slack-samples/deno-starter-template/archive/refs/heads/main.zip", url, "should return zip download link with main")
tests := map[string]struct {
Copy link
Member

Choose a reason for hiding this comment

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

@mwbrooks Huge unlock for test stabilities all around here πŸ” ✨

While it'd be so surprising to me, I can imagine missing branches might cause some of the past tests to fail πŸ˜‰

Comment on lines 100 to 102
res := slackdeps.MockHTTPResponse(http.StatusOK, "OK")
httpClientMock.On("Get", "https://github.com/slack-samples/deno-starter-template/archive/refs/heads/main.zip").Return(nil, fmt.Errorf("HttpClient error"))
httpClientMock.On("Get", "https://github.com/slack-samples/deno-starter-template/archive/refs/heads/master.zip").Return(res, nil)
Copy link
Member

Choose a reason for hiding this comment

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

πŸ§ͺ ✨

Comment on lines 45 to 46
// NewHTTPClient returns an http.Client configured with HTTPClientOptions.
func NewHTTPClient(opts HTTPClientOptions) *http.Client {
Copy link
Member

Choose a reason for hiding this comment

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

I'm excited for the future refactors that bring the http logic out of api πŸ€– ✨

Comment on lines 56 to 57
// MockHTTPResponse is a helper that returns a mocked http.Response with the provided httpStatus and body.
func MockHTTPResponse(httpStatus int, body string) *http.Response {
Copy link
Member

Choose a reason for hiding this comment

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

	"net/http/httptest"

This is such an impressive package IMO! I'm glad we're making tests at the right level here πŸŽ‰

Copy link
Member

Choose a reason for hiding this comment

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

πŸ“ Edit to an above comment!

I didn't realize these files were new, but we might take this PR as a chance to start a separate package:

internal/slackhttp/http.go

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point! I was going to hold off for a new PR, but since these files are new, we might as well do it now. I'll start the refactor now...

Copy link
Member Author

Choose a reason for hiding this comment

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

Commit 96adc92 refactors this PR to use a separate packaged:

internal/slackhttp/http.go
internal/slackhttp/http_mock.go
internal/slackhttp/http_test.go

@mwbrooks
Copy link
Member Author

Thanks for the great review @zimeg! πŸ™‡πŸ» And for suggesting that we refactor the package slackhttp now rather than later πŸ‘ŒπŸ» This PR is looking good, so I'm going to merge it now and follow-up with a few of the additional tasks.

@mwbrooks mwbrooks merged commit 4406cb0 into main May 27, 2025
6 checks passed
@mwbrooks mwbrooks deleted the mwbrooks-http-mock branch May 27, 2025 00:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code health M-T: Test improvements and anything that improves code health semver:patch Use on pull requests to describe the release version increment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants