Add configuration files and update project documentation
- Introduced .editorconfig for consistent coding styles across the project. - Added .golangci.yml for Go linting configuration. - Updated AGENTS.md to clarify project structure and components. - Enhanced CONTRIBUTING.md with Makefile usage for common tasks. - Updated Dockerfiles to use Go 1.24 and improved build instructions. - Refined README.md and deployment documentation for clarity. - Added testing documentation in testing.md for backend and frontend tests. - Introduced Makefile for streamlined development commands and tasks.
This commit is contained in:
339
internal/app/services/auth_test.go
Normal file
339
internal/app/services/auth_test.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package services_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/andyleap/hnh-map/internal/app"
|
||||
"github.com/andyleap/hnh-map/internal/app/services"
|
||||
"github.com/andyleap/hnh-map/internal/app/store"
|
||||
"go.etcd.io/bbolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func newTestDB(t *testing.T) *bbolt.DB {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
db, err := bbolt.Open(filepath.Join(dir, "test.db"), 0600, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func newTestAuth(t *testing.T) (*services.AuthService, *store.Store) {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
st := store.New(db)
|
||||
return services.NewAuthService(st), st
|
||||
}
|
||||
|
||||
func createUser(t *testing.T, st *store.Store, username, password string, auths app.Auths) {
|
||||
t.Helper()
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u := app.User{Pass: hash, Auths: auths}
|
||||
raw, _ := json.Marshal(u)
|
||||
st.Update(context.Background(), func(tx *bbolt.Tx) error {
|
||||
return st.PutUser(tx, username, raw)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetupRequired_EmptyDB(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
if !auth.SetupRequired(context.Background()) {
|
||||
t.Fatal("expected setup required on empty DB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupRequired_WithUsers(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
createUser(t, st, "admin", "pass", app.Auths{app.AUTH_ADMIN})
|
||||
if auth.SetupRequired(context.Background()) {
|
||||
t.Fatal("expected setup not required when users exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser_ValidPassword(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
createUser(t, st, "alice", "secret", app.Auths{app.AUTH_MAP})
|
||||
|
||||
u := auth.GetUser(context.Background(), "alice", "secret")
|
||||
if u == nil {
|
||||
t.Fatal("expected user with correct password")
|
||||
}
|
||||
if !u.Auths.Has(app.AUTH_MAP) {
|
||||
t.Fatal("expected map auth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser_InvalidPassword(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
createUser(t, st, "alice", "secret", nil)
|
||||
|
||||
u := auth.GetUser(context.Background(), "alice", "wrong")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil with wrong password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser_NonExistent(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
u := auth.GetUser(context.Background(), "ghost", "pass")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil for non-existent user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserByUsername(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
createUser(t, st, "alice", "secret", app.Auths{app.AUTH_MAP})
|
||||
|
||||
u := auth.GetUserByUsername(context.Background(), "alice")
|
||||
if u == nil {
|
||||
t.Fatal("expected user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserByUsername_NonExistent(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
u := auth.GetUserByUsername(context.Background(), "ghost")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSession_GetSession(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP, app.AUTH_UPLOAD})
|
||||
|
||||
sid := auth.CreateSession(ctx, "alice", false)
|
||||
if sid == "" {
|
||||
t.Fatal("expected non-empty session ID")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sid})
|
||||
|
||||
sess := auth.GetSession(ctx, req)
|
||||
if sess == nil {
|
||||
t.Fatal("expected session")
|
||||
}
|
||||
if sess.Username != "alice" {
|
||||
t.Fatalf("expected alice, got %s", sess.Username)
|
||||
}
|
||||
if !sess.Auths.Has(app.AUTH_MAP) {
|
||||
t.Fatal("expected map auth from user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSession_NoCookie(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
sess := auth.GetSession(context.Background(), req)
|
||||
if sess != nil {
|
||||
t.Fatal("expected nil session without cookie")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSession_InvalidSession(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: "invalid"})
|
||||
sess := auth.GetSession(context.Background(), req)
|
||||
if sess != nil {
|
||||
t.Fatal("expected nil for invalid session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSession_TempAdmin(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
|
||||
sid := auth.CreateSession(ctx, "temp", true)
|
||||
if sid == "" {
|
||||
t.Fatal("expected session ID")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sid})
|
||||
|
||||
sess := auth.GetSession(ctx, req)
|
||||
if sess == nil {
|
||||
t.Fatal("expected session")
|
||||
}
|
||||
if !sess.TempAdmin {
|
||||
t.Fatal("expected temp admin")
|
||||
}
|
||||
if !sess.Auths.Has(app.AUTH_ADMIN) {
|
||||
t.Fatal("expected admin auth for temp admin")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSession(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", nil)
|
||||
|
||||
sid := auth.CreateSession(ctx, "alice", false)
|
||||
sess := &app.Session{ID: sid, Username: "alice"}
|
||||
auth.DeleteSession(ctx, sess)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(&http.Cookie{Name: "session", Value: sid})
|
||||
if auth.GetSession(ctx, req) != nil {
|
||||
t.Fatal("expected nil after deletion")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUserPassword(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "old", app.Auths{app.AUTH_MAP})
|
||||
|
||||
if err := auth.SetUserPassword(ctx, "alice", "new"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
u := auth.GetUser(ctx, "alice", "new")
|
||||
if u == nil {
|
||||
t.Fatal("expected user with new password")
|
||||
}
|
||||
if auth.GetUser(ctx, "alice", "old") != nil {
|
||||
t.Fatal("old password should not work")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUserPassword_EmptyIsNoop(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", nil)
|
||||
|
||||
if err := auth.SetUserPassword(ctx, "alice", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if auth.GetUser(ctx, "alice", "pass") == nil {
|
||||
t.Fatal("password should be unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTokenForUser(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
|
||||
|
||||
tokens := auth.GenerateTokenForUser(ctx, "alice")
|
||||
if len(tokens) != 1 {
|
||||
t.Fatalf("expected 1 token, got %d", len(tokens))
|
||||
}
|
||||
|
||||
tokens2 := auth.GenerateTokenForUser(ctx, "alice")
|
||||
if len(tokens2) != 2 {
|
||||
t.Fatalf("expected 2 tokens, got %d", len(tokens2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserTokensAndPrefix(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.PutConfig(tx, "prefix", []byte("myprefix"))
|
||||
})
|
||||
|
||||
auth.GenerateTokenForUser(ctx, "alice")
|
||||
tokens, prefix := auth.GetUserTokensAndPrefix(ctx, "alice")
|
||||
if len(tokens) != 1 {
|
||||
t.Fatalf("expected 1 token, got %d", len(tokens))
|
||||
}
|
||||
if prefix != "myprefix" {
|
||||
t.Fatalf("expected myprefix, got %s", prefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientToken_Valid(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_UPLOAD})
|
||||
|
||||
tokens := auth.GenerateTokenForUser(ctx, "alice")
|
||||
username, err := auth.ValidateClientToken(ctx, tokens[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if username != "alice" {
|
||||
t.Fatalf("expected alice, got %s", username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientToken_Invalid(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
_, err := auth.ValidateClientToken(context.Background(), "bad-token")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientToken_NoUploadPerm(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "alice", "pass", app.Auths{app.AUTH_MAP})
|
||||
|
||||
st.Update(ctx, func(tx *bbolt.Tx) error {
|
||||
return st.PutToken(tx, "tok123", "alice")
|
||||
})
|
||||
|
||||
_, err := auth.ValidateClientToken(ctx, "tok123")
|
||||
if err == nil {
|
||||
t.Fatal("expected error without upload permission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapAdmin_Success(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
|
||||
u := auth.BootstrapAdmin(ctx, "admin", "bootstrap123", "bootstrap123")
|
||||
if u == nil {
|
||||
t.Fatal("expected user creation")
|
||||
}
|
||||
if !u.Auths.Has(app.AUTH_ADMIN) {
|
||||
t.Fatal("expected admin auth")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapAdmin_WrongUsername(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
u := auth.BootstrapAdmin(context.Background(), "notadmin", "pass", "pass")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil for non-admin username")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapAdmin_MismatchPassword(t *testing.T) {
|
||||
auth, _ := newTestAuth(t)
|
||||
u := auth.BootstrapAdmin(context.Background(), "admin", "pass", "different")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil for mismatched password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapAdmin_AlreadyExists(t *testing.T) {
|
||||
auth, st := newTestAuth(t)
|
||||
ctx := context.Background()
|
||||
createUser(t, st, "admin", "existing", app.Auths{app.AUTH_ADMIN})
|
||||
|
||||
u := auth.BootstrapAdmin(ctx, "admin", "pass", "pass")
|
||||
if u != nil {
|
||||
t.Fatal("expected nil when admin already exists")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user