- TestOAuthProviders: old bytes without a source, order, per-request lookup - TestOAuthIdentitiesDestroyRule: the PHP last-identity rule table, 404s, microsoft by default, source names and opts.Providers precedence - TestOAuthIdentitiesDestroyRace: two row-locked transactions in both orders, the handler waiting for the lock, concurrent handlers - TestRegisterUserPasswordLess, TestFinishLogin, TestRegistrationExports
137 lines
4.7 KiB
Go
137 lines
4.7 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-user-plugin/classes"
|
|
"git.golem15.com/golem15/sm-user-plugin/controllers"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
)
|
|
|
|
// countingProviderSource answers a mutable provider list and counts the
|
|
// lookups, so a test can prove the handler asks on every request.
|
|
type countingProviderSource struct {
|
|
enabled atomic.Pointer[[]classes.OAuthProvider]
|
|
calls atomic.Int32
|
|
ctxSeen atomic.Bool
|
|
}
|
|
|
|
type ctxProbeKey struct{}
|
|
|
|
func (s *countingProviderSource) EnabledOAuthProviders(ctx context.Context) []classes.OAuthProvider {
|
|
s.calls.Add(1)
|
|
if ctx.Value(ctxProbeKey{}) == "probe" {
|
|
s.ctxSeen.Store(true)
|
|
}
|
|
if p := s.enabled.Load(); p != nil {
|
|
return *p
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *countingProviderSource) KnownOAuthProviders() []string { return nil }
|
|
|
|
// TestOAuthProviders pins GET /_user/api/v1/oauth-providers (D-15) without a
|
|
// database: the old bytes without a source, an empty list from a source
|
|
// with nothing enabled, the source's order, and a lookup on every request.
|
|
func TestOAuthProviders(t *testing.T) {
|
|
get := func(t *testing.T, h http.HandlerFunc, ctx context.Context) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(http.MethodGet, "/_user/api/v1/oauth-providers", nil)
|
|
if ctx != nil {
|
|
req = req.WithContext(ctx)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
|
|
}
|
|
if cc := rec.Header().Get("Cache-Control"); cc != "no-cache, private" {
|
|
t.Fatalf("Cache-Control = %q", cc)
|
|
}
|
|
return rec
|
|
}
|
|
body := func(rec *httptest.ResponseRecorder) string {
|
|
return strings.TrimRight(rec.Body.String(), "\n")
|
|
}
|
|
|
|
t.Run("no_source_old_bytes", func(t *testing.T) {
|
|
app := backpack.New(sessionConfig(t))
|
|
if got := body(get(t, controllers.OAuthProviders(app), nil)); got != `{"providers":[],"success":true}` {
|
|
t.Fatalf("body = %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("source_without_enabled_providers", func(t *testing.T) {
|
|
app := backpack.New(sessionConfig(t))
|
|
if err := app.Publish[classes.OAuthProviderSource](fakeProviderSource{known: []string{"google"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := body(get(t, controllers.OAuthProviders(app), nil)); got != `{"providers":[],"success":true}` {
|
|
t.Fatalf("body = %s", got)
|
|
}
|
|
})
|
|
|
|
t.Run("order_preserved", func(t *testing.T) {
|
|
app := backpack.New(sessionConfig(t))
|
|
src := fakeProviderSource{enabled: []classes.OAuthProvider{
|
|
{Name: "microsoft", Label: "Microsoft"},
|
|
{Name: "github", Label: "GitHub"},
|
|
{Name: "google", Label: "Google"},
|
|
}}
|
|
if err := app.Publish[classes.OAuthProviderSource](src); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := `{"providers":[{"label":"Microsoft","name":"microsoft"},{"label":"GitHub","name":"github"},{"label":"Google","name":"google"}],"success":true}`
|
|
if got := body(get(t, controllers.OAuthProviders(app), nil)); got != want {
|
|
t.Fatalf("body = %s\nwant %s", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("lookup_per_request", func(t *testing.T) {
|
|
app := backpack.New(sessionConfig(t))
|
|
h := controllers.OAuthProviders(app)
|
|
if got := body(get(t, h, nil)); got != `{"providers":[],"success":true}` {
|
|
t.Fatalf("before publishing: %s", got)
|
|
}
|
|
src := &countingProviderSource{}
|
|
if err := app.Publish[classes.OAuthProviderSource](src); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := []classes.OAuthProvider{{Name: "google", Label: "Google"}}
|
|
src.enabled.Store(&first)
|
|
probe := context.WithValue(context.Background(), ctxProbeKey{}, "probe")
|
|
if got := body(get(t, h, probe)); got != `{"providers":[{"label":"Google","name":"google"}],"success":true}` {
|
|
t.Fatalf("after publishing: %s", got)
|
|
}
|
|
second := []classes.OAuthProvider{{Name: "github", Label: "GitHub"}}
|
|
src.enabled.Store(&second)
|
|
if got := body(get(t, h, nil)); got != `{"providers":[{"label":"GitHub","name":"github"}],"success":true}` {
|
|
t.Fatalf("after the source changed: %s", got)
|
|
}
|
|
if n := src.calls.Load(); n != 2 {
|
|
t.Fatalf("EnabledOAuthProviders called %d times for 2 requests", n)
|
|
}
|
|
if !src.ctxSeen.Load() {
|
|
t.Fatal("EnabledOAuthProviders did not receive the request context")
|
|
}
|
|
})
|
|
|
|
t.Run("bare_publish_is_not_found", func(t *testing.T) {
|
|
// A source published without the explicit type argument is keyed on
|
|
// its concrete type; the list stays empty (the documented pitfall).
|
|
app := backpack.New(sessionConfig(t))
|
|
if err := app.Publish(fakeProviderSource{enabled: []classes.OAuthProvider{{Name: "google", Label: "Google"}}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := body(get(t, controllers.OAuthProviders(app), nil)); got != `{"providers":[],"success":true}` {
|
|
t.Fatalf("body = %s", got)
|
|
}
|
|
})
|
|
}
|