Files
T
cgalo5758 6f1effe58e Harden FedWiki sync schedule recovery
Only create schedules after Temporal NotFound and update after
concurrent
creation. Resolve the system workspace on every workflow run so
persisted
schedule arguments cannot retain stale IDs after database resets.
2026-07-11 16:08:52 -05:00

138 lines
4.9 KiB
Go

package workflows
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/mock"
"go.temporal.io/sdk/testsuite"
"git.coopcloud.tech/wiki-cafe/member-console/internal/systemtenant"
)
// TestSyncWorkflowResolvesWorkspaceFirst asserts Step 0
// (ResolveSystemWorkspaceActivity) runs before Step 1 (ListFedWikiSitesActivity)
// and that the workspace ID it returns is threaded into SyncSitesToDBActivity's
// input — i.e. the sync projects into the run-time resolved System workspace,
// not a value carried in workflow input.
func TestSyncWorkflowResolvesWorkspaceFirst(t *testing.T) {
var suite testsuite.WorkflowTestSuite
env := suite.NewTestWorkflowEnvironment()
// A nil *Activities is sufficient: OnActivity reflects on the method value
// only to derive the registered activity name; the mock intercepts before
// the (nil) receiver is ever dereferenced — the same idiom the workflow uses.
var acts *Activities
const wantWS = "workspace-system-abc"
var callOrder []string
var syncInputWS string
sawSyncInput := false
env.OnActivity(acts.ResolveSystemWorkspaceActivity, mock.Anything, mock.Anything).
Run(func(mock.Arguments) { callOrder = append(callOrder, "resolve") }).
Return(&ResolveSystemWorkspaceOutput{WorkspaceID: wantWS}, nil)
env.OnActivity(acts.ListFedWikiSitesActivity, mock.Anything, mock.Anything).
Run(func(mock.Arguments) { callOrder = append(callOrder, "list") }).
Return(&ListFedWikiSitesOutput{Sites: []SiteInfo{}}, nil)
env.OnActivity(acts.SyncSitesToDBActivity, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
in, ok := args.Get(1).(SyncSitesToDBInput)
if !ok {
t.Fatalf("SyncSitesToDBActivity arg 1 = %T, want SyncSitesToDBInput", args.Get(1))
}
sawSyncInput = true
syncInputWS = in.DefaultWorkspaceID
}).
Return(&SyncSitesToDBOutput{}, nil)
env.OnActivity(acts.ReconcileFedWikiQuotaActivity, mock.Anything, mock.Anything).
Return(&ReconcileFedWikiQuotaOutput{}, nil)
env.OnActivity(acts.PurgeExpiredArchivedActivity, mock.Anything, mock.Anything).
Return(&PurgeExpiredArchivedOutput{}, nil)
env.ExecuteWorkflow(SyncFedWikiSitesWorkflow, SyncFedWikiSitesWorkflowInput{})
if !env.IsWorkflowCompleted() {
t.Fatal("workflow did not complete")
}
if err := env.GetWorkflowError(); err != nil {
t.Fatalf("workflow error: %v", err)
}
var out *SyncFedWikiSitesWorkflowOutput
if err := env.GetWorkflowResult(&out); err != nil {
t.Fatalf("get result: %v", err)
}
if !out.Success {
t.Errorf("Success = false, want true (%q)", out.ErrorMessage)
}
if len(callOrder) < 2 || callOrder[0] != "resolve" || callOrder[1] != "list" {
t.Fatalf("call order = %v, want resolve before list", callOrder)
}
if !sawSyncInput {
t.Fatal("SyncSitesToDBActivity was never called")
}
if syncInputWS != wantWS {
t.Errorf("SyncSitesToDBInput.DefaultWorkspaceID = %q, want %q", syncInputWS, wantWS)
}
}
// TestSyncWorkflowResolveFailureReturnsFriendlyOutput asserts that a Step 0
// failure yields (*SyncFedWikiSitesWorkflowOutput{Success:false}, nil) — the same
// log-and-return shape Step 1 uses — rather than a workflow-level error.
func TestSyncWorkflowResolveFailureReturnsFriendlyOutput(t *testing.T) {
var suite testsuite.WorkflowTestSuite
env := suite.NewTestWorkflowEnvironment()
var acts *Activities
env.OnActivity(acts.ResolveSystemWorkspaceActivity, mock.Anything, mock.Anything).
Return(nil, errors.New("system tenant unavailable"))
env.ExecuteWorkflow(SyncFedWikiSitesWorkflow, SyncFedWikiSitesWorkflowInput{})
if !env.IsWorkflowCompleted() {
t.Fatal("workflow did not complete")
}
if err := env.GetWorkflowError(); err != nil {
t.Fatalf("workflow returned an error, want friendly output: %v", err)
}
var out *SyncFedWikiSitesWorkflowOutput
if err := env.GetWorkflowResult(&out); err != nil {
t.Fatalf("get result: %v", err)
}
if out.Success {
t.Error("Success = true, want false on resolve failure")
}
if out.ErrorMessage == "" {
t.Error("ErrorMessage is empty, want a friendly message")
}
}
// TestResolveSystemWorkspaceActivity_DB checks that the activity resolves the
// same workspace systemtenant.Ensure resolves directly against the same DB.
// DB-gated (TEST_DATABASE_URL), reusing reconcile_db_test.go's env setup.
func TestResolveSystemWorkspaceActivity_DB(t *testing.T) {
acts, database := newReconcileTestEnv(t)
ctx := context.Background()
out, err := acts.ResolveSystemWorkspaceActivity(ctx, ResolveSystemWorkspaceInput{})
if err != nil {
t.Fatalf("ResolveSystemWorkspaceActivity: %v", err)
}
if out.WorkspaceID == "" {
t.Fatal("resolved WorkspaceID is empty")
}
// Ensure is idempotent, so a direct call must yield the identical workspace.
want, err := systemtenant.Ensure(ctx, database)
if err != nil {
t.Fatalf("systemtenant.Ensure: %v", err)
}
if out.WorkspaceID != want {
t.Errorf("activity WorkspaceID = %q, systemtenant.Ensure = %q", out.WorkspaceID, want)
}
}