package main

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"math/big"
	"net"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/netutil"
)

func TestRequestHandler(t *testing.T) {
	f := func(cfgStr, requestURL string, backendHandler http.HandlerFunc, responseExpected string) {
		t.Helper()

		ts := httptest.NewServer(backendHandler)
		defer ts.Close()

		cfgStr = strings.ReplaceAll(cfgStr, "{BACKEND}", ts.URL)
		responseExpected = strings.ReplaceAll(responseExpected, "{BACKEND}", ts.URL)

		cfgOrigP := authConfigData.Load()
		if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
			t.Fatalf("cannot load config data: %s", err)
		}
		defer func() {
			cfgOrig := []byte("unauthorized_user:\n  url_prefix: http://foo/bar")
			if cfgOrigP != nil {
				cfgOrig = *cfgOrigP
			}
			_, err := reloadAuthConfigData(cfgOrig)
			if err != nil {
				t.Fatalf("cannot load the original config: %s", err)
			}
		}()

		r, err := http.NewRequest(http.MethodGet, requestURL, nil)
		if err != nil {
			t.Fatalf("cannot initialize http request: %s", err)
		}

		r.RequestURI = r.URL.RequestURI()
		r.RemoteAddr = "42.2.3.84:6789"
		r.Header.Set("X-Forwarded-For", "12.34.56.78")
		r.Header.Set("Connection", "Some-Header,Other-Header")
		r.Header.Set("Some-Header", "foobar")
		r.Header.Set("Pass-Header", "abc")

		w := &fakeResponseWriter{}
		if !requestHandlerWithInternalRoutes(w, r) {
			t.Fatalf("unexpected false is returned from requestHandler")
		}

		response := w.getResponse()
		response = strings.ReplaceAll(response, "\r\n", "\n")
		response = strings.TrimSpace(response)
		responseExpected = strings.TrimSpace(responseExpected)
		if response != responseExpected {
			t.Fatalf("unexpected response\ngot\n%s\nwant\n%s", response, responseExpected)
		}
	}

	// regular url_prefix
	cfgStr := `
unauthorized_user:
  url_prefix: {BACKEND}/foo?bar=baz`
	requestURL := "http://some-host.com/abc/def?some_arg=some_value"
	backendHandler := func(w http.ResponseWriter, r *http.Request) {
		h := w.Header()
		h.Set("Connection", "close")
		h.Set("Foo", "bar")

		var bb bytes.Buffer
		if err := r.Header.Write(&bb); err != nil {
			panic(fmt.Errorf("unexpected error when marshaling headers: %w", err))
		}
		fmt.Fprintf(w, "requested_url=http://%s%s\n%s", r.Host, r.URL, bb.String())
	}
	responseExpected := `
statusCode=200
Foo: bar
requested_url={BACKEND}/foo/abc/def?bar=baz&some_arg=some_value
Pass-Header: abc
User-Agent: vmauth
X-Forwarded-For: 12.34.56.78, 42.2.3.84`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// with default_url
	cfgStr = `
unauthorized_user:
  default_url: {BACKEND}/default
  url_map:
  - src_paths:
    - /empty
    url_prefix: {BACKEND}/empty`
	requestURL = "http://some-host.com/abc/def?some_arg=some_value"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		h := w.Header()
		h.Set("Connection", "close")
		h.Set("Foo", "bar")

		var bb bytes.Buffer
		if err := r.Header.Write(&bb); err != nil {
			panic(fmt.Errorf("unexpected error when marshaling headers: %w", err))
		}
		fmt.Fprintf(w, "requested_url=http://%s%s\n%s", r.Host, r.URL, bb.String())
	}
	responseExpected = `
statusCode=200
Foo: bar
requested_url={BACKEND}/default?request_path=http%3A%2F%2Fsome-host.com%2Fabc%2Fdef%3Fsome_arg%3Dsome_value
Pass-Header: abc
User-Agent: vmauth
X-Forwarded-For: 12.34.56.78, 42.2.3.84`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// routing of all failed to authorize requests to unauthorized_user (issue #7543)
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo"
  keep_original_host: true`
	requestURL = "http://foo:invalid-secret@some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url=http://some-host.com/foo/abc/def`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// keep_original_host
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo?bar=baz"
  keep_original_host: true`
	requestURL = "http://some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url=http://some-host.com/foo/abc/def?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// override user-agent header
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo?bar=baz"
  headers:
  - "User-Agent: foobar"`
	requestURL = "http://some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s\nUser-Agent=%s", r.Host, r.URL, r.Header.Get("User-Agent"))
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo/abc/def?bar=baz
User-Agent=foobar`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// delete user-agent header
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo?bar=baz"
  headers:
  - "User-Agent:"`
	requestURL = "http://some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s\nUser-Agent=%s", r.Host, r.URL, r.Header.Get("User-Agent"))
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo/abc/def?bar=baz
User-Agent=Go-http-client/1.1`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// override request host with non-empty host
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo?bar=baz"
  headers:
  - "Host: other-host:12345"
  - "abc:"`
	requestURL = "http://some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url=http://other-host:12345/foo/abc/def?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// override request host with empty host
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo?bar=baz"
  headers:
  - "Host:"`
	requestURL = "http://some-host.com/abc/def"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo/abc/def?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// /-/reload handler failure
	origAuthKey := reloadAuthKey.Get()
	if err := reloadAuthKey.Set("secret"); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}
	cfgStr = `
unauthorized_user:
  url_prefix: "{BACKEND}/foo"`
	requestURL = "http://some-host.com/-/reload"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=401
Expected to receive non-empty authKey when -reloadAuthKey is set`
	f(cfgStr, requestURL, backendHandler, responseExpected)
	if err := reloadAuthKey.Set(origAuthKey); err != nil {
		t.Fatalf("unexpected error: %s", err)
	}

	// missing authorization
	cfgStr = `
users:
- username: foo
  url_prefix: "{BACKEND}/bar"`
	requestURL = "http://some-host.com/a/b"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=401
Www-Authenticate: Basic realm="Restricted"
missing 'Authorization' request header`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// incorrect authorization
	cfgStr = `
users:
- username: foo
  password: secret
  url_prefix: "{BACKEND}/bar"`
	requestURL = "http://foo:invalid-secret@some-host.com/a/b"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=401
Unauthorized`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// incorrect authorization with logging invalid auth tokens
	origLogInvalidAuthTokens := *logInvalidAuthTokens
	*logInvalidAuthTokens = true
	cfgStr = `
users:
- username: foo
  password: secret
  url_prefix: "{BACKEND}/bar"`
	requestURL = "http://foo:invalid-secret@some-host.com/a/b?c=d"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=401
