Files
probo/pkg/cachecontrol/cachecontrol_test.go
Bryan Frimin d7e23fd890 Harden CIMD client resolution and caching
Tighten redirect URI validation for metadata documents, honor
Cache-Control no-store when caching fetched documents, and resolve
clients on the same transaction as authorization. Load
external_client_id from the database and parse unbounded max-stale
directives in cachecontrol.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-19 17:13:39 +02:00

444 lines
9.5 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cachecontrol_test
import (
"math"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/pkg/cachecontrol"
)
func TestParseRequestDirective(t *testing.T) {
t.Parallel()
tests := []struct {
name string
source string
wantErr bool
want *cachecontrol.TokenPair
}{
{
name: "no-store flag",
source: "no-store",
want: &cachecontrol.TokenPair{Name: "no-store"},
},
{
name: "max-age token",
source: "max-age=4649",
want: &cachecontrol.TokenPair{Name: "max-age", Value: "4649"},
},
{
name: "max-age quoted rejected",
source: `max-age="4649"`,
wantErr: true,
},
{
name: "no-store with argument rejected",
source: `no-store="foo"`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(
tt.name,
func(t *testing.T) {
t.Parallel()
got, err := cachecontrol.ParseRequestDirective(tt.source)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
},
)
}
}
func TestParseResponseDirective(t *testing.T) {
t.Parallel()
tests := []struct {
name string
source string
wantErr bool
want *cachecontrol.TokenPair
}{
{
name: "s-maxage token",
source: "s-maxage=4649",
want: &cachecontrol.TokenPair{Name: "s-maxage", Value: "4649"},
},
{
name: "no-store flag",
source: "no-store",
want: &cachecontrol.TokenPair{Name: "no-store"},
},
{
name: "extension with quoted value",
source: `community="UCI"`,
want: &cachecontrol.TokenPair{Name: "community", Value: "UCI"},
},
{
name: "max-age quoted rejected",
source: `max-age="4649"`,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(
tt.name,
func(t *testing.T) {
t.Parallel()
got, err := cachecontrol.ParseResponseDirective(tt.source)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
},
)
}
}
func TestParseRequestDirectives(t *testing.T) {
t.Parallel()
tokens, err := cachecontrol.ParseRequestDirectives(` max-age=4649 , no-store `)
require.NoError(t, err)
require.Len(t, tokens, 2)
assert.Equal(t, &cachecontrol.TokenPair{Name: "max-age", Value: "4649"}, tokens[0])
assert.Equal(t, &cachecontrol.TokenPair{Name: "no-store"}, tokens[1])
}
func TestParseResponseDirectives(t *testing.T) {
t.Parallel()
tokens, err := cachecontrol.ParseResponseDirectives(`max-age=4649, no-store, community="UCI"`)
require.NoError(t, err)
require.Len(t, tokens, 3)
assert.Equal(t, &cachecontrol.TokenPair{Name: "max-age", Value: "4649"}, tokens[0])
assert.Equal(t, &cachecontrol.TokenPair{Name: "no-store"}, tokens[1])
assert.Equal(t, &cachecontrol.TokenPair{Name: "community", Value: "UCI"}, tokens[2])
}
func TestParseRequest(t *testing.T) {
t.Parallel()
t.Run(
"max-age and no-store",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseRequest("max-age=4649, no-store")
require.NoError(t, err)
seconds, ok := dir.MaxAge()
require.True(t, ok)
assert.Equal(t, uint64(4649), seconds)
assert.True(t, dir.NoStore())
},
)
t.Run(
"invalid max-age rejected",
func(t *testing.T) {
t.Parallel()
_, err := cachecontrol.ParseRequest(`max-age="4649"`)
require.Error(t, err)
},
)
t.Run(
"max-stale without value",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseRequest("max-stale")
require.NoError(t, err)
assert.True(t, dir.MaxStaleUnbounded())
_, bounded, ok := dir.MaxStale()
require.True(t, ok)
assert.False(t, bounded)
},
)
t.Run(
"max-stale with value",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseRequest("max-stale=120")
require.NoError(t, err)
assert.False(t, dir.MaxStaleUnbounded())
seconds, bounded, ok := dir.MaxStale()
require.True(t, ok)
assert.True(t, bounded)
assert.Equal(t, uint64(120), seconds)
},
)
}
func TestParseResponse(t *testing.T) {
t.Parallel()
t.Run(
"response directives and extension",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseResponse(`max-age=4649, no-store, community="UCI"`)
require.NoError(t, err)
seconds, ok := dir.MaxAge()
require.True(t, ok)
assert.Equal(t, uint64(4649), seconds)
assert.True(t, dir.NoStore())
assert.Equal(t, map[string]string{"community": "UCI"}, dir.Extensions())
},
)
t.Run(
"multiple max-age uses minimum",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseResponse("max-age=3600, max-age=60")
require.NoError(t, err)
seconds, ok := dir.MaxAge()
require.True(t, ok)
assert.Equal(t, uint64(60), seconds)
},
)
t.Run(
"s-maxage and flags",
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseResponse("public, max-age=604800, s-maxage=86400, must-revalidate")
require.NoError(t, err)
maxAge, ok := dir.MaxAge()
require.True(t, ok)
assert.Equal(t, uint64(604800), maxAge)
sMaxAge, ok := dir.SMaxAge()
require.True(t, ok)
assert.Equal(t, uint64(86400), sMaxAge)
assert.True(t, dir.Public())
assert.True(t, dir.MustRevalidate())
},
)
t.Run(
"invalid max-age rejected",
func(t *testing.T) {
t.Parallel()
_, err := cachecontrol.ParseResponse(`max-age="4649"`)
require.Error(t, err)
},
)
}
func TestResponseMaxAgeDuration(t *testing.T) {
t.Parallel()
tests := []struct {
name string
header string
wantAge time.Duration
wantOK bool
}{
{
name: "empty header",
header: "",
wantOK: false,
},
{
name: "whitespace only",
header: " ",
wantOK: false,
},
{
name: "no max-age directive",
header: "public, private, no-cache",
wantOK: false,
},
{
name: "single max-age",
header: "max-age=120",
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "max-age with other directives",
header: "public, max-age=120, private",
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "max-age zero",
header: "max-age=0",
wantAge: 0,
wantOK: true,
},
{
name: "case insensitive directive name",
header: "Max-Age=90",
wantAge: 90 * time.Second,
wantOK: true,
},
{
name: "whitespace around comma separators",
header: "public , max-age=120 , private",
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "whitespace around equals sign",
header: "max-age = 120",
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "multiple max-age uses minimum",
header: "max-age=3600, max-age=60",
wantAge: 60 * time.Second,
wantOK: true,
},
{
name: "invalid max-age makes header invalid",
header: "max-age=bad, max-age=30",
wantOK: false,
},
{
name: "all max-age values invalid",
header: "max-age=, max-age=abc",
wantOK: false,
},
{
name: "negative max-age rejected",
header: "max-age=-1",
wantOK: false,
},
{
name: "decimal max-age rejected",
header: "max-age=1.5",
wantOK: false,
},
{
name: "quoted max-age rejected",
header: `max-age="120"`,
wantOK: false,
},
{
name: "leading zeros preserved",
header: "max-age=0060",
wantAge: 60 * time.Second,
wantOK: true,
},
{
name: "s-maxage ignored by MaxAge helper",
header: "s-maxage=3600",
wantOK: false,
},
{
name: "s-maxage and max-age both present",
header: "s-maxage=3600, max-age=120",
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "directive name must match exactly",
header: "foo-max-age=120",
wantOK: false,
},
{
name: "comma inside quoted extension value",
header: `foo="bar,baz", max-age=120`,
wantAge: 120 * time.Second,
wantOK: true,
},
{
name: "real world nginx style",
header: "max-age=31536000, public, immutable",
wantAge: 365 * 24 * time.Hour,
wantOK: true,
},
}
for _, tt := range tests {
t.Run(
tt.name,
func(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseResponse(tt.header)
if !tt.wantOK {
if err == nil {
_, gotOK := dir.MaxAgeDuration()
assert.False(t, gotOK)
}
return
}
require.NoError(t, err)
gotAge, gotOK := dir.MaxAgeDuration()
assert.True(t, gotOK)
assert.Equal(t, tt.wantAge, gotAge)
},
)
}
}
func TestResponseMaxAgeDuration_Overflow(t *testing.T) {
t.Parallel()
dir, err := cachecontrol.ParseResponse("max-age=9223372036854775807")
require.NoError(t, err)
age, ok := dir.MaxAgeDuration()
require.True(t, ok)
assert.Equal(t, time.Duration(math.MaxInt64), age)
}
func TestParseResponseDirectives_NoSpaceAfterComma(t *testing.T) {
t.Parallel()
tokens, err := cachecontrol.ParseResponseDirectives("max-age=120,no-store")
require.NoError(t, err)
require.Len(t, tokens, 2)
}