Repository: onflow/flow-cli (go)

Issue:
Add command to pull staged contract status
### Issue

Currently, `flow-c1 migrate is-validated` pulls the contract status from the on-chain repository.  However, this is not currently being populated and there are no existing plans to do so, so we need to determine a workaround

### Suggested solution

Add cli command to allow user to pull current status from the published GitHub report on https://github.com/onflow/cadence repository, as this is where the reports are currently being published

Pull request:
Use GitHub reports for `is-validated` command
Closes #1508 

## Description

Fetches the report from GitHub instead of looking at the on chain data.

Failure result:

<img width="1082" alt="Screenshot 2024-04-24 at 2 34 39 PM" src="https://github.com/onflow/flow-cli/assets/17958158/e9a1c4fa-779d-40c9-aa3b-ad0483a18d35">


Success result:

<img width="1088" alt="Screenshot 2024-04-24 at 2 38 16 PM" src="https://github.com/onflow/flow-cli/assets/17958158/7048d654-5a52-40fb-bd2a-718f22b2cdde">

______

For contributor use:

- [ ] Targeted PR against `master` branch
- [x] Linked to Github issue with discussion and accepted design OR link to spec that describes this work
- [x] Code follows the [standards mentioned here](https://github.com/onflow/flow-cli/blob/master/CONTRIBUTING.md#styleguides)
- [x] Updated relevant documentation
- [ ] Re-reviewed `Files changed` in the Github PR explorer
- [ ] Added appropriate labels


Patch:
diff --git a/go.mod b/go.mod
index 17d0ce11..eaafaef4 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/dukex/mixpanel v1.0.1
 	github.com/getsentry/sentry-go v0.27.0
 	github.com/go-git/go-git/v5 v5.11.0
+	github.com/google/go-github v17.0.0+incompatible
 	github.com/gosuri/uilive v0.0.4
 	github.com/logrusorgru/aurora/v4 v4.0.0
 	github.com/manifoldco/promptui v0.9.0
@@ -112,7 +113,6 @@ require (
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
 	github.com/google/go-dap v0.11.0 // indirect
-	github.com/google/go-github v17.0.0+incompatible // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/google/uuid v1.6.0 // indirect
diff --git a/internal/dependencymanager/add.go b/internal/dependencymanager/add.go
index 22e3a71a..22a01c78 100644
--- a/internal/dependencymanager/add.go
+++ b/internal/dependencymanager/add.go
@@ -20,6 +20,7 @@ package dependencymanager
 
 import (
 	"fmt"
+
 	"github.com/onflow/flow-cli/internal/util"
 
 	"github.com/spf13/cobra"
@@ -46,7 +47,8 @@ var addCommand = &command.Command{
 		Example: "flow dependencies add testnet://0afe396ebc8eee65.FlowToken",
 		Args:    cobra.ExactArgs(1),
 	},
-	RunS: add,
+	RunS:  add,
+	Flags: &struct{}{},
 }
 
 func init() {
diff --git a/internal/migrate/is_validated.go b/internal/migrate/is_validated.go
index 5f77f04d..e7d8f890 100644
--- a/internal/migrate/is_validated.go
+++ b/internal/migrate/is_validated.go
@@ -20,18 +20,51 @@ package migrate
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"io"
+	"path"
+	"regexp"
+	"strings"
+	"time"
 
-	"github.com/onflow/cadence"
-	"github.com/onflow/contract-updater/lib/go/templates"
+	"github.com/google/go-github/github"
+	"github.com/logrusorgru/aurora/v4"
 	"github.com/onflow/flowkit/v2"
+	"github.com/onflow/flowkit/v2/config"
 	"github.com/onflow/flowkit/v2/output"
 	"github.com/spf13/cobra"
 
 	"github.com/onflow/flow-cli/internal/command"
-	"github.com/onflow/flow-cli/internal/scripts"
+	"github.com/onflow/flow-cli/internal/util"
 )
 
+//go:generate mockery --name GitHubRepositoriesService --output ./mocks --case underscore
+type GitHubRepositoriesService interface {
+	GetContents(ctx context.Context, owner string, repo string, path string, opt *github.RepositoryContentGetOptions) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, resp *github.Response, err error)
+	DownloadContents(ctx context.Context, owner string, repo string, filepath string, opt *github.RepositoryContentGetOptions) (io.ReadCloser, error)
+}
+
+type validator struct {
+	repoService GitHubRepositoriesService
+	state       *flowkit.State
+	logger      output.Logger
+	network     config.Network
+}
+
+type contractUpdateStatus struct {
+	Kind           string `json:"kind,omitempty"`
+	AccountAddress string `json:"account_address"`
+	ContractName   string `json:"contract_name"`
+	Error          string `json:"error,omitempty"`
+}
+
+type validationResult struct {
+	Timestamp time.Time
+	Status    contractUpdateStatus
+	Network   string
+}
+
 var isValidatedflags struct{}
 
 var IsValidatedCommand = &command.Command{
@@ -45,42 +78,283 @@ var IsValidatedCommand = &command.Command{
 	RunS:  isValidated,
 }
 
+const (
+	repoOwner = "onflow"
+	repoName  = "cadence"
+	repoPath  = "migrations_data"
+	repoRef   = "master"
+)
+
+const moreInformationMessage = "For more information, please find the latest full migration report on GitHub (https://github.com/onflow/cadence/tree/master/migrations_data).\n\nNew reports are generated after each weekly emulated migration and your contract's status may change, so please actively monitor this status and stay tuned for the latest announcements until the migration deadline."
+const contractUpdateFailureKind = "contract-update-failure"
+
 func isValidated(
 	args []string,
-	globalFlags command.GlobalFlags,
-	_ output.Logger,
+	_ command.GlobalFlags,
+	logger output.Logger,
 	flow flowkit.Services,
 	state *flowkit.State,
 ) (command.Result, error) {
-	err := checkNetwork(flow.Network())
+	repoService := github.NewClient(nil).Repositories
+	v := newValidator(repoService, flow.Network(), state, logger)
+
+	contractName := args[0]
+	return v.validate(contractName)
+}
+
+func newValidator(repoService GitHubRepositoriesService, network config.Network, state *flowkit.State, logger output.Logger) *validator {
+	return &validator{
+		repoService: repoService,
+		state:       state,
+		logger:      logger,
+		network:     network,
+	}
+}
+
+func (v *validator) validate(contractName string) (validationResult, error) {
+	err := checkNetwork(v.network)
 	if err != nil {
-		return nil, err
+		return validationResult{}, err
 	}
 
-	contractName := args[0]
-	addr, err := getAddressByContractName(state, contractName, flow.Network())
+	v.logger.StartProgress("Checking if contract has been validated")
+	defer v.logger.StopProgress()
+
+	addr, err := getAddressByContractName(v.state, contractName, v.network)
+	if err != nil {
+		return validationResult{}, err
+	}
+
+	status, timestamp, err := v.getContractValidationStatus(
+		v.network,
+		addr.HexWithPrefix(),
+		contractName,
+	)
 	if err != nil {
-		return nil, fmt.Errorf("error getting address by contract name: %w", err)
+		// Append more information message to the error
+		// this way we can ensure that if, for whatever reason, we fail to fetch the report
+		// the user will still understand that they can find the report on GitHub
+		return validationResult{}, fmt.Errorf("%w\n\n%s%s", err, moreInformationMessage, "\n")
 	}
 
-	caddr := cadence.NewAddress(addr)
+	return validationResult{
+		Timestamp: *timestamp,
+		Status:    status,
+		Network:   v.network.Name,
+	}, nil
+}
+
+func (v *validator) getContractValidationStatus(network config.Network, address string, contractName string) (contractUpdateStatus, *time.Time, error) {
+	// Get last migration report
+	report, timestamp, err := v.getLatestMigrationReport(network)
+	if err != nil {
+		return contractUpdateStatus{}, nil, err
+	}
 
-	cname, err := cadence.NewString(contractName)
+	// Get all the contract statuses from the report
+	statuses, err := v.fetchAndParseReport(report.GetPath())
 	if err != nil {
-		return nil, fmt.Errorf("failed to get cadence string from contract name: %w", err)
+		return contractUpdateStatus{}, nil, err
+	}
+
+	// Get the validation result related to the contract
+	var status *contractUpdateStatus
+	for _, s := range statuses {
+		if s.ContractName == contractName && s.AccountAddress == address {
+			status = &s
+			break
+		}
+	}
+
+	// Throw error if contract was not part of the last migration
+	if status == nil {
+		builder := strings.Builder{}
+		builder.WriteString("the contract does not appear to have been a part of any emulated migrations yet, please ensure that it has been staged & wait for the next emulated migration (last migration report was at ")
+		builder.WriteString(timestamp.Format(time.RFC3339))
+		builder.WriteString(")\n\n")
+
+		builder.WriteString(" - Account: ")
+		builder.WriteString(address)
+		builder.WriteString("\n - Contract: ")
+		builder.WriteString(contractName)
+		builder.WriteString("\n - Network: ")
+		builder.WriteString(network.Name)
+
+		return contractUpdateStatus{}, nil, fmt.Errorf(builder.String())
+	}
+
+	return *status, timestamp, nil
+}
+
+func (v *validator) getLatestMigrationReport(network config.Network) (*github.RepositoryContent, *time.Time, error) {
+	// Get the content of the migration reports folder
+	_, folderContent, _, err := v.repoService.GetContents(
+		context.Background(),
+		repoOwner,
+		repoName,
+		repoPath,
+		&github.RepositoryContentGetOptions{
+			Ref: repoRef,
+		},
+	)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Find the latest report file
+	var latestReport *github.RepositoryContent
+	var latestReportTime *time.Time
+	for _, content := range folderContent {
+		if content.Type != nil && *content.Type == "file" {
+			contentPath := content.GetPath()
+
+			// Try to extract the time from the filename
+			networkStr, t, err := extractInfoFromFilename(contentPath)
+			if err != nil {
+				// Ignore files that don't match the expected format
+				// Or have any another error while parsing
+				continue
+			}
+
+			// Ignore reports from other networks
+			if networkStr != strings.ToLower(network.Name) {
+				continue
+			}
+
+			// Check if this is the latest report
+			if latestReportTime == nil || t.After(*latestReportTime) {
+				latestReport = content
+				latestReportTime = t
+			}
+		}
+	}
+
+	if latestReport == nil {
+		return nil, nil, fmt.Errorf("no emulated migration reports found for network `%s` within the remote repository - have any migrations been run yet for this network?", network.Name)
 	}
 
-	value, err := flow.ExecuteScript(
+	return latestReport, latestReportTime, nil
+}
+
+func (v *validator) fetchAndParseReport(reportPath string) ([]contractUpdateStatus, error) {
+	// Get the content of the latest report
+	rc, err := v.repoService.DownloadContents(
 		context.Background(),
-		flowkit.Script{
-			Code: templates.GenerateIsValidatedScript(MigrationContractStagingAddress(flow.Network().Name)),
-			Args: []cadence.Value{caddr, cname},
+		repoOwner,
+		repoName,
+		reportPath,
+		&github.RepositoryContentGetOptions{
+			Ref: repoRef,
 		},
-		flowkit.LatestScriptQuery,
 	)
 	if err != nil {
-		return nil, fmt.Errorf("error executing script: %w", err)
+		return nil, err
+	}
+	defer rc.Close()
+
+	// Read the report content
+	reportContent, err := io.ReadAll(rc)
+	if err != nil {
+		return nil, err
+	}
+
+	// Parse the report
+	var statuses []contractUpdateStatus
+	err = json.Unmarshal(reportContent, &statuses)
+	if err != nil {
+		return nil, err
+	}
+
+	return statuses, nil
+}
+
+func extractInfoFromFilename(filename string) (string, *time.Time, error) {
+	// Extracts the timestamp from the filename in the format: migrations_data/raw/XXXXXX-MM-DD-YYYY-<network>-XXXXXX.json
+	fileName := path.Base(filename)
+
+	expr := regexp.MustCompile(`^staged-contracts-report.*(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)-([a-z]+).json$`)
+	regexpMatches := expr.FindStringSubmatch(fileName)
+	if regexpMatches == nil {
+		return "", nil, fmt.Errorf("filename does not match the expected format")
+	}
+
+	// Extract the timestamp
+	timestampStr := regexpMatches[1]
+	timestamp, err := time.Parse("2006-01-02T15-04-05Z", timestampStr)
+	if err != nil {
+		return "", nil, fmt.Errorf("failed to parse timestamp from filename")
 	}
 
-	return scripts.NewScriptResult(value), nil
+	// Extract the network
+	network := regexpMatches[2]
+
+	return network, &timestamp, nil
+}
+
+func (s contractUpdateStatus) IsFailure() bool {
+	// Just in case there are failures without an error message in the future
+	// we will also check the kind of the status
+	return s.Error != "" || s.Kind == contractUpdateFailureKind
+}
+
+func (v validationResult) String() string {
+	status := v.Status
+
+	builder := strings.Builder{}
+	builder.WriteString("Last emulated migration report was created at ")
+	builder.WriteString(v.Timestamp.Format(time.RFC3339))
+	builder.WriteString("\n\n")
+
+	statusBuilder := strings.Builder{}
+	emoji := "✅ "
+	statusColor := aurora.Green
+	if status.IsFailure() {
+		emoji = "❌ "
+		statusColor = aurora.Red
+	}
+
+	statusBuilder.WriteString(util.PrintEmoji(emoji))
+	statusBuilder.WriteString("The contract has ")
+
+	if status.IsFailure() {
+		statusBuilder.WriteString("FAILED")
+	} else {
+		statusBuilder.WriteString("PASSED")
+	}
+	statusBuilder.WriteString(" the last emulated migration")
+
+	statusBuilder.WriteString("\n\n - Account: ")
+	statusBuilder.WriteString(status.AccountAddress)
+	statusBuilder.WriteString("\n - Contract: ")
+	statusBuilder.WriteString(status.ContractName)
+	statusBuilder.WriteString("\n - Network: ")
+	statusBuilder.WriteString(v.Network)
+	statusBuilder.WriteString("\n\n")
+
+	// Write colored status
+	builder.WriteString(statusColor(statusBuilder.String()).String())
+
+	if status.Error != "" {
+		builder.WriteString(status.Error)
+		builder.WriteString("\n")
+	}
+
+	if status.IsFailure() {
+		builder.WriteString(aurora.Red(">> Please review the error and re-stage the contract to resolve these issues if necessary").String())
+		builder.WriteString("\n\n")
+	}
+
+	builder.WriteString(moreInformationMessage)
+	return builder.String()
+}
+
+func (v validationResult) JSON() interface{} {
+	return v
+}
+
+func (v validationResult) Oneliner() string {
+	if v.Status.IsFailure() {
+		return util.MessageWithEmojiPrefix("❌", "FAILED")
+	}
+	return util.MessageWithEmojiPrefix("✅", "FAIlED")
 }
diff --git a/internal/migrate/mocks/git_hub_repositories_service.go b/internal/migrate/mocks/git_hub_repositories_service.go
new file mode 100644
index 00000000..e586ee01
--- /dev/null
+++ b/internal/migrate/mocks/git_hub_repositories_service.go
@@ -0,0 +1,109 @@
+// Code generated by mockery v2.38.0. DO NOT EDIT.
+
+package mocks
+
+import (
+	context "context"
+	io "io"
+
+	github "github.com/google/go-github/github"
+
+	mock "github.com/stretchr/testify/mock"
+)
+
+// GitHubRepositoriesService is an autogenerated mock type for the GitHubRepositoriesService type
+type GitHubRepositoriesService struct {
+	mock.Mock
+}
+
+// DownloadContents provides a mock function with given fields: ctx, owner, repo, filepath, opt
+func (_m *GitHubRepositoriesService) DownloadContents(ctx context.Context, owner string, repo string, filepath string, opt *github.RepositoryContentGetOptions) (io.ReadCloser, error) {
+	ret := _m.Called(ctx, owner, repo, filepath, opt)
+
+	if len(ret) == 0 {
+		panic("no return value specified for DownloadContents")
+	}
+
+	var r0 io.ReadCloser
+	var r1 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) (io.ReadCloser, error)); ok {
+		return rf(ctx, owner, repo, filepath, opt)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) io.ReadCloser); ok {
+		r0 = rf(ctx, owner, repo, filepath, opt)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(io.ReadCloser)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) error); ok {
+		r1 = rf(ctx, owner, repo, filepath, opt)
+	} else {
+		r1 = ret.Error(1)
+	}
+
+	return r0, r1
+}
+
+// GetContents provides a mock function with given fields: ctx, owner, repo, path, opt
+func (_m *GitHubRepositoriesService) GetContents(ctx context.Context, owner string, repo string, path string, opt *github.RepositoryContentGetOptions) (*github.RepositoryContent, []*github.RepositoryContent, *github.Response, error) {
+	ret := _m.Called(ctx, owner, repo, path, opt)
+
+	if len(ret) == 0 {
+		panic("no return value specified for GetContents")
+	}
+
+	var r0 *github.RepositoryContent
+	var r1 []*github.RepositoryContent
+	var r2 *github.Response
+	var r3 error
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) (*github.RepositoryContent, []*github.RepositoryContent, *github.Response, error)); ok {
+		return rf(ctx, owner, repo, path, opt)
+	}
+	if rf, ok := ret.Get(0).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) *github.RepositoryContent); ok {
+		r0 = rf(ctx, owner, repo, path, opt)
+	} else {
+		if ret.Get(0) != nil {
+			r0 = ret.Get(0).(*github.RepositoryContent)
+		}
+	}
+
+	if rf, ok := ret.Get(1).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) []*github.RepositoryContent); ok {
+		r1 = rf(ctx, owner, repo, path, opt)
+	} else {
+		if ret.Get(1) != nil {
+			r1 = ret.Get(1).([]*github.RepositoryContent)
+		}
+	}
+
+	if rf, ok := ret.Get(2).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) *github.Response); ok {
+		r2 = rf(ctx, owner, repo, path, opt)
+	} else {
+		if ret.Get(2) != nil {
+			r2 = ret.Get(2).(*github.Response)
+		}
+	}
+
+	if rf, ok := ret.Get(3).(func(context.Context, string, string, string, *github.RepositoryContentGetOptions) error); ok {
+		r3 = rf(ctx, owner, repo, path, opt)
+	} else {
+		r3 = ret.Error(3)
+	}
+
+	return r0, r1, r2, r3
+}
+
+// NewGitHubRepositoriesService creates a new instance of GitHubRepositoriesService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
+// The first argument is typically a *testing.T value.
+func NewGitHubRepositoriesService(t interface {
+	mock.TestingT
+	Cleanup(func())
+}) *GitHubRepositoriesService {
+	mock := &GitHubRepositoriesService{}
+	mock.Mock.Test(t)
+
+	t.Cleanup(func() { mock.AssertExpectations(t) })
+
+	return mock
+}