cannot authorize request with auth tokens ["http_auth:Basic Zm9vOmludmFsaWQtc2VjcmV0"]`
	f(cfgStr, requestURL, backendHandler, responseExpected)
	*logInvalidAuthTokens = origLogInvalidAuthTokens

	// correct authorization
	cfgStr = `
users:
- username: foo
  password: secret
  url_prefix: "{BACKEND}/bar"`
	requestURL = "http://foo:secret@some-host.com/a/b"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/bar/a/b`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify how path cleanup works
	cfgStr = `
unauthorized_user:
  url_prefix: {BACKEND}/foo?bar=baz`
	requestURL = "http://some-host.com/../../a//.///bar/"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo/a/bar/?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify how path cleanup works for url without path
	cfgStr = `
unauthorized_user:
  url_prefix: {BACKEND}/foo?bar=baz`
	requestURL = "http://some-host.com/"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify how path cleanup works for url without path if url_prefix path ends with /
	cfgStr = `
unauthorized_user:
  url_prefix: {BACKEND}/foo/?bar=baz`
	requestURL = "http://some-host.com/"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/foo/?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify how path cleanup works for url without path and the url_prefix without path prefix
	cfgStr = `
unauthorized_user:
  url_prefix: {BACKEND}/?bar=baz`
	requestURL = "http://some-host.com/"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/?bar=baz`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify routing to default_url
	cfgStr = `
unauthorized_user:
  url_map:
  - src_paths: ["/foo/.+"]
    url_prefix: {BACKEND}/x-foo/
  default_url: {BACKEND}/404.html`
	requestURL = "http://some-host.com/abc?de=fg"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/404.html?request_path=http%3A%2F%2Fsome-host.com%2Fabc%3Fde%3Dfg`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// verify routing to default url_prefix
	cfgStr = `
unauthorized_user:
  url_map:
  - src_paths: ["/foo/.+"]
    url_prefix: {BACKEND}/x-foo/
  url_prefix: {BACKEND}/default`
	requestURL = "http://some-host.com/abc?de=fg"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/default/abc?de=fg`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// missing default_url and default url_prefix for unauthorized user
	cfgStr = `
unauthorized_user:
  url_map:
  - src_paths: ["/foo/.+"]
    url_prefix: {BACKEND}/x-foo/`
	requestURL = "http://some-host.com/abc?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=400
missing route for "http://some-host.com/abc?de=fg"`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// missing default_url and default url_prefix for unauthorized user with dump_request_on_errors enabled
	cfgStr = `
unauthorized_user:
  dump_request_on_errors: true
  url_map:
  - src_paths: ["/foo/.+"]
    url_prefix: {BACKEND}/x-foo/`
	requestURL = "http://some-host.com/abc?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=400
missing route for "http://some-host.com/abc?de=fg" (host: "some-host.com"; path: "/abc"; args: "de=fg"; headers:Connection: Some-Header,Other-Header
Pass-Header: abc
Some-Header: foobar
X-Forwarded-For: 12.34.56.78
)`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// missing default_url and default url_prefix for unauthorized user when there are configs for authorized users
	cfgStr = `
users:
- username: some-user
  url_map:
  - src_paths: ["/foo/.+"]
    url_prefix: {BACKEND}/x-foo/
unauthorized_user:
  url_map:
  - src_paths: ["/abc/.*"]
    url_prefix: {BACKEND}/x-bar`
	requestURL = "http://some-host.com/abc?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=401
Www-Authenticate: Basic realm="Restricted"
missing 'Authorization' request header`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// all the backend_urls are unavailable for unauthorized user
	cfgStr = `
unauthorized_user:
  url_map:
  - src_paths: ["/foo/.*"]
    url_prefix:
    - http://127.0.0.1:1/
    - http://127.0.0.1:2/`
	requestURL = "http://some-host.com/foo/?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=502
all the 2 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// all the backend_urls are unavailable for authorized user
	cfgStr = `
users:
- username: some-user
  url_map:
  - src_paths: ["/foo/.*"]
    url_prefix:
    - http://127.0.0.1:1/
    - http://127.0.0.1:2/`
	requestURL = "http://some-user@some-host.com/foo/?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=502
all the 2 backends for the user "some-user" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
	f(cfgStr, requestURL, backendHandler, responseExpected)

	// zero discovered backend IPs
	customResolver := &fakeResolver{
		Resolver: &net.Resolver{},
		lookupIPAddrResults: map[string][]net.IPAddr{
			"some-addr": {},
		},
	}
	origResolver := netutil.Resolver
	netutil.Resolver = customResolver
	cfgStr = `
unauthorized_user:
  url_prefix: ['http://some-addr:1234/foo/bar']
  discover_backend_ips: true`
	requestURL = "http://abc.com/def/?de=fg"
	backendHandler = func(_ http.ResponseWriter, _ *http.Request) {
		panic(fmt.Errorf("backend handler shouldn't be called"))
	}
	responseExpected = `
statusCode=502
all the 0 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
	f(cfgStr, requestURL, backendHandler, responseExpected)
	netutil.Resolver = origResolver

	// retry_status_codes failure
	var retries atomic.Int64
	cfgStr = `
unauthorized_user:
  url_prefix: ['{BACKEND}/path1', '{BACKEND}/path2']
  retry_status_codes: [500, 502]`
	requestURL = "http://some-host.com/foo/?de=fg"
	backendHandler = func(w http.ResponseWriter, _ *http.Request) {
		retries.Add(1)
		w.WriteHeader(500)
	}
	responseExpected = `
statusCode=502
all the 2 backends for the user "" are unavailable for proxying the request - check previous WARN logs to see the exact error for each failed backend`
	f(cfgStr, requestURL, backendHandler, responseExpected)
	if n := retries.Load(); n != 2 {
		t.Fatalf("unexpected number of retries; got %d; want 2", n)
	}

	// retry_status_codes success
	retries.Store(0)
	cfgStr = `
unauthorized_user:
  url_prefix: ['{BACKEND}/path1', '{BACKEND}/path2']
  retry_status_codes: [500, 502]`
	requestURL = "http://some-host.com/foo/?de=fg"
	backendHandler = func(w http.ResponseWriter, r *http.Request) {
		if n := retries.Add(1); n < 2 {
			w.WriteHeader(500)
			return
		}
		fmt.Fprintf(w, "requested_url=http://%s%s", r.Host, r.URL)
	}
	responseExpected = `
