240 lines
7.9 KiB
Go
240 lines
7.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
assets "galo"
|
|
"galo/internal/db"
|
|
"galo/internal/httpx"
|
|
)
|
|
|
|
func newTestService(t *testing.T) *Service {
|
|
t.Helper()
|
|
migFS, err := fs.Sub(assets.Migrations, "migrations/sqlite")
|
|
if err != nil {
|
|
t.Fatalf("migrations fs: %v", err)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "auth.db")
|
|
if err := db.MigrateSQLite(path, migFS); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
database, err := db.OpenSQLite(path)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
t.Cleanup(func() { database.Close() })
|
|
return NewService(NewSQLiteStore(database), NewTokenManager("secret", 15*time.Minute, time.Hour))
|
|
}
|
|
|
|
func errCode(t *testing.T, err error) string {
|
|
t.Helper()
|
|
var appErr *httpx.AppError
|
|
if !errors.As(err, &appErr) {
|
|
t.Fatalf("expected AppError, got %v", err)
|
|
}
|
|
return appErr.Code
|
|
}
|
|
|
|
func TestRegisterByEmailOrPhone(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
u, pair, err := svc.Register(ctx, "User@Test.Local", "", "password123", "Tester")
|
|
if err != nil {
|
|
t.Fatalf("register email: %v", err)
|
|
}
|
|
if u.Email != "user@test.local" || u.Phone != "" {
|
|
t.Fatalf("email registration stored %+v", u)
|
|
}
|
|
if pair == nil || pair.AccessToken == "" || pair.RefreshToken == "" {
|
|
t.Fatalf("no tokens issued")
|
|
}
|
|
|
|
p, _, err := svc.Register(ctx, "", "+15550000001", "password123", "Phone")
|
|
if err != nil {
|
|
t.Fatalf("register phone: %v", err)
|
|
}
|
|
if p.Email != "" || p.Phone != "+15550000001" {
|
|
t.Fatalf("phone registration stored %+v", p)
|
|
}
|
|
|
|
both, _, err := svc.Register(ctx, "both@test.local", "+15550000002", "password123", "Both")
|
|
if err != nil {
|
|
t.Fatalf("register both: %v", err)
|
|
}
|
|
if both.Email != "both@test.local" || both.Phone != "+15550000002" {
|
|
t.Fatalf("both registration stored %+v", both)
|
|
}
|
|
}
|
|
|
|
func TestRegisterValidation(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
if _, _, err := svc.Register(ctx, "", "", "password123", "N"); errCode(t, err) != "identifier_required" {
|
|
t.Fatalf("no identifier code = %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "a@test.local", "", "short", "N"); errCode(t, err) != "weak_password" {
|
|
t.Fatalf("weak password code = %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "a@test.local", "", "password123", ""); errCode(t, err) != "invalid_name" {
|
|
t.Fatalf("empty name code = %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "not-an-email", "", "password123", "N"); errCode(t, err) != "invalid_email" {
|
|
t.Fatalf("invalid email code = %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "", "5550000", "password123", "N"); errCode(t, err) != "invalid_phone" {
|
|
t.Fatalf("invalid phone code = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRegisterConflicts(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
if _, _, err := svc.Register(ctx, "dup@test.local", "", "password123", "A"); err != nil {
|
|
t.Fatalf("first register: %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "Dup@Test.Local", "", "password123", "B"); errCode(t, err) != "email_taken" {
|
|
t.Fatalf("duplicate email code = %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "", "+15550000009", "password123", "C"); err != nil {
|
|
t.Fatalf("phone register: %v", err)
|
|
}
|
|
if _, _, err := svc.Register(ctx, "", "+15550000009", "password123", "D"); errCode(t, err) != "phone_taken" {
|
|
t.Fatalf("duplicate phone code = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoginByIdentifier(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
if _, _, err := svc.Register(ctx, "login@test.local", "+15550000010", "password123", "Login"); err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
|
|
if _, pair, err := svc.Login(ctx, "Login@Test.Local", "password123"); err != nil || pair == nil {
|
|
t.Fatalf("login by email: %v", err)
|
|
}
|
|
if _, pair, err := svc.Login(ctx, "+15550000010", "password123"); err != nil || pair == nil {
|
|
t.Fatalf("login by phone: %v", err)
|
|
}
|
|
|
|
_, _, errMissing := svc.Login(ctx, "nobody@test.local", "password123")
|
|
_, _, errWrong := svc.Login(ctx, "login@test.local", "wrong-password")
|
|
if errCode(t, errMissing) != "invalid_credentials" {
|
|
t.Fatalf("missing user code = %v", errMissing)
|
|
}
|
|
if errCode(t, errWrong) != "invalid_credentials" {
|
|
t.Fatalf("wrong password code = %v", errWrong)
|
|
}
|
|
if errMissing.Error() != errWrong.Error() {
|
|
t.Fatalf("errors distinguishable: %q vs %q", errMissing, errWrong)
|
|
}
|
|
}
|
|
|
|
func TestUpdateName(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
u, _, err := svc.Register(ctx, "name@test.local", "", "password123", "Old")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
if _, err := svc.UpdateName(ctx, u.ID, " "); errCode(t, err) != "invalid_name" {
|
|
t.Fatalf("empty name code = %v", err)
|
|
}
|
|
got, err := svc.UpdateName(ctx, u.ID, " New Name ")
|
|
if err != nil || got.Name != "New Name" {
|
|
t.Fatalf("update name = %+v err=%v", got, err)
|
|
}
|
|
}
|
|
|
|
func TestChangePasswordRevokesTokens(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
u, pair, err := svc.Register(ctx, "pass@test.local", "", "password123", "Pass")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
if err := svc.ChangePassword(ctx, u.ID, "wrong", "newpassword"); errCode(t, err) != "current_password_incorrect" {
|
|
t.Fatalf("wrong current password code = %v", err)
|
|
}
|
|
if err := svc.ChangePassword(ctx, u.ID, "password123", "short"); errCode(t, err) != "weak_password" {
|
|
t.Fatalf("weak new password code = %v", err)
|
|
}
|
|
if err := svc.ChangePassword(ctx, u.ID, "password123", "newpassword"); err != nil {
|
|
t.Fatalf("change password: %v", err)
|
|
}
|
|
if _, _, err := svc.Login(ctx, "pass@test.local", "newpassword"); err != nil {
|
|
t.Fatalf("login with new password: %v", err)
|
|
}
|
|
if _, _, err := svc.Login(ctx, "pass@test.local", "password123"); errCode(t, err) != "invalid_credentials" {
|
|
t.Fatalf("old password still works: %v", err)
|
|
}
|
|
if _, err := svc.Refresh(ctx, pair.RefreshToken); errCode(t, err) != "invalid_refresh_token" {
|
|
t.Fatalf("refresh token not revoked: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAddIdentifier(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
u, _, err := svc.Register(ctx, "add@test.local", "", "password123", "Add")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
if _, err := svc.AddIdentifier(ctx, u.ID, "other@test.local", ""); errCode(t, err) != "email_already_set" {
|
|
t.Fatalf("re-set email code = %v", err)
|
|
}
|
|
if _, err := svc.AddIdentifier(ctx, u.ID, "", ""); errCode(t, err) != "identifier_required" {
|
|
t.Fatalf("no identifier code = %v", err)
|
|
}
|
|
if _, err := svc.AddIdentifier(ctx, u.ID, "x@test.local", "+15550000011"); errCode(t, err) != "invalid_request" {
|
|
t.Fatalf("both at once code = %v", err)
|
|
}
|
|
|
|
got, err := svc.AddIdentifier(ctx, u.ID, "", "+15550000011")
|
|
if err != nil || got.Phone != "+15550000011" {
|
|
t.Fatalf("add phone = %+v err=%v", got, err)
|
|
}
|
|
if _, err := svc.AddIdentifier(ctx, u.ID, "later@test.local", ""); errCode(t, err) != "identifiers_complete" {
|
|
t.Fatalf("both set code = %v", err)
|
|
}
|
|
|
|
other, _, _ := svc.Register(ctx, "owner@test.local", "", "password123", "Owner")
|
|
if _, err := svc.AddIdentifier(ctx, other.ID, "", "+15550000011"); errCode(t, err) != "phone_taken" {
|
|
t.Fatalf("phone conflict code = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteAccount(t *testing.T) {
|
|
ctx := context.Background()
|
|
svc := newTestService(t)
|
|
|
|
u, _, err := svc.Register(ctx, "delete@test.local", "", "password123", "Delete")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
if err := svc.DeleteAccount(ctx, u.ID, "wrong"); errCode(t, err) != "current_password_incorrect" {
|
|
t.Fatalf("wrong password code = %v", err)
|
|
}
|
|
if err := svc.DeleteAccount(ctx, u.ID, "password123"); err != nil {
|
|
t.Fatalf("delete account: %v", err)
|
|
}
|
|
if _, _, err := svc.Login(ctx, "delete@test.local", "password123"); errCode(t, err) != "invalid_credentials" {
|
|
t.Fatalf("deleted account can still log in: %v", err)
|
|
}
|
|
if _, err := svc.store.GetUserByID(ctx, u.ID); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("user row survived deletion: %v", err)
|
|
}
|
|
}
|