Ctrl K
ComunidadGuías…

internal/server/sso_status_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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")
	}
}