statusCode=200
requested_url={BACKEND}/path2/foo/?de=fg`
	f(cfgStr, requestURL, backendHandler, responseExpected)
	if n := retries.Load(); n != 2 {
		t.Fatalf("unexpected number of retries; got %d; want 2", n)
	}
}

func TestJWTRequestHandler(t *testing.T) {
	// Generate RSA key pair for testing
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("cannot generate RSA key: %s", err)
	}

	// Generate public key PEM
	publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
	if err != nil {
		t.Fatalf("cannot marshal public key: %s", err)
	}
	publicKeyPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "PUBLIC KEY",
		Bytes: publicKeyBytes,
	})

	genToken := func(t *testing.T, body map[string]any, valid bool) string {
		t.Helper()

		headerJSON, err := json.Marshal(map[string]any{
			"alg": "RS256",
			"typ": "JWT",
		})
		if err != nil {
			t.Fatalf("cannot marshal header: %s", err)
		}
		headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)

		bodyJSON, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("cannot marshal body: %s", err)
		}
		bodyB64 := base64.RawURLEncoding.EncodeToString(bodyJSON)

		payload := headerB64 + "." + bodyB64

		var signatureB64 string
		if valid {
			// Create real RSA signature
			hash := crypto.SHA256
			h := hash.New()
			h.Write([]byte(payload))
			digest := h.Sum(nil)

			signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, hash, digest)
			if err != nil {
				t.Fatalf("cannot sign token: %s", err)
			}
			signatureB64 = base64.RawURLEncoding.EncodeToString(signature)
		} else {
			signatureB64 = base64.RawURLEncoding.EncodeToString([]byte("invalid_signature"))
		}

		return payload + "." + signatureB64
	}

	f := func(cfgStr string, r *http.Request, responseExpected string) {
		t.Helper()

		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if _, err := w.Write([]byte("path: " + r.URL.Path + "\n")); err != nil {
				panic(fmt.Errorf("cannot write response: %w", err))
			}
			if _, err := w.Write([]byte("query:\n")); err != nil {
				panic(fmt.Errorf("cannot write response: %w", err))
			}
			names := make([]string, 0, len(r.URL.Query()))
			query := r.URL.Query()
			for n := range query {
				names = append(names, n)
			}
			sort.Strings(names)
			for _, n := range names {
				for _, v := range query[n] {
					if _, err := w.Write([]byte("    " + n + "=" + v + "\n")); err != nil {
						panic(fmt.Errorf("cannot write response: %w", err))
					}
				}
			}

			if _, err := w.Write([]byte("headers:\n")); err != nil {
				panic(fmt.Errorf("cannot write response: %w", err))
			}
			if v := r.Header.Get(`AccountID`); v != "" {
				if _, err := w.Write([]byte(`    AccountID=` + v + "\n")); err != nil {
					panic(fmt.Errorf("cannot write response: %w", err))
				}
			}
			if v := r.Header.Get(`ProjectID`); v != "" {
				if _, err := w.Write([]byte(`    ProjectID=` + v + "\n")); err != nil {
					panic(fmt.Errorf("cannot write response: %w", err))
				}
			}
		}))
		defer ts.Close()

		cfgStr = strings.ReplaceAll(cfgStr, "{BACKEND}", ts.URL)
		responseExpected = strings.ReplaceAll(responseExpected, "{BACKEND}", ts.URL)

		cfgOrigP := authConfigData.Load()
		if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
			t.Fatalf("cannot load config data: %s", err)
		}
		defer func() {
			cfgOrig := []byte("unauthorized_user:\n  url_prefix: http://foo/bar")
			if cfgOrigP != nil {
				cfgOrig = *cfgOrigP
			}
			_, err := reloadAuthConfigData(cfgOrig)
			if err != nil {
				t.Fatalf("cannot load the original config: %s", err)
			}
		}()

		w := &fakeResponseWriter{}
		if !requestHandlerWithInternalRoutes(w, r) {
			t.Fatalf("unexpected false is returned from requestHandler")
		}

		response := w.getResponse()
		response = strings.ReplaceAll(response, "\r\n", "\n")
		response = strings.TrimSpace(response)
		responseExpected = strings.TrimSpace(responseExpected)
		if response != responseExpected {
			t.Fatalf("unexpected response\ngot\n%s\nwant\n%s", response, responseExpected)
		}
	}

	simpleCfgStr := fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/foo`, string(publicKeyPEM))
	noVMAccessClaimToken := genToken(t, nil, true)
	minimalToken := genToken(t, map[string]any{
		"exp":       time.Now().Add(10 * time.Minute).Unix(),
		"vm_access": map[string]any{},
	}, true)
	expiredToken := genToken(t, map[string]any{
		"exp":       10,
		"vm_access": map[string]any{},
	}, true)
	invalidSignatureToken := genToken(t, map[string]any{
		"exp":       time.Now().Add(10 * time.Minute).Unix(),
		"vm_access": map[string]any{},
	}, false)

	fullToken := genToken(t, map[string]any{
		"exp": time.Now().Add(10 * time.Minute).Unix(),
		"vm_access": map[string]any{
			"metrics_account_id": 123,
			"metrics_project_id": 234,
			"metrics_extra_labels": []string{
				"label1=value1",
				"label2=value2",
			},
			"metrics_extra_filters": []string{
				`{label3="value3"}`,
				`{label4="value4"}`,
			},
			"logs_account_id": 345,
			"logs_project_id": 456,
			"logs_extra_filters": []string{
				`{"namespace":"my-app","env":"prod"}`,
			},
			"logs_extra_stream_filters": []string{
				`{"team":"dev"}`,
			},
		},
	}, true)

	// missing authorization
	request := httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	responseExpected := `
statusCode=401
Www-Authenticate: Basic realm="Restricted"
missing 'Authorization' request header`
	f(simpleCfgStr, request, responseExpected)

	// token without vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+noVMAccessClaimToken)
	responseExpected = `
statusCode=401
Unauthorized`
	f(simpleCfgStr, request, responseExpected)

	// expired token
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+expiredToken)
	responseExpected = `
statusCode=401
Unauthorized`
	f(simpleCfgStr, request, responseExpected)

	// invalid signature token
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+invalidSignatureToken)
	responseExpected = `
statusCode=401
Unauthorized`
	f(simpleCfgStr, request, responseExpected)

	// invalid signature token and skip verify
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+invalidSignatureToken)
	responseExpected = `
statusCode=200
path: /foo/abc
query:
headers:`
	f(`
users:
- jwt:
    skip_verify: true
  url_prefix: {BACKEND}/foo`, request, responseExpected)

	// token with default valid vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /foo/abc
