181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/localreset"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
|
)
|
|
|
|
type localRecoveryServerRepo struct {
|
|
repository.ServerRepository
|
|
}
|
|
|
|
func (localRecoveryServerRepo) GetServerByID(_ context.Context, id string) (deployment.Server, error) {
|
|
return deployment.Server{ID: id}, nil
|
|
}
|
|
|
|
type testLocalRecoveryReader struct {
|
|
lists int
|
|
inspects int
|
|
hook func()
|
|
err error
|
|
}
|
|
|
|
func (r *testLocalRecoveryReader) ListPendingRecoveries(context.Context, string) (deployment.PendingRecoveryPage, error) {
|
|
r.lists++
|
|
if r.hook != nil {
|
|
r.hook()
|
|
}
|
|
return deployment.PendingRecoveryPage{Items: []deployment.PendingRecovery{{ID: "tx-1"}}}, r.err
|
|
}
|
|
|
|
func (r *testLocalRecoveryReader) InspectPending(ctx context.Context, id string, resolve func(context.Context, string) (deployment.Server, error)) (deployment.RecoveryInspection, error) {
|
|
r.inspects++
|
|
server, err := resolve(ctx, "server-1")
|
|
if err != nil {
|
|
return deployment.RecoveryInspection{}, err
|
|
}
|
|
if r.hook != nil {
|
|
r.hook()
|
|
}
|
|
return deployment.RecoveryInspection{TransactionID: id, ServerID: server.ID}, r.err
|
|
}
|
|
|
|
func newLocalRecoverySession(t *testing.T, server *Server, reader *testLocalRecoveryReader) *localResetSession {
|
|
t.Helper()
|
|
server.recoveryInspection = reader
|
|
server.servers = localRecoveryServerRepo{}
|
|
session, err := server.localResetFactory(func(context.Context, string, string) error {
|
|
t.Error("read-only inspection attempted to send a reset code")
|
|
return errors.New("unexpected mail")
|
|
})(t.Context(), localreset.Peer{Identity: "local-admin"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(session.Close)
|
|
return session.(*localResetSession)
|
|
}
|
|
|
|
func TestLocalRecoveryReadOnlyAndResetIsolation(t *testing.T) {
|
|
server := newLocalResetTestServer(t)
|
|
oldHash := server.config.Admin.PasswordHash
|
|
reader := &testLocalRecoveryReader{}
|
|
session := newLocalRecoverySession(t, server, reader)
|
|
if !session.RecoveryAvailable() {
|
|
t.Fatal("missing capability")
|
|
}
|
|
page, err := session.ListRecoveries(t.Context(), "")
|
|
if err != nil || len(page.Items) != 1 {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := session.InspectRecovery(t.Context(), "tx-1")
|
|
if err != nil || result.TransactionID != "tx-1" || result.ServerID != "server-1" {
|
|
t.Fatalf("inspection: %+v %v", result, err)
|
|
}
|
|
if err := session.Select(t.Context(), "email"); err == nil {
|
|
t.Fatal("recovery switched to password reset")
|
|
}
|
|
if err := session.Verify(t.Context(), "12345678"); err == nil {
|
|
t.Fatal("recovery verified reset")
|
|
}
|
|
if err := session.Reset(t.Context(), "NewPass123!"); err == nil {
|
|
t.Fatal("recovery reset password")
|
|
}
|
|
if reader.lists != 1 || reader.inspects != 1 {
|
|
t.Fatal("unexpected business calls")
|
|
}
|
|
assertLocalResetUnchanged(t, server, oldHash)
|
|
}
|
|
|
|
func TestLocalRecoveryRejectsInvalidSession(t *testing.T) {
|
|
for _, scenario := range []string{"expired", "cancelled", "changed-admin", "closed", "reset-stage", "missing-reader", "missing-repository"} {
|
|
t.Run(scenario, func(t *testing.T) {
|
|
server := newLocalResetTestServer(t)
|
|
reader := &testLocalRecoveryReader{}
|
|
session := newLocalRecoverySession(t, server, reader)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
switch scenario {
|
|
case "expired":
|
|
session.expires = time.Time{}
|
|
case "cancelled":
|
|
cancel()
|
|
case "changed-admin":
|
|
server.config.Admin.Email = "changed@example.test"
|
|
case "closed":
|
|
session.Close()
|
|
case "reset-stage":
|
|
session.stage = 1
|
|
case "missing-reader":
|
|
server.recoveryInspection = nil
|
|
case "missing-repository":
|
|
server.servers = nil
|
|
}
|
|
if _, err := session.ListRecoveries(ctx, ""); err == nil {
|
|
t.Fatal("invalid session listed transactions")
|
|
}
|
|
if _, err := session.InspectRecovery(ctx, "tx-1"); err == nil {
|
|
t.Fatal("invalid session inspected files")
|
|
}
|
|
if reader.lists != 0 || reader.inspects != 0 {
|
|
t.Fatal("invalid session reached service")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLocalRecoveryDiscardsStaleOrFailedResults(t *testing.T) {
|
|
for _, action := range []string{"list", "inspect"} {
|
|
for _, failure := range []string{"changed-admin", "cancelled", "read-error"} {
|
|
t.Run(action+"/"+failure, func(t *testing.T) {
|
|
server := newLocalResetTestServer(t)
|
|
reader := &testLocalRecoveryReader{}
|
|
session := newLocalRecoverySession(t, server, reader)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
switch failure {
|
|
case "changed-admin":
|
|
reader.hook = func() {
|
|
server.configMu.Lock()
|
|
defer server.configMu.Unlock()
|
|
server.config.Admin.Email = "changed@example.test"
|
|
}
|
|
case "cancelled":
|
|
reader.hook = cancel
|
|
case "read-error":
|
|
reader.err = errors.New("secret-error")
|
|
}
|
|
if action == "list" {
|
|
page, err := session.ListRecoveries(ctx, "")
|
|
if err != localreset.ErrUnavailable || len(page.Items) != 0 {
|
|
t.Fatal("stale list leaked")
|
|
}
|
|
} else {
|
|
result, err := session.InspectRecovery(ctx, "tx-1")
|
|
if err != localreset.ErrUnavailable || result.TransactionID != "" {
|
|
t.Fatal("stale inspection leaked")
|
|
}
|
|
}
|
|
if session.stage != -1 {
|
|
t.Fatal("failed session remains reusable")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLocalRecoveryNotRegisteredAsHTTPRoute(t *testing.T) {
|
|
server := newLocalResetTestServer(t)
|
|
for _, route := range server.router.Routes() {
|
|
if strings.Contains(strings.ToLower(route.Path), "recovery") || strings.Contains(route.Handler, "InspectRecovery") || strings.Contains(route.Handler, "ListRecoveries") {
|
|
t.Fatalf("local recovery exposed over HTTP: %+v", route)
|
|
}
|
|
}
|
|
}
|