Files
terdut-server/internal/config/config_test.go
T
Niklas Ye 5b4683febf Let each team name its own OIDC group, not a global mapping
Team membership from single sign-on used to come from one env var,
TERDUT_OIDC_GROUP_MAPPINGS, matched against a team by name and creating
the team if none existed. That put the decision in the server's
environment rather than the team's own hands, needed a restart to
change, and let a typo in a team name silently create a stray team.

Each team now carries its own oidc_member_group and oidc_owner_group,
set by its owner (or an administrator) from the Members tab, or PUT
/api/teams/{teamID}/oidc-groups. The "highest role wins" rule
TERDUT_OIDC_GROUP_MAPPINGS used to apply across mappings now applies
across one team's own two fields: being in both makes somebody an
owner. The sync no longer creates a team by name; a group only ever
grants into a team that already exists.

This is a breaking change for anyone already using
TERDUT_OIDC_GROUP_MAPPINGS, deliberately not auto-migrated: an
OIDC-sourced membership is dropped at a user's next sign-in until its
team's owner re-sets the group. The README's OIDC section spells out
the migration and the risk of a visible access gap during it.

TERDUT_OIDC_ADMIN_GROUP and TERDUT_OIDC_ALLOWED_GROUPS are untouched --
only team membership moved. terdut-tui needs no change: it only reads
GET /api/teams and GET /api/teams/{id}/members, and neither response
shape moved.
2026-09-27 11:43:57 +02:00

80 lines
2.6 KiB
Go

package config
import (
"strings"
"testing"
)
func TestValidate(t *testing.T) {
base := func() map[string]string {
return map[string]string{
"TERDUT_PUBLIC_URL": "https://terdut.example.com",
"TERDUT_OIDC_ISSUER": "https://auth.example.com/application/o/terdut/",
"TERDUT_OIDC_CLIENT_ID": "id",
"TERDUT_OIDC_CLIENT_SECRET": "secret",
}
}
tests := []struct {
name string
env func(map[string]string)
wantErr string // substring; empty means valid
}{
{"off by default", func(m map[string]string) { clear(m) }, ""},
{"minimal sso", func(m map[string]string) {}, ""},
{"groups without issuer", func(m map[string]string) {
clear(m)
m["TERDUT_OIDC_ADMIN_GROUP"] = "admins"
}, "ISSUER is not"},
{"missing secret", func(m map[string]string) { delete(m, "TERDUT_OIDC_CLIENT_SECRET") }, "CLIENT_SECRET"},
{"missing public url", func(m map[string]string) { delete(m, "TERDUT_PUBLIC_URL") }, "PUBLIC_URL"},
{"bad issuer", func(m map[string]string) { m["TERDUT_OIDC_ISSUER"] = "not a url" }, "not a URL"},
{"password off without sso", func(m map[string]string) {
clear(m)
m["TERDUT_PASSWORD_LOGIN"] = "false"
}, "no way to sign in"},
{"password off with sso but no grants", func(m map[string]string) {
m["TERDUT_PASSWORD_LOGIN"] = "false"
}, "nobody able"},
{"password off with admin group", func(m map[string]string) {
m["TERDUT_PASSWORD_LOGIN"] = "false"
m["TERDUT_OIDC_ADMIN_GROUP"] = "admins"
}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env := base()
tt.env(env)
for _, k := range []string{
"TERDUT_PUBLIC_URL", "TERDUT_PASSWORD_LOGIN", "TERDUT_OIDC_ISSUER", "TERDUT_OIDC_CLIENT_ID",
"TERDUT_OIDC_CLIENT_SECRET", "TERDUT_OIDC_ADMIN_GROUP",
} {
t.Setenv(k, env[k])
}
err := Load().Validate()
switch {
case tt.wantErr == "" && err != nil:
t.Errorf("unexpected error: %v", err)
case tt.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tt.wantErr)):
t.Errorf("error %v, want one containing %q", err, tt.wantErr)
}
})
}
}
func TestLoad_OIDCDefaults(t *testing.T) {
t.Setenv("TERDUT_OIDC_ISSUER", "https://auth.example.com/")
o := Load().OIDC
if o.UsernameClaim != "preferred_username" || o.EmailClaim != "email" || o.GroupsClaim != "groups" {
t.Errorf("claim defaults: %+v", o)
}
if strings.Join(o.Scopes, " ") != "openid profile email" {
t.Errorf("scopes: %v", o.Scopes)
}
if o.SessionMaxAge.Hours() != 12 {
t.Errorf("max age: %v", o.SessionMaxAge)
}
if Load().DisablePasswordLogin {
t.Error("password login should be on by default")
}
}