query:
headers:`
	f(simpleCfgStr, request, responseExpected)

	// jwt token used but no matching user with JWT token in config
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=401
Unauthorized`
	f(`
users:
- password: a-password
  username: a-user
  url_prefix: {BACKEND}/foo`, request, responseExpected)

	// auth with key from file
	publicKeyFile := filepath.Join(t.TempDir(), "a_public_key.pem")
	if err := os.WriteFile(publicKeyFile, []byte(publicKeyPEM), 0o644); err != nil {
		t.Fatalf("failed to write public key file: %s", err)
	}
	request = httptest.NewRequest(`GET`, "http://some-host.com/abc", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /foo/abc
query:
headers:`
	f(fmt.Sprintf(`
users:
- jwt:
    public_key_files:
    - %q
  url_prefix: {BACKEND}/foo`, publicKeyFile), request, responseExpected)

	// ---- VictoriaMetrics specific tests ----

	// extra_label and extra_filters dropped if empty in vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /select/0:0/api/v1/query
query:
headers:`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// extra_label and extra_filters set if present in vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters={label3="value3"}
    extra_filters={label4="value4"}
    extra_label=label1=value1
    extra_label=label2=value2
headers:`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// test header injection and URL templating with individual placeholders
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123/234/api/v1/query
query:
headers:
    AccountID=123
    ProjectID=234`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsAccountID}}/{{.MetricsProjectID}}
  headers:
  - "AccountID: {{.MetricsAccountID}}"
  - "ProjectID: {{.MetricsProjectID}}"`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// extra_label and extra_filters from vm_access claim merged with statically defined
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters=aStaticFilter
    extra_filters={label3="value3"}
    extra_filters={label4="value4"}
    extra_label=aStaticLabel
    extra_label=label1=value1
    extra_label=label2=value2
headers:`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label=aStaticLabel&extra_filters=aStaticFilter&extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// extra_labels and extra_filters set from vm_access claim should override user provided query args
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query?extra_label=userProvidedLabel&extra_filters=userProvidedFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters={label3="value3"}
    extra_filters={label4="value4"}
    extra_label=label1=value1
    extra_label=label2=value2
