Skip to content

Commit

Permalink
Move existing HTTP client code
Browse files Browse the repository at this point in the history
  • Loading branch information
horgh committed Mar 23, 2024
1 parent 9ea24db commit 2697024
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 184 deletions.
38 changes: 38 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package client

import (
"net/http"
)

// HTTPReader is a Reader that uses an HTTP client to retrieve
// databases.
type HTTPReader struct {
// client is an http client responsible of fetching database updates.
client *http.Client
// path is the request path.
path string
// accountID is used for request auth.
accountID int
// licenseKey is used for request auth.
licenseKey string
// verbose turns on/off debug logs.
verbose bool
}

// NewHTTPReader creates a Reader that downloads database updates via
// HTTP.
func NewHTTPReader(
path string,
accountID int,
licenseKey string,
verbose bool,
httpClient *http.Client,
) *HTTPReader {
return &HTTPReader{
client: httpClient,
path: path,
accountID: accountID,
licenseKey: licenseKey,
verbose: verbose,
}
}
107 changes: 3 additions & 104 deletions internal/geoipupdate/database/http_reader.go → client/download.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
// Package database provides an abstraction over getting and writing a
// database file.
package database
package client

import (
"archive/tar"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
Expand All @@ -21,44 +17,6 @@ import (
"github.com/maxmind/geoipupdate/v6/internal/vars"
)

const (
metadataEndpoint = "%s/geoip/updates/metadata?"
downloadEndpoint = "%s/geoip/databases/%s/download?"
)

// HTTPReader is a Reader that uses an HTTP client to retrieve
// databases.
type HTTPReader struct {
// client is an http client responsible of fetching database updates.
client *http.Client
// path is the request path.
path string
// accountID is used for request auth.
accountID int
// licenseKey is used for request auth.
licenseKey string
// verbose turns on/off debug logs.
verbose bool
}

// NewHTTPReader creates a Reader that downloads database updates via
// HTTP.
func NewHTTPReader(
path string,
accountID int,
licenseKey string,
verbose bool,
httpClient *http.Client,
) *HTTPReader {
return &HTTPReader{
client: httpClient,
path: path,
accountID: accountID,
licenseKey: licenseKey,
verbose: verbose,
}
}

// Read attempts to fetch database updates for a specific editionID.
// It takes an editionID and its previously downloaded hash if available
// as arguments and returns a ReadResult struct as a response.
Expand All @@ -73,6 +31,8 @@ func (r *HTTPReader) Read(ctx context.Context, editionID, hash string) (*ReadRes
return result, nil
}

const downloadEndpoint = "%s/geoip/databases/%s/download?"

// get makes an http request to fetch updates for a specific editionID if any.
func (r *HTTPReader) get(
ctx context.Context,
Expand Down Expand Up @@ -178,67 +138,6 @@ func (r *HTTPReader) get(
}, nil
}

// metadata represents the metadata content for a certain database returned by the
// metadata endpoint.
type metadata struct {
Date string `json:"date"`
EditionID string `json:"edition_id"`
MD5 string `json:"md5"`
}

func (r *HTTPReader) getMetadata(ctx context.Context, editionID string) (*metadata, error) {
params := url.Values{}
params.Add("edition_id", editionID)

metadataRequestURL := fmt.Sprintf(metadataEndpoint, r.path) + params.Encode()

if r.verbose {
log.Printf("Requesting metadata for %s: %s", editionID, metadataRequestURL)
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataRequestURL, nil)
if err != nil {
return nil, fmt.Errorf("creating metadata request: %w", err)
}
req.Header.Add("User-Agent", "geoipupdate/"+vars.Version)
req.SetBasicAuth(strconv.Itoa(r.accountID), r.licenseKey)

response, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("performing metadata request: %w", err)
}
defer response.Body.Close()

responseBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading metadata response body: %w", err)
}

if response.StatusCode != http.StatusOK {
httpErr := internal.HTTPError{
Body: string(responseBody),
StatusCode: response.StatusCode,
}
return nil, fmt.Errorf("unexpected HTTP status code: %w", httpErr)
}

var metadataResponse struct {
Databases []metadata `json:"databases"`
}

