package mcptools import ( "context" "errors" "strings" "testing" "time" "xprem/internal/services" "xprem/internal/types" ) type fakeReadServices struct{} func (fakeReadServices) GetBranches(_ context.Context, _ string) ([]types.BranchMapping, error) { mainId, stagingId := "branch-id-1", "branch-id-2" return []types.BranchMapping{ {BranchName: "main", BranchId: &mainId}, {BranchName: "staging", BranchId: &stagingId}, }, nil } func (fakeReadServices) GetRuntimeVersionsWithUpdateStats(_ context.Context, _ string, _ string) ([]types.RuntimeVersionWithStats, error) { return []types.RuntimeVersionWithStats{{RuntimeVersion: "1.0.0", NumberOfUpdates: 3}}, nil } func (fakeReadServices) GetChannels(_ context.Context, _ string) ([]types.ChannelMapping, error) { return []types.ChannelMapping{ {ReleaseChannelName: "production", ReleaseChannelId: "channel-id-1", Rollout: &types.ChannelRollout{ChannelName: "production", Percentage: 25}}, {ReleaseChannelName: "staging", ReleaseChannelId: "channel-id-2"}, }, nil } func (fakeReadServices) GetUpdateFeed(_ context.Context, _ string, query types.UpdateFeedQuery) ([]types.UpdateFeedItem, error) { // Always a full page plus one, so pagination has a next page to signal. items := make([]types.UpdateFeedItem, query.Limit) for i := range items { items[i].UpdateId = "1" items[i].BranchID = 7 items[i].FeedCreatedAt = time.Date(2026, 7, 30, 0, 0, 0, 0, time.UTC).Add(-time.Duration(i) * time.Minute) } return items, nil } func (fakeReadServices) GetUpdateRollout(_ context.Context, _ string, _ string, _ string) ([]types.RolloutUpdate, error) { return []types.RolloutUpdate{{UpdateId: "u1", Platform: "ios", Percentage: 50}}, nil } func (fakeReadServices) ListPatches(_ context.Context, _ string, _ string, _ string) ([]types.BundlePatch, error) { return []types.BundlePatch{{TargetUpdateId: "2", SourceUpdateId: "1", Status: types.BundlePatchStored}}, nil } func (fakeReadServices) RetrieveAppCertificate(_ context.Context, _ string) (string, error) { return "-----BEGIN CERTIFICATE-----", nil } func readDeps() Deps { deps := testDeps() fake := fakeReadServices{} deps.Branches = fake deps.Channels = fake deps.UpdateFeed = fake deps.UpdateRollouts = fake deps.BundlePatches = fake deps.Certificates = fake deps.SSOEnabled = func(context.Context) bool { return false } return deps } // Every app-scoped tool must refuse an invisible app with the 404 answer and // an empty appId with the guidance message. func TestAppScopedToolsRequireVisibleApp(t *testing.T) { deps := readDeps() deps.VisibleApps = func(_ context.Context, _ *services.DashboardPrincipal) (bool, map[string]bool, error) { return true, map[string]bool{"app-visible": true}, nil } principal := &services.DashboardPrincipal{UserId: "user-1"} req := callToolRequestFor(principal) ctx := context.Background() calls := map[string]func(appID string) error{ "get_branches": func(appID string) error { _, _, err := getBranchesHandler(deps)(ctx, req, GetBranchesInput{AppId: appID}) return err }, "get_runtime_versions": func(appID string) error { _, _, err := getRuntimeVersionsHandler(deps)(ctx, req, GetRuntimeVersionsInput{AppId: appID, Branch: "main"}) return err }, "get_channels": func(appID string) error { _, _, err := getChannelsHandler(deps)(ctx, req, GetChannelsInput{AppId: appID}) return err }, "get_channel_rollouts": func(appID string) error { _, _, err := getChannelRolloutsHandler(deps)(ctx, req, GetChannelsInput{AppId: appID}) return err }, "get_updates": func(appID string) error { _, _, err := getUpdatesHandler(deps)(ctx, req, GetUpdatesInput{AppId: appID}) return err }, "get_update_rollout": func(appID string) error { _, _, err := getUpdateRolloutHandler(deps)(ctx, req, GetUpdateRolloutInput{AppId: appID, Branch: "main", RuntimeVersion: "1.0.0"}) return err }, "get_update_patches": func(appID string) error { _, _, err := getUpdatePatchesHandler(deps)(ctx, req, GetUpdatePatchesInput{AppId: appID, Branch: "main", UpdateId: "2"}) return err }, } for name, call := range calls { t.Run(name, func(t *testing.T) { if err := call("app-visible"); err != nil { t.Fatalf("visible app must pass, got %v", err) } err := call("app-hidden") if err == nil || err.Error() != ErrAppNotFound.Error() { t.Fatalf("invisible app must read as 404, got %v", err) } if err := call(""); err == nil || !strings.Contains(err.Error(), "appId is required") { t.Fatalf("empty appId must guide to get_apps, got %v", err) } }) } } // An app id that does not exist must read as not found for every account, // including an admin and any deployment where roles are not enforced: those // report restricted=false, so visibility alone cannot answer. func TestAppScopedToolsRefuseUnknownApp(t *testing.T) { deps := readDeps() deps.VisibleApps = func(_ context.Context, _ *services.DashboardPrincipal) (bool, map[string]bool, error) { return false, nil, nil } admin := &services.DashboardPrincipal{UserId: "admin-1", IsAdmin: true} _, _, err := getBranchesHandler(deps)(context.Background(), callToolRequestFor(admin), GetBranchesInput{AppId: "does-not-exist"}) if err == nil || err.Error() != ErrAppNotFound.Error() { t.Fatalf("an unknown app must be refused, got %v", err) } if _, _, err := getBranchesHandler(deps)(context.Background(), callToolRequestFor(admin), GetBranchesInput{AppId: "app-1"}); err != nil { t.Fatalf("a known app must pass, got %v", err) } } func TestGetBranchesNameFilter(t *testing.T) { deps := readDeps() principal := &services.DashboardPrincipal{UserId: "user-1"} _, output, err := getBranchesHandler(deps)(context.Background(), callToolRequestFor(principal), GetBranchesInput{AppId: "app-1", Name: "MAIN"}) if err != nil { t.Fatal(err) } if len(output.Branches) != 1 || output.Branches[0].BranchName != "main" { t.Fatalf("expected the main branch only, got %+v", output.Branches) } } func TestChannelLookupById(t *testing.T) { deps := readDeps() principal := &services.DashboardPrincipal{UserId: "user-1"} _, output, err := getChannelsHandler(deps)(context.Background(), callToolRequestFor(principal), GetChannelsInput{AppId: "app-1", Id: "channel-id-1"}) if err != nil { t.Fatal(err) } if len(output.Channels) != 1 || output.Channels[0].ReleaseChannelName != "production" { t.Fatalf("expected production by id, got %+v", output.Channels) } } func TestGetChannelRolloutsOnlyActive(t *testing.T) { deps := readDeps() principal := &services.DashboardPrincipal{UserId: "user-1"} _, output, err := getChannelRolloutsHandler(deps)(context.Background(), callToolRequestFor(principal), GetChannelsInput{AppId: "app-1"}) if err != nil { t.Fatal(err) } if len(output.Rollouts) != 1 || output.Rollouts[0].ChannelName != "production" || output.Rollouts[0].Rollout.Percentage != 25 { t.Fatalf("expected the production rollout only, got %+v", output.Rollouts) } } func TestGetUpdatesPagination(t *testing.T) { deps := readDeps() principal := &services.DashboardPrincipal{UserId: "user-1"} req := callToolRequestFor(principal) ctx := context.Background() _, output, err := getUpdatesHandler(deps)(ctx, req, GetUpdatesInput{AppId: "app-1", Limit: 5}) if err != nil { t.Fatal(err) } if len(output.Updates) != 5 { t.Fatalf("expected the page capped at 5, got %d", len(output.Updates)) } if output.NextCursor == "" { t.Fatal("a full page plus one must signal a next page") } // The cursor round-trips into the next query. if _, _, err := getUpdatesHandler(deps)(ctx, req, GetUpdatesInput{AppId: "app-1", Limit: 5, Cursor: output.NextCursor}); err != nil { t.Fatalf("nextCursor must be accepted back, got %v", err) } if _, _, err := getUpdatesHandler(deps)(ctx, req, GetUpdatesInput{AppId: "app-1", Cursor: "not-a-cursor!!"}); err == nil { t.Fatal("a malformed cursor must be refused") } } func TestGetCertificateAuthorize(t *testing.T) { deps := readDeps() principal := &services.DashboardPrincipal{UserId: "user-1"} // The deps' Authorize fake denies everything: the tool must refuse. if _, _, err := getCertificateHandler(deps)(context.Background(), callToolRequestFor(principal), GetCertificateInput{AppId: "app-1"}); err == nil { t.Fatal("expected the authorization denial to propagate") } deps.Authorize = func(_ context.Context, _ *services.DashboardPrincipal, appID string, access Access) error { if appID != "app-1" || access.Perm != "certificate:read" { return errors.New("wrong authorization request") } return nil } _, output, err := getCertificateHandler(deps)(context.Background(), callToolRequestFor(principal), GetCertificateInput{AppId: "app-1"}) if err != nil { t.Fatal(err) } if !strings.HasPrefix(output.CertificatePEM, "-----BEGIN CERTIFICATE-----") { t.Fatalf("expected a PEM, got %q", output.CertificatePEM) } }