// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo 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) } }