Compare commits
7 Commits
tmdb/v0.9.
...
hwsauth/v0
| Author | SHA1 | Date | |
|---|---|---|---|
| 87027e9761 | |||
| 2f49063432 | |||
| 1c49b19197 | |||
| f25bc437c4 | |||
| 378bd8006d | |||
| e9b96fedb1 | |||
| da6ad0cf2e |
@@ -18,16 +18,16 @@ type EnvVar struct {
|
|||||||
|
|
||||||
// ConfigLoader manages configuration loading from multiple sources
|
// ConfigLoader manages configuration loading from multiple sources
|
||||||
type ConfigLoader struct {
|
type ConfigLoader struct {
|
||||||
configFuncs map[string]ConfigFunc // Map of config names to ConfigFromEnv functions
|
configFuncs map[string]ConfigFunc // Map of config names to ConfigFromEnv functions
|
||||||
packagePaths []string // Paths to packages to parse for ENV comments
|
packagePaths []string // Paths to packages to parse for ENV comments
|
||||||
groupNames map[string]string // Map of package paths to group names
|
groupNames map[string]string // Map of package paths to group names
|
||||||
extraEnvVars []EnvVar // Additional environment variables to track
|
extraEnvVars []EnvVar // Additional environment variables to track
|
||||||
envVars []EnvVar // All extracted environment variables
|
envVars []EnvVar // All extracted environment variables
|
||||||
configs map[string]interface{} // Loaded configurations
|
configs map[string]any // Loaded configurations
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigFunc is a function that loads configuration from environment variables
|
// ConfigFunc is a function that loads configuration from environment variables
|
||||||
type ConfigFunc func() (interface{}, error)
|
type ConfigFunc func() (any, error)
|
||||||
|
|
||||||
// New creates a new ConfigLoader
|
// New creates a new ConfigLoader
|
||||||
func New() *ConfigLoader {
|
func New() *ConfigLoader {
|
||||||
@@ -37,7 +37,7 @@ func New() *ConfigLoader {
|
|||||||
groupNames: make(map[string]string),
|
groupNames: make(map[string]string),
|
||||||
extraEnvVars: make([]EnvVar, 0),
|
extraEnvVars: make([]EnvVar, 0),
|
||||||
envVars: make([]EnvVar, 0),
|
envVars: make([]EnvVar, 0),
|
||||||
configs: make(map[string]interface{}),
|
configs: make(map[string]any),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +72,12 @@ func (cl *ConfigLoader) AddEnvVar(envVar EnvVar) {
|
|||||||
cl.extraEnvVars = append(cl.extraEnvVars, envVar)
|
cl.extraEnvVars = append(cl.extraEnvVars, envVar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads all configurations and extracts environment variables
|
// ParseEnvVars extracts environment variables from packages and extra vars
|
||||||
func (cl *ConfigLoader) Load() error {
|
// This can be called without having actual environment variables set
|
||||||
|
func (cl *ConfigLoader) ParseEnvVars() error {
|
||||||
|
// Clear existing env vars to prevent duplicates
|
||||||
|
cl.envVars = make([]EnvVar, 0)
|
||||||
|
|
||||||
// Parse packages for ENV comments
|
// Parse packages for ENV comments
|
||||||
for _, pkgPath := range cl.packagePaths {
|
for _, pkgPath := range cl.packagePaths {
|
||||||
envVars, err := ParseConfigPackage(pkgPath)
|
envVars, err := ParseConfigPackage(pkgPath)
|
||||||
@@ -102,6 +106,12 @@ func (cl *ConfigLoader) Load() error {
|
|||||||
cl.envVars[i].CurrentValue = os.Getenv(cl.envVars[i].Name)
|
cl.envVars[i].CurrentValue = os.Getenv(cl.envVars[i].Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfigs executes the config functions to load actual configurations
|
||||||
|
// This should be called after environment variables are properly set
|
||||||
|
func (cl *ConfigLoader) LoadConfigs() error {
|
||||||
// Load configurations
|
// Load configurations
|
||||||
for name, fn := range cl.configFuncs {
|
for name, fn := range cl.configFuncs {
|
||||||
cfg, err := fn()
|
cfg, err := fn()
|
||||||
@@ -114,14 +124,22 @@ func (cl *ConfigLoader) Load() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load loads all configurations and extracts environment variables
|
||||||
|
func (cl *ConfigLoader) Load() error {
|
||||||
|
if err := cl.ParseEnvVars(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cl.LoadConfigs()
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfig returns a loaded configuration by name
|
// GetConfig returns a loaded configuration by name
|
||||||
func (cl *ConfigLoader) GetConfig(name string) (interface{}, bool) {
|
func (cl *ConfigLoader) GetConfig(name string) (any, bool) {
|
||||||
cfg, ok := cl.configs[name]
|
cfg, ok := cl.configs[name]
|
||||||
return cfg, ok
|
return cfg, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllConfigs returns all loaded configurations
|
// GetAllConfigs returns all loaded configurations
|
||||||
func (cl *ConfigLoader) GetAllConfigs() map[string]interface{} {
|
func (cl *ConfigLoader) GetAllConfigs() map[string]any {
|
||||||
return cl.configs
|
return cl.configs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package ezconf
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -240,6 +241,163 @@ func TestGetEnvVars(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseEnvVars(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add a test config function
|
||||||
|
loader.AddConfigFunc("test", func() (interface{}, error) {
|
||||||
|
return "test config", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add current package path
|
||||||
|
loader.AddPackagePath(".")
|
||||||
|
|
||||||
|
// Add an extra env var
|
||||||
|
loader.AddEnvVar(EnvVar{
|
||||||
|
Name: "EXTRA_VAR",
|
||||||
|
Description: "Extra test variable",
|
||||||
|
Default: "extra",
|
||||||
|
})
|
||||||
|
|
||||||
|
err := loader.ParseEnvVars()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that env vars were extracted
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) == 0 {
|
||||||
|
t.Error("expected at least one env var")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for extra var
|
||||||
|
foundExtra := false
|
||||||
|
for _, ev := range envVars {
|
||||||
|
if ev.Name == "EXTRA_VAR" {
|
||||||
|
foundExtra = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundExtra {
|
||||||
|
t.Error("extra env var not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that configs are NOT loaded (should be empty)
|
||||||
|
configs := loader.GetAllConfigs()
|
||||||
|
if len(configs) != 0 {
|
||||||
|
t.Errorf("expected no configs loaded after ParseEnvVars, got %d", len(configs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigs(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add a test config function
|
||||||
|
testCfg := struct {
|
||||||
|
Value string
|
||||||
|
}{Value: "test"}
|
||||||
|
|
||||||
|
loader.AddConfigFunc("test", func() (interface{}, error) {
|
||||||
|
return testCfg, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manually set some env vars (simulating ParseEnvVars already called)
|
||||||
|
loader.envVars = []EnvVar{
|
||||||
|
{Name: "TEST_VAR", Description: "Test variable"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := loader.LoadConfigs()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfigs failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that config was loaded
|
||||||
|
cfg, ok := loader.GetConfig("test")
|
||||||
|
if !ok {
|
||||||
|
t.Error("test config not loaded")
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
t.Error("test config is nil")
|
||||||
|
}
|
||||||
|
_ = cfg // Use the variable to avoid unused variable error
|
||||||
|
|
||||||
|
// Check that env vars are NOT modified (should remain as set)
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) != 1 {
|
||||||
|
t.Errorf("expected 1 env var, got %d", len(envVars))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigs_Error(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
loader.AddConfigFunc("error", func() (interface{}, error) {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
})
|
||||||
|
|
||||||
|
err := loader.LoadConfigs()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error from failing config func")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseEnvVars_Then_LoadConfigs(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add a test config function
|
||||||
|
testCfg := struct {
|
||||||
|
Value string
|
||||||
|
}{Value: "test"}
|
||||||
|
|
||||||
|
loader.AddConfigFunc("test", func() (interface{}, error) {
|
||||||
|
return testCfg, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add current package path
|
||||||
|
loader.AddPackagePath(".")
|
||||||
|
|
||||||
|
// Add an extra env var
|
||||||
|
loader.AddEnvVar(EnvVar{
|
||||||
|
Name: "EXTRA_VAR",
|
||||||
|
Description: "Extra test variable",
|
||||||
|
Default: "extra",
|
||||||
|
})
|
||||||
|
|
||||||
|
// First parse env vars
|
||||||
|
err := loader.ParseEnvVars()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check env vars are extracted but configs are not loaded
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) == 0 {
|
||||||
|
t.Error("expected env vars to be extracted")
|
||||||
|
}
|
||||||
|
|
||||||
|
configs := loader.GetAllConfigs()
|
||||||
|
if len(configs) != 0 {
|
||||||
|
t.Error("expected no configs loaded yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then load configs
|
||||||
|
err = loader.LoadConfigs()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfigs failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check both env vars and configs are loaded
|
||||||
|
_, ok := loader.GetConfig("test")
|
||||||
|
if !ok {
|
||||||
|
t.Error("test config not loaded after LoadConfigs")
|
||||||
|
}
|
||||||
|
|
||||||
|
configs = loader.GetAllConfigs()
|
||||||
|
if len(configs) != 1 {
|
||||||
|
t.Errorf("expected 1 config loaded, got %d", len(configs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoad_Integration(t *testing.T) {
|
func TestLoad_Integration(t *testing.T) {
|
||||||
// Integration test with real hlog package
|
// Integration test with real hlog package
|
||||||
hlogPath := filepath.Join("..", "hlog")
|
hlogPath := filepath.Join("..", "hlog")
|
||||||
@@ -269,3 +427,62 @@ func TestLoad_Integration(t *testing.T) {
|
|||||||
t.Logf(" %s: %s (default: %s, required: %t)", ev.Name, ev.Description, ev.Default, ev.Required)
|
t.Logf(" %s: %s (default: %s, required: %t)", ev.Name, ev.Description, ev.Default, ev.Required)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseEnvVars_GenerateEnvFile_Integration(t *testing.T) {
|
||||||
|
// Test the new separated ParseEnvVars functionality
|
||||||
|
hlogPath := filepath.Join("..", "hlog")
|
||||||
|
if _, err := os.Stat(hlogPath); os.IsNotExist(err) {
|
||||||
|
t.Skip("hlog package not found, skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add hlog package
|
||||||
|
if err := loader.AddPackagePath(hlogPath); err != nil {
|
||||||
|
t.Fatalf("failed to add hlog package: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse env vars without loading configs (this should work even if required env vars are missing)
|
||||||
|
if err := loader.ParseEnvVars(); err != nil {
|
||||||
|
t.Fatalf("ParseEnvVars failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
envVars := loader.GetEnvVars()
|
||||||
|
if len(envVars) == 0 {
|
||||||
|
t.Error("expected env vars from hlog package")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now test that we can generate an env file without calling Load()
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
envFile := filepath.Join(tempDir, "test-generated.env")
|
||||||
|
|
||||||
|
err := loader.GenerateEnvFile(envFile, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GenerateEnvFile failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the file was created and contains expected content
|
||||||
|
content, err := os.ReadFile(envFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read generated file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := string(content)
|
||||||
|
if !strings.Contains(output, "# Environment Configuration") {
|
||||||
|
t.Error("expected header in generated file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should contain environment variables from hlog
|
||||||
|
foundHlogVar := false
|
||||||
|
for _, ev := range envVars {
|
||||||
|
if strings.Contains(output, ev.Name) {
|
||||||
|
foundHlogVar = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHlogVar {
|
||||||
|
t.Error("expected to find at least one hlog environment variable in generated file")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully generated env file with %d variables", len(envVars))
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
// PrintEnvVars prints all environment variables to the provided writer
|
// PrintEnvVars prints all environment variables to the provided writer
|
||||||
func (cl *ConfigLoader) PrintEnvVars(w io.Writer, showValues bool) error {
|
func (cl *ConfigLoader) PrintEnvVars(w io.Writer, showValues bool) error {
|
||||||
if cl.envVars == nil || len(cl.envVars) == 0 {
|
if len(cl.envVars) == 0 {
|
||||||
return errors.New("no environment variables loaded (did you call Load()?)")
|
return errors.New("no environment variables loaded (did you call Load()?)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -360,3 +360,46 @@ func TestPrintEnvVarsStdout_NoEnvVars(t *testing.T) {
|
|||||||
t.Error("expected error when no env vars are loaded")
|
t.Error("expected error when no env vars are loaded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrintEnvVars_AfterParseEnvVars(t *testing.T) {
|
||||||
|
loader := New()
|
||||||
|
|
||||||
|
// Add some env vars manually to simulate ParseEnvVars
|
||||||
|
loader.envVars = []EnvVar{
|
||||||
|
{
|
||||||
|
Name: "LOG_LEVEL",
|
||||||
|
Description: "Log level for the application",
|
||||||
|
Required: false,
|
||||||
|
Default: "info",
|
||||||
|
CurrentValue: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "DATABASE_URL",
|
||||||
|
Description: "Database connection string",
|
||||||
|
Required: true,
|
||||||
|
Default: "",
|
||||||
|
CurrentValue: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that PrintEnvVars works after ParseEnvVars (without Load)
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
err := loader.PrintEnvVars(buf, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PrintEnvVars failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := buf.String()
|
||||||
|
if !strings.Contains(output, "LOG_LEVEL") {
|
||||||
|
t.Error("output should contain LOG_LEVEL")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "DATABASE_URL") {
|
||||||
|
t.Error("output should contain DATABASE_URL")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "(required)") {
|
||||||
|
t.Error("output should indicate required variables")
|
||||||
|
}
|
||||||
|
if !strings.Contains(output, "(default: info)") {
|
||||||
|
t.Error("output should contain default value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ func main() {
|
|||||||
Method: hws.MethodGET,
|
Method: hws.MethodGET,
|
||||||
Handler: http.HandlerFunc(getUserHandler),
|
Handler: http.HandlerFunc(getUserHandler),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Single route handling multiple HTTP methods
|
||||||
|
Path: "/api/resource",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: http.HandlerFunc(resourceHandler),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add routes and middleware
|
// Add routes and middleware
|
||||||
@@ -73,6 +79,18 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
id := r.PathValue("id")
|
id := r.PathValue("id")
|
||||||
w.Write([]byte("User ID: " + id))
|
w.Write([]byte("User ID: " + id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Handle GET, POST, and PUT for the same path
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
w.Write([]byte("Getting resource"))
|
||||||
|
case "POST":
|
||||||
|
w.Write([]byte("Creating resource"))
|
||||||
|
case "PUT":
|
||||||
|
w.Write([]byte("Updating resource"))
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
12
hws/doc.go
12
hws/doc.go
@@ -74,6 +74,18 @@
|
|||||||
// },
|
// },
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
// A single route can handle multiple HTTP methods using the Methods field:
|
||||||
|
//
|
||||||
|
// routes := []hws.Route{
|
||||||
|
// {
|
||||||
|
// Path: "/api/resource",
|
||||||
|
// Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
// Handler: http.HandlerFunc(resourceHandler),
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note: The Methods field takes precedence over Method if both are provided.
|
||||||
|
//
|
||||||
// Path parameters can be accessed using r.PathValue():
|
// Path parameters can be accessed using r.PathValue():
|
||||||
//
|
//
|
||||||
// func getUser(w http.ResponseWriter, r *http.Request) {
|
// func getUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const (
|
|||||||
|
|
||||||
// ErrorPageFunc is a function that returns an ErrorPage with the specified HTTP Status code
|
// ErrorPageFunc is a function that returns an ErrorPage with the specified HTTP Status code
|
||||||
// This will be called by the server when it needs to render an error page
|
// This will be called by the server when it needs to render an error page
|
||||||
type ErrorPageFunc func(errorCode int) (ErrorPage, error)
|
type ErrorPageFunc func(error HWSError) (ErrorPage, error)
|
||||||
|
|
||||||
// ErrorPage must implement a Render() function that takes in a context and ResponseWriter,
|
// ErrorPage must implement a Render() function that takes in a context and ResponseWriter,
|
||||||
// and should write a reponse as output to the ResponseWriter.
|
// and should write a reponse as output to the ResponseWriter.
|
||||||
@@ -40,11 +40,11 @@ type ErrorPage interface {
|
|||||||
Render(ctx context.Context, w io.Writer) error
|
Render(ctx context.Context, w io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add test for ErrorPageFunc that returns an error
|
// AddErrorPage registers a handler that returns an ErrorPage
|
||||||
func (server *Server) AddErrorPage(pageFunc ErrorPageFunc) error {
|
func (server *Server) AddErrorPage(pageFunc ErrorPageFunc) error {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("GET", "/", nil)
|
req := httptest.NewRequest("GET", "/", nil)
|
||||||
page, err := pageFunc(http.StatusInternalServerError)
|
page, err := pageFunc(HWSError{StatusCode: http.StatusInternalServerError})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "An error occured when trying to get the error page")
|
return errors.Wrap(err, "An error occured when trying to get the error page")
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ func (server *Server) ThrowError(w http.ResponseWriter, r *http.Request, error H
|
|||||||
}
|
}
|
||||||
if error.RenderErrorPage {
|
if error.RenderErrorPage {
|
||||||
server.LogError(HWSError{Message: "Error page rendering", Error: nil, Level: ErrorDEBUG})
|
server.LogError(HWSError{Message: "Error page rendering", Error: nil, Level: ErrorDEBUG})
|
||||||
errPage, err := server.errorPage(error.StatusCode)
|
errPage, err := server.errorPage(error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
server.LogError(HWSError{Message: "Failed to get a valid error page", Error: err})
|
server.LogError(HWSError{Message: "Failed to get a valid error page", Error: err})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import (
|
|||||||
type goodPage struct{}
|
type goodPage struct{}
|
||||||
type badPage struct{}
|
type badPage struct{}
|
||||||
|
|
||||||
func goodRender(code int) (hws.ErrorPage, error) {
|
func goodRender(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
return goodPage{}, nil
|
return goodPage{}, nil
|
||||||
}
|
}
|
||||||
func badRender1(code int) (hws.ErrorPage, error) {
|
func badRender1(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
return badPage{}, nil
|
return badPage{}, nil
|
||||||
}
|
}
|
||||||
func badRender2(code int) (hws.ErrorPage, error) {
|
func badRender2(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
return nil, errors.New("I'm an error")
|
return nil, errors.New("I'm an error")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Route struct {
|
type Route struct {
|
||||||
Path string // Absolute path to the requested resource
|
Path string // Absolute path to the requested resource
|
||||||
Method Method // HTTP Method
|
Method Method // HTTP Method
|
||||||
|
// Methods is an optional slice of Methods to use, if more than one can use the same handler.
|
||||||
|
// Will take precedence over the Method field if provided
|
||||||
|
Methods []Method
|
||||||
Handler http.Handler // Handler to use for the request
|
Handler http.Handler // Handler to use for the request
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,21 +32,33 @@ const (
|
|||||||
|
|
||||||
// Server.AddRoutes registers the page handlers for the server.
|
// Server.AddRoutes registers the page handlers for the server.
|
||||||
// At least one route must be provided.
|
// At least one route must be provided.
|
||||||
|
// If any route patterns (path + method) are defined multiple times, the first
|
||||||
|
// instance will be added and any additional conflicts will be discarded.
|
||||||
func (server *Server) AddRoutes(routes ...Route) error {
|
func (server *Server) AddRoutes(routes ...Route) error {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
return errors.New("No routes provided")
|
return errors.New("No routes provided")
|
||||||
}
|
}
|
||||||
|
patterns := []string{}
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
|
mux.HandleFunc("GET /healthz", func(http.ResponseWriter, *http.Request) {})
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
if !validMethod(route.Method) {
|
if len(route.Methods) == 0 {
|
||||||
return fmt.Errorf("Invalid method %s for path %s", route.Method, route.Path)
|
route.Methods = []Method{route.Method}
|
||||||
}
|
}
|
||||||
if route.Handler == nil {
|
for _, method := range route.Methods {
|
||||||
return fmt.Errorf("No handler provided for %s %s", route.Method, route.Path)
|
if !validMethod(method) {
|
||||||
|
return fmt.Errorf("Invalid method %s for path %s", method, route.Path)
|
||||||
|
}
|
||||||
|
if route.Handler == nil {
|
||||||
|
return fmt.Errorf("No handler provided for %s %s", method, route.Path)
|
||||||
|
}
|
||||||
|
pattern := fmt.Sprintf("%s %s", method, route.Path)
|
||||||
|
if slices.Contains(patterns, pattern) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
patterns = append(patterns, pattern)
|
||||||
|
mux.Handle(pattern, route.Handler)
|
||||||
}
|
}
|
||||||
pattern := fmt.Sprintf("%s %s", route.Method, route.Path)
|
|
||||||
mux.Handle(pattern, route.Handler)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.server.Handler = mux
|
server.server.Handler = mux
|
||||||
|
|||||||
@@ -122,6 +122,111 @@ func Test_AddRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_AddRoutes_MultipleMethods(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
t.Run("Single route with multiple methods", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(r.Method + " response"))
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/api/resource",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GET request
|
||||||
|
req := httptest.NewRequest("GET", "/api/resource", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "GET response", rr.Body.String())
|
||||||
|
|
||||||
|
// Test POST request
|
||||||
|
req = httptest.NewRequest("POST", "/api/resource", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "POST response", rr.Body.String())
|
||||||
|
|
||||||
|
// Test PUT request
|
||||||
|
req = httptest.NewRequest("PUT", "/api/resource", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
assert.Equal(t, "PUT response", rr.Body.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Methods field takes precedence over Method field", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Method: hws.MethodGET, // This should be ignored
|
||||||
|
Methods: []hws.Method{hws.MethodPOST, hws.MethodPUT},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GET should not work (Method field ignored)
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
|
||||||
|
|
||||||
|
// POST should work (from Methods field)
|
||||||
|
req = httptest.NewRequest("POST", "/test", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
|
||||||
|
// PUT should work (from Methods field)
|
||||||
|
req = httptest.NewRequest("PUT", "/test", nil)
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid method in Methods slice", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Methods: []hws.Method{hws.MethodGET, hws.Method("INVALID")},
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Invalid method")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Empty Methods slice falls back to Method field", func(t *testing.T) {
|
||||||
|
server := createTestServer(t, &buf)
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
})
|
||||||
|
err := server.AddRoutes(hws.Route{
|
||||||
|
Path: "/test",
|
||||||
|
Method: hws.MethodGET,
|
||||||
|
Methods: []hws.Method{}, // Empty slice
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// GET should work (from Method field)
|
||||||
|
req := httptest.NewRequest("GET", "/test", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
server.Handler().ServeHTTP(rr, req)
|
||||||
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func Test_Routes_EndToEnd(t *testing.T) {
|
func Test_Routes_EndToEnd(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
server := createTestServer(t, &buf)
|
server := createTestServer(t, &buf)
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ func Test_Start_Errors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
err = server.Start(t.Context())
|
var nilCtx context.Context = nil
|
||||||
|
err = server.Start(nilCtx)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Context cannot be nil")
|
assert.Contains(t, err.Error(), "Context cannot be nil")
|
||||||
})
|
})
|
||||||
@@ -163,7 +164,8 @@ func Test_Shutdown_Errors(t *testing.T) {
|
|||||||
startTestServer(t, server)
|
startTestServer(t, server)
|
||||||
<-server.Ready()
|
<-server.Ready()
|
||||||
|
|
||||||
err := server.Shutdown(t.Context())
|
var nilCtx context.Context = nil
|
||||||
|
err := server.Shutdown(nilCtx)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "Context cannot be nil")
|
assert.Contains(t, err.Error(), "Context cannot be nil")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
package hwsauth
|
package hwsauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hlog"
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
@@ -30,6 +35,7 @@ func NewAuthenticator[T Model, TX DBTransaction](
|
|||||||
beginTx BeginTX,
|
beginTx BeginTX,
|
||||||
logger *hlog.Logger,
|
logger *hlog.Logger,
|
||||||
errorPage hws.ErrorPageFunc,
|
errorPage hws.ErrorPageFunc,
|
||||||
|
db *sql.DB,
|
||||||
) (*Authenticator[T, TX], error) {
|
) (*Authenticator[T, TX], error) {
|
||||||
if load == nil {
|
if load == nil {
|
||||||
return nil, errors.New("No function to load model supplied")
|
return nil, errors.New("No function to load model supplied")
|
||||||
@@ -55,7 +61,10 @@ func NewAuthenticator[T Model, TX DBTransaction](
|
|||||||
return nil, errors.New("SecretKey is required")
|
return nil, errors.New("SecretKey is required")
|
||||||
}
|
}
|
||||||
if cfg.SSL && cfg.TrustedHost == "" {
|
if cfg.SSL && cfg.TrustedHost == "" {
|
||||||
return nil, errors.New("TrustedHost is required when SSL is enabled")
|
cfg.SSL = false // Disable SSL if TrustedHost is not configured
|
||||||
|
}
|
||||||
|
if cfg.TrustedHost == "" {
|
||||||
|
cfg.TrustedHost = "localhost" // Default TrustedHost for JWT
|
||||||
}
|
}
|
||||||
if cfg.AccessTokenExpiry == 0 {
|
if cfg.AccessTokenExpiry == 0 {
|
||||||
cfg.AccessTokenExpiry = 5
|
cfg.AccessTokenExpiry = 5
|
||||||
@@ -69,12 +78,35 @@ func NewAuthenticator[T Model, TX DBTransaction](
|
|||||||
if cfg.LandingPage == "" {
|
if cfg.LandingPage == "" {
|
||||||
cfg.LandingPage = "/profile"
|
cfg.LandingPage = "/profile"
|
||||||
}
|
}
|
||||||
|
if cfg.DatabaseType == "" {
|
||||||
|
cfg.DatabaseType = "postgres"
|
||||||
|
}
|
||||||
|
if cfg.DatabaseVersion == "" {
|
||||||
|
cfg.DatabaseVersion = "15"
|
||||||
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
return nil, errors.New("No Database provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test database connectivity
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := db.PingContext(ctx); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "database connection test failed")
|
||||||
|
}
|
||||||
|
|
||||||
// Configure JWT table
|
// Configure JWT table
|
||||||
tableConfig := jwt.DefaultTableConfig()
|
tableConfig := jwt.DefaultTableConfig()
|
||||||
if cfg.JWTTableName != "" {
|
if cfg.JWTTableName != "" {
|
||||||
tableConfig.TableName = cfg.JWTTableName
|
tableConfig.TableName = cfg.JWTTableName
|
||||||
}
|
}
|
||||||
|
// Disable auto-creation for tests
|
||||||
|
// Check for test environment or mock database
|
||||||
|
if os.Getenv("GO_TEST") == "1" {
|
||||||
|
tableConfig.AutoCreate = false
|
||||||
|
tableConfig.EnableAutoCleanup = false
|
||||||
|
}
|
||||||
|
|
||||||
// Create token generator
|
// Create token generator
|
||||||
tokenGen, err := jwt.CreateGenerator(jwt.GeneratorConfig{
|
tokenGen, err := jwt.CreateGenerator(jwt.GeneratorConfig{
|
||||||
@@ -87,6 +119,7 @@ func NewAuthenticator[T Model, TX DBTransaction](
|
|||||||
Type: cfg.DatabaseType,
|
Type: cfg.DatabaseType,
|
||||||
Version: cfg.DatabaseVersion,
|
Version: cfg.DatabaseVersion,
|
||||||
},
|
},
|
||||||
|
DB: db,
|
||||||
TableConfig: tableConfig,
|
TableConfig: tableConfig,
|
||||||
}, beginTx)
|
}, beginTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,20 +5,25 @@ go 1.25.5
|
|||||||
require (
|
require (
|
||||||
git.haelnorr.com/h/golib/cookies v0.9.0
|
git.haelnorr.com/h/golib/cookies v0.9.0
|
||||||
git.haelnorr.com/h/golib/env v0.9.1
|
git.haelnorr.com/h/golib/env v0.9.1
|
||||||
git.haelnorr.com/h/golib/hws v0.2.0
|
git.haelnorr.com/h/golib/hlog v0.10.4
|
||||||
git.haelnorr.com/h/golib/jwt v0.10.0
|
git.haelnorr.com/h/golib/hws v0.3.0
|
||||||
|
git.haelnorr.com/h/golib/jwt v0.10.1
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.1
|
github.com/stretchr/testify v1.11.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
k8s.io/apimachinery v0.35.0 // indirect
|
k8s.io/apimachinery v0.35.0 // indirect
|
||||||
k8s.io/klog/v2 v2.130.1 // indirect
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
k8s.io/utils v0.0.0-20260108192941-914a6e750570 // indirect
|
k8s.io/utils v0.0.0-20260108192941-914a6e750570 // indirect
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ git.haelnorr.com/h/golib/cookies v0.9.0 h1:Vf+eX1prHkKuGrQon1BHY87yaPc1H+HJFRXDO
|
|||||||
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
|
git.haelnorr.com/h/golib/cookies v0.9.0/go.mod h1:y1385YExI9gLwckCVDCYVcsFXr6N7T3brJjnJD2QIuo=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
git.haelnorr.com/h/golib/env v0.9.1 h1:2Vsj+mJKnO5f1Md1GO5v9ggLN5zWa0baCewcSHTjoNY=
|
||||||
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
git.haelnorr.com/h/golib/env v0.9.1/go.mod h1:glUQVdA1HMKX1avTDyTyuhcr36SSxZtlJxKDT5KTztg=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.1 h1:9VmE/IQTfD8LAEyTbUCZLy/+8PbcHA1Kob/WQHRHKzc=
|
git.haelnorr.com/h/golib/hlog v0.10.4 h1:vpCsV/OddjIYx8F48U66WxojjmhEbeLGQAOBG4ViSRQ=
|
||||||
git.haelnorr.com/h/golib/hlog v0.9.1/go.mod h1:oOlzb8UVHUYP1k7dN5PSJXVskAB2z8EYgRN85jAi0Zk=
|
git.haelnorr.com/h/golib/hlog v0.10.4/go.mod h1:+wJ8vecQY/JITTXKmI3JfkHiUGyMs7N6wooj2wuWZbc=
|
||||||
git.haelnorr.com/h/golib/hws v0.2.0 h1:MR2Tu2qPaW+/oK8aXFJLRFaYZIHgKiex3t3zE41cu1U=
|
git.haelnorr.com/h/golib/hws v0.3.0 h1:/YGzxd3sRR3DFU6qVZxpJMKV3W2wCONqZKYUDIercCo=
|
||||||
git.haelnorr.com/h/golib/hws v0.2.0/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo=
|
git.haelnorr.com/h/golib/hws v0.3.0/go.mod h1:6ZlRKnt8YMpv5XcMXmyBGmD1/euvBo3d1azEvHJjOLo=
|
||||||
git.haelnorr.com/h/golib/jwt v0.10.0 h1:8cI8mSnb8X+EmJtrBO/5UZwuBMtib0IE9dv85gkm94E=
|
git.haelnorr.com/h/golib/jwt v0.10.1 h1:1Adxt9H3Y4fWFvFjWpvg/vSFhbgCMDMxgiE3m7KvDMI=
|
||||||
git.haelnorr.com/h/golib/jwt v0.10.0/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
|
git.haelnorr.com/h/golib/jwt v0.10.1/go.mod h1:fbuPrfucT9lL0faV5+Q5Gk9WFJxPlwzRPpbMQKYZok4=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
@@ -20,6 +20,7 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
|
|||||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
@@ -41,6 +42,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||||
|
|||||||
481
hwsauth/hwsauth_test.go
Normal file
481
hwsauth/hwsauth_test.go
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
package hwsauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"io"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.haelnorr.com/h/golib/hlog"
|
||||||
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestModel struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm TestModel) GetID() int {
|
||||||
|
return tm.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestTransaction struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TestTransaction) Exec(query string, args ...any) (sql.Result, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TestTransaction) Query(query string, args ...any) (*sql.Rows, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TestTransaction) Commit() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tt *TestTransaction) Rollback() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestErrorPage struct{}
|
||||||
|
|
||||||
|
func (tep TestErrorPage) Render(ctx context.Context, w io.Writer) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMockDB creates a mock SQL database for testing
|
||||||
|
func createMockDB() (*sql.DB, sqlmock.Sqlmock, error) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expect a ping to succeed for database connectivity test
|
||||||
|
mock.ExpectPing()
|
||||||
|
|
||||||
|
// Expect table existence check (returns a row = table exists)
|
||||||
|
mock.ExpectQuery(`SELECT 1 FROM information_schema\.tables WHERE table_schema = 'public' AND table_name = \$1`).
|
||||||
|
WithArgs("jwtblacklist").
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow(1))
|
||||||
|
|
||||||
|
// Expect cleanup function creation
|
||||||
|
mock.ExpectExec(`CREATE OR REPLACE FUNCTION cleanup_jwtblacklist\(\) RETURNS void AS \$\$ BEGIN DELETE FROM jwtblacklist WHERE exp < EXTRACT\(EPOCH FROM NOW\(\)\); END; \$\$ LANGUAGE plpgsql;`).
|
||||||
|
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||||
|
|
||||||
|
return db, mock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNil(t *testing.T) {
|
||||||
|
var zero TestModel
|
||||||
|
result := getNil[TestModel]()
|
||||||
|
assert.Equal(t, zero, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetAndGetAuthenticatedModel(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
model := TestModel{ID: 123}
|
||||||
|
authModel := authenticatedModel[TestModel]{
|
||||||
|
model: model,
|
||||||
|
fresh: 1234567890,
|
||||||
|
}
|
||||||
|
|
||||||
|
newCtx := setAuthenticatedModel(ctx, authModel)
|
||||||
|
|
||||||
|
retrieved, ok := getAuthorizedModel[TestModel](newCtx)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, model, retrieved.model)
|
||||||
|
assert.Equal(t, int64(1234567890), retrieved.fresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAuthorizedModel_NotSet(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
retrieved, ok := getAuthorizedModel[TestModel](ctx)
|
||||||
|
assert.False(t, ok)
|
||||||
|
var zero TestModel
|
||||||
|
assert.Equal(t, zero, retrieved.model)
|
||||||
|
assert.Equal(t, int64(0), retrieved.fresh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentModel(t *testing.T) {
|
||||||
|
auth := &Authenticator[TestModel, DBTransaction]{}
|
||||||
|
|
||||||
|
t.Run("nil context", func(t *testing.T) {
|
||||||
|
var nilContext context.Context = nil
|
||||||
|
result := auth.CurrentModel(nilContext)
|
||||||
|
var zero TestModel
|
||||||
|
assert.Equal(t, zero, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context without authenticated model", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
result := auth.CurrentModel(ctx)
|
||||||
|
var zero TestModel
|
||||||
|
assert.Equal(t, zero, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("context with authenticated model", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
model := TestModel{ID: 456}
|
||||||
|
authModel := authenticatedModel[TestModel]{
|
||||||
|
model: model,
|
||||||
|
fresh: 1234567890,
|
||||||
|
}
|
||||||
|
ctx = setAuthenticatedModel(ctx, authModel)
|
||||||
|
|
||||||
|
result := auth.CurrentModel(ctx)
|
||||||
|
assert.Equal(t, model, result)
|
||||||
|
assert.Equal(t, 456, result.GetID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromEnv_MissingSecretKey(t *testing.T) {
|
||||||
|
// Clear environment variables
|
||||||
|
originalSecret := os.Getenv("HWSAUTH_SECRET_KEY")
|
||||||
|
os.Setenv("HWSAUTH_SECRET_KEY", "")
|
||||||
|
defer os.Setenv("HWSAUTH_SECRET_KEY", originalSecret)
|
||||||
|
|
||||||
|
_, err := ConfigFromEnv()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "Envar not set: HWSAUTH_SECRET_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromEnv_SSLWithoutTrustedHost(t *testing.T) {
|
||||||
|
// Clear environment variables
|
||||||
|
t.Setenv("HWSAUTH_SECRET_KEY", "test-secret")
|
||||||
|
t.Setenv("HWSAUTH_SSL", "true")
|
||||||
|
t.Setenv("HWSAUTH_TRUSTED_HOST", "")
|
||||||
|
defer func() {
|
||||||
|
t.Setenv("HWSAUTH_SECRET_KEY", "")
|
||||||
|
t.Setenv("HWSAUTH_SSL", "")
|
||||||
|
t.Setenv("HWSAUTH_TRUSTED_HOST", "")
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err := ConfigFromEnv()
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "SSL is enabled and no HWS_TRUSTED_HOST set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromEnv_ValidMinimalConfig(t *testing.T) {
|
||||||
|
// Set environment variables
|
||||||
|
t.Setenv("HWSAUTH_SECRET_KEY", "test-secret-key")
|
||||||
|
defer t.Setenv("HWSAUTH_SECRET_KEY", "")
|
||||||
|
|
||||||
|
cfg, err := ConfigFromEnv()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "test-secret-key", cfg.SecretKey)
|
||||||
|
assert.Equal(t, false, cfg.SSL)
|
||||||
|
assert.Equal(t, int64(5), cfg.AccessTokenExpiry)
|
||||||
|
assert.Equal(t, int64(1440), cfg.RefreshTokenExpiry)
|
||||||
|
assert.Equal(t, int64(5), cfg.TokenFreshTime)
|
||||||
|
assert.Equal(t, "/profile", cfg.LandingPage)
|
||||||
|
assert.Equal(t, "postgres", cfg.DatabaseType)
|
||||||
|
assert.Equal(t, "15", cfg.DatabaseVersion)
|
||||||
|
assert.Equal(t, "jwtblacklist", cfg.JWTTableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFromEnv_ValidFullConfig(t *testing.T) {
|
||||||
|
// Set environment variables
|
||||||
|
t.Setenv("HWSAUTH_SECRET_KEY", "custom-secret")
|
||||||
|
t.Setenv("HWSAUTH_SSL", "true")
|
||||||
|
t.Setenv("HWSAUTH_TRUSTED_HOST", "example.com")
|
||||||
|
t.Setenv("HWSAUTH_ACCESS_TOKEN_EXPIRY", "15")
|
||||||
|
t.Setenv("HWSAUTH_REFRESH_TOKEN_EXPIRY", "2880")
|
||||||
|
t.Setenv("HWSAUTH_TOKEN_FRESH_TIME", "10")
|
||||||
|
t.Setenv("HWSAUTH_LANDING_PAGE", "/dashboard")
|
||||||
|
t.Setenv("HWSAUTH_DATABASE_TYPE", "mysql")
|
||||||
|
t.Setenv("HWSAUTH_DATABASE_VERSION", "8.0")
|
||||||
|
t.Setenv("HWSAUTH_JWT_TABLE_NAME", "custom_tokens")
|
||||||
|
defer func() {
|
||||||
|
t.Setenv("HWSAUTH_SECRET_KEY", "")
|
||||||
|
t.Setenv("HWSAUTH_SSL", "")
|
||||||
|
t.Setenv("HWSAUTH_TRUSTED_HOST", "")
|
||||||
|
t.Setenv("HWSAUTH_ACCESS_TOKEN_EXPIRY", "")
|
||||||
|
t.Setenv("HWSAUTH_REFRESH_TOKEN_EXPIRY", "")
|
||||||
|
t.Setenv("HWSAUTH_TOKEN_FRESH_TIME", "")
|
||||||
|
t.Setenv("HWSAUTH_LANDING_PAGE", "")
|
||||||
|
t.Setenv("HWSAUTH_DATABASE_TYPE", "")
|
||||||
|
t.Setenv("HWSAUTH_DATABASE_VERSION", "")
|
||||||
|
t.Setenv("HWSAUTH_JWT_TABLE_NAME", "")
|
||||||
|
}()
|
||||||
|
|
||||||
|
cfg, err := ConfigFromEnv()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "custom-secret", cfg.SecretKey)
|
||||||
|
assert.Equal(t, true, cfg.SSL)
|
||||||
|
assert.Equal(t, "example.com", cfg.TrustedHost)
|
||||||
|
assert.Equal(t, int64(15), cfg.AccessTokenExpiry)
|
||||||
|
assert.Equal(t, int64(2880), cfg.RefreshTokenExpiry)
|
||||||
|
assert.Equal(t, int64(10), cfg.TokenFreshTime)
|
||||||
|
assert.Equal(t, "/dashboard", cfg.LandingPage)
|
||||||
|
assert.Equal(t, "mysql", cfg.DatabaseType)
|
||||||
|
assert.Equal(t, "8.0", cfg.DatabaseVersion)
|
||||||
|
assert.Equal(t, "custom_tokens", cfg.JWTTableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAuthenticator_NilConfig(t *testing.T) {
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
nil, // cfg
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
nil, // db
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, auth)
|
||||||
|
assert.Contains(t, err.Error(), "Config is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAuthenticator_MissingSecretKey(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
cfg,
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
nil, // db - will fail before db check since SecretKey is missing
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, auth)
|
||||||
|
assert.Contains(t, err.Error(), "SecretKey is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAuthenticator_NilLoadFunction(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "test-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator[TestModel, DBTransaction](
|
||||||
|
cfg,
|
||||||
|
nil,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
nil, // db
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, auth)
|
||||||
|
assert.Contains(t, err.Error(), "No function to load model supplied")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAuthenticator_SSLWithoutTrustedHost(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "test-secret",
|
||||||
|
SSL: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db, _, err := createMockDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
cfg,
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, auth)
|
||||||
|
|
||||||
|
assert.Equal(t, false, auth.SSL)
|
||||||
|
assert.Equal(t, "/profile", auth.LandingPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAuthenticator_NilDatabase(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "test-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
cfg,
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
nil, // db
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, auth)
|
||||||
|
assert.Contains(t, err.Error(), "No Database provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModelInterface(t *testing.T) {
|
||||||
|
t.Run("TestModel implements Model interface", func(t *testing.T) {
|
||||||
|
var _ Model = TestModel{}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("GetID method", func(t *testing.T) {
|
||||||
|
model := TestModel{ID: 789}
|
||||||
|
assert.Equal(t, 789, model.GetID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAuthenticatedUser_NoTokens(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "test-secret",
|
||||||
|
TrustedHost: "example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db, _, err := createMockDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
cfg,
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tx := &TestTransaction{}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
|
||||||
|
model, err := auth.getAuthenticatedUser(tx, w, r)
|
||||||
|
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "No token strings provided")
|
||||||
|
var zero TestModel
|
||||||
|
assert.Equal(t, zero, model.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin_BasicFunctionality(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
SecretKey: "test-secret",
|
||||||
|
TrustedHost: "example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
load := func(ctx context.Context, tx DBTransaction, id int) (TestModel, error) {
|
||||||
|
return TestModel{ID: id}, nil
|
||||||
|
}
|
||||||
|
server := &hws.Server{}
|
||||||
|
beginTx := func(ctx context.Context) (DBTransaction, error) {
|
||||||
|
return &TestTransaction{}, nil
|
||||||
|
}
|
||||||
|
logger := &hlog.Logger{}
|
||||||
|
errorPage := func(error hws.HWSError) (hws.ErrorPage, error) {
|
||||||
|
return TestErrorPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db, _, err := createMockDB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
auth, err := NewAuthenticator(
|
||||||
|
cfg,
|
||||||
|
load,
|
||||||
|
server,
|
||||||
|
beginTx,
|
||||||
|
logger,
|
||||||
|
errorPage,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "/", nil)
|
||||||
|
|
||||||
|
user := TestModel{ID: 123}
|
||||||
|
rememberMe := true
|
||||||
|
|
||||||
|
// This test mainly checks that the function doesn't panic and has right call signature
|
||||||
|
// The actual JWT functionality is tested in jwt package itself
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
auth.Login(w, r, user, rememberMe)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/hws"
|
"git.haelnorr.com/h/golib/hws"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoginReq returns a middleware that requires the user to be authenticated.
|
// LoginReq returns a middleware that requires the user to be authenticated.
|
||||||
@@ -18,23 +19,14 @@ func (auth *Authenticator[T, TX]) LoginReq(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, ok := getAuthorizedModel[T](r.Context())
|
_, ok := getAuthorizedModel[T](r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
page, err := auth.errorPage(http.StatusUnauthorized)
|
err := auth.server.ThrowError(w, r, hws.HWSError{
|
||||||
|
Error: errors.New("Login required"),
|
||||||
|
Message: "Please login to view this page",
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
RenderErrorPage: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
auth.server.ThrowError(w, r, hws.HWSError{
|
auth.server.ThrowFatal(w, err)
|
||||||
Error: err,
|
|
||||||
Message: "Failed to get valid error page",
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
RenderErrorPage: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
err = page.Render(r.Context(), w)
|
|
||||||
if err != nil {
|
|
||||||
auth.server.ThrowError(w, r, hws.HWSError{
|
|
||||||
Error: err,
|
|
||||||
Message: "Failed to render error page",
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
RenderErrorPage: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,23 +66,14 @@ func (auth *Authenticator[T, TX]) FreshReq(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
model, ok := getAuthorizedModel[T](r.Context())
|
model, ok := getAuthorizedModel[T](r.Context())
|
||||||
if !ok {
|
if !ok {
|
||||||
page, err := auth.errorPage(http.StatusUnauthorized)
|
err := auth.server.ThrowError(w, r, hws.HWSError{
|
||||||
|
Error: errors.New("Login required"),
|
||||||
|
Message: "Please login to view this page",
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
RenderErrorPage: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
auth.server.ThrowError(w, r, hws.HWSError{
|
auth.server.ThrowFatal(w, err)
|
||||||
Error: err,
|
|
||||||
Message: "Failed to get valid error page",
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
RenderErrorPage: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
err = page.Render(r.Context(), w)
|
|
||||||
if err != nil {
|
|
||||||
auth.server.ThrowError(w, r, hws.HWSError{
|
|
||||||
Error: err,
|
|
||||||
Message: "Failed to render error page",
|
|
||||||
StatusCode: http.StatusInternalServerError,
|
|
||||||
RenderErrorPage: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package hwsauth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"git.haelnorr.com/h/golib/jwt"
|
"git.haelnorr.com/h/golib/jwt"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -18,7 +19,9 @@ func (auth *Authenticator[T, TX]) refreshAuthTokens(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return getNil[T](), errors.Wrap(err, "auth.load")
|
return getNil[T](), errors.Wrap(err, "auth.load")
|
||||||
}
|
}
|
||||||
|
if reflect.ValueOf(model).IsNil() {
|
||||||
|
return getNil[T](), errors.New("no user matching JWT in database")
|
||||||
|
}
|
||||||
rememberMe := map[string]bool{
|
rememberMe := map[string]bool{
|
||||||
"session": false,
|
"session": false,
|
||||||
"exp": true,
|
"exp": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user