github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/data/datasource_test.go (about)

     1  package data
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"net/url"
     8  	"os"
     9  	"runtime"
    10  	"testing"
    11  	"testing/fstest"
    12  
    13  	"github.com/hairyhenderson/go-fsimpl"
    14  	"github.com/hairyhenderson/go-fsimpl/httpfs"
    15  	"github.com/hairyhenderson/gomplate/v4/internal/config"
    16  	"github.com/hairyhenderson/gomplate/v4/internal/datafs"
    17  
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  const osWindows = "windows"
    23  
    24  func mustParseURL(in string) *url.URL {
    25  	u, _ := url.Parse(in)
    26  	return u
    27  }
    28  
    29  func TestNewData(t *testing.T) {
    30  	d, err := NewData(nil, nil)
    31  	require.NoError(t, err)
    32  	assert.Empty(t, d.Sources)
    33  
    34  	d, err = NewData([]string{"foo=http:///foo.json"}, nil)
    35  	require.NoError(t, err)
    36  	assert.Equal(t, "/foo.json", d.Sources["foo"].URL.Path)
    37  
    38  	d, err = NewData([]string{"foo=http:///foo.json"}, []string{})
    39  	require.NoError(t, err)
    40  	assert.Equal(t, "/foo.json", d.Sources["foo"].URL.Path)
    41  	assert.Empty(t, d.Sources["foo"].Header)
    42  
    43  	d, err = NewData([]string{"foo=http:///foo.json"}, []string{"bar=Accept: blah"})
    44  	require.NoError(t, err)
    45  	assert.Equal(t, "/foo.json", d.Sources["foo"].URL.Path)
    46  	assert.Empty(t, d.Sources["foo"].Header)
    47  
    48  	d, err = NewData([]string{"foo=http:///foo.json"}, []string{"foo=Accept: blah"})
    49  	require.NoError(t, err)
    50  	assert.Equal(t, "/foo.json", d.Sources["foo"].URL.Path)
    51  	assert.Equal(t, "blah", d.Sources["foo"].Header["Accept"][0])
    52  }
    53  
    54  func TestDatasource(t *testing.T) {
    55  	setup := func(ext string, contents []byte) *Data {
    56  		fname := "foo." + ext
    57  		var uPath string
    58  		if runtime.GOOS == osWindows {
    59  			uPath = "C:/tmp/" + fname
    60  		} else {
    61  			uPath = "/tmp/" + fname
    62  		}
    63  
    64  		fsys := datafs.WrapWdFS(fstest.MapFS{
    65  			"tmp/" + fname: &fstest.MapFile{Data: contents},
    66  		})
    67  		ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
    68  
    69  		sources := map[string]config.DataSource{
    70  			"foo": {
    71  				URL: &url.URL{Scheme: "file", Path: uPath},
    72  			},
    73  		}
    74  		return &Data{Sources: sources, Ctx: ctx}
    75  	}
    76  
    77  	test := func(ext, mime string, contents []byte, expected interface{}) {
    78  		data := setup(ext, contents)
    79  
    80  		actual, err := data.Datasource("foo", "?type="+mime)
    81  		require.NoError(t, err)
    82  		assert.Equal(t, expected, actual)
    83  	}
    84  
    85  	testObj := func(ext, mime string, contents []byte) {
    86  		test(ext, mime, contents,
    87  			map[string]interface{}{
    88  				"hello": map[string]interface{}{"cruel": "world"},
    89  			})
    90  	}
    91  
    92  	testObj("json", jsonMimetype, []byte(`{"hello":{"cruel":"world"}}`))
    93  	testObj("yml", yamlMimetype, []byte("hello:\n  cruel: world\n"))
    94  	test("json", jsonMimetype, []byte(`[1, "two", true]`),
    95  		[]interface{}{1, "two", true})
    96  	test("yaml", yamlMimetype, []byte("---\n- 1\n- two\n- true\n"),
    97  		[]interface{}{1, "two", true})
    98  
    99  	d := setup("", nil)
   100  	actual, err := d.Datasource("foo")
   101  	require.NoError(t, err)
   102  	assert.Equal(t, "", actual)
   103  
   104  	_, err = d.Datasource("bar")
   105  	require.Error(t, err)
   106  }
   107  
   108  func TestDatasourceReachable(t *testing.T) {
   109  	fname := "foo.json"
   110  	var uPath string
   111  	if runtime.GOOS == osWindows {
   112  		uPath = "C:/tmp/" + fname
   113  	} else {
   114  		uPath = "/tmp/" + fname
   115  	}
   116  
   117  	fsys := datafs.WrapWdFS(fstest.MapFS{
   118  		"tmp/" + fname: &fstest.MapFile{Data: []byte("{}")},
   119  	})
   120  	ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
   121  
   122  	sources := map[string]config.DataSource{
   123  		"foo": {
   124  			URL: &url.URL{Scheme: "file", Path: uPath},
   125  		},
   126  		"bar": {
   127  			URL: &url.URL{Scheme: "file", Path: "/bogus"},
   128  		},
   129  	}
   130  	data := &Data{Sources: sources, Ctx: ctx}
   131  
   132  	assert.True(t, data.DatasourceReachable("foo"))
   133  	assert.False(t, data.DatasourceReachable("bar"))
   134  }
   135  
   136  func TestDatasourceExists(t *testing.T) {
   137  	sources := map[string]config.DataSource{
   138  		"foo": {},
   139  	}
   140  	data := &Data{Sources: sources}
   141  	assert.True(t, data.DatasourceExists("foo"))
   142  	assert.False(t, data.DatasourceExists("bar"))
   143  }
   144  
   145  func TestInclude(t *testing.T) {
   146  	ext := "txt"
   147  	contents := "hello world"
   148  	fname := "foo." + ext
   149  
   150  	var uPath string
   151  	if runtime.GOOS == osWindows {
   152  		uPath = "C:/tmp/" + fname
   153  	} else {
   154  		uPath = "/tmp/" + fname
   155  	}
   156  
   157  	fsys := datafs.WrapWdFS(fstest.MapFS{
   158  		"tmp/" + fname: &fstest.MapFile{Data: []byte(contents)},
   159  	})
   160  	ctx := datafs.ContextWithFSProvider(context.Background(), datafs.WrappedFSProvider(fsys, "file", ""))
   161  
   162  	sources := map[string]config.DataSource{
   163  		"foo": {
   164  			URL: &url.URL{Scheme: "file", Path: uPath},
   165  		},
   166  	}
   167  	data := &Data{Sources: sources, Ctx: ctx}
   168  	actual, err := data.Include("foo")
   169  	require.NoError(t, err)
   170  	assert.Equal(t, contents, actual)
   171  }
   172  
   173  func TestDefineDatasource(t *testing.T) {
   174  	d := &Data{}
   175  	_, err := d.DefineDatasource("", "foo.json")
   176  	require.Error(t, err)
   177  
   178  	d = &Data{}
   179  	_, err = d.DefineDatasource("", "../foo.json")
   180  	require.Error(t, err)
   181  
   182  	d = &Data{}
   183  	_, err = d.DefineDatasource("", "ftp://example.com/foo.yml")
   184  	require.Error(t, err)
   185  
   186  	d = &Data{}
   187  	_, err = d.DefineDatasource("data", "foo.json")
   188  	s := d.Sources["data"]
   189  	require.NoError(t, err)
   190  	assert.EqualValues(t, &url.URL{Path: "foo.json"}, s.URL)
   191  
   192  	d = &Data{}
   193  	_, err = d.DefineDatasource("data", "/otherdir/foo.json")
   194  	s = d.Sources["data"]
   195  	require.NoError(t, err)
   196  	assert.Equal(t, "file", s.URL.Scheme)
   197  	assert.True(t, s.URL.IsAbs())
   198  	assert.Equal(t, "/otherdir/foo.json", s.URL.Path)
   199  
   200  	d = &Data{}
   201  	_, err = d.DefineDatasource("data", "sftp://example.com/blahblah/foo.json")
   202  	s = d.Sources["data"]
   203  	require.NoError(t, err)
   204  	assert.Equal(t, "sftp", s.URL.Scheme)
   205  	assert.True(t, s.URL.IsAbs())
   206  	assert.Equal(t, "/blahblah/foo.json", s.URL.Path)
   207  
   208  	d = &Data{
   209  		Sources: map[string]config.DataSource{
   210  			"data": {},
   211  		},
   212  	}
   213  	_, err = d.DefineDatasource("data", "/otherdir/foo.json")
   214  	s = d.Sources["data"]
   215  	require.NoError(t, err)
   216  	assert.Nil(t, s.URL)
   217  
   218  	d = &Data{}
   219  	_, err = d.DefineDatasource("data", "/otherdir/foo?type=application/x-env")
   220  	require.NoError(t, err)
   221  	s = d.Sources["data"]
   222  	require.NotNil(t, s)
   223  	assert.Equal(t, "/otherdir/foo", s.URL.Path)
   224  }
   225  
   226  func TestFromConfig(t *testing.T) {
   227  	ctx := context.Background()
   228  
   229  	cfg := &config.Config{}
   230  	actual := FromConfig(ctx, cfg)
   231  	expected := &Data{
   232  		Ctx:     actual.Ctx,
   233  		Sources: map[string]config.DataSource{},
   234  	}
   235  	assert.EqualValues(t, expected, actual)
   236  
   237  	cfg = &config.Config{
   238  		DataSources: map[string]config.DataSource{
   239  			"foo": {
   240  				URL: mustParseURL("http://example.com"),
   241  			},
   242  		},
   243  	}
   244  	actual = FromConfig(ctx, cfg)
   245  	expected = &Data{
   246  		Ctx: actual.Ctx,
   247  		Sources: map[string]config.DataSource{
   248  			"foo": {
   249  				URL: mustParseURL("http://example.com"),
   250  			},
   251  		},
   252  	}
   253  	assert.EqualValues(t, expected, actual)
   254  
   255  	cfg = &config.Config{
   256  		DataSources: map[string]config.DataSource{
   257  			"foo": {
   258  				URL: mustParseURL("http://foo.com"),
   259  			},
   260  		},
   261  		Context: map[string]config.DataSource{
   262  			"bar": {
   263  				URL: mustParseURL("http://bar.com"),
   264  				Header: http.Header{
   265  					"Foo": []string{"bar"},
   266  				},
   267  			},
   268  		},
   269  		ExtraHeaders: map[string]http.Header{
   270  			"baz": {
   271  				"Foo": []string{"bar"},
   272  			},
   273  		},
   274  	}
   275  	actual = FromConfig(ctx, cfg)
   276  	expected = &Data{
   277  		Ctx: actual.Ctx,
   278  		Sources: map[string]config.DataSource{
   279  			"foo": {
   280  				URL: mustParseURL("http://foo.com"),
   281  			},
   282  			"bar": {
   283  				URL: mustParseURL("http://bar.com"),
   284  				Header: http.Header{
   285  					"Foo": []string{"bar"},
   286  				},
   287  			},
   288  		},
   289  		ExtraHeaders: map[string]http.Header{
   290  			"baz": {
   291  				"Foo": []string{"bar"},
   292  			},
   293  		},
   294  	}
   295  	assert.EqualValues(t, expected, actual)
   296  }
   297  
   298  func TestListDatasources(t *testing.T) {
   299  	sources := map[string]config.DataSource{
   300  		"foo": {},
   301  		"bar": {},
   302  	}
   303  	data := &Data{Sources: sources}
   304  
   305  	assert.Equal(t, []string{"bar", "foo"}, data.ListDatasources())
   306  }
   307  
   308  func TestResolveURL(t *testing.T) {
   309  	out, err := resolveURL(mustParseURL("http://example.com/foo.json"), "bar.json")
   310  	require.NoError(t, err)
   311  	assert.Equal(t, "http://example.com/bar.json", out.String())
   312  
   313  	out, err = resolveURL(mustParseURL("http://example.com/a/b/?n=2"), "bar.json?q=1")
   314  	require.NoError(t, err)
   315  	assert.Equal(t, "http://example.com/a/b/bar.json?n=2&q=1", out.String())
   316  
   317  	out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo"), "//myfile?type=application/json")
   318  	require.NoError(t, err)
   319  	assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
   320  
   321  	out, err = resolveURL(mustParseURL("git+file:///tmp/foo/bar/"), "//myfile?type=application/json")
   322  	require.NoError(t, err)
   323  	assert.Equal(t, "git+file:///tmp/foo/bar//myfile?type=application/json", out.String())
   324  
   325  	out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo/"), ".//myfile?type=application/json")
   326  	require.NoError(t, err)
   327  	assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
   328  
   329  	out, err = resolveURL(mustParseURL("git+file:///tmp/repo//foo.txt"), "")
   330  	require.NoError(t, err)
   331  	assert.Equal(t, "git+file:///tmp/repo//foo.txt", out.String())
   332  
   333  	out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo"), ".//myfile?type=application/json")
   334  	require.NoError(t, err)
   335  	assert.Equal(t, "git+file:///tmp/myrepo//myfile?type=application/json", out.String())
   336  
   337  	out, err = resolveURL(mustParseURL("git+file:///tmp/myrepo//foo/?type=application/json"), "bar/myfile")
   338  	require.NoError(t, err)
   339  	// note that the '/' in the query string is encoded to %2F - that's OK
   340  	assert.Equal(t, "git+file:///tmp/myrepo//foo/bar/myfile?type=application%2Fjson", out.String())
   341  
   342  	// both base and relative may not contain "//"
   343  	_, err = resolveURL(mustParseURL("git+ssh://git@example.com/foo//bar"), ".//myfile")
   344  	require.Error(t, err)
   345  
   346  	_, err = resolveURL(mustParseURL("git+ssh://git@example.com/foo//bar"), "baz//myfile")
   347  	require.Error(t, err)
   348  
   349  	// relative urls must remain relative
   350  	out, err = resolveURL(mustParseURL("tmp/foo.json"), "")
   351  	require.NoError(t, err)
   352  	assert.Equal(t, "tmp/foo.json", out.String())
   353  }
   354  
   355  func TestReadFileContent(t *testing.T) {
   356  	wd, _ := os.Getwd()
   357  	t.Cleanup(func() {
   358  		_ = os.Chdir(wd)
   359  	})
   360  	_ = os.Chdir("/")
   361  
   362  	mux := http.NewServeMux()
   363  	mux.HandleFunc("/foo.json", func(w http.ResponseWriter, _ *http.Request) {
   364  		w.Header().Set("Content-Type", jsonMimetype)
   365  		w.Write([]byte(`{"foo": "bar"}`))
   366  	})
   367  
   368  	srv := httptest.NewServer(mux)
   369  	t.Cleanup(srv.Close)
   370  
   371  	fsys := datafs.WrapWdFS(fstest.MapFS{
   372  		"foo.json":          &fstest.MapFile{Data: []byte(`{"foo": "bar"}`)},
   373  		"dir/1.yaml":        &fstest.MapFile{Data: []byte(`foo: bar`)},
   374  		"dir/2.yaml":        &fstest.MapFile{Data: []byte(`baz: qux`)},
   375  		"dir/sub/sub1.yaml": &fstest.MapFile{Data: []byte(`quux: corge`)},
   376  	})
   377  
   378  	fsp := fsimpl.NewMux()
   379  	fsp.Add(httpfs.FS)
   380  	fsp.Add(datafs.WrappedFSProvider(fsys, "file", ""))
   381  
   382  	ctx := datafs.ContextWithFSProvider(context.Background(), fsp)
   383  
   384  	d := Data{}
   385  
   386  	fc, err := d.readFileContent(ctx, mustParseURL("file:///foo.json"), nil)
   387  	require.NoError(t, err)
   388  	assert.Equal(t, []byte(`{"foo": "bar"}`), fc.b)
   389  
   390  	fc, err = d.readFileContent(ctx, mustParseURL("dir/"), nil)
   391  	require.NoError(t, err)
   392  	assert.JSONEq(t, `["1.yaml", "2.yaml", "sub"]`, string(fc.b))
   393  
   394  	fc, err = d.readFileContent(ctx, mustParseURL(srv.URL+"/foo.json"), nil)
   395  	require.NoError(t, err)
   396  	assert.Equal(t, []byte(`{"foo": "bar"}`), fc.b)
   397  }