headers:`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// merge user provided query args with extra_labels and extra_filters from vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query?extra_label=userProvidedLabel&extra_filters=userProvidedFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters={label3="value3"}
    extra_filters={label4="value4"}
    extra_filters=userProvidedFilter
    extra_label=label1=value1
    extra_label=label2=value2
    extra_label=userProvidedLabel
headers:`
	f(fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  merge_query_args: [extra_filters, extra_label]
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// pass user provided query args if vm_access claim has no extra_labels and extra_filters
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query?extra_label=userProvidedLabel&extra_filters=userProvidedFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters=userProvidedFilter
    extra_label=userProvidedLabel
headers:`
	f(fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  merge_query_args: [extra_filters, extra_label]
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// pass user provided query args if vm_access claim has no extra_labels and extra_filters
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query?extra_label=userProvidedLabel&extra_filters=userProvidedFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters=userProvidedFilter
    extra_label=userProvidedLabel
headers:`
	f(fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  url_prefix: {BACKEND}/select/{{.MetricsTenant}}/`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// placeholders in url_map
	request = httptest.NewRequest(`GET`, "http://some-host.com/api/v1/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/123:234/api/v1/query
query:
    extra_filters={label3="value3"}
    extra_filters={label4="value4"}
    extra_label=label1=value1
    extra_label=label2=value2
headers:`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_map:
    - src_paths: ["/api/.*"]
      url_prefix: {BACKEND}/select/{{.MetricsTenant}}/?extra_label={{.MetricsExtraLabels}}&extra_filters={{.MetricsExtraFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// ---- VictoriaLogs specific tests ----

	// tenant headers not overwritten if set statically
	// extra_filters extra_stream_filters dropped if empty in vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
headers:
    AccountID=555
    ProjectID=666`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: 555"
    - "ProjectID: 666"
  url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// tenant headers are overwritten if set as placeholders
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
headers:
    AccountID=0
    ProjectID=0`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// tenant headers are overwritten if set as placeholders
	// extra_filters extra_stream_filters from vm_access claim merged with statically defined
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters=aStaticFilter
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters=aStaticStreamFilter
    extra_stream_filters={"team":"dev"}
headers:
    AccountID=345
    ProjectID=456`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  url_prefix: {BACKEND}/select/logsql/?extra_filters=aStaticFilter&extra_stream_filters=aStaticStreamFilter&extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// tenant headers are overwritten if set as placeholders
	// extra_filters extra_stream_filters from vm_access claim merged with statically defined
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters=aStaticFilter
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters=aStaticStreamFilter
    extra_stream_filters={"team":"dev"}
headers:
    AccountID=345
    ProjectID=456`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  url_prefix: {BACKEND}/select/logsql/?extra_filters=aStaticFilter&extra_stream_filters=aStaticStreamFilter&extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// claim info should overwrite user provided query args and headers
	request = httptest.NewRequest(`GET`, "http://some-host.com/query?extra_filters=aUserFilter&extra_stream_filters=aUserStreamFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	request.Header.Set(`AccountID`, `aUserAccountID`)
	request.Header.Set(`ProjectID`, `aUserProjectID`)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters={"team":"dev"}
headers:
    AccountID=345
    ProjectID=456`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// merge user provided query args with extra_filters and extra_stream_filters from vm_access claim
	request = httptest.NewRequest(`GET`, "http://some-host.com/query?extra_filters=aUserFilter&extra_stream_filters=aUserStreamFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_filters=aUserFilter
    extra_stream_filters={"team":"dev"}
    extra_stream_filters=aUserStreamFilter
headers:
    AccountID=345
    ProjectID=456`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  merge_query_args: [extra_filters, extra_stream_filters]
  url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// pass user provided query args if vm_access claim has no extra_labels and extra_filters
	request = httptest.NewRequest(`GET`, "http://some-host.com/query?extra_filters=aUserFilter&extra_stream_filters=aUserStreamFilter", nil)
	request.Header.Set(`Authorization`, `Bearer `+minimalToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters=aUserFilter
    extra_stream_filters=aUserStreamFilter
headers:
    AccountID=0
    ProjectID=0`
	f(
		fmt.Sprintf(`
users:
- jwt:
    public_keys:
    - %q
  headers:
    - "AccountID: {{.LogsAccountID}}"
    - "ProjectID: {{.LogsProjectID}}"
  merge_query_args: [extra_filters, extra_stream_filters]
  url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// placeholders in url_map
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters={"team":"dev"}
headers:
    AccountID=345
    ProjectID=456`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_map:
    - src_paths: ["/query"]
      headers:
        - "AccountID: {{.LogsAccountID}}"
        - "ProjectID: {{.LogsProjectID}}"
      url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)

	// multiple placeholders in url_map for the same param
	request = httptest.NewRequest(`GET`, "http://some-host.com/query", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters={"team":"dev"}
    tenant_info=static=value
    tenant_info=345
    tenant_info=456
headers:
    AccountID=345
    ProjectID=456`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_map:
    - src_paths: ["/query"]
      headers:
        - "AccountID: {{.LogsAccountID}}"
        - "ProjectID: {{.LogsProjectID}}"
      url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}&tenant_info=static=value&tenant_info={{.LogsAccountID}}&tenant_info={{.LogsProjectID}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)
	// client request params must be ignored by placeholders
	request = httptest.NewRequest(`GET`, "http://some-host.com/query?template_attack={{.LogsExtraFilters}}", nil)
	request.Header.Set(`Authorization`, `Bearer `+fullToken)
	request.Header.Set(`AccountID`, `{{.LogsAccountID}}`)
	responseExpected = `
statusCode=200
path: /select/logsql/query
query:
    extra_filters={"namespace":"my-app","env":"prod"}
    extra_stream_filters={"team":"dev"}
    template_attack={{.LogsExtraFilters}}
headers:
    AccountID={{.LogsAccountID}}`
	f(fmt.Sprintf(
		`
users:
- jwt:
    public_keys:
    - %q
  url_map:
    - src_paths: ["/query"]
      url_prefix: {BACKEND}/select/logsql/?extra_filters={{.LogsExtraFilters}}&extra_stream_filters={{.LogsExtraStreamFilters}}`, string(publicKeyPEM)),
		request,
		responseExpected,
	)
	nestedToken := genToken(t, map[string]any{
		"exp":  time.Now().Add(10 * time.Minute).Unix(),
		"team": "dev",
		"nested": map[string]any{
			"department_id": 0,
			"scopes":        []string{"metrics", "logs"},
			"team_permissions": map[string]any{
				"read":  0,
				"write": 1,
			},
		},
		"vm_access": map[string]any{
			"metrics_account_id": 123,
			"metrics_project_id": 234,
			"metrics_extra_labels": []string{
				"label1=value1",
				"label2=value2",
			},
			"metrics_extra_filters": []string{
				`{label3="value3"}`,
				`{label4="value4"}`,
			},
			"logs_account_id": 345,
			"logs_project_id": 456,
			"logs_extra_filters": []string{
				`{"namespace":"my-app","env":"prod"}`,
			},
			"logs_extra_stream_filters": []string{
				`{"team":"dev"}`,
			},
		},
	}, true)

	// use claim for routing, must specific match wins
	request = httptest.NewRequest(`GET`, "http://some-host.com/route", nil)
	request.Header.Set(`Authorization`, `Bearer `+nestedToken)
	responseExpected = `
statusCode=200
path: /dev/route
query:
headers:
`
	f(`
users:
- jwt:
    skip_verify: true
    match_claims:
     team: dev
     nested.scopes.1: "logs"
     nested.department_id: "0"
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/dev
- jwt:
    skip_verify: true
    match_claims:
     team: dev
     nested.scopes.1: "logs"
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/ops
`,
		request,
		responseExpected,
	)

	// use claim for routing, most specific not matching
	request = httptest.NewRequest(`GET`, "http://some-host.com/route", nil)
	request.Header.Set(`Authorization`, `Bearer `+nestedToken)
	responseExpected = `
statusCode=200
path: /less_claims/route
query:
headers:
`
	f(`
users:
- jwt:
    skip_verify: true
    match_claims:
     team: ops
     nested.scopes.1: "logs"
     nested.department_id: "0"
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/more_claims
- jwt:
    skip_verify: true
    match_claims:
     team: dev
     nested.team_permissions.write: "1"
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/less_claims
`,
		request,
		responseExpected,
	)

	// use claim for routing, empty claim match
	request = httptest.NewRequest(`GET`, "http://some-host.com/route", nil)
	request.Header.Set(`Authorization`, `Bearer `+nestedToken)
	responseExpected = `
statusCode=200
path: /empty/route
query:
headers:
`
	f(`
users:
- jwt:
    skip_verify: true
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/empty
- jwt:
    skip_verify: true
    match_claims:
     team: ops
     nested.team_permissions.write: "1"
  url_map:
    - src_paths: ["/route"]
      url_prefix: {BACKEND}/ops
`,
		request,
		responseExpected,
	)

}

func TestOIDCRequestHandler(t *testing.T) {
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("cannot generate RSA key: %s", err)
	}

	var oidcSrv *httptest.Server
	oidcRespOK := atomic.Bool{}
	oidcRespOK.Store(true)

	oidcSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/openid-configuration":
			w.Header().Set("Content-Type", "application/json")
			if err := json.NewEncoder(w).Encode(map[string]string{
				"issuer":   oidcSrv.URL,
				"jwks_uri": oidcSrv.URL + "/jwks",
			}); err != nil {
				panic(fmt.Errorf("cannot write openid-configuration response: %w", err))
			}
		case "/jwks":
			if !oidcRespOK.Load() {
				http.Error(w, "internal server error", http.StatusInternalServerError)
				return
			}

			// Encode the RSA public key in JWK format (base64url, no padding)
			nBytes := privateKey.N.Bytes()
			eBytes := big.NewInt(int64(privateKey.E)).Bytes()
			jwksBody := fmt.Sprintf(`{"keys":[{"kty":"RSA","kid":%q,"n":%q,"e":%q}]}`,
				`test-key-id`,
				base64.RawURLEncoding.EncodeToString(nBytes),
				base64.RawURLEncoding.EncodeToString(eBytes),
			)

			w.Header().Set("Content-Type", "application/json")
			if _, err := w.Write([]byte(jwksBody)); err != nil {
				panic(fmt.Errorf("cannot write jwks response: %w", err))
			}
		default:
			http.NotFound(w, r)
		}
	}))
	defer oidcSrv.Close()

	headerJSON, err := json.Marshal(map[string]any{
		"alg": "RS256",
		"typ": "JWT",
		"iss": oidcSrv.URL,
		"kid": `test-key-id`,
	})
	if err != nil {
		t.Fatalf("cannot marshal JWT header: %s", err)
	}
	headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)

	bodyJSON, err := json.Marshal(map[string]any{
		"exp":       time.Now().Add(time.Minute).Unix(),
		"iss":       oidcSrv.URL,
		"vm_access": map[string]any{},
	})
	if err != nil {
		t.Fatalf("cannot marshal JWT body: %s", err)
	}
	bodyB64 := base64.RawURLEncoding.EncodeToString(bodyJSON)

	payload := headerB64 + "." + bodyB64

	var signatureB64 string
	hash := crypto.SHA256
	h := hash.New()
	h.Write([]byte(payload))
	digest := h.Sum(nil)

	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, hash, digest)
	if err != nil {
		t.Fatalf("cannot sign JWT token: %s", err)
	}
	signatureB64 = base64.RawURLEncoding.EncodeToString(signature)

	tkn := payload + "." + signatureB64

	backSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}))
	defer backSrv.Close()

	f := func(responseExpected string) {
		t.Helper()

		cfgStr := `
users:
- jwt:
    oidc:
      issuer: ` + oidcSrv.URL + `
  url_prefix: ` + backSrv.URL + `/
`

		cfgOrigP := authConfigData.Load()
		if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
			t.Fatalf("cannot load config data: %s", err)
		}
		defer func() {
			cfgOrig := []byte("unauthorized_user:\n  url_prefix: http://foo/bar")
			if cfgOrigP != nil {
				cfgOrig = *cfgOrigP
			}
			if _, err := reloadAuthConfigData(cfgOrig); err != nil {
				t.Fatalf("cannot restore original config: %s", err)
			}
		}()

		r := httptest.NewRequest("GET", "http://some-host.com/api/v1/query", nil)
		r.Header.Set("Authorization", "Bearer "+tkn)

		w := &fakeResponseWriter{}
		if !requestHandlerWithInternalRoutes(w, r) {
			t.Fatalf("unexpected false returned from requestHandler")
		}

		if response := w.getResponse(); response != responseExpected {
			t.Fatalf("unexpected response\ngot\n%s\nwant\n%s", response, responseExpected)
		}
	}

	// successful
	f(`statusCode=200
`)

	oidcRespOK.Store(false)
	// OIDC server error
	f(`statusCode=401
Unauthorized
`)
}

