snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
193
internal/driver/config/opl_cache_test.go
Normal file
193
internal/driver/config/opl_cache_test.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
"github.com/ory/x/configx"
|
||||
"github.com/ory/x/fetcher"
|
||||
"github.com/ory/x/logrusx"
|
||||
)
|
||||
|
||||
func TestNewOPLConfigWatcher(t *testing.T) {
|
||||
var hits atomic.Int64
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hits.Add(1)
|
||||
if _, err := io.WriteString(w, testOPL); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
ctx := context.Background()
|
||||
cfg, err := NewDefault(ctx, nil, logrusx.New("", ""), configx.SkipValidation())
|
||||
require.NoError(t, err)
|
||||
cw, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, hits.Load(), "HTTP request made")
|
||||
_, err = cw.GetNamespaceByName(ctx, "User")
|
||||
require.NoError(t, err)
|
||||
_, err = cw.GetNamespaceByName(ctx, "Document")
|
||||
require.NoError(t, err)
|
||||
|
||||
cache.Wait()
|
||||
|
||||
cw, err = newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, hits.Load(), "content was cached")
|
||||
_, err = cw.GetNamespaceByName(ctx, "User")
|
||||
require.NoError(t, err)
|
||||
_, err = cw.GetNamespaceByName(ctx, "Document")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestOPLConfigWatcher(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("caches parsed namespaces", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts, hits := newCountingServer(t, http.StatusOK, testOPL)
|
||||
cfg := newTestConfig(t)
|
||||
ctx := t.Context()
|
||||
|
||||
first, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err)
|
||||
cache.Wait()
|
||||
|
||||
second, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, hits.Load(), "content was fetched only once")
|
||||
|
||||
firstUser, err := first.GetNamespaceByName(ctx, "User")
|
||||
require.NoError(t, err)
|
||||
secondUser, err := second.GetNamespaceByName(ctx, "User")
|
||||
require.NoError(t, err)
|
||||
require.Same(t, firstUser, secondUser, "the parsed namespaces are shared, not re-parsed per construction")
|
||||
})
|
||||
|
||||
t.Run("deduplicates concurrent fetches", func(t *testing.T) {
|
||||
const parallel = 5
|
||||
const target = "https://opl.invalid/namespaces.ts"
|
||||
// The cache is process-global and outlives the test binary's runs.
|
||||
cache.Del(target)
|
||||
// The config warms package-level caches whose goroutines live outside
|
||||
// the bubble, so build it here.
|
||||
cfg := newTestConfig(t)
|
||||
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
var hits atomic.Int64
|
||||
release := make(chan struct{})
|
||||
hc := retryablehttp.NewClient()
|
||||
hc.Logger = nil
|
||||
hc.HTTPClient = &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
hits.Add(1)
|
||||
<-release
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(testOPL)),
|
||||
Header: make(http.Header),
|
||||
Request: r,
|
||||
}, nil
|
||||
})}
|
||||
newFetcher := func() *fetcher.Fetcher { return fetcher.NewFetcher(fetcher.WithClient(hc)) }
|
||||
ctx := t.Context()
|
||||
|
||||
watchers := make([]*oplConfigWatcher, parallel)
|
||||
var eg errgroup.Group
|
||||
for i := range parallel {
|
||||
eg.Go(func() error {
|
||||
cw, err := newOPLConfigWatcher(ctx, cfg.l, newFetcher, target)
|
||||
watchers[i] = cw
|
||||
return err
|
||||
})
|
||||
}
|
||||
synctest.Wait() // One caller is inside the fetch, the rest joined its flight.
|
||||
close(release)
|
||||
|
||||
require.NoError(t, eg.Wait())
|
||||
require.EqualValues(t, 1, hits.Load(), "concurrent constructions share a single fetch")
|
||||
|
||||
for _, cw := range watchers {
|
||||
_, err := cw.GetNamespaceByName(ctx, "User")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("retries failed fetches", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts, hits := newCountingServer(t, http.StatusNotFound, "")
|
||||
cfg := newTestConfig(t)
|
||||
ctx := t.Context()
|
||||
|
||||
_, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.ErrorContains(t, err, "404")
|
||||
cache.Wait()
|
||||
|
||||
_, err = newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.ErrorContains(t, err, "404")
|
||||
require.EqualValues(t, 2, hits.Load(), "failed fetches are not cached")
|
||||
})
|
||||
|
||||
t.Run("caches parse failures", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts, hits := newCountingServer(t, http.StatusOK, "/* unclosed comment")
|
||||
cfg := newTestConfig(t)
|
||||
ctx := t.Context()
|
||||
|
||||
first, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err, "a parse failure does not fail construction")
|
||||
_, err = first.GetNamespaceByName(ctx, "User")
|
||||
require.ErrorIs(t, err, herodot.ErrNotFound())
|
||||
cache.Wait()
|
||||
|
||||
second, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 1, hits.Load(), "a parse failure is cached and does not trigger a fetch per construction")
|
||||
_, err = second.GetNamespaceByName(ctx, "User")
|
||||
require.ErrorIs(t, err, herodot.ErrNotFound())
|
||||
})
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *Config {
|
||||
t.Helper()
|
||||
cfg, err := NewDefault(t.Context(), nil, logrusx.New("", ""), configx.SkipValidation())
|
||||
require.NoError(t, err)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func newCountingServer(t *testing.T, status int, body string) (*httptest.Server, *atomic.Int64) {
|
||||
t.Helper()
|
||||
hits := new(atomic.Int64)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hits.Add(1)
|
||||
w.WriteHeader(status)
|
||||
_, _ = io.WriteString(w, body)
|
||||
}))
|
||||
t.Cleanup(ts.Close)
|
||||
return ts, hits
|
||||
}
|
||||
|
||||
var testOPL = `
|
||||
import { Namespace } from "@ory/keto-namespace-types"
|
||||
|
||||
class User implements Namespace {}
|
||||
class Document implements Namespace {}
|
||||
`
|
||||
|
||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||
Reference in New Issue
Block a user