if err := json.Unmarshal(responseBody, &metadataResponse); err != nil {
return nil, fmt.Errorf("parsing metadata body: %w", err)
}

if len(metadataResponse.Databases) != 1 {
return nil, fmt.Errorf("response does not contain edition %s", editionID)
}

edition := metadataResponse.Databases[0]

return &edition, nil
}

// parseTime parses a string representation of a time into time.Time according to the
// RFC1123 format.
func parseTime(s string) (time.Time, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package database
package client

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -243,77 +238,3 @@ func TestRead(t *testing.T) {
})
}
}

// TestGetMetadata checks the metadata fetching functionality.
func TestGetMetadata(t *testing.T) {
tests := []struct {
description string
preserveFileTime bool
server func(t *testing.T) *httptest.Server
checkResult func(t *testing.T, receivedMetadata *metadata, err error)
}{
{
description: "successful request",
preserveFileTime: false,
server: func(t *testing.T) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jsonData := `
{
"databases": [
{ "edition_id": "edition-1", "md5": "123456", "date": "2024-02-23" }
]
}
`
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(jsonData))
require.NoError(t, err)
}))
return server
},
checkResult: func(t *testing.T, receivedMetadata *metadata, err error) {
require.NoError(t, err)

expectedMetadata := &metadata{
EditionID: "edition-1", MD5: "123456", Date: "2024-02-23",
}
require.Equal(t, expectedMetadata, receivedMetadata)
},
},
{
description: "server error",
preserveFileTime: false,
server: func(_ *testing.T) *httptest.Server {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
return server
},
checkResult: func(t *testing.T, receivedMetadata *metadata, err error) {
require.Nil(t, receivedMetadata)
require.Error(t, err)
require.Regexp(t, "^unexpected HTTP status code", err.Error())
},
},
}

ctx := context.Background()

for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
server := test.server(t)
defer server.Close()

r := NewHTTPReader(
server.URL, // fixed, as the server is mocked above.
10, // fixed, as it's not valuable for the purpose of the test.
"license", // fixed, as it's not valuable for the purpose of the test.
false, // verbose
http.DefaultClient,
)

result, err := r.getMetadata(ctx, "edition-1")
test.checkResult(t, result, err)
})
}
}
78 changes: 78 additions & 0 deletions client/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package client

import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"

"github.com/maxmind/geoipupdate/v6/internal"
"github.com/maxmind/geoipupdate/v6/internal/vars"
)

const metadataEndpoint = "%s/geoip/updates/metadata?"

// metadata represents the metadata content for a certain database returned by the
// metadata endpoint.
type metadata struct {
Date string `json:"date"`
EditionID string `json:"edition_id"`
MD5 string `json:"md5"`
}

func (r *HTTPReader) getMetadata(ctx context.Context, editionID string) (*metadata, error) {
params := url.Values{}
params.Add("edition_id", editionID)

metadataRequestURL := fmt.Sprintf(metadataEndpoint, r.path) + params.Encode()

if r.verbose {
log.Printf("Requesting metadata for %s: %s", editionID, metadataRequestURL)
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, metadataRequestURL, nil)
if err != nil {
return nil, fmt.Errorf("creating metadata request: %w", err)
}
req.Header.Add("User-Agent", "geoipupdate/"+vars.Version)
req.SetBasicAuth(strconv.Itoa(r.accountID), r.licenseKey)

response, err := r.client.Do(req)
if err != nil {
return nil, fmt.Errorf("performing metadata request: %w", err)
}
defer response.Body.Close()

responseBody, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading metadata response body: %w", err)
}

if response.StatusCode != http.StatusOK {
httpErr := internal.HTTPError{
Body: string(responseBody),
StatusCode: response.StatusCode,
}
return nil, fmt.Errorf("unexpected HTTP status code: %w", httpErr)
}

var metadataResponse struct {
Databases []metadata `json:"databases"`
}

if err := json.Unmarshal(responseBody, &metadataResponse); err != nil {
return nil, fmt.Errorf("parsing metadata body: %w", err)
}

if len(metadataResponse.Databases) != 1 {
return nil, fmt.Errorf("response does not contain edition %s", editionID)
}

edition := metadataResponse.Databases[0]

return &edition, nil
}
Loading

0 comments on commit 2697024

Please sign in to comment.