mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-08-10 05:28:14 +00:00
dd275d2e78
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
333 lines
9.5 KiB
Go
333 lines
9.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"Noooste/garage-ui/internal/auth"
|
|
"Noooste/garage-ui/internal/config"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
)
|
|
|
|
// newAuthTestService builds a real auth.Service with OIDC disabled. The JWT
|
|
// key is auto-generated, matching the production default.
|
|
func newAuthTestService(t *testing.T, admin config.AdminAuthConfig) *auth.Service {
|
|
t.Helper()
|
|
svc, err := auth.NewAuthService(
|
|
&config.AuthConfig{
|
|
Admin: admin,
|
|
OIDC: config.OIDCConfig{Enabled: false},
|
|
},
|
|
&config.ServerConfig{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("NewAuthService: %v", err)
|
|
}
|
|
return svc
|
|
}
|
|
|
|
// newAuthTestApp builds a bare Fiber app with the auth handler mounted.
|
|
// The admin and OIDC config are reflected both in cfg (for the handler) and
|
|
// in the auth service.
|
|
func newAuthTestApp(t *testing.T, cfg *config.Config) (*fiber.App, *AuthHandler) {
|
|
t.Helper()
|
|
svc := newAuthTestService(t, cfg.Auth.Admin)
|
|
h := NewAuthHandler(cfg, svc)
|
|
app := fiber.New()
|
|
app.Get("/auth/config", h.GetAuthConfig)
|
|
app.Post("/auth/login", h.LoginAdmin)
|
|
app.Get("/auth/me", h.GetMe)
|
|
return app, h
|
|
}
|
|
|
|
func TestGetAuthConfig_AdminOnly(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "p"},
|
|
OIDC: config.OIDCConfig{Enabled: false},
|
|
},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var body struct {
|
|
Admin struct {
|
|
Enabled bool `json:"enabled"`
|
|
} `json:"admin"`
|
|
OIDC struct {
|
|
Enabled bool `json:"enabled"`
|
|
Provider string `json:"provider"`
|
|
} `json:"oidc"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !body.Admin.Enabled {
|
|
t.Error("admin.enabled = false, want true")
|
|
}
|
|
if body.OIDC.Enabled {
|
|
t.Error("oidc.enabled = true, want false")
|
|
}
|
|
if body.OIDC.Provider != "" {
|
|
t.Errorf("oidc.provider = %q, want empty", body.OIDC.Provider)
|
|
}
|
|
}
|
|
|
|
func TestGetAuthConfig_OIDCOnly_WithExplicitProvider(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: false},
|
|
OIDC: config.OIDCConfig{
|
|
Enabled: false, // service init skipped (newAuthTestService disables OIDC); handler only reads flags
|
|
ProviderName: "Keycloak",
|
|
},
|
|
},
|
|
}
|
|
// Re-enable OIDC only on the cfg the handler sees — the service is still
|
|
// constructed with OIDC disabled above, which is fine because
|
|
// GetAuthConfig does not touch the service at all.
|
|
cfg.Auth.OIDC.Enabled = true
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var body struct {
|
|
OIDC struct {
|
|
Enabled bool `json:"enabled"`
|
|
Provider string `json:"provider"`
|
|
} `json:"oidc"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !body.OIDC.Enabled {
|
|
t.Error("oidc.enabled = false, want true")
|
|
}
|
|
if body.OIDC.Provider != "Keycloak" {
|
|
t.Errorf("oidc.provider = %q, want Keycloak", body.OIDC.Provider)
|
|
}
|
|
}
|
|
|
|
func TestGetAuthConfig_OIDCEnabled_DefaultProviderName(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: false},
|
|
OIDC: config.OIDCConfig{Enabled: true, ProviderName: ""},
|
|
},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/config", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var body struct {
|
|
OIDC struct {
|
|
Provider string `json:"provider"`
|
|
} `json:"oidc"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&body)
|
|
if body.OIDC.Provider != "OIDC Provider" {
|
|
t.Errorf("provider = %q, want default 'OIDC Provider'", body.OIDC.Provider)
|
|
}
|
|
}
|
|
|
|
func TestLoginAdmin_HappyPath(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{
|
|
Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"},
|
|
},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
|
|
body, _ := json.Marshal(map[string]string{"username": "admin", "password": "s3cret"})
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("status = %d, want 200\nbody: %s", resp.StatusCode, raw)
|
|
}
|
|
|
|
var decoded struct {
|
|
Success bool `json:"success"`
|
|
Token string `json:"token"`
|
|
User struct {
|
|
Username string `json:"username"`
|
|
} `json:"user"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !decoded.Success {
|
|
t.Error("success = false")
|
|
}
|
|
if decoded.Token == "" {
|
|
t.Error("token empty")
|
|
}
|
|
if decoded.User.Username != "admin" {
|
|
t.Errorf("username = %q, want admin", decoded.User.Username)
|
|
}
|
|
}
|
|
|
|
func TestLoginAdmin_WrongPasswordReturns401(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"}},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
body, _ := json.Marshal(map[string]string{"username": "admin", "password": "WRONG"})
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginAdmin_WrongUsernameReturns401(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "s3cret"}},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
body, _ := json.Marshal(map[string]string{"username": "root", "password": "s3cret"})
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginAdmin_MalformedJSONReturns400(t *testing.T) {
|
|
cfg := &config.Config{
|
|
Auth: config.AuthConfig{Admin: config.AdminAuthConfig{Enabled: true, Username: "admin", Password: "p"}},
|
|
}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
req := httptest.NewRequest(http.MethodPost, "/auth/login", strings.NewReader("{not-json"))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestGetMe_OIDCUserInfoLocal(t *testing.T) {
|
|
cfg := &config.Config{Auth: config.AuthConfig{}}
|
|
app, h := newAuthTestApp(t, cfg)
|
|
// Re-register /auth/me with a pre-handler that seeds c.Locals("userInfo").
|
|
// The default registration in newAuthTestApp lacks Locals; we mount a
|
|
// second path that does.
|
|
app.Get("/me-oidc", func(c fiber.Ctx) error {
|
|
c.Locals("userInfo", &auth.UserInfo{
|
|
Username: "alice",
|
|
Email: "alice@example.com",
|
|
Name: "Alice Example",
|
|
})
|
|
return h.GetMe(c)
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/me-oidc", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var decoded struct {
|
|
Success bool `json:"success"`
|
|
User struct {
|
|
Username string `json:"username"`
|
|
Email string `json:"email"`
|
|
Name string `json:"name"`
|
|
} `json:"user"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if decoded.User.Username != "alice" || decoded.User.Email != "alice@example.com" || decoded.User.Name != "Alice Example" {
|
|
t.Errorf("got %+v", decoded.User)
|
|
}
|
|
}
|
|
|
|
func TestGetMe_BasicAuthUsernameLocal(t *testing.T) {
|
|
cfg := &config.Config{Auth: config.AuthConfig{}}
|
|
app, h := newAuthTestApp(t, cfg)
|
|
app.Get("/me-basic", func(c fiber.Ctx) error {
|
|
c.Locals("username", "admin")
|
|
return h.GetMe(c)
|
|
})
|
|
req := httptest.NewRequest(http.MethodGet, "/me-basic", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", resp.StatusCode)
|
|
}
|
|
var decoded struct {
|
|
User struct {
|
|
Username string `json:"username"`
|
|
} `json:"user"`
|
|
}
|
|
_ = json.NewDecoder(resp.Body).Decode(&decoded)
|
|
if decoded.User.Username != "admin" {
|
|
t.Errorf("username = %q, want admin", decoded.User.Username)
|
|
}
|
|
}
|
|
|
|
func TestGetMe_NoLocalsReturns401(t *testing.T) {
|
|
cfg := &config.Config{Auth: config.AuthConfig{}}
|
|
app, _ := newAuthTestApp(t, cfg)
|
|
req := httptest.NewRequest(http.MethodGet, "/auth/me", nil)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
t.Fatalf("app.Test: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want 401", resp.StatusCode)
|
|
}
|
|
}
|