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.
138 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|