backend: support external SPARQL and named-graph snapshots
This commit is contained in:
273
backend_go/keycloak_token_test.go
Normal file
273
backend_go/keycloak_token_test.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestKeycloakTokenManagerFetchTokenParsesAccessToken(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/token" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if got := r.Header.Get("Content-Type"); got != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("unexpected content-type: %s", got)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"access_token":"fresh-token"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{
|
||||
KeycloakTokenEndpoint: server.URL + "/token",
|
||||
KeycloakClientID: "anzograph",
|
||||
KeycloakUsername: "user",
|
||||
KeycloakPassword: "pass",
|
||||
KeycloakScope: "openid",
|
||||
}
|
||||
manager := newKeycloakTokenManager(cfg, server.Client())
|
||||
|
||||
token, err := manager.fetchToken(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("fetchToken returned error: %v", err)
|
||||
}
|
||||
if token != "fresh-token" {
|
||||
t.Fatalf("expected fresh-token, got %q", token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnzoGraphClientStartupFetchesFreshToken(t *testing.T) {
|
||||
var tokenCalls atomic.Int32
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/token":
|
||||
tokenCalls.Add(1)
|
||||
_, _ = io.WriteString(w, `{"access_token":"startup-token"}`)
|
||||
case "/sparql":
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer startup-token" {
|
||||
t.Errorf("expected startup bearer token, got %q", got)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"head":{},"boolean":true}`)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{
|
||||
SparqlSourceMode: "external",
|
||||
ExternalSparqlEndpoint: server.URL + "/sparql",
|
||||
KeycloakTokenEndpoint: server.URL + "/token",
|
||||
KeycloakClientID: "anzograph",
|
||||
KeycloakUsername: "user",
|
||||
KeycloakPassword: "pass",
|
||||
KeycloakScope: "openid",
|
||||
SparqlReadyTimeout: 2 * time.Second,
|
||||
SparqlReadyRetries: 1,
|
||||
SparqlReadyDelay: 1 * time.Millisecond,
|
||||
SparqlTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
client := NewAnzoGraphClient(cfg)
|
||||
client.client = server.Client()
|
||||
client.tokenManager.client = server.Client()
|
||||
|
||||
if err := client.Startup(context.Background()); err != nil {
|
||||
t.Fatalf("Startup returned error: %v", err)
|
||||
}
|
||||
if tokenCalls.Load() != 1 {
|
||||
t.Fatalf("expected 1 startup token request, got %d", tokenCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryRetriesOnceWhenJWTExpires(t *testing.T) {
|
||||
var tokenCalls atomic.Int32
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/token":
|
||||
call := tokenCalls.Add(1)
|
||||
if call != 1 {
|
||||
t.Errorf("expected exactly 1 refresh call, got %d", call)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(w, `{"access_token":"fresh-token"}`)
|
||||
case "/sparql":
|
||||
switch r.Header.Get("Authorization") {
|
||||
case "Bearer expired-token":
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = io.WriteString(w, "Jwt is expired")
|
||||
case "Bearer fresh-token":
|
||||
_, _ = io.WriteString(w, `{"results":{"bindings":[{"s":{"type":"uri","value":"http://example.com/s"},"p":{"type":"uri","value":"http://example.com/p"},"o":{"type":"uri","value":"http://example.com/o"}}]}}`)
|
||||
default:
|
||||
t.Errorf("unexpected authorization header %q", r.Header.Get("Authorization"))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{
|
||||
SparqlSourceMode: "external",
|
||||
ExternalSparqlEndpoint: server.URL + "/sparql",
|
||||
KeycloakTokenEndpoint: server.URL + "/token",
|
||||
KeycloakClientID: "anzograph",
|
||||
KeycloakUsername: "user",
|
||||
KeycloakPassword: "pass",
|
||||
KeycloakScope: "openid",
|
||||
SparqlTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
client := NewAnzoGraphClient(cfg)
|
||||
client.client = server.Client()
|
||||
client.tokenManager.client = server.Client()
|
||||
client.tokenManager.token = "expired-token"
|
||||
|
||||
raw, err := client.Query(context.Background(), "SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
||||
if err != nil {
|
||||
t.Fatalf("Query returned error: %v", err)
|
||||
}
|
||||
if string(raw) == "" {
|
||||
t.Fatalf("expected successful response body after refresh")
|
||||
}
|
||||
if tokenCalls.Load() != 1 {
|
||||
t.Fatalf("expected 1 refresh call, got %d", tokenCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryDoesNotRefreshForNonExpiry401(t *testing.T) {
|
||||
var tokenCalls atomic.Int32
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/token":
|
||||
tokenCalls.Add(1)
|
||||
_, _ = io.WriteString(w, `{"access_token":"fresh-token"}`)
|
||||
case "/sparql":
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = io.WriteString(w, "RBAC: access denied")
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{
|
||||
SparqlSourceMode: "external",
|
||||
ExternalSparqlEndpoint: server.URL + "/sparql",
|
||||
KeycloakTokenEndpoint: server.URL + "/token",
|
||||
KeycloakClientID: "anzograph",
|
||||
KeycloakUsername: "user",
|
||||
KeycloakPassword: "pass",
|
||||
KeycloakScope: "openid",
|
||||
SparqlTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
client := NewAnzoGraphClient(cfg)
|
||||
client.client = server.Client()
|
||||
client.tokenManager.client = server.Client()
|
||||
client.tokenManager.token = "still-bad-token"
|
||||
|
||||
_, err := client.Query(context.Background(), "SELECT ?s ?p ?o WHERE { ?s ?p ?o }")
|
||||
if err == nil {
|
||||
t.Fatalf("expected non-expiry 401 to fail")
|
||||
}
|
||||
if tokenCalls.Load() != 0 {
|
||||
t.Fatalf("expected no token refresh for non-expiry 401, got %d", tokenCalls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentExpiredQueriesShareOneRefresh(t *testing.T) {
|
||||
var tokenCalls atomic.Int32
|
||||
var sparqlCalls atomic.Int32
|
||||
var mu sync.Mutex
|
||||
seenFresh := 0
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/token":
|
||||
tokenCalls.Add(1)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
_, _ = io.WriteString(w, `{"access_token":"fresh-token"}`)
|
||||
case "/sparql":
|
||||
sparqlCalls.Add(1)
|
||||
switch r.Header.Get("Authorization") {
|
||||
case "Bearer expired-token":
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = io.WriteString(w, "Jwt is expired")
|
||||
case "Bearer fresh-token":
|
||||
mu.Lock()
|
||||
seenFresh++
|
||||
mu.Unlock()
|
||||
_, _ = io.WriteString(w, `{"head":{},"boolean":true}`)
|
||||
default:
|
||||
t.Errorf("unexpected authorization header %q", r.Header.Get("Authorization"))
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfg := Config{
|
||||
SparqlSourceMode: "external",
|
||||
ExternalSparqlEndpoint: server.URL + "/sparql",
|
||||
KeycloakTokenEndpoint: server.URL + "/token",
|
||||
KeycloakClientID: "anzograph",
|
||||
KeycloakUsername: "user",
|
||||
KeycloakPassword: "pass",
|
||||
KeycloakScope: "openid",
|
||||
SparqlTimeout: 2 * time.Second,
|
||||
}
|
||||
|
||||
client := NewAnzoGraphClient(cfg)
|
||||
client.client = server.Client()
|
||||
client.tokenManager.client = server.Client()
|
||||
client.tokenManager.token = "expired-token"
|
||||
|
||||
const workers = 5
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, err := client.Query(context.Background(), "ASK WHERE { ?s ?p ?o }")
|
||||
errs <- err
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
t.Fatalf("concurrent query returned error: %v", err)
|
||||
}
|
||||
}
|
||||
if tokenCalls.Load() != 1 {
|
||||
t.Fatalf("expected exactly 1 shared refresh, got %d", tokenCalls.Load())
|
||||
}
|
||||
if seenFresh != workers {
|
||||
t.Fatalf("expected %d successful retried queries, got %d", workers, seenFresh)
|
||||
}
|
||||
if sparqlCalls.Load() < workers*2 {
|
||||
t.Fatalf("expected each worker to hit sparql before and after refresh, got %d calls", sparqlCalls.Load())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user