type fakeResponseWriter struct {
	statusCode int
	h          http.Header

	bb bytes.Buffer
}

func (w *fakeResponseWriter) getResponse() string {
	return w.bb.String()
}

// Flush implements net/http.Flusher
func (w *fakeResponseWriter) Flush() {
	// Nothing to do.
}

func (w *fakeResponseWriter) Header() http.Header {
	if w.h == nil {
		w.h = http.Header{}
	}
	return w.h
}

func (w *fakeResponseWriter) Write(p []byte) (int, error) {
	return w.bb.Write(p)
}

func (w *fakeResponseWriter) WriteHeader(statusCode int) {
	w.statusCode = statusCode
	fmt.Fprintf(&w.bb, "statusCode=%d\n", statusCode)
	if w.h == nil {
		return
	}
	err := w.h.WriteSubset(&w.bb, map[string]bool{
		"Content-Length":         true,
		"Content-Type":           true,
		"Date":                   true,
		"X-Content-Type-Options": true,
	})
	if err != nil {
		panic(fmt.Errorf("cannot marshal headers: %s", err))
	}
}

// This is needed for net/http.ResponseController
func (w *fakeResponseWriter) SetReadDeadline(deadline time.Time) error {
	return nil
}

func (w *fakeResponseWriter) reset() {
	w.bb.Reset()
	w.statusCode = 0
	clear(w.h)
}

func TestBufferRequestBody_Success(t *testing.T) {
	defaultRequestBufferSize := requestBufferSize.String()
	defer func() {
		if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
			t.Fatalf("cannot reset requestBufferSize: %s", err)
		}
	}()

	defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
	defer func() {
		if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
			t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
		}
	}()

	f := func(body *bytes.Buffer, requestBufferSizeFlag, maxRequestBodySizeToRetryFlag string) {
		t.Helper()

		expectedResponse := "statusCode=200"
		if body.Len() > 0 {
			expectedResponse += "\n" + body.String()
		}

		if err := requestBufferSize.Set(requestBufferSizeFlag); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}
		if err := maxRequestBodySizeToRetry.Set(maxRequestBodySizeToRetryFlag); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}

		var backendCalled bool
		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			backendCalled = true

			b, err := io.ReadAll(r.Body)
			if err != nil {
				http.Error(w, fmt.Sprintf("cannot read body: %s", err), http.StatusBadRequest)
				return
			}
			if _, err := w.Write(b); err != nil {
				http.Error(w, fmt.Sprintf("cannot write body: %s", err), http.StatusInternalServerError)
				return
			}
		}))
		defer ts.Close()

		// regular url_prefix
		cfgStr := strings.ReplaceAll(`
unauthorized_user:
  url_prefix: {BACKEND}/foo`, "{BACKEND}", ts.URL)

		cfgOrigP := authConfigData.Load()
		if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
			t.Fatalf("cannot load config data: %s", err)
		}
		defer func() {
			cfgOrig := []byte("unauthorized_user:\n  url_prefix: http://foo/bar")
			if cfgOrigP != nil {
				cfgOrig = *cfgOrigP
			}
			_, err := reloadAuthConfigData(cfgOrig)
			if err != nil {
				t.Fatalf("cannot load the original config: %s", err)
			}
		}()

		r, err := http.NewRequest(http.MethodPost, `http://some-host.com`, body)
		if err != nil {
			t.Fatalf("cannot initialize http request: %s", err)
		}

		w := &fakeResponseWriter{}
		if !requestHandlerWithInternalRoutes(w, r) {
			t.Fatalf("unexpected false is returned from requestHandler")
		}

		response := w.getResponse()
		response = strings.ReplaceAll(response, "\r\n", "\n")
		response = strings.TrimSpace(response)

		if response != expectedResponse {
			t.Fatalf("unexpected response\ngot\n%s\nwant\n%s", response, expectedResponse)
		}
		if !backendCalled {
			t.Fatalf("backend is not called")
		}
	}

	// no body, no buffering, no retry
	f(bytes.NewBuffer(nil), "0", "0")

	// no body, buffering on, no retry
	f(bytes.NewBuffer(nil), "100", "0")

	// no body, no buffering, retry on
	f(bytes.NewBuffer(nil), "0", "100")

	// no body, buffering on, retry on
	f(bytes.NewBuffer(nil), "100", "100")

	// body smaller than buffer, retry max on
	f(bytes.NewBufferString(strings.Repeat("abcdf", 100)), "101", "101")

	// body smaller than buffer
	f(bytes.NewBufferString(strings.Repeat("abcdf", 100)), "501", "0")

	// body same size as buffer
	f(bytes.NewBufferString(strings.Repeat("abcdf", 100)), "500", "0")

	// body bigger than a buffer
	f(bytes.NewBufferString(strings.Repeat("abcdf", 100)), "499", "0")

	// body bigger than tmpBuf 8KiB used in buffering
	f(bytes.NewBufferString(strings.Repeat("a", 32*1024)), "16384", "")

	f(bytes.NewBufferString(strings.Repeat("a", 32*1024)), "16385", "")

	f(bytes.NewBufferString(strings.Repeat("a", 32*1024)), "16383", "")
}

