github.com/deadlysurgeon/weather@v0.0.0-20240402201029-3925d9f784b1/weather/impl_test.go (about)

     1  package weather
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"strings"
     9  	"testing"
    10  )
    11  
    12  func TestFeelsLike(t *testing.T) {
    13  	for name, test := range map[string]float32{
    14  		"freezing": -4,
    15  		"cold":     5,
    16  		"moderate": 11,
    17  		"hot":      22,
    18  		"burning":  42,
    19  	} {
    20  		test := test
    21  		t.Run(name, func(t *testing.T) {
    22  			if s := feelsLike(test); s != name {
    23  				t.Fatalf("Expected %s got %s", name, s)
    24  			}
    25  		})
    26  	}
    27  }
    28  
    29  func TestFormRequest(t *testing.T) {
    30  	for name, test := range map[string]struct {
    31  		url         string
    32  		expectError bool
    33  	}{
    34  		"good url": {
    35  			url: "https://example.com",
    36  		},
    37  		"bad url": {
    38  			url:         "http://%41:8080/",
    39  			expectError: true,
    40  		},
    41  	} {
    42  		test := test
    43  		t.Run(name, func(t *testing.T) {
    44  			_ = test
    45  			_, err := (&impl{endpoint: test.url}).formRequest("", "")
    46  			if (err != nil) != test.expectError {
    47  				t.Fatalf("Expected error (%v) got %v", test.expectError, err)
    48  			}
    49  		})
    50  	}
    51  }
    52  
    53  func TestWeatherAt(t *testing.T) {
    54  	for name, test := range map[string]struct {
    55  		lat, lon       string
    56  		mockResponse   string
    57  		endpoint       string
    58  		mockStatusCode int
    59  		clientErr      error
    60  		wantErr        bool
    61  		wantReport     Report
    62  	}{
    63  		"successful report": {
    64  			lat:            "0",
    65  			lon:            "0",
    66  			mockResponse:   `{"current":{"feels_like":25.3,"weather":[{"main":"Clear"}]}}`,
    67  			mockStatusCode: http.StatusOK,
    68  			wantErr:        false,
    69  			wantReport: Report{
    70  				Condition:      "Clear",
    71  				Temperature:    "hot",
    72  				TemperatureRaw: 25.3,
    73  			},
    74  		},
    75  		"bad status code": {
    76  			lat:            "invalid",
    77  			lon:            "invalid",
    78  			mockResponse:   `Bad Request`,
    79  			mockStatusCode: http.StatusBadRequest,
    80  			wantErr:        true,
    81  		},
    82  		"invalid JSON response": {
    83  			lat:            "0",
    84  			lon:            "0",
    85  			mockResponse:   `Invalid JSON`,
    86  			mockStatusCode: http.StatusOK,
    87  			wantErr:        true,
    88  		},
    89  		"do client error": {
    90  			lat:            "0",
    91  			lon:            "0",
    92  			clientErr:      fmt.Errorf("client error"),
    93  			mockStatusCode: http.StatusOK,
    94  			wantErr:        true,
    95  		},
    96  		"bad endpoint": {
    97  			lat:      "0",
    98  			lon:      "0",
    99  			endpoint: "http://%41:8080/",
   100  			wantErr:  true,
   101  		},
   102  	} {
   103  		t.Run(name, func(t *testing.T) {
   104  			// Setup mock HTTP server
   105  			server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   106  				w.WriteHeader(test.mockStatusCode)
   107  				fmt.Fprintln(w, test.mockResponse)
   108  			}))
   109  			defer server.Close()
   110  
   111  			// Create an instance of `impl` with a mocked client
   112  			s := &impl{
   113  				endpoint: test.endpoint,
   114  				client: mockHTTPClient(func(req *http.Request) *http.Response {
   115  					return &http.Response{
   116  						StatusCode: test.mockStatusCode,
   117  						Body:       io.NopCloser(strings.NewReader(test.mockResponse)),
   118  						Header:     make(http.Header),
   119  					}
   120  				}, test.clientErr),
   121  			}
   122  
   123  			// Execute WeatherAt
   124  			gotReport, err := s.At(test.lat, test.lon)
   125  			if (err != nil) != test.wantErr {
   126  				t.Errorf("WeatherAt() error = %v, wantErr %v", err, test.wantErr)
   127  				return
   128  			}
   129  
   130  			if !test.wantErr && !compareReports(gotReport, test.wantReport) {
   131  				t.Errorf("WeatherAt() gotReport = %v, want %v", gotReport, test.wantReport)
   132  			}
   133  		})
   134  	}
   135  }
   136  
   137  type mockTransport struct {
   138  	URL string
   139  }
   140  
   141  func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   142  	req.URL.Scheme = "http"  // Override scheme to match mock server
   143  	req.URL.Host = m.URL[7:] // Remove "http://" from mock server URL
   144  	return http.DefaultTransport.RoundTrip(req)
   145  }
   146  
   147  func compareReports(a, b Report) bool {
   148  	return a.Condition == b.Condition &&
   149  		a.Temperature == b.Temperature &&
   150  		a.TemperatureRaw == b.TemperatureRaw
   151  }
   152  
   153  // mockRoundTripper mocks the RoundTrip function for http.Client
   154  type mockRoundTripper struct {
   155  	fun func(req *http.Request) *http.Response
   156  	err error
   157  }
   158  
   159  // RoundTrip executes the mock round trip function
   160  func (m mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
   161  	return m.fun(req), m.err
   162  }
   163  
   164  // mockHTTPClient helps in setting up a client with a mock transport
   165  func mockHTTPClient(fn func(req *http.Request) *http.Response, err error) *http.Client {
   166  	mrt := &mockRoundTripper{
   167  		fun: fn,
   168  		err: err,
   169  	}
   170  	return &http.Client{
   171  		Transport: mrt,
   172  	}
   173  }