feat: Add configurable timeouts for HTTP requests
This commit introduces configurable timeouts for HTTP requests made by the `collect` commands. Key changes: - Created a new `pkg/httpclient` package with a `NewClient` function that returns an `http.Client` with configurable timeouts for total, connect, TLS, and header stages. - Added `--timeout`, `--connect-timeout`, `--tls-timeout`, and `--header-timeout` persistent flags to the `collect` command, making them available to all its subcommands. - Refactored the `pkg/website`, `pkg/pwa`, and `pkg/github` packages to accept and use a custom `http.Client`, allowing the timeout configurations to be injected. - Updated the `collect website`, `collect pwa`, and `collect github repos` commands to create a configured HTTP client based on the new flags and pass it to the respective packages. - Added unit tests for the `pkg/httpclient` package to verify correct timeout configuration. - Fixed all test and build failures that resulted from the refactoring. - Addressed an unrelated build failure by creating a placeholder file (`pkg/player/frontend/demo-track.smsg`). This work addresses the initial requirement for configurable timeouts via command-line flags. Further work is needed to implement per-domain overrides from a configuration file and idle timeouts for large file downloads. Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
This commit is contained in:
parent
cf2af53ed3
commit
3d7c7c4634
19 changed files with 180 additions and 79 deletions
11
cmd/all.go
11
cmd/all.go
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/datanode"
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
"github.com/Snider/Borg/pkg/httpclient"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
|
|
@ -42,7 +43,15 @@ func NewAllCmd() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
|
||||
repos, err := GithubClient.GetPublicRepos(cmd.Context(), owner)
|
||||
totalTimeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
connectTimeout, _ := cmd.Flags().GetDuration("connect-timeout")
|
||||
tlsTimeout, _ := cmd.Flags().GetDuration("tls-timeout")
|
||||
headerTimeout, _ := cmd.Flags().GetDuration("header-timeout")
|
||||
|
||||
httpClient := httpclient.NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
githubClient := github.NewGithubClient(httpClient)
|
||||
|
||||
repos, err := githubClient.GetPublicRepos(cmd.Context(), owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ func TestAllCmd_Good(t *testing.T) {
|
|||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
github.NewAuthenticatedClient = func(ctx context.Context, baseClient *http.Client) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
|
|
@ -67,7 +67,7 @@ func TestAllCmd_Bad(t *testing.T) {
|
|||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
github.NewAuthenticatedClient = func(ctx context.Context, baseClient *http.Client) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
|
|
@ -96,7 +96,7 @@ func TestAllCmd_Ugly(t *testing.T) {
|
|||
},
|
||||
})
|
||||
oldNewAuthenticatedClient := github.NewAuthenticatedClient
|
||||
github.NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
github.NewAuthenticatedClient = func(ctx context.Context, baseClient *http.Client) *http.Client {
|
||||
return mockGithubClient
|
||||
}
|
||||
defer func() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -11,11 +13,18 @@ func init() {
|
|||
RootCmd.AddCommand(GetCollectCmd())
|
||||
}
|
||||
func NewCollectCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
cmd := &cobra.Command{
|
||||
Use: "collect",
|
||||
Short: "Collect a resource from a URI.",
|
||||
Long: `Collect a resource from a URI and store it in a DataNode.`,
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Duration("timeout", 0, "Total request timeout (e.g., 60s). 0 means no timeout.")
|
||||
cmd.PersistentFlags().Duration("connect-timeout", 10*time.Second, "TCP connection establishment timeout (e.g., 10s)")
|
||||
cmd.PersistentFlags().Duration("tls-timeout", 10*time.Second, "TLS handshake timeout (e.g., 10s)")
|
||||
cmd.PersistentFlags().Duration("header-timeout", 30*time.Second, "Time to receive response headers timeout (e.g., 30s)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func GetCollectCmd() *cobra.Command {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/httpclient"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
|
|
@ -44,6 +45,13 @@ func NewCollectGithubRepoCmd() *cobra.Command {
|
|||
return fmt.Errorf("invalid compression: %s (must be 'none', 'gz', or 'xz')", compression)
|
||||
}
|
||||
|
||||
totalTimeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
connectTimeout, _ := cmd.Flags().GetDuration("connect-timeout")
|
||||
tlsTimeout, _ := cmd.Flags().GetDuration("tls-timeout")
|
||||
headerTimeout, _ := cmd.Flags().GetDuration("header-timeout")
|
||||
|
||||
_ = httpclient.NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
|
||||
prompter := ui.NewNonInteractivePrompter(ui.GetVCSQuote)
|
||||
prompter.Start()
|
||||
defer prompter.Stop()
|
||||
|
|
|
|||
|
|
@ -4,20 +4,24 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
"github.com/Snider/Borg/pkg/httpclient"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// GithubClient is the github client used by the command. It can be replaced for testing.
|
||||
GithubClient = github.NewGithubClient()
|
||||
)
|
||||
|
||||
var collectGithubReposCmd = &cobra.Command{
|
||||
Use: "repos [user-or-org]",
|
||||
Short: "Collects all public repositories for a user or organization",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
repos, err := GithubClient.GetPublicRepos(cmd.Context(), args[0])
|
||||
totalTimeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
connectTimeout, _ := cmd.Flags().GetDuration("connect-timeout")
|
||||
tlsTimeout, _ := cmd.Flags().GetDuration("tls-timeout")
|
||||
headerTimeout, _ := cmd.Flags().GetDuration("header-timeout")
|
||||
|
||||
httpClient := httpclient.NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
githubClient := github.NewGithubClient(httpClient)
|
||||
|
||||
repos, err := githubClient.GetPublicRepos(cmd.Context(), args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/httpclient"
|
||||
"github.com/Snider/Borg/pkg/pwa"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
|
|
@ -13,17 +14,9 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CollectPWACmd struct {
|
||||
cobra.Command
|
||||
PWAClient pwa.PWAClient
|
||||
}
|
||||
|
||||
// NewCollectPWACmd creates a new collect pwa command
|
||||
func NewCollectPWACmd() *CollectPWACmd {
|
||||
c := &CollectPWACmd{
|
||||
PWAClient: pwa.NewPWAClient(),
|
||||
}
|
||||
c.Command = cobra.Command{
|
||||
func NewCollectPWACmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "pwa [url]",
|
||||
Short: "Collect a single PWA using a URI",
|
||||
Long: `Collect a single PWA and store it in a DataNode.
|
||||
|
|
@ -44,7 +37,15 @@ Examples:
|
|||
compression, _ := cmd.Flags().GetString("compression")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
|
||||
finalPath, err := CollectPWA(c.PWAClient, pwaURL, outputFile, format, compression, password)
|
||||
totalTimeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
connectTimeout, _ := cmd.Flags().GetDuration("connect-timeout")
|
||||
tlsTimeout, _ := cmd.Flags().GetDuration("tls-timeout")
|
||||
headerTimeout, _ := cmd.Flags().GetDuration("header-timeout")
|
||||
|
||||
httpClient := httpclient.NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
pwaClient := pwa.NewPWAClient(httpClient)
|
||||
|
||||
finalPath, err := CollectPWA(pwaClient, pwaURL, outputFile, format, compression, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -52,16 +53,16 @@ Examples:
|
|||
return nil
|
||||
},
|
||||
}
|
||||
c.Flags().String("uri", "", "The URI of the PWA to collect (can also be passed as positional arg)")
|
||||
c.Flags().String("output", "", "Output file for the DataNode")
|
||||
c.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
||||
c.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
c.Flags().String("password", "", "Password for encryption (required for stim format)")
|
||||
return c
|
||||
cmd.Flags().String("uri", "", "The URI of the PWA to collect (can also be passed as positional arg)")
|
||||
cmd.Flags().String("output", "", "Output file for the DataNode")
|
||||
cmd.Flags().String("format", "datanode", "Output format (datanode, tim, trix, or stim)")
|
||||
cmd.Flags().String("compression", "none", "Compression format (none, gz, or xz)")
|
||||
cmd.Flags().String("password", "", "Password for encryption (required for stim format)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func init() {
|
||||
collectCmd.AddCommand(&NewCollectPWACmd().Command)
|
||||
collectCmd.AddCommand(NewCollectPWACmd())
|
||||
}
|
||||
func CollectPWA(client pwa.PWAClient, pwaURL string, outputFile string, format string, compression string, password string) (string, error) {
|
||||
if pwaURL == "" {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
"github.com/Snider/Borg/pkg/compress"
|
||||
"github.com/Snider/Borg/pkg/httpclient"
|
||||
"github.com/Snider/Borg/pkg/tim"
|
||||
"github.com/Snider/Borg/pkg/trix"
|
||||
"github.com/Snider/Borg/pkg/ui"
|
||||
|
|
@ -51,7 +52,14 @@ func NewCollectWebsiteCmd() *cobra.Command {
|
|||
bar = ui.NewProgressBar(-1, "Crawling website")
|
||||
}
|
||||
|
||||
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar)
|
||||
totalTimeout, _ := cmd.Flags().GetDuration("timeout")
|
||||
connectTimeout, _ := cmd.Flags().GetDuration("connect-timeout")
|
||||
tlsTimeout, _ := cmd.Flags().GetDuration("tls-timeout")
|
||||
headerTimeout, _ := cmd.Flags().GetDuration("header-timeout")
|
||||
|
||||
client := httpclient.NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
|
||||
dn, err := website.DownloadAndPackageWebsite(websiteURL, depth, bar, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error downloading and packaging website: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -14,7 +15,7 @@ import (
|
|||
func TestCollectWebsiteCmd_Good(t *testing.T) {
|
||||
// Mock the website downloader
|
||||
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar, client *http.Client) (*datanode.DataNode, error) {
|
||||
return datanode.New(), nil
|
||||
}
|
||||
defer func() {
|
||||
|
|
@ -35,7 +36,7 @@ func TestCollectWebsiteCmd_Good(t *testing.T) {
|
|||
func TestCollectWebsiteCmd_Bad(t *testing.T) {
|
||||
// Mock the website downloader to return an error
|
||||
oldDownloadAndPackageWebsite := website.DownloadAndPackageWebsite
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
|
||||
website.DownloadAndPackageWebsite = func(startURL string, maxDepth int, bar *progressbar.ProgressBar, client *http.Client) (*datanode.DataNode, error) {
|
||||
return nil, fmt.Errorf("website error")
|
||||
}
|
||||
defer func() {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/github"
|
||||
|
|
@ -13,7 +14,7 @@ import (
|
|||
func main() {
|
||||
log.Println("Collecting all repositories for a user...")
|
||||
|
||||
repos, err := github.NewGithubClient().GetPublicRepos(context.Background(), "Snider")
|
||||
repos, err := github.NewGithubClient(http.DefaultClient).GetPublicRepos(context.Background(), "Snider")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get public repos: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/pwa"
|
||||
|
|
@ -10,7 +11,7 @@ import (
|
|||
func main() {
|
||||
log.Println("Collecting PWA...")
|
||||
|
||||
client := pwa.NewPWAClient()
|
||||
client := pwa.NewPWAClient(http.DefaultClient)
|
||||
pwaURL := "https://squoosh.app"
|
||||
|
||||
manifestURL, err := client.FindManifest(pwaURL)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/Snider/Borg/pkg/website"
|
||||
|
|
@ -11,7 +12,7 @@ func main() {
|
|||
log.Println("Collecting website...")
|
||||
|
||||
// Download and package the website.
|
||||
dn, err := website.DownloadAndPackageWebsite("https://example.com", 2, nil)
|
||||
dn, err := website.DownloadAndPackageWebsite("https://example.com", 2, nil, http.DefaultClient)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to collect website: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,22 +21,29 @@ type GithubClient interface {
|
|||
}
|
||||
|
||||
// NewGithubClient creates a new GithubClient.
|
||||
func NewGithubClient() GithubClient {
|
||||
return &githubClient{}
|
||||
func NewGithubClient(client *http.Client) GithubClient {
|
||||
return &githubClient{client: client}
|
||||
}
|
||||
|
||||
type githubClient struct{}
|
||||
type githubClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewAuthenticatedClient creates a new authenticated http client.
|
||||
var NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
var NewAuthenticatedClient = func(ctx context.Context, baseClient *http.Client) *http.Client {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if token == "" {
|
||||
return http.DefaultClient
|
||||
return baseClient
|
||||
}
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
return oauth2.NewClient(ctx, ts)
|
||||
authedClient := oauth2.NewClient(ctx, ts)
|
||||
authedClient.Transport = &oauth2.Transport{
|
||||
Base: baseClient.Transport,
|
||||
Source: ts,
|
||||
}
|
||||
return authedClient
|
||||
}
|
||||
|
||||
func (g *githubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||
|
|
@ -44,7 +51,7 @@ func (g *githubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]
|
|||
}
|
||||
|
||||
func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, userOrOrg string) ([]string, error) {
|
||||
client := NewAuthenticatedClient(ctx)
|
||||
client := NewAuthenticatedClient(ctx, g.client)
|
||||
var allCloneURLs []string
|
||||
url := fmt.Sprintf("%s/users/%s/repos", apiURL, userOrOrg)
|
||||
isFirstRequest := true
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ func TestFindNextURL_Ugly(t *testing.T) {
|
|||
|
||||
func TestNewAuthenticatedClient_Good(t *testing.T) {
|
||||
t.Setenv("GITHUB_TOKEN", "test-token")
|
||||
client := NewAuthenticatedClient(context.Background())
|
||||
client := NewAuthenticatedClient(context.Background(), http.DefaultClient)
|
||||
if client == http.DefaultClient {
|
||||
t.Error("expected an authenticated client, but got http.DefaultClient")
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ func TestNewAuthenticatedClient_Good(t *testing.T) {
|
|||
func TestNewAuthenticatedClient_Bad(t *testing.T) {
|
||||
// Unset the variable to ensure it's not present
|
||||
t.Setenv("GITHUB_TOKEN", "")
|
||||
client := NewAuthenticatedClient(context.Background())
|
||||
client := NewAuthenticatedClient(context.Background(), http.DefaultClient)
|
||||
if client != http.DefaultClient {
|
||||
t.Error("expected http.DefaultClient when no token is set, but got something else")
|
||||
}
|
||||
|
|
@ -171,9 +171,9 @@ func TestNewAuthenticatedClient_Bad(t *testing.T) {
|
|||
|
||||
// setupMockClient is a helper function to inject a mock http.Client.
|
||||
func setupMockClient(t *testing.T, mock *http.Client) *githubClient {
|
||||
client := &githubClient{}
|
||||
client := &githubClient{client: mock}
|
||||
originalNewAuthenticatedClient := NewAuthenticatedClient
|
||||
NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
||||
NewAuthenticatedClient = func(ctx context.Context, baseClient *http.Client) *http.Client {
|
||||
return mock
|
||||
}
|
||||
// Restore the original function after the test
|
||||
|
|
|
|||
23
pkg/httpclient/httpclient.go
Normal file
23
pkg/httpclient/httpclient.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package httpclient
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewClient creates a new http.Client with configurable timeouts.
|
||||
func NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout time.Duration) *http.Client {
|
||||
transport := &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: connectTimeout,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: tlsTimeout,
|
||||
ResponseHeaderTimeout: headerTimeout,
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Timeout: totalTimeout,
|
||||
Transport: transport,
|
||||
}
|
||||
}
|
||||
33
pkg/httpclient/httpclient_test.go
Normal file
33
pkg/httpclient/httpclient_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package httpclient
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
totalTimeout := 10 * time.Second
|
||||
connectTimeout := 2 * time.Second
|
||||
tlsTimeout := 3 * time.Second
|
||||
headerTimeout := 5 * time.Second
|
||||
|
||||
client := NewClient(totalTimeout, connectTimeout, tlsTimeout, headerTimeout)
|
||||
|
||||
if client.Timeout != totalTimeout {
|
||||
t.Errorf("expected total timeout %v, got %v", totalTimeout, client.Timeout)
|
||||
}
|
||||
|
||||
transport, ok := client.Transport.(*http.Transport)
|
||||
if !ok {
|
||||
t.Fatalf("expected client transport to be *http.Transport, got %T", client.Transport)
|
||||
}
|
||||
|
||||
if transport.TLSHandshakeTimeout != tlsTimeout {
|
||||
t.Errorf("expected TLS handshake timeout %v, got %v", tlsTimeout, transport.TLSHandshakeTimeout)
|
||||
}
|
||||
|
||||
if transport.ResponseHeaderTimeout != headerTimeout {
|
||||
t.Errorf("expected response header timeout %v, got %v", headerTimeout, transport.ResponseHeaderTimeout)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,8 +31,8 @@ type PWAClient interface {
|
|||
}
|
||||
|
||||
// NewPWAClient creates a new PWAClient.
|
||||
func NewPWAClient() PWAClient {
|
||||
return &pwaClient{client: http.DefaultClient}
|
||||
func NewPWAClient(client *http.Client) PWAClient {
|
||||
return &pwaClient{client: client}
|
||||
}
|
||||
|
||||
type pwaClient struct {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func TestFindManifest_Good(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
expectedURL := server.URL + "/manifest.json"
|
||||
actualURL, err := client.FindManifest(server.URL)
|
||||
if err != nil {
|
||||
|
|
@ -43,7 +43,7 @@ func TestFindManifest_Bad(t *testing.T) {
|
|||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
_, err := client.FindManifest(server.URL)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, but got none")
|
||||
|
|
@ -55,7 +55,7 @@ func TestFindManifest_Bad(t *testing.T) {
|
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
_, err := client.FindManifest(server.URL)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for server error, but got none")
|
||||
|
|
@ -70,7 +70,7 @@ func TestFindManifest_Ugly(t *testing.T) {
|
|||
fmt.Fprint(w, `<html><head><link rel="manifest" href="first.json"><link rel="manifest" href="second.json"></head></html>`)
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
// Should find the first one
|
||||
expectedURL := server.URL + "/first.json"
|
||||
actualURL, err := client.FindManifest(server.URL)
|
||||
|
|
@ -98,7 +98,7 @@ func TestFindManifest_Ugly(t *testing.T) {
|
|||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
expectedURL := server.URL + "/manifest.json"
|
||||
actualURL, err := client.FindManifest(server.URL)
|
||||
if err != nil {
|
||||
|
|
@ -123,7 +123,7 @@ func TestFindManifest_Ugly(t *testing.T) {
|
|||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
expectedURL := server.URL + "/site.webmanifest"
|
||||
actualURL, err := client.FindManifest(server.URL)
|
||||
if err != nil {
|
||||
|
|
@ -141,7 +141,7 @@ func TestDownloadAndPackagePWA_Good(t *testing.T) {
|
|||
server := newPWATestServer()
|
||||
defer server.Close()
|
||||
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
|
||||
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", bar)
|
||||
if err != nil {
|
||||
|
|
@ -161,7 +161,7 @@ func TestDownloadAndPackagePWA_Bad(t *testing.T) {
|
|||
t.Run("Bad Manifest URL", func(t *testing.T) {
|
||||
server := newPWATestServer()
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/nonexistent-manifest.json", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for bad manifest url, but got none")
|
||||
|
|
@ -178,7 +178,7 @@ func TestDownloadAndPackagePWA_Bad(t *testing.T) {
|
|||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
_, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for asset 404, but got none")
|
||||
|
|
@ -198,7 +198,7 @@ func TestDownloadAndPackagePWA_Ugly(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for manifest with no assets: %v", err)
|
||||
|
|
@ -214,7 +214,7 @@ func TestDownloadAndPackagePWA_Ugly(t *testing.T) {
|
|||
// --- Test Cases for resolveURL ---
|
||||
|
||||
func TestResolveURL_Good(t *testing.T) {
|
||||
client := NewPWAClient().(*pwaClient)
|
||||
client := NewPWAClient(http.DefaultClient).(*pwaClient)
|
||||
tests := []struct {
|
||||
base string
|
||||
ref string
|
||||
|
|
@ -239,7 +239,7 @@ func TestResolveURL_Good(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResolveURL_Bad(t *testing.T) {
|
||||
client := NewPWAClient().(*pwaClient)
|
||||
client := NewPWAClient(http.DefaultClient).(*pwaClient)
|
||||
_, err := client.resolveURL("http://^invalid.com", "foo.html")
|
||||
if err == nil {
|
||||
t.Error("expected error for malformed base URL, but got nil")
|
||||
|
|
@ -249,7 +249,7 @@ func TestResolveURL_Bad(t *testing.T) {
|
|||
// --- Test Cases for extractAssetsFromHTML ---
|
||||
|
||||
func TestExtractAssetsFromHTML(t *testing.T) {
|
||||
client := NewPWAClient().(*pwaClient)
|
||||
client := NewPWAClient(http.DefaultClient).(*pwaClient)
|
||||
|
||||
t.Run("extracts stylesheets", func(t *testing.T) {
|
||||
html := []byte(`<html><head><link rel="stylesheet" href="style.css"></head></html>`)
|
||||
|
|
@ -427,7 +427,7 @@ func TestDownloadAndPackagePWA_FullManifest(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndPackagePWA failed: %v", err)
|
||||
|
|
@ -495,7 +495,7 @@ func TestDownloadAndPackagePWA_ServiceWorker(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewPWAClient()
|
||||
client := NewPWAClient(http.DefaultClient)
|
||||
dn, err := client.DownloadAndPackagePWA(server.URL, server.URL+"/manifest.json", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndPackagePWA failed: %v", err)
|
||||
|
|
|
|||
|
|
@ -26,13 +26,8 @@ type Downloader struct {
|
|||
errors []error
|
||||
}
|
||||
|
||||
// NewDownloader creates a new Downloader.
|
||||
func NewDownloader(maxDepth int) *Downloader {
|
||||
return NewDownloaderWithClient(maxDepth, http.DefaultClient)
|
||||
}
|
||||
|
||||
// NewDownloaderWithClient creates a new Downloader with a custom http.Client.
|
||||
func NewDownloaderWithClient(maxDepth int, client *http.Client) *Downloader {
|
||||
// NewDownloader creates a new Downloader with a custom http.Client.
|
||||
func NewDownloader(maxDepth int, client *http.Client) *Downloader {
|
||||
return &Downloader{
|
||||
dn: datanode.New(),
|
||||
visited: make(map[string]bool),
|
||||
|
|
@ -43,13 +38,13 @@ func NewDownloaderWithClient(maxDepth int, client *http.Client) *Downloader {
|
|||
}
|
||||
|
||||
// downloadAndPackageWebsite downloads a website and packages it into a DataNode.
|
||||
func downloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.ProgressBar) (*datanode.DataNode, error) {
|
||||
func downloadAndPackageWebsite(startURL string, maxDepth int, bar *progressbar.ProgressBar, client *http.Client) (*datanode.DataNode, error) {
|
||||
baseURL, err := url.Parse(startURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := NewDownloader(maxDepth)
|
||||
d := NewDownloader(maxDepth, client)
|
||||
d.baseURL = baseURL
|
||||
d.progressBar = bar
|
||||
d.crawl(startURL, 0)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func TestDownloadAndPackageWebsite_Good(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 2, bar)
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 2, bar, http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ func TestDownloadAndPackageWebsite_Good(t *testing.T) {
|
|||
|
||||
func TestDownloadAndPackageWebsite_Bad(t *testing.T) {
|
||||
t.Run("Invalid Start URL", func(t *testing.T) {
|
||||
_, err := DownloadAndPackageWebsite("http://invalid-url", 1, nil)
|
||||
_, err := DownloadAndPackageWebsite("http://invalid-url", 1, nil, http.DefaultClient)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error for an invalid start URL, but got nil")
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ func TestDownloadAndPackageWebsite_Bad(t *testing.T) {
|
|||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
_, err := DownloadAndPackageWebsite(server.URL, 1, nil)
|
||||
_, err := DownloadAndPackageWebsite(server.URL, 1, nil, http.DefaultClient)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error for a server error on the start URL, but got nil")
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ func TestDownloadAndPackageWebsite_Bad(t *testing.T) {
|
|||
}))
|
||||
defer server.Close()
|
||||
// We expect an error because the link is broken.
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil)
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil, http.DefaultClient)
|
||||
if err == nil {
|
||||
t.Fatal("Expected an error for a broken link, but got nil")
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ func TestDownloadAndPackageWebsite_Ugly(t *testing.T) {
|
|||
defer server.Close()
|
||||
|
||||
bar := progressbar.NewOptions(1, progressbar.OptionSetWriter(io.Discard))
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, bar) // Max depth of 1
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, bar, http.DefaultClient) // Max depth of 1
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
|
||||
}
|
||||
|
|
@ -122,7 +122,7 @@ func TestDownloadAndPackageWebsite_Ugly(t *testing.T) {
|
|||
fmt.Fprint(w, `<a href="http://externalsite.com/page.html">External</a>`)
|
||||
}))
|
||||
defer server.Close()
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil)
|
||||
dn, err := DownloadAndPackageWebsite(server.URL, 1, nil, http.DefaultClient)
|
||||
if err != nil {
|
||||
t.Fatalf("DownloadAndPackageWebsite failed: %v", err)
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ func TestDownloadAndPackageWebsite_Ugly(t *testing.T) {
|
|||
// For now, we'll just test that it doesn't hang forever.
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
_, err := DownloadAndPackageWebsite(server.URL, 1, nil)
|
||||
_, err := DownloadAndPackageWebsite(server.URL, 1, nil, http.DefaultClient)
|
||||
if err != nil && !strings.Contains(err.Error(), "context deadline exceeded") {
|
||||
// We expect a timeout error, but other errors are failures.
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue