1233 lines
38 KiB
Go
1233 lines
38 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cache
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-test/deep"
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager"
|
|
"github.com/hashicorp/vault/helper/useragent"
|
|
vaulthttp "github.com/hashicorp/vault/http"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/atomic"
|
|
)
|
|
|
|
func testNewLeaseCache(t *testing.T, responses []*SendResponse) *LeaseCache {
|
|
t.Helper()
|
|
|
|
client, err := api.NewClient(api.DefaultConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lc, err := NewLeaseCache(&LeaseCacheConfig{
|
|
Client: client,
|
|
BaseContext: context.Background(),
|
|
Proxier: NewMockProxier(responses),
|
|
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return lc
|
|
}
|
|
|
|
func testNewLeaseCacheWithDelay(t *testing.T, cacheable bool, delay int) *LeaseCache {
|
|
t.Helper()
|
|
|
|
client, err := api.NewClient(api.DefaultConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
lc, err := NewLeaseCache(&LeaseCacheConfig{
|
|
Client: client,
|
|
BaseContext: context.Background(),
|
|
Proxier: &mockDelayProxier{cacheable, delay},
|
|
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return lc
|
|
}
|
|
|
|
func testNewLeaseCacheWithPersistence(t *testing.T, responses []*SendResponse, storage *cacheboltdb.BoltStorage) *LeaseCache {
|
|
t.Helper()
|
|
|
|
client, err := api.NewClient(api.DefaultConfig())
|
|
require.NoError(t, err)
|
|
|
|
lc, err := NewLeaseCache(&LeaseCacheConfig{
|
|
Client: client,
|
|
BaseContext: context.Background(),
|
|
Proxier: NewMockProxier(responses),
|
|
Logger: logging.NewVaultLogger(hclog.Trace).Named("cache.leasecache"),
|
|
Storage: storage,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return lc
|
|
}
|
|
|
|
func TestCache_ComputeIndexID(t *testing.T) {
|
|
type args struct {
|
|
req *http.Request
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
req *SendRequest
|
|
want string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
"basic",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "test",
|
|
},
|
|
},
|
|
},
|
|
"7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717",
|
|
false,
|
|
},
|
|
{
|
|
"ignore consistency headers",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "test",
|
|
},
|
|
Header: http.Header{
|
|
vaulthttp.VaultIndexHeaderName: []string{"foo"},
|
|
vaulthttp.VaultInconsistentHeaderName: []string{"foo"},
|
|
vaulthttp.VaultForwardHeaderName: []string{"foo"},
|
|
},
|
|
},
|
|
},
|
|
"7b5db388f211fd9edca8c6c254831fb01ad4e6fe624dbb62711f256b5e803717",
|
|
false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := computeIndexID(tt.req)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("actual_error: %v, expected_error: %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(got, string(tt.want)) {
|
|
t.Errorf("bad: index id; actual: %q, expected: %q", got, string(tt.want))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_EmptyToken(t *testing.T) {
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`),
|
|
}
|
|
lc := testNewLeaseCache(t, responses)
|
|
|
|
// Even if the send request doesn't have a token on it, a successful
|
|
// cacheable response should result in the index properly getting populated
|
|
// with a token and memdb shouldn't complain while inserting the index.
|
|
urlPath := "http://example.com/v1/sample/api"
|
|
sendReq := &SendRequest{
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatalf("expected a non empty response")
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_SendCacheable(t *testing.T) {
|
|
// Emulate 2 responses from the api proxy. One returns a new token and the
|
|
// other returns a lease.
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(http.StatusCreated, `{"auth": {"client_token": "testtoken", "renewable": true}}`),
|
|
newTestSendResponse(http.StatusOK, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`),
|
|
}
|
|
|
|
lc := testNewLeaseCache(t, responses)
|
|
// Register an token so that the token and lease requests are cached
|
|
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
|
|
|
|
// Make a request. A response with a new token is returned to the lease
|
|
// cache and that will be cached.
|
|
urlPath := "http://example.com/v1/sample/api"
|
|
sendReq := &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Send the same request again to get the cached response
|
|
sendReq = &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response.StatusCode, responses[0].Response.StatusCode); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Check TokenParent
|
|
cachedItem, err := lc.db.Get(cachememdb.IndexNameToken, "testtoken")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cachedItem == nil {
|
|
t.Fatalf("expected token entry from cache")
|
|
}
|
|
if cachedItem.TokenParent != "autoauthtoken" {
|
|
t.Fatalf("unexpected value for tokenparent: %s", cachedItem.TokenParent)
|
|
}
|
|
|
|
// Modify the request a little bit to ensure the second response is
|
|
// returned to the lease cache.
|
|
sendReq = &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Make the same request again and ensure that the same response is returned
|
|
// again.
|
|
sendReq = &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input_changed"}`)),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response.StatusCode, responses[1].Response.StatusCode); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_SendNonCacheable(t *testing.T) {
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(http.StatusOK, `{"value": "output"}`),
|
|
newTestSendResponse(http.StatusNotFound, `{"value": "invalid"}`),
|
|
newTestSendResponse(http.StatusOK, `<html>Hello</html>`),
|
|
newTestSendResponse(http.StatusTemporaryRedirect, ""),
|
|
}
|
|
|
|
lc := testNewLeaseCache(t, responses)
|
|
|
|
// Send a request through the lease cache which is not cacheable (there is
|
|
// no lease information or auth information in the response)
|
|
sendReq := &SendRequest{
|
|
Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Since the response is non-cacheable, the second response will be
|
|
// returned.
|
|
sendReq = &SendRequest{
|
|
Token: "foo",
|
|
Request: httptest.NewRequest("GET", "http://example.com", strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Since the response is non-cacheable, the third response will be
|
|
// returned.
|
|
sendReq = &SendRequest{
|
|
Token: "foo",
|
|
Request: httptest.NewRequest("GET", "http://example.com", nil),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[2].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
// Since the response is non-cacheable, the fourth response will be
|
|
// returned.
|
|
sendReq = &SendRequest{
|
|
Token: "foo",
|
|
Request: httptest.NewRequest("GET", "http://example.com", nil),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[3].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_SendNonCacheableNonTokenLease(t *testing.T) {
|
|
// Create the cache
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(http.StatusOK, `{"value": "output", "lease_id": "foo"}`),
|
|
newTestSendResponse(http.StatusCreated, `{"value": "invalid", "auth": {"client_token": "testtoken"}}`),
|
|
}
|
|
lc := testNewLeaseCache(t, responses)
|
|
|
|
// Send a request through lease cache which returns a response containing
|
|
// lease_id. Response will not be cached because it doesn't belong to a
|
|
// token that is managed by the lease cache.
|
|
urlPath := "http://example.com/v1/sample/api"
|
|
sendReq := &SendRequest{
|
|
Token: "foo",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[0].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
idx, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if idx != nil {
|
|
t.Fatalf("expected nil entry, got: %#v", idx)
|
|
}
|
|
|
|
// Verify that the response is not cached by sending the same request and
|
|
// by expecting a different response.
|
|
sendReq = &SendRequest{
|
|
Token: "foo",
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err = lc.Send(context.Background(), sendReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if diff := deep.Equal(resp.Response, responses[1].Response); diff != nil {
|
|
t.Fatalf("expected getting proxied response: got %v", diff)
|
|
}
|
|
|
|
idx, err = lc.db.Get(cachememdb.IndexNameRequestPath, "root/", urlPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if idx != nil {
|
|
t.Fatalf("expected nil entry, got: %#v", idx)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_HandleCacheClear(t *testing.T) {
|
|
lc := testNewLeaseCache(t, nil)
|
|
|
|
handler := lc.HandleCacheClear(context.Background())
|
|
ts := httptest.NewServer(handler)
|
|
defer ts.Close()
|
|
|
|
// Test missing body, should return 400
|
|
resp, err := http.Post(ts.URL, "application/json", nil)
|
|
if err != nil {
|
|
t.Fatal()
|
|
}
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status code mismatch: expected = %v, got = %v", http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
testCases := []struct {
|
|
name string
|
|
reqType string
|
|
reqValue string
|
|
expectedStatusCode int
|
|
}{
|
|
{
|
|
"invalid_type",
|
|
"foo",
|
|
"",
|
|
http.StatusBadRequest,
|
|
},
|
|
{
|
|
"invalid_value",
|
|
"",
|
|
"bar",
|
|
http.StatusBadRequest,
|
|
},
|
|
{
|
|
"all",
|
|
"all",
|
|
"",
|
|
http.StatusOK,
|
|
},
|
|
{
|
|
"by_request_path",
|
|
"request_path",
|
|
"foo",
|
|
http.StatusOK,
|
|
},
|
|
{
|
|
"by_token",
|
|
"token",
|
|
"foo",
|
|
http.StatusOK,
|
|
},
|
|
{
|
|
"by_lease",
|
|
"lease",
|
|
"foo",
|
|
http.StatusOK,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
reqBody := fmt.Sprintf("{\"type\": \"%s\", \"value\": \"%s\"}", tc.reqType, tc.reqValue)
|
|
resp, err := http.Post(ts.URL, "application/json", strings.NewReader(reqBody))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tc.expectedStatusCode != resp.StatusCode {
|
|
t.Fatalf("status code mismatch: expected = %v, got = %v", tc.expectedStatusCode, resp.StatusCode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCache_DeriveNamespaceAndRevocationPath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
req *SendRequest
|
|
wantNamespace string
|
|
wantRelativePath string
|
|
}{
|
|
{
|
|
"non_revocation_full_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns1/sys/mounts",
|
|
},
|
|
},
|
|
},
|
|
"root/",
|
|
"/v1/ns1/sys/mounts",
|
|
},
|
|
{
|
|
"non_revocation_relative_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/sys/mounts",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/sys/mounts",
|
|
},
|
|
{
|
|
"non_revocation_relative_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns2/sys/mounts",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/ns2/sys/mounts",
|
|
},
|
|
{
|
|
"revocation_full_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns1/sys/leases/revoke",
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/sys/leases/revoke",
|
|
},
|
|
{
|
|
"revocation_relative_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/sys/leases/revoke",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/sys/leases/revoke",
|
|
},
|
|
{
|
|
"revocation_relative_partial_ns",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns2/sys/leases/revoke",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/ns2/",
|
|
"/v1/sys/leases/revoke",
|
|
},
|
|
{
|
|
"revocation_prefix_full_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns1/sys/leases/revoke-prefix/foo",
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/sys/leases/revoke-prefix/foo",
|
|
},
|
|
{
|
|
"revocation_prefix_relative_path",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/sys/leases/revoke-prefix/foo",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/",
|
|
"/v1/sys/leases/revoke-prefix/foo",
|
|
},
|
|
{
|
|
"revocation_prefix_partial_ns",
|
|
&SendRequest{
|
|
Request: &http.Request{
|
|
URL: &url.URL{
|
|
Path: "/v1/ns2/sys/leases/revoke-prefix/foo",
|
|
},
|
|
Header: http.Header{
|
|
consts.NamespaceHeaderName: []string{"ns1/"},
|
|
},
|
|
},
|
|
},
|
|
"ns1/ns2/",
|
|
"/v1/sys/leases/revoke-prefix/foo",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotNamespace, gotRelativePath := deriveNamespaceAndRevocationPath(tt.req)
|
|
if gotNamespace != tt.wantNamespace {
|
|
t.Errorf("deriveNamespaceAndRevocationPath() gotNamespace = %v, want %v", gotNamespace, tt.wantNamespace)
|
|
}
|
|
if gotRelativePath != tt.wantRelativePath {
|
|
t.Errorf("deriveNamespaceAndRevocationPath() gotRelativePath = %v, want %v", gotRelativePath, tt.wantRelativePath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_Concurrent_NonCacheable(t *testing.T) {
|
|
lc := testNewLeaseCacheWithDelay(t, false, 50)
|
|
|
|
// We are going to send 100 requests, each taking 50ms to process. If these
|
|
// requests are processed serially, it will take ~5seconds to finish. we
|
|
// use a ContextWithTimeout to tell us if this is the case by giving ample
|
|
// time for it process them concurrently but time out if they get processed
|
|
// serially.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
wgDoneCh := make(chan struct{})
|
|
errCh := make(chan error)
|
|
|
|
go func() {
|
|
var wg sync.WaitGroup
|
|
// 100 concurrent requests
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// Send a request through the lease cache which is not cacheable (there is
|
|
// no lease information or auth information in the response)
|
|
sendReq := &SendRequest{
|
|
Request: httptest.NewRequest("GET", "http://example.com", nil),
|
|
}
|
|
|
|
_, err := lc.Send(ctx, sendReq)
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
close(wgDoneCh)
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatalf("request timed out: %s", ctx.Err())
|
|
case <-wgDoneCh:
|
|
case err := <-errCh:
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_Concurrent_Cacheable(t *testing.T) {
|
|
lc := testNewLeaseCacheWithDelay(t, true, 50)
|
|
|
|
if err := lc.RegisterAutoAuthToken("autoauthtoken"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// We are going to send 100 requests, each taking 50ms to process. If these
|
|
// requests are processed serially, it will take ~5seconds to finish, so we
|
|
// use a ContextWithTimeout to tell us if this is the case.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
|
|
var cacheCount atomic.Uint32
|
|
wgDoneCh := make(chan struct{})
|
|
errCh := make(chan error)
|
|
|
|
go func() {
|
|
var wg sync.WaitGroup
|
|
// Start 100 concurrent requests
|
|
for i := 0; i < 100; i++ {
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
sendReq := &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", nil),
|
|
}
|
|
|
|
resp, err := lc.Send(ctx, sendReq)
|
|
if err != nil {
|
|
errCh <- err
|
|
}
|
|
|
|
if resp.CacheMeta != nil && resp.CacheMeta.Hit {
|
|
cacheCount.Inc()
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
close(wgDoneCh)
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatalf("request timed out: %s", ctx.Err())
|
|
case <-wgDoneCh:
|
|
case err := <-errCh:
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Ensure that all but one request got proxied. The other 99 should be
|
|
// returned from the cache.
|
|
if cacheCount.Load() != 99 {
|
|
t.Fatalf("Should have returned a cached response 99 times, got %d", cacheCount.Load())
|
|
}
|
|
}
|
|
|
|
func setupBoltStorage(t *testing.T) (tempCacheDir string, boltStorage *cacheboltdb.BoltStorage) {
|
|
t.Helper()
|
|
|
|
km, err := keymanager.NewPassthroughKeyManager(context.Background(), nil)
|
|
require.NoError(t, err)
|
|
|
|
tempCacheDir, err = ioutil.TempDir("", "agent-cache-test")
|
|
require.NoError(t, err)
|
|
boltStorage, err = cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{
|
|
Path: tempCacheDir,
|
|
Logger: hclog.Default(),
|
|
Wrapper: km.Wrapper(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, boltStorage)
|
|
// The calling function should `defer boltStorage.Close()` and `defer os.RemoveAll(tempCacheDir)`
|
|
return tempCacheDir, boltStorage
|
|
}
|
|
|
|
func compareBeforeAndAfter(t *testing.T, before, after *LeaseCache, beforeLen, afterLen int) {
|
|
beforeDB, err := before.db.GetByPrefix(cachememdb.IndexNameID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, beforeDB, beforeLen)
|
|
afterDB, err := after.db.GetByPrefix(cachememdb.IndexNameID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, afterDB, afterLen)
|
|
for _, cachedItem := range beforeDB {
|
|
if strings.Contains(cachedItem.RequestPath, "expect-missing") {
|
|
continue
|
|
}
|
|
restoredItem, err := after.db.Get(cachememdb.IndexNameID, cachedItem.ID)
|
|
require.NoError(t, err)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, cachedItem.ID, restoredItem.ID)
|
|
assert.Equal(t, cachedItem.Lease, restoredItem.Lease)
|
|
assert.Equal(t, cachedItem.LeaseToken, restoredItem.LeaseToken)
|
|
assert.Equal(t, cachedItem.Namespace, restoredItem.Namespace)
|
|
assert.EqualValues(t, cachedItem.RequestHeader, restoredItem.RequestHeader)
|
|
assert.Equal(t, cachedItem.RequestMethod, restoredItem.RequestMethod)
|
|
assert.Equal(t, cachedItem.RequestPath, restoredItem.RequestPath)
|
|
assert.Equal(t, cachedItem.RequestToken, restoredItem.RequestToken)
|
|
assert.Equal(t, cachedItem.Response, restoredItem.Response)
|
|
assert.Equal(t, cachedItem.Token, restoredItem.Token)
|
|
assert.Equal(t, cachedItem.TokenAccessor, restoredItem.TokenAccessor)
|
|
assert.Equal(t, cachedItem.TokenParent, restoredItem.TokenParent)
|
|
|
|
// check what we can in the renewal context
|
|
assert.NotEmpty(t, restoredItem.RenewCtxInfo.CancelFunc)
|
|
assert.NotZero(t, restoredItem.RenewCtxInfo.DoneCh)
|
|
require.NotEmpty(t, restoredItem.RenewCtxInfo.Ctx)
|
|
assert.Equal(t,
|
|
cachedItem.RenewCtxInfo.Ctx.Value(contextIndexID),
|
|
restoredItem.RenewCtxInfo.Ctx.Value(contextIndexID),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_PersistAndRestore(t *testing.T) {
|
|
// Emulate responses from the api proxy. The first two use the auto-auth
|
|
// token, and the others use another token.
|
|
// The test re-sends each request to ensure that the response is cached
|
|
// so the number of responses and cacheTests specified should always be equal.
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 600}}`),
|
|
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 600}`),
|
|
// The auth token will get manually deleted from the bolt DB storage, causing both of the following two responses
|
|
// to be missing from the cache after a restore, because the lease is a child of the auth token.
|
|
newTestSendResponse(202, `{"auth": {"client_token": "testtoken2", "renewable": true, "orphan": true, "lease_duration": 600}}`),
|
|
newTestSendResponse(203, `{"lease_id": "secret2-lease", "renewable": true, "data": {"number": "two"}, "lease_duration": 600}`),
|
|
// 204 No content gets special handling - avoid.
|
|
newTestSendResponse(250, `{"auth": {"client_token": "testtoken3", "renewable": true, "orphan": true, "lease_duration": 600}}`),
|
|
newTestSendResponse(251, `{"lease_id": "secret3-lease", "renewable": true, "data": {"number": "three"}, "lease_duration": 600}`),
|
|
}
|
|
|
|
tempDir, boltStorage := setupBoltStorage(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer boltStorage.Close()
|
|
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
|
|
|
|
// Register an auto-auth token so that the token and lease requests are cached
|
|
err := lc.RegisterAutoAuthToken("autoauthtoken")
|
|
require.NoError(t, err)
|
|
|
|
cacheTests := []struct {
|
|
token string
|
|
method string
|
|
urlPath string
|
|
body string
|
|
deleteFromPersistentStore bool // If true, will be deleted from bolt DB to induce an error on restore
|
|
expectMissingAfterRestore bool // If true, the response is not expected to be present in the restored cache
|
|
}{
|
|
{
|
|
// Make a request. A response with a new token is returned to the
|
|
// lease cache and that will be cached.
|
|
token: "autoauthtoken",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample/api",
|
|
body: `{"value": "input"}`,
|
|
},
|
|
{
|
|
// Modify the request a little bit to ensure the second response is
|
|
// returned to the lease cache.
|
|
token: "autoauthtoken",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample/api",
|
|
body: `{"value": "input_changed"}`,
|
|
},
|
|
{
|
|
// Simulate an approle login to get another token
|
|
method: "PUT",
|
|
urlPath: "http://example.com/v1/auth/approle-expect-missing/login",
|
|
body: `{"role_id": "my role", "secret_id": "my secret"}`,
|
|
deleteFromPersistentStore: true,
|
|
expectMissingAfterRestore: true,
|
|
},
|
|
{
|
|
// Test caching with the token acquired from the approle login
|
|
token: "testtoken2",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample-expect-missing/api",
|
|
body: `{"second": "input"}`,
|
|
// This will be missing from the restored cache because its parent token was deleted
|
|
expectMissingAfterRestore: true,
|
|
},
|
|
{
|
|
// Simulate another approle login to get another token
|
|
method: "PUT",
|
|
urlPath: "http://example.com/v1/auth/approle/login",
|
|
body: `{"role_id": "my role", "secret_id": "my secret"}`,
|
|
},
|
|
{
|
|
// Test caching with the token acquired from the latest approle login
|
|
token: "testtoken3",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample3/api",
|
|
body: `{"third": "input"}`,
|
|
},
|
|
}
|
|
|
|
var deleteIDs []string
|
|
for i, ct := range cacheTests {
|
|
// Send once to cache
|
|
req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
|
|
req.Header.Set("User-Agent", useragent.AgentProxyString())
|
|
|
|
sendReq := &SendRequest{
|
|
Token: ct.token,
|
|
Request: req,
|
|
}
|
|
if ct.deleteFromPersistentStore {
|
|
deleteID, err := computeIndexID(sendReq)
|
|
require.NoError(t, err)
|
|
deleteIDs = append(deleteIDs, deleteID)
|
|
// Now reset the body after calculating the index
|
|
req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
|
|
req.Header.Set("User-Agent", useragent.AgentProxyString())
|
|
sendReq.Request = req
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, responses[i].Response.StatusCode, resp.Response.StatusCode, "expected proxied response")
|
|
assert.Nil(t, resp.CacheMeta)
|
|
|
|
// Send again to test cache. If this isn't cached, the response returned
|
|
// will be the next in the list and the status code will not match.
|
|
req = httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
|
|
req.Header.Set("User-Agent", useragent.AgentProxyString())
|
|
sendCacheReq := &SendRequest{
|
|
Token: ct.token,
|
|
Request: req,
|
|
}
|
|
respCached, err := lc.Send(context.Background(), sendCacheReq)
|
|
require.NoError(t, err, "failed to send request %+v", ct)
|
|
assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response")
|
|
require.NotNil(t, respCached.CacheMeta)
|
|
assert.True(t, respCached.CacheMeta.Hit)
|
|
}
|
|
|
|
require.NotEmpty(t, deleteIDs)
|
|
for _, deleteID := range deleteIDs {
|
|
err = boltStorage.Delete(deleteID, cacheboltdb.LeaseType)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Now we know the cache is working, so try restoring from the persisted
|
|
// cache's storage. Responses 3 and 4 have been cleared from the cache, so
|
|
// re-send those.
|
|
restoredCache := testNewLeaseCache(t, responses[2:4])
|
|
|
|
err = restoredCache.Restore(context.Background(), boltStorage)
|
|
errors, ok := err.(*multierror.Error)
|
|
require.True(t, ok)
|
|
assert.Len(t, errors.Errors, 1)
|
|
assert.Contains(t, errors.Error(), "could not find parent Token testtoken2")
|
|
|
|
// Now compare the cache contents before and after
|
|
compareBeforeAndAfter(t, lc, restoredCache, 7, 5)
|
|
|
|
// And finally send the cache requests once to make sure they're all being
|
|
// served from the restoredCache unless they were intended to be missing after restore.
|
|
for i, ct := range cacheTests {
|
|
req := httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body))
|
|
req.Header.Set("User-Agent", useragent.AgentProxyString())
|
|
sendCacheReq := &SendRequest{
|
|
Token: ct.token,
|
|
Request: req,
|
|
}
|
|
respCached, err := restoredCache.Send(context.Background(), sendCacheReq)
|
|
require.NoError(t, err, "failed to send request %+v", ct)
|
|
assert.Equal(t, responses[i].Response.StatusCode, respCached.Response.StatusCode, "expected proxied response")
|
|
if ct.expectMissingAfterRestore {
|
|
require.Nil(t, respCached.CacheMeta)
|
|
} else {
|
|
require.NotNil(t, respCached.CacheMeta)
|
|
assert.True(t, respCached.CacheMeta.Hit)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_PersistAndRestore_WithManyDependencies(t *testing.T) {
|
|
tempDir, boltStorage := setupBoltStorage(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer boltStorage.Close()
|
|
|
|
var requests []*SendRequest
|
|
var responses []*SendResponse
|
|
var orderedRequestPaths []string
|
|
|
|
// helper func to generate new auth leases with a child secret lease attached
|
|
authAndSecretLease := func(id int, parentToken, newToken string) {
|
|
t.Helper()
|
|
path := fmt.Sprintf("/v1/auth/approle-%d/login", id)
|
|
orderedRequestPaths = append(orderedRequestPaths, path)
|
|
requests = append(requests, &SendRequest{
|
|
Token: parentToken,
|
|
Request: httptest.NewRequest("PUT", "http://example.com"+path, strings.NewReader("")),
|
|
})
|
|
responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"auth": {"client_token": "%s", "renewable": true, "lease_duration": 600}}`, newToken)))
|
|
|
|
// Fetch a leased secret using the new token
|
|
path = fmt.Sprintf("/v1/kv/%d", id)
|
|
orderedRequestPaths = append(orderedRequestPaths, path)
|
|
requests = append(requests, &SendRequest{
|
|
Token: newToken,
|
|
Request: httptest.NewRequest("GET", "http://example.com"+path, strings.NewReader("")),
|
|
})
|
|
responses = append(responses, newTestSendResponse(200, fmt.Sprintf(`{"lease_id": "secret-%d-lease", "renewable": true, "data": {"number": %d}, "lease_duration": 600}`, id, id)))
|
|
}
|
|
|
|
// Pathological case: a long chain of child tokens
|
|
authAndSecretLease(0, "autoauthtoken", "many-ancestors-token;0")
|
|
for i := 1; i <= 50; i++ {
|
|
// Create a new generation of child token
|
|
authAndSecretLease(i, fmt.Sprintf("many-ancestors-token;%d", i-1), fmt.Sprintf("many-ancestors-token;%d", i))
|
|
}
|
|
|
|
// Lots of sibling tokens with auto auth token as their parent
|
|
for i := 51; i <= 100; i++ {
|
|
authAndSecretLease(i, "autoauthtoken", fmt.Sprintf("many-siblings-token;%d", i))
|
|
}
|
|
|
|
// Also create some extra siblings for an auth token further down the chain
|
|
for i := 101; i <= 110; i++ {
|
|
authAndSecretLease(i, "many-ancestors-token;25", fmt.Sprintf("many-siblings-for-ancestor-token;%d", i))
|
|
}
|
|
|
|
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
|
|
|
|
// Register an auto-auth token so that the token and lease requests are cached
|
|
err := lc.RegisterAutoAuthToken("autoauthtoken")
|
|
require.NoError(t, err)
|
|
|
|
for _, req := range requests {
|
|
// Send once to cache
|
|
resp, err := lc.Send(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 200, resp.Response.StatusCode, "expected success")
|
|
assert.Nil(t, resp.CacheMeta)
|
|
}
|
|
|
|
// Ensure leases are retrieved in the correct order
|
|
var processed int
|
|
|
|
leases, err := boltStorage.GetByType(context.Background(), cacheboltdb.LeaseType)
|
|
require.NoError(t, err)
|
|
for _, lease := range leases {
|
|
index, err := cachememdb.Deserialize(lease)
|
|
require.NoError(t, err)
|
|
require.Equal(t, orderedRequestPaths[processed], index.RequestPath)
|
|
processed++
|
|
}
|
|
|
|
assert.Equal(t, len(orderedRequestPaths), processed)
|
|
|
|
restoredCache := testNewLeaseCache(t, nil)
|
|
err = restoredCache.Restore(context.Background(), boltStorage)
|
|
require.NoError(t, err)
|
|
|
|
// Now compare the cache contents before and after
|
|
compareBeforeAndAfter(t, lc, restoredCache, 223, 223)
|
|
}
|
|
|
|
func TestEvictPersistent(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}}`),
|
|
}
|
|
|
|
tempDir, boltStorage := setupBoltStorage(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer boltStorage.Close()
|
|
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
|
|
|
|
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
|
|
|
|
// populate cache by sending request through
|
|
sendReq := &SendRequest{
|
|
Token: "autoauthtoken",
|
|
Request: httptest.NewRequest("GET", "http://example.com/v1/sample/api", strings.NewReader(`{"value": "some_input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resp.Response.StatusCode, 201, "expected proxied response")
|
|
assert.Nil(t, resp.CacheMeta)
|
|
|
|
// Check bolt for the cached lease
|
|
secrets, err := lc.ps.GetByType(ctx, cacheboltdb.LeaseType)
|
|
require.NoError(t, err)
|
|
assert.Len(t, secrets, 1)
|
|
|
|
// Call clear for the request path
|
|
err = lc.handleCacheClear(context.Background(), &cacheClearInput{
|
|
Type: "request_path",
|
|
RequestPath: "/v1/sample/api",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Check that cached item is gone
|
|
secrets, err = lc.ps.GetByType(ctx, cacheboltdb.LeaseType)
|
|
require.NoError(t, err)
|
|
assert.Len(t, secrets, 0)
|
|
}
|
|
|
|
func TestRegisterAutoAuth_sameToken(t *testing.T) {
|
|
// If the auto-auth token already exists in the cache, it should not be
|
|
// stored again in a new index.
|
|
lc := testNewLeaseCache(t, nil)
|
|
err := lc.RegisterAutoAuthToken("autoauthtoken")
|
|
assert.NoError(t, err)
|
|
|
|
oldTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken")
|
|
assert.NoError(t, err)
|
|
oldTokenID := oldTokenIndex.ID
|
|
|
|
// register the same token again
|
|
err = lc.RegisterAutoAuthToken("autoauthtoken")
|
|
assert.NoError(t, err)
|
|
|
|
// check that there's only one index for autoauthtoken
|
|
entries, err := lc.db.GetByPrefix(cachememdb.IndexNameToken, "autoauthtoken")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, entries, 1)
|
|
|
|
newTokenIndex, err := lc.db.Get(cachememdb.IndexNameToken, "autoauthtoken")
|
|
assert.NoError(t, err)
|
|
|
|
// compare the ID's since those are randomly generated when an index for a
|
|
// token is added to the cache, so if a new token was added, the id's will
|
|
// not match.
|
|
assert.Equal(t, oldTokenID, newTokenIndex.ID)
|
|
}
|
|
|
|
func Test_hasExpired(t *testing.T) {
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": 60}}`),
|
|
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": 60}`),
|
|
}
|
|
lc := testNewLeaseCache(t, responses)
|
|
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
|
|
|
|
cacheTests := []struct {
|
|
token string
|
|
urlPath string
|
|
leaseType string
|
|
wantStatusCode int
|
|
}{
|
|
{
|
|
// auth lease
|
|
token: "autoauthtoken",
|
|
urlPath: "/v1/sample/auth",
|
|
leaseType: cacheboltdb.LeaseType,
|
|
wantStatusCode: responses[0].Response.StatusCode,
|
|
},
|
|
{
|
|
// secret lease
|
|
token: "autoauthtoken",
|
|
urlPath: "/v1/sample/secret",
|
|
leaseType: cacheboltdb.LeaseType,
|
|
wantStatusCode: responses[1].Response.StatusCode,
|
|
},
|
|
}
|
|
|
|
for _, ct := range cacheTests {
|
|
// Send once to cache
|
|
urlPath := "http://example.com" + ct.urlPath
|
|
sendReq := &SendRequest{
|
|
Token: ct.token,
|
|
Request: httptest.NewRequest("GET", urlPath, strings.NewReader(`{"value": "input"}`)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response")
|
|
assert.Nil(t, resp.CacheMeta)
|
|
|
|
// get the Index out of the mem cache
|
|
index, err := lc.db.Get(cachememdb.IndexNameRequestPath, "root/", ct.urlPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ct.leaseType, index.Type)
|
|
|
|
// The lease duration is 60 seconds, so time.Now() should be within that
|
|
notExpired, err := lc.hasExpired(time.Now().UTC(), index)
|
|
require.NoError(t, err)
|
|
assert.False(t, notExpired)
|
|
|
|
// In 90 seconds the index should be "expired"
|
|
futureTime := time.Now().UTC().Add(time.Second * 90)
|
|
expired, err := lc.hasExpired(futureTime, index)
|
|
require.NoError(t, err)
|
|
assert.True(t, expired)
|
|
}
|
|
}
|
|
|
|
func TestLeaseCache_hasExpired_wrong_type(t *testing.T) {
|
|
index := &cachememdb.Index{
|
|
Type: cacheboltdb.TokenType,
|
|
Response: []byte(`HTTP/0.0 200 OK
|
|
Content-Type: application/json
|
|
Date: Tue, 02 Mar 2021 17:54:16 GMT
|
|
|
|
{}`),
|
|
}
|
|
|
|
lc := testNewLeaseCache(t, nil)
|
|
expired, err := lc.hasExpired(time.Now().UTC(), index)
|
|
assert.False(t, expired)
|
|
assert.EqualError(t, err, `secret without lease encountered in expiration check`)
|
|
}
|
|
|
|
func TestLeaseCacheRestore_expired(t *testing.T) {
|
|
// Emulate 2 responses from the api proxy, both expired
|
|
responses := []*SendResponse{
|
|
newTestSendResponse(200, `{"auth": {"client_token": "testtoken", "renewable": true, "lease_duration": -600}}`),
|
|
newTestSendResponse(201, `{"lease_id": "foo", "renewable": true, "data": {"value": "foo"}, "lease_duration": -600}`),
|
|
}
|
|
|
|
tempDir, boltStorage := setupBoltStorage(t)
|
|
defer os.RemoveAll(tempDir)
|
|
defer boltStorage.Close()
|
|
lc := testNewLeaseCacheWithPersistence(t, responses, boltStorage)
|
|
|
|
// Register an auto-auth token so that the token and lease requests are cached in mem
|
|
require.NoError(t, lc.RegisterAutoAuthToken("autoauthtoken"))
|
|
|
|
cacheTests := []struct {
|
|
token string
|
|
method string
|
|
urlPath string
|
|
body string
|
|
wantStatusCode int
|
|
}{
|
|
{
|
|
// Make a request. A response with a new token is returned to the
|
|
// lease cache and that will be cached.
|
|
token: "autoauthtoken",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample/api",
|
|
body: `{"value": "input"}`,
|
|
wantStatusCode: responses[0].Response.StatusCode,
|
|
},
|
|
{
|
|
// Modify the request a little bit to ensure the second response is
|
|
// returned to the lease cache.
|
|
token: "autoauthtoken",
|
|
method: "GET",
|
|
urlPath: "http://example.com/v1/sample/api",
|
|
body: `{"value": "input_changed"}`,
|
|
wantStatusCode: responses[1].Response.StatusCode,
|
|
},
|
|
}
|
|
|
|
for _, ct := range cacheTests {
|
|
// Send once to cache
|
|
sendReq := &SendRequest{
|
|
Token: ct.token,
|
|
Request: httptest.NewRequest(ct.method, ct.urlPath, strings.NewReader(ct.body)),
|
|
}
|
|
resp, err := lc.Send(context.Background(), sendReq)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resp.Response.StatusCode, ct.wantStatusCode, "expected proxied response")
|
|
assert.Nil(t, resp.CacheMeta)
|
|
}
|
|
|
|
// Restore from the persisted cache's storage
|
|
restoredCache := testNewLeaseCache(t, nil)
|
|
|
|
err := restoredCache.Restore(context.Background(), boltStorage)
|
|
assert.NoError(t, err)
|
|
|
|
// The original mem cache should have all three items
|
|
beforeDB, err := lc.db.GetByPrefix(cachememdb.IndexNameID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, beforeDB, 3)
|
|
|
|
// There should only be one item in the restored cache: the autoauth token
|
|
afterDB, err := restoredCache.db.GetByPrefix(cachememdb.IndexNameID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, afterDB, 1)
|
|
|
|
// Just verify that the one item in the restored mem cache matches one in the original mem cache, and that it's the auto-auth token
|
|
beforeItem, err := lc.db.Get(cachememdb.IndexNameID, afterDB[0].ID)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, beforeItem)
|
|
|
|
assert.Equal(t, "autoauthtoken", afterDB[0].Token)
|
|
assert.Equal(t, cacheboltdb.TokenType, afterDB[0].Type)
|
|
}
|