Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update VxAdmin to support importing new CVR export format #3956

Merged
merged 10 commits into from
Sep 13, 2023

Conversation

arsalansufi
Copy link
Contributor

Easiest reviewed by commit

Overview

Issue link: #3926

This PR completes the end-to-end wiring of continuous export! It specifically updates VxAdmin to support importing the new CVR export format. The user experience is unchanged by this PR. Just a lot of under-the-hood tweaks.

All of the new logic is feature flag gated. Once I merge this PR, I'll work on pruning the feature flag and old CVR export/import logic.

Testing

  • Tested CVR export and import manually
  • Updated some unit tests

I've mostly punted on automated tests, which I'll add in follow-up PRs, since this PR is large enough as is.

Checklist

  • I have added logging where appropriate to any new user actions, system updates such as file reads or storage writes, or errors introduced

expectedDirectoryName
);
}
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test.each is just moved down from above. The one below is new.

directoryNameComponents.unshift('TEST');
}
return directoryNameComponents.join(SECTION_SEPARATOR);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this function is just moved down from above. The one below is new.

);
const parseResult = safeParseJson(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the commit description, the mistake here was that unsafeParse expects an object, not a JSON string. safeParseJson, on the other hand, expects a JSON string, which is what we're providing.

Tests missed this because of how I'd incorrectly mocked unsafeParse 🙈

I recognize that I should just not mock, but here's why I mocked for context:

// Avoid having to prepare a complete CVR.CastVoteRecordReport object for
// CastVoteRecordExportMetadata

* are not read/parsed, but their existence is validated such that consumers can safely access
* them.
*/
export async function readCastVoteRecordExport(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mental model for the separation of responsibilities between libs/backend and apps/admin/backend re import is that:

  • libs/backend reads/parses data and performs validation internal to an export (i.e. it doesn't perform contextual validation like whether the fields in an export and the election definition correspond)
  • apps/admin/backend actually persists data and performs contextual validation

This separation will allow us to reuse libs/backend import code in VxScan, when we build support for bootstrapping a VxScan with another VxScan's CVRs. The persistence logic and contextual validation for VxScan bootstrapping differ from the persistence logic and contextual validation for VxAdmin tabulation, so it's nice that those pieces are separated out.

scannerIds: [directoryNameComponents.machineId],
});
continue;
}
Copy link
Contributor Author

@arsalansufi arsalansufi Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whereas previously, we could extract everything that we needed for the "list of exports on the USB" UI from the directory name, now, we have to pull some info from the metadata file.

Note that I think it's okay for us to extract this information without authenticating the export (which can be time consuming since we have to re-hash), as we aren't actually importing anything yet. If someone tries to trick the system by changing a directory name or the metadata file, the worst that they'll be able to do is get the "list of exports on the USB" UI to display something misleading. Importing will still fail.

We've applied similar principles to Java Card auth (e.g. here).

@@ -62,6 +65,89 @@ const Content = styled.div`
overflow: hidden;
`;

/* c8 ignore start */
function userReadableMessageFromError(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this function lives on the frontend. The old import code has an equivalent function live on the backend and sends a user-readable message down to the frontend for it to use. Ultimately, this message-preparation is a UI/frontend concern and not a backend concern, hence my moving it to the frontend.

This will become even more necessary once we start working on multi-language support (as noted here).

}
}
}
/* c8 ignore stop */
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I've added a lot of code coverage ignores. I've opened issues to track all the tests that I'm deferring to later PRs.

@arsalansufi arsalansufi marked this pull request as ready for review September 12, 2023 17:44
@arsalansufi arsalansufi requested a review from a team as a code owner September 12, 2023 17:44
@arsalansufi arsalansufi requested review from kofi-q and removed request for a team and kofi-q September 12, 2023 17:44
return err({ type: 'invalid-mode', currentMode });
}

const existingImportId = store.getCastVoteRecordFileByHash(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throughout the code and UI, we talk about a singular CVR file per export, which is no longer the case. I'm going to update terminology in a follow-up PR

* An error encountered while reading an individual cast vote record
*/
export type ReadCastVoteRecordError = { type: 'invalid-cast-vote-record' } & (
| { subType: 'batch-id-not-found' }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the subType approach.

Comment on lines +198 to +205
if (!imageFilePaths.every((filePath) => existsSync(filePath))) {
yield wrapError({ subType: 'image-file-not-found' });
return;
}
if (!layoutFilePaths.every((filePath) => existsSync(filePath))) {
yield wrapError({ subType: 'layout-file-not-found' });
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I correct in understanding that, unlike before, where we passed in a list of filenames (like you pass in a list of batchIds), now you just check whether the files exist in the file system? Makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct!

Comment on lines 57 to 60
const precinct = getPrecinctById({
election,
precinctId: castVoteRecord.BallotStyleUnitId,
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted, you could use the cached versions of these functions from utils to guarantee O(1).

Copy link
Contributor Author

@arsalansufi arsalansufi Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 84480ad

well-arent-you-fancy

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haters always gonna hate on the caching but we've got lots of data to process no time for haters.

Comment on lines +265 to +270
// TODO: Calculate the precinct list before iterating through cast vote records, once there is
// only one geopolitical unit per batch
store.updateCastVoteRecordFileRecord({
id: importId,
precinctIds,
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have a metadata file, I'm hoping we can eventually get the precinct list in the file up front to avoid this.

Copy link
Contributor Author

@arsalansufi arsalansufi Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Opened an issue for myself so that I don't forget to revisit this: #3968

)
) {
const userRole = assertDefined(await getUserRole());
const importResult = await importCastVoteRecords(store, input.path);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How are you handling the manual file selection use case? As in line 446 below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch, I'm not handling that case.

That said, it turns out that manual file selection is already broken because it only allows you to select an individual file, but for quite some time now, we've been exporting a directory and not a file.

Gonna revisit this holistically as its own issue, potentially just removing the manual option altogether, after checking in with others: #3967

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit of a hidden feature that I failed to properly publicize, but manual file selection is not broken. When I switched cast vote records to directories, I added the indicated logic so that you could select the cast-vote-record-report.json at the root of the directory, which would import the directory.

I use this escape hatch all the time when doing manual testing in VxAdmin. It's far easier than having to always get a cast vote record report on a real or mocked USB drive, so I'd vote strongly to keep the option or something like it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah really great to know! In that case, will plan on maintaining

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants