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) } }) }