Files
hnh-map/internal/app/services/auth_test.go
Nikolay Tatarinov 761fbaed55 Refactor Docker and Makefile configurations for improved build processes
- Updated docker-compose.tools.yml to mount source code at /src and set working directory for backend tools, ensuring proper Go module caching.
- Modified Dockerfile.tools to install the latest golangci-lint version compatible with Go 1.24 and adjusted working directory for build-time operations.
- Enhanced Makefile to build backend tools before running tests and linting, ensuring dependencies are up-to-date and improving overall workflow efficiency.
- Refactored test and handler files to include error handling for database operations, enhancing reliability and debugging capabilities.
2026-03-04 13:59:00 +03:00

346 lines
9.1 KiB
Go

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)
if err := st.Update(context.Background(), func(tx *bbolt.Tx) error {
return st.PutUser(tx, username, raw)
}); err != nil {
t.Fatal(err)
}
}
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})
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutConfig(tx, "prefix", []byte("myprefix"))
}); err != nil {
t.Fatal(err)
}
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})
if err := st.Update(ctx, func(tx *bbolt.Tx) error {
return st.PutToken(tx, "tok123", "alice")
}); err != nil {
t.Fatal(err)
}
_, 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")
}
}