func TestBufferRequestBody_Failure(t *testing.T) {
	defaultRequestBufferSize := requestBufferSize.String()
	defer func() {
		if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
			t.Fatalf("cannot reset requestBufferSize: %s", err)
		}
	}()

	defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
	defer func() {
		if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
			t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
		}
	}()

	defaultMaxQueueDuration := *maxQueueDuration
	defer func() {
		*maxQueueDuration = defaultMaxQueueDuration
	}()

	f := func(body *mockBody, expectedResponse string) {
		t.Helper()

		if err := maxRequestBodySizeToRetry.Set("0"); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}
		if err := requestBufferSize.Set("2048"); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}
		*maxQueueDuration = 100 * time.Millisecond

		var backendCalled bool
		ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			backendCalled = true

			b, err := io.ReadAll(r.Body)
			if err != nil {
				http.Error(w, fmt.Sprintf("cannot read body: %s", err), http.StatusBadRequest)
				return
			}
			if _, err := w.Write(b); err != nil {
				http.Error(w, fmt.Sprintf("cannot write body: %s", err), http.StatusInternalServerError)
				return
			}
		}))
		defer ts.Close()

		// regular url_prefix
		cfgStr := strings.ReplaceAll(`
unauthorized_user:
  url_prefix: {BACKEND}/foo`, "{BACKEND}", ts.URL)

		cfgOrigP := authConfigData.Load()
		if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
			t.Fatalf("cannot load config data: %s", err)
		}
		defer func() {
			cfgOrig := []byte("unauthorized_user:\n  url_prefix: http://foo/bar")
			if cfgOrigP != nil {
				cfgOrig = *cfgOrigP
			}
			_, err := reloadAuthConfigData(cfgOrig)
			if err != nil {
				t.Fatalf("cannot load the original config: %s", err)
			}
		}()

		r, err := http.NewRequest(http.MethodPost, `http://some-host.com`, body)
		if err != nil {
			t.Fatalf("cannot initialize http request: %s", err)
		}

		w := &fakeResponseWriter{}
		if !requestHandlerWithInternalRoutes(w, r) {
			t.Fatalf("unexpected false is returned from requestHandler")
		}

		response := w.getResponse()
		response = strings.ReplaceAll(response, "\r\n", "\n")
		response = strings.TrimSpace(response)

		if response != expectedResponse {
			t.Fatalf("unexpected response\ngot\n%s\nwant\n%s", response, expectedResponse)
		}
		if backendCalled {
			t.Fatalf("backend is called")
		}
	}

	// an error at the beginning of reading
	f(&mockBody{err: fmt.Errorf("an error")}, `statusCode=400
cannot read request body: an error`)

	// an error after reading 1024 bytes, buffer size is 2048 bytes
	f(&mockBody{head: make([]byte, 1024), err: fmt.Errorf("an error")}, `statusCode=400
cannot read request body: an error`)
}

type mockBody struct {
	head []byte
	err  error
	tail []byte
}

func (r *mockBody) Read(p []byte) (n int, err error) {
	if len(r.head) > 0 {
		n = copy(p, r.head)
		r.head = r.head[n:]
		return n, nil
	}

	if r.err != nil {
		return 0, r.err
	}

	if len(r.tail) > 0 {
		n = copy(p, r.tail)
		r.tail = r.tail[n:]
		return n, nil
	}

	return 0, io.EOF
}

func TestBufferedBody_RetrySuccess(t *testing.T) {
	f := func(s string, maxSizeToRetry, bufferSize int) {
		t.Helper()

		defaultRequestBufferSize := requestBufferSize.String()
		defer func() {
			if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
				t.Fatalf("cannot reset requestBufferSize: %s", err)
			}
		}()
		if err := requestBufferSize.Set(strconv.Itoa(bufferSize)); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}

		defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
		defer func() {
			if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
				t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
			}
		}()
		if err := maxRequestBodySizeToRetry.Set(strconv.Itoa(maxSizeToRetry)); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}

		ctx := context.Background()
		rb, err := bufferRequestBody(ctx, io.NopCloser(bytes.NewBufferString(s)), "foo")
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		bb, ok := rb.(*bufferedBody)
		canRetry := !ok || bb.canRetry()

		if !canRetry {
			t.Fatalf("canRetry() must return true before reading anything")
		}
		for i := range 5 {
			data, err := io.ReadAll(rb)
			if err != nil {
				t.Fatalf("unexpected error when reading all the data at iteration %d: %s", i, err)
			}
			if string(data) != s {
				t.Fatalf("unexpected data read at iteration %d\ngot\n%s\nwant\n%s", i, data, s)
			}
			if err := rb.Close(); err != nil {
				t.Fatalf("unexpected error when closing bufferedBody at iteration %d: %s", i, err)
			}
		}
	}

	f("", 0, 2000)
	f("", 0, 0)
	f("", -1, 2000)
	f("", 100, 2000)
	f("foo", 100, 2000)
	f("foobar", 100, 2000)
	f("foobar", 100, 0)
	f("foobar", 100, -1)
	f(newTestString(1000), 1001, 2000)
	f(newTestString(1000), 1001, 500)
}