Test patch:
diff --git a/internal/migrate/is_validated_test.go b/internal/migrate/is_validated_test.go
index b83cdf48..18e59b44 100644
--- a/internal/migrate/is_validated_test.go
+++ b/internal/migrate/is_validated_test.go
@@ -19,27 +19,66 @@
 package migrate
 
 import (
+	"bytes"
+	"encoding/json"
+	"io"
 	"testing"
+	"time"
 
-	"github.com/onflow/cadence"
-	"github.com/onflow/contract-updater/lib/go/templates"
-	"github.com/onflow/flowkit/v2"
+	"github.com/google/go-github/github"
 	"github.com/onflow/flowkit/v2/config"
 	"github.com/onflow/flowkit/v2/tests"
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
 
 	"github.com/onflow/flow-cli/internal/command"
+	"github.com/onflow/flow-cli/internal/migrate/mocks"
 	"github.com/onflow/flow-cli/internal/util"
 )
 
 func Test_IsValidated(t *testing.T) {
-	srv, state, _ := util.TestMocks(t)
-
+	_, state, _ := util.TestMocks(t)
 	testContract := tests.ContractSimple
 
-	t.Run("Success", func(t *testing.T) {
+	// use emulator-account because it already exists in mock, so we don't need to create it
+	emuAccount, err := state.EmulatorServiceAccount()
+	require.NoError(t, err)
+
+	// Helper function to test the isValidated function
+	// with all of the necessary mocks
+	testIsValidatedWithStatuses := func(statuses []contractUpdateStatus) (command.Result, error) {
+		mockClient := mocks.NewGitHubRepositoriesService(t)
+
+		// mock github file download
+		data, _ := json.Marshal(statuses)
+		mockClient.On("DownloadContents", mock.Anything, "onflow", "cadence", "migrations_data/staged-contracts-report-2024-04-17T20-05-50Z-testnet.json", mock.MatchedBy(func(opts *github.RepositoryContentGetOptions) bool {
+			return opts.Ref == "master"
+		})).Return(io.NopCloser(bytes.NewReader(data)), nil).Once()
+
+		// mock github folder response
+		fileType := "file"
+		olderPath := "migrations_data/staged-contracts-report-2019-04-17T20-05-50Z-testnet.json"
+		wrongNetworkPath := "migrations_data/staged-contracts-report-2025-04-17T20-05-50Z-mainnet.json"
+		latestPath := "migrations_data/staged-contracts-report-2024-04-17T20-05-50Z-testnet.json"
+		mockFolderContent := []*github.RepositoryContent{
+			{
+				Path: &olderPath,
+				Type: &fileType,
+			},
+			{
+				Path: &wrongNetworkPath,
+				Type: &fileType,
+			},
+			{
+				Path: &latestPath,
+				Type: &fileType,
+			},
+		}
+		mockClient.On("GetContents", mock.Anything, "onflow", "cadence", "migrations_data", mock.MatchedBy(func(opts *github.RepositoryContentGetOptions) bool {
+			return opts.Ref == "master"
+		})).Return(nil, mockFolderContent, nil, nil).Once()
 
+		// mock flowkit contract
 		state.Contracts().AddOrUpdate(
 			config.Contract{
 				Name:     testContract.Name,
@@ -51,7 +90,7 @@ func Test_IsValidated(t *testing.T) {
 		state.Deployments().AddOrUpdate(
 			config.Deployment{
 				Network: "testnet",
-				Account: "emulator-account",
+				Account: emuAccount.Name,
 				Contracts: []config.ContractDeployment{
 					{
 						Name: testContract.Name,
@@ -60,38 +99,56 @@ func Test_IsValidated(t *testing.T) {
 			},
 		)
 
-		srv.Network.Return(config.Network{
-			Name: "testnet",
-		}, nil)
+		// call the isValidated function
+		validator := newValidator(mockClient, config.TestnetNetwork, state, util.NoLogger)
 
-		account, err := state.EmulatorServiceAccount()
-		assert.NoError(t, err)
-
-		srv.ExecuteScript.Run(func(args mock.Arguments) {
-			script := args.Get(1).(flowkit.Script)
-
-			assert.Equal(t, templates.GenerateIsValidatedScript(MigrationContractStagingAddress("testnet")), script.Code)
+		res, err := validator.validate(
+			testContract.Name,
+		)
 
-			assert.Equal(t, 2, len(script.Args))
-			actualContractAddressArg, actualContractNameArg := script.Args[0], script.Args[1]
+		require.Equal(t, true, mockClient.AssertExpectations(t))
+		return res, err
+	}
 
-			contractName, _ := cadence.NewString(testContract.Name)
-			contractAddr := cadence.NewAddress(account.Address)
-			assert.Equal(t, contractName, actualContractNameArg)
-			assert.Equal(t, contractAddr, actualContractAddressArg)
-		}).Return(cadence.NewBool(true), nil)
+	t.Run("isValidated gets status from latest report on github", func(t *testing.T) {
+		res, err := testIsValidatedWithStatuses([]contractUpdateStatus{
+			{
+				AccountAddress: "0x01",
+				ContractName:   "some-other-contract",
+				Error:          "4567",
+			},
+			{
+				AccountAddress: emuAccount.Address.HexWithPrefix(),
+				ContractName:   testContract.Name,
+				Error:          "1234",
+			},
+		})
+		require.NoError(t, err)
+		require.NotNil(t, res)
+
+		// use a different time format for expected time to better ensure parsing is correct
+		expectedTime, err := time.Parse(time.RFC3339, "2024-04-17T20:05:50Z")
+		require.NoError(t, err)
+		require.Equal(t, res.JSON(), validationResult{
+			Timestamp: expectedTime,
+			Status: contractUpdateStatus{
+				AccountAddress: emuAccount.Address.HexWithPrefix(),
+				ContractName:   testContract.Name,
+				Error:          "1234",
+			},
+			Network: "testnet",
+		})
+	})
 
-		result, err := isValidated(
-			[]string{testContract.Name},
-			command.GlobalFlags{
-				Network: "testnet",
+	t.Run("isValidated errors if contract was not in last migration", func(t *testing.T) {
+		_, err := testIsValidatedWithStatuses([]contractUpdateStatus{
+			{
+				AccountAddress: "0x01",
+				ContractName:   "some-other-contract",
+				Error:          "4567",
 			},
-			util.NoLogger,
-			srv.Mock,
-			state,
-		)
-		assert.NoError(t, err)
-		// TODO: fix this
-		assert.NotNil(t, result)
+		})
+
+		require.ErrorContains(t, err, "does not appear to have been a part of any emulated migrations yet")
 	})
 }