- 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.
346 lines
9.1 KiB
Go
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")
|
|
}
|
|
}
|