diff --git a/pkg/externalIpProvider/plain/plain.go b/pkg/externalIpProvider/plain/plain.go new file mode 100644 index 0000000..3fed9ad --- /dev/null +++ b/pkg/externalIpProvider/plain/plain.go @@ -0,0 +1,56 @@ +package externalIpProvider + +import ( + "errors" + "net" + "net/http" + "net/url" + externalIpProvider "realdnydns/pkg/externalIpProvider" +) + +type ExternalIpProviderImplPlain struct { + IPProviderURL string + HTTPClient *http.Client +} + +type PlainExternalIpProviderConfig struct { + Url string `yaml:"url"` +} + +func New(config PlainExternalIpProviderConfig) (externalIpProvider.ExternalIpProvider, error) { + if config.Url == "" { + return nil, errors.New("url is required") + } + + return &ExternalIpProviderImplPlain{ + config.Url, + &http.Client{}, + }, nil +} + +func (p *ExternalIpProviderImplPlain) GetExternalIp() (net.IP, error) { + parsedUrl, err := url.Parse(p.IPProviderURL) + if err != nil { + return nil, err + } + + res, err := p.HTTPClient.Get(parsedUrl.String()) + if err != nil { + return nil, err + } + + if res.StatusCode != 200 { + return nil, errors.New("unexpected status code") + } + + responseBody := make([]byte, res.ContentLength) + res.Body.Read(responseBody) + defer res.Body.Close() + + parsedIp := net.ParseIP(string(responseBody)) + if parsedIp == nil { + return nil, errors.New("unable to parse ip") + } + + return parsedIp, nil +} diff --git a/pkg/externalIpProvider/plain/plain_test.go b/pkg/externalIpProvider/plain/plain_test.go new file mode 100644 index 0000000..edc7032 --- /dev/null +++ b/pkg/externalIpProvider/plain/plain_test.go @@ -0,0 +1,150 @@ +package externalIpProvider + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestNew(t *testing.T) { + t.Run("returns external ip provider", testNew()) + t.Run("returns error when url is empty", testNewEmptyUrl()) +} + +func testNew() func(t *testing.T) { + return func(t *testing.T) { + provider, err := New(PlainExternalIpProviderConfig{ + Url: "http://localhost", + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + if provider == nil { + t.Fatalf("NewPlain() returned nil provider") + } + } +} + +func testNewEmptyUrl() func(t *testing.T) { + return func(t *testing.T) { + _, err := New(PlainExternalIpProviderConfig{}) + if err == nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + } +} + +func TestGetExternal(t *testing.T) { + t.Run("returns external ip", testGetExternalIp()) + t.Run("returns error when unable to parse ip", testGetExternalIpUnparsableIp()) + t.Run("returns error when unexpected status code", testGetExternalIpUnexpectedStatusCode()) + t.Run("returns error when unable to parse url", testGetExternalIpUnableToParseUrl()) + t.Run("returns error when unable to get", testGetExternalIpUnableToGet()) +} + +func testGetExternalIp() func(t *testing.T) { + return func(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("127.0.0.1")) + }, + )) + defer mockServer.Close() + + provider, err := New(PlainExternalIpProviderConfig{ + Url: mockServer.URL, + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + ip, err := provider.GetExternalIp() + if err != nil { + t.Fatalf("GetExternalIp() returned unexpected error: %v", err) + } + + if ip.String() != "127.0.0.1" { + t.Fatalf("GetExternalIp() returned unexpected ip: %v instead of 127.0.0.1", ip) + } + } +} + +func testGetExternalIpUnparsableIp() func(t *testing.T) { + return func(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("not an ip")) + }, + )) + defer mockServer.Close() + + provider, err := New(PlainExternalIpProviderConfig{ + Url: mockServer.URL, + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + _, err = provider.GetExternalIp() + if err == nil { + t.Fatalf("GetExternalIp() returned unexpected error: %v", err) + } + } +} + +func testGetExternalIpUnexpectedStatusCode() func(t *testing.T) { + return func(t *testing.T) { + mockServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + }, + )) + defer mockServer.Close() + + provider, err := New(PlainExternalIpProviderConfig{ + Url: mockServer.URL, + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + _, err = provider.GetExternalIp() + if err == nil { + t.Fatalf("GetExternalIp() returned unexpected error: %v", err) + } + } +} + +func testGetExternalIpUnableToParseUrl() func(t *testing.T) { + return func(t *testing.T) { + provider, err := New(PlainExternalIpProviderConfig{ + Url: "not a url !'ยง%&", + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + _, err = provider.GetExternalIp() + if err == nil { + t.Fatalf("GetExternalIp() returned unexpected error: %v", err) + } + } +} + +func testGetExternalIpUnableToGet() func(t *testing.T) { + return func(t *testing.T) { + // force error by using a non-responding url, beware of side effects + provider, err := New(PlainExternalIpProviderConfig{ + Url: "http://localhost:1234", + }) + if err != nil { + t.Fatalf("NewPlain() returned unexpected error: %v", err) + } + + _, err = provider.GetExternalIp() + if err == nil { + t.Fatalf("GetExternalIp() returned unexpected error: %v", err) + } + } +}