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:
2026-03-01 01:51:47 +03:00
parent 0466ff3087
commit 6529d7370e
92 changed files with 13411 additions and 8438 deletions

View 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")
}
}