func TestBufferedBody_RetrySuccessPartialRead(t *testing.T) {
	f := func(s string, maxSizeToRetry, bufferSize int) {
		t.Helper()

		// Check the case with partial read
		defaultRequestBufferSize := requestBufferSize.String()
		defer func() {
			if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
				t.Fatalf("cannot reset requestBufferSize: %s", err)
			}
		}()
		if err := requestBufferSize.Set(strconv.Itoa(bufferSize)); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}

		defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
		defer func() {
			if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
				t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
			}
		}()
		if err := maxRequestBodySizeToRetry.Set(strconv.Itoa(maxSizeToRetry)); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}

		ctx := context.Background()
		rb, err := bufferRequestBody(ctx, io.NopCloser(bytes.NewBufferString(s)), "foo")
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		bb, ok := rb.(*bufferedBody)
		canRetry := !ok || bb.canRetry()

		if !canRetry {
			t.Fatalf("canRetry must return true")
		}
		for i := range len(s) {
			buf := make([]byte, i)
			n, err := io.ReadFull(rb, buf)
			if err != nil {
				t.Fatalf("unexpected error when reading %d bytes: %s", i, err)
			}
			if n != i {
				t.Fatalf("unexpected number of bytes read; got %d; want %d", n, i)
			}
			if string(buf) != s[:i] {
				t.Fatalf("unexpected data read with the length %d\ngot\n%s\nwant\n%s", i, buf, s[:i])
			}
			if err := rb.Close(); err != nil {
				t.Fatalf("unexpected error when closing reader after reading %d bytes", i)
			}
		}

		data, err := io.ReadAll(rb)
		if err != nil {
			t.Fatalf("unexpected error when reading all the data: %s", err)
		}
		if string(data) != s {
			t.Fatalf("unexpected data read\ngot\n%s\nwant\n%s", data, s)
		}
		if err := rb.Close(); err != nil {
			t.Fatalf("unexpected error when closing bufferedBody: %s", err)
		}
	}

	f("", 0, 2000)
	f("", 0, 0)
	f("", -1, 2000)
	f("", 100, 2000)
	f("foo", 100, 2000)
	f("foobar", 100, 2000)
	f("foobar", 100, 0)
	f("foobar", 100, -1)
	f(newTestString(1000), 1001, 2000)
	f(newTestString(1000), 1001, 500)
}

func TestBufferedBody_RetryFailureTooBigBody(t *testing.T) {
	f := func(s string, maxSizeToRetry, bufferSize int) {
		t.Helper()

		defaultRequestBufferSize := requestBufferSize.String()
		defer func() {
			if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
				t.Fatalf("cannot reset requestBufferSize: %s", err)
			}
		}()
		if err := requestBufferSize.Set(strconv.Itoa(bufferSize)); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}

		defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
		defer func() {
			if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
				t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
			}
		}()
		if err := maxRequestBodySizeToRetry.Set(strconv.Itoa(maxSizeToRetry)); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}

		ctx := context.Background()
		rb, err := bufferRequestBody(ctx, io.NopCloser(bytes.NewBufferString(s)), "foo")
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		bb, ok := rb.(*bufferedBody)
		canRetry := !ok || bb.canRetry()

		if canRetry {
			t.Fatalf("canRetry() must return false because of too big request body")
		}
		buf := make([]byte, 1)
		n, err := io.ReadFull(rb, buf)
		if err != nil {
			t.Fatalf("unexpected error when reading a single byte: %s", err)
		}
		if n != 1 {
			t.Fatalf("unexpected number of bytes read; got %d; want 1", n)
		}
		data, err := io.ReadAll(rb)
		if err != nil {
			t.Fatalf("unexpected error when reading all the data: %s", err)
		}
		dataRead := string(buf) + string(data)
		if dataRead != s {
			t.Fatalf("unexpected data read\ngot\n%s\nwant\n%s", dataRead, s)
		}
		if err := rb.Close(); err != nil {
			t.Fatalf("unexpected error when closing bufferedBody: %s", err)
		}

		data, err = io.ReadAll(rb)
		if err == nil {
			t.Fatalf("expecting non-nil error")
		}
		if len(data) != 0 {
			t.Fatalf("unexpected non-empty data read: %q", data)
		}
	}

	const maxBodySize = 1000
	f(newTestString(maxBodySize+1), 0, 2*maxBodySize)
	f(newTestString(maxBodySize+1), -1, 2*maxBodySize)
	f(newTestString(maxBodySize+1), maxBodySize, 0)
	f(newTestString(maxBodySize+1), maxBodySize, -1)
	f(newTestString(maxBodySize+1), maxBodySize, maxBodySize)
	f(newTestString(maxBodySize+1), maxBodySize, 2*maxBodySize)
	f(newTestString(2*maxBodySize), maxBodySize, 0)
}

func TestBufferedBody_RetryDisabledByMaxRequestBodySizeToRetry(t *testing.T) {
	f := func(s string, maxSizeToRetry, bufferSize int) {
		t.Helper()

		defaultRequestBufferSize := requestBufferSize.String()
		defer func() {
			if err := requestBufferSize.Set(defaultRequestBufferSize); err != nil {
				t.Fatalf("cannot reset requestBufferSize: %s", err)
			}
		}()
		if err := requestBufferSize.Set(strconv.Itoa(bufferSize)); err != nil {
			t.Fatalf("cannot set requestBufferSize: %s", err)
		}

		defaultMaxRequestBodySizeToRetry := maxRequestBodySizeToRetry.String()
		defer func() {
			if err := maxRequestBodySizeToRetry.Set(defaultMaxRequestBodySizeToRetry); err != nil {
				t.Fatalf("cannot reset maxRequestBodySizeToRetry: %s", err)
			}
		}()
		if err := maxRequestBodySizeToRetry.Set(strconv.Itoa(maxSizeToRetry)); err != nil {
			t.Fatalf("cannot set maxRequestBodySizeToRetry: %s", err)
		}

		ctx := context.Background()
		rb, err := bufferRequestBody(ctx, io.NopCloser(bytes.NewBufferString(s)), "foo")
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		bb, ok := rb.(*bufferedBody)
		canRetry := !ok || bb.canRetry()

		if canRetry {
			t.Fatalf("canRetry() must return false before reading anything")
		}
		data, err := io.ReadAll(rb)
		if err != nil {
			t.Fatalf("unexpected error when reading all the data: %s", err)
		}
		if string(data) != s {
			t.Fatalf("unexpected data read\ngot\n%s\nwant\n%s", data, s)
		}
		if err := rb.Close(); err != nil {
			t.Fatalf("unexpected error when closing bufferedBody: %s", err)
		}

		data, err = io.ReadAll(rb)
		if err == nil {
			t.Fatalf("expecting non-nil error")
		}
		if len(data) != 0 {
			t.Fatalf("unexpected non-empty data read: %q", data)
		}
	}

	f("foobar", 0, 2048)
	f(newTestString(1000), 0, 2048)

	f("foobar", -1, 2048)
	f(newTestString(1000), -1, 2048)
}

func newTestString(sLen int) string {
	data := make([]byte, sLen)
	for i := range data {
		data[i] = byte(i)
	}
	return string(data)
}
