internal/server/sso_status_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"database/sql"
"path/filepath"
"testing"
"time"
appdb "github.com/X4Applegate/caddyui/internal/db"
"github.com/X4Applegate/caddyui/internal/models"
)
// v2.68.0: the Users page shows how each account signs in.
func TestSSOProviderLabel(t *testing.T) {
for in, want := range map[string]string{
"https://accounts.google.com": "Google",
"https://sso.example.com": "Example",
"https://auth.authentik.io/app/o/x/": "Authentik",
"": "SSO", // never configured
"not a url": "SSO",
"https://localhost": "SSO", // single label, nothing to shorten
} {
if got := ssoProviderLabel(in); got != want {
t.Errorf("ssoProviderLabel(%q) = %q, want %q", in, got, want)
}
}
}
// An SSO-provisioned account is given a random string rather than a hash, so
// no password can ever match it. The Users page leans on that to warn that
// the account depends entirely on the provider, so it has to hold.
func TestCanPasswordLoginDistinguishesRealHashes(t *testing.T) {
for name, tc := range map[string]struct {
hash string
want bool
}{
"bcrypt 2a": {"$2a$10$abcdefghijklmnopqrstuv", true},
"bcrypt 2b": {"$2b$12$abcdefghijklmnopqrstuv", true},
"bcrypt 2y": {"$2y$10$abcdefghijklmnopqrstuv", true},
"oidcRandom() output": {oidcRandom(), false},
"empty": {"", false},
"looks almost right": {"2a$10$notreallyahash", false},
} {
t.Run(name, func(t *testing.T) {
if got := (models.User{PasswordHash: tc.hash}).CanPasswordLogin(); got != tc.want {
t.Errorf("CanPasswordLogin(%q) = %v, want %v", tc.hash, got, tc.want)
}
})
}
}
func TestUsesSSOOnlyWhenRecorded(t *testing.T) {
if (models.User{}).UsesSSO() {
t.Error("an account that has never signed in via SSO must not report that it has")
}
u := models.User{SSOLastLogin: sql.NullTime{Time: time.Now(), Valid: true}}
if !u.UsesSSO() {
t.Error("a recorded SSO login was not reported")
}
}
// Recording is what makes the badge truthful: it must persist, and it must
// apply to an account created locally that later uses SSO — not only to
// accounts the provider created.
func TestRecordSSOLoginPersistsAndAppliesToLocalAccounts(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// A perfectly ordinary local account with a real password hash.
id, err := models.CreateUser(conn, "local@example.com", "$2a$10$abcdefghijklmnopqrstuv", "Local", models.RoleView)
if err != nil {
t.Fatal(err)
}
before, err := models.GetUserByEmail(conn, "local@example.com")
if err != nil {
t.Fatal(err)
}
if before.UsesSSO() {
t.Fatal("a freshly created local account must not look like an SSO user")
}
if !before.CanPasswordLogin() {
t.Fatal("a real bcrypt hash should permit password login")
}
if err := models.RecordSSOLogin(conn, id, "https://accounts.google.com"); err != nil {
t.Fatal(err)
}
after, err := models.GetUserByEmail(conn, "local@example.com")
if err != nil {
t.Fatal(err)
}
if !after.UsesSSO() {
t.Error("the recorded SSO login did not survive a round trip")
}
if after.SSOIssuer != "https://accounts.google.com" {
t.Errorf("issuer = %q, want the provider that was used", after.SSOIssuer)
}
// Both routes remain open for this account, and the page should say so.
if !after.CanPasswordLogin() {
t.Error("using SSO must not take away an existing password")
}
if ssoProviderLabel(after.SSOIssuer) != "Google" {
t.Errorf("badge label = %q, want Google", ssoProviderLabel(after.SSOIssuer))
}
}
// An account the provider created has no usable password, which is exactly
// the case the Users page flags as depending on SSO.
func TestProvisionedAccountIsFlaggedAsSSOOnly(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// Mirrors what the OIDC callback does.
id, err := models.CreateUser(conn, "joe@example.com", oidcRandom(), "Joe", models.RoleView)
if err != nil {
t.Fatal(err)
}
if err := models.RecordSSOLogin(conn, id, "https://accounts.google.com"); err != nil {
t.Fatal(err)
}
u, err := models.GetUserByEmail(conn, "joe@example.com")
if err != nil {
t.Fatal(err)
}
if !u.UsesSSO() {
t.Error("a provisioned account should report SSO use")
}
if u.CanPasswordLogin() {
t.Error("a provisioned account has no usable password and must not claim otherwise")
}
}