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

Feat(Site Launch): Mock calls to Amplify #704

Merged
merged 12 commits into from
Apr 19, 2023

Conversation

kishore03109
Copy link
Contributor

@kishore03109 kishore03109 commented Apr 13, 2023

Problem

During development, the CreateDomainAssociation calls are rate-limited to 10 per hour. This low rate limit can cause issues during actual site launches that may affect operations.

Solution

To avoid impeding with the actual infrastructure rate limits, this PR mocks all calls to CreateDomainAssociation and GetDomainAssociation in LaunchesService. Only staging, vapt, and prod environments should be making actual calls to Amplify. Additionally, since we are mocking Amplify calls now, we no longer need to wait for 90 seconds after creating domain associations for Amplify to generate the required values.

The mocked objects may seem verbose, but they closely resemble the actual values received when calling Amplify.

Breaking Changes

There are no breaking changes.

Reviewer Notes

I am concerned that future devs working around site launch feature might be confused why they are not able to create domain associations during development. Not too sure if there is a way to highlight clearly in code that the calls are mocks apart from the proposed solution of already naming them mockCreateDomainAssociationOutput and mockCreateDomainAssociationOutput in LaunchesClient. Open to feedback on this!

Tests

To mock form responses, you can call the following code directly from server.js:

const formResponses = [
  {
    submissionId: "",
    requesterEmail: "[email protected]",
    repoName: "kishore-test",
    primaryDomain: "kishorewithwww.isomer.gov.sg",
    redirectionDomain: "www.kishorewithwww.isomer.gov.sg",
    agencyEmail: "[email protected]",
  },
  {
    submissionId: "",
    requesterEmail: "[email protected]",
    repoName: "test-vincent",
    primaryDomain: "kishorewithoutwww.isomer.gov.sg",
    redirectionDomain: "",
    agencyEmail: "[email protected]",
  },
]

formsgSiteLaunchRouter.handleSiteLaunchResults(formResponses, "test")

After mocking a site launch form submission, the launches and redirections in the database are populated, and emails are sent as expected.
Screenshot 2023-04-13 at 2 12 43 PM
Screenshot 2023-04-13 at 2 12 57 PM

Deploy Notes

New environment variables:

  • MOCK_AMPLIFY_CREATE_DOMAIN_CALLS : mocking amplify calls so as to not impede with operations

Values in 1pw prod and staging env vars are also updated. TLDR; MOCK_AMPLIFY_CREATE_DOMAIN_CALLS is true in prod and staging env, false in local dev.

@kishore03109 kishore03109 marked this pull request as ready for review April 13, 2023 06:25
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Show resolved Hide resolved
src/services/identity/LaunchClient.ts Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Show resolved Hide resolved
src/services/identity/LaunchClient.ts Show resolved Hide resolved
* this directly within the Isomer CMS.
* todo: push this check into a queue-like system when integrating this with cms
*/
await new Promise((resolve) => setTimeout(resolve, 90000))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is going to block the main thread for 90s right?

Seems like "todo: push this check into a queue-like system when integrating this with cms" is crucial for integration with CMS

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hmm correct me if I am wrong, but set setTimeout does not block the main thread right?

Copy link
Contributor

Choose a reason for hiding this comment

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

@kishore03109 hmm, you can do a simple test on console -

async function test() { if(true) { await new Promise((resolve) => setTimeout(resolve, 2000)) } console.log("DONE") }

"DONE" wont be printed until the promise is done. you might want to double check the actual behaviour on this

Copy link
Contributor Author

@kishore03109 kishore03109 Apr 14, 2023

Choose a reason for hiding this comment

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

Hmm wait isnt that the point? I want to only resolve this after 90 seconds to give Amplify enough time to generate the DNS results

eg for the code

async function test() {
  await new Promise((resolve) => setTimeout(resolve, 10000))
  console.log("PROMISE DONE")
}
console.log("STARTING")
test()
console.log("ENDED")

in the console log, we would see:
STARTING
ENDED
PROMISE DONE

thus the other functions in the thread will not be blocked?

Copy link
Contributor

Choose a reason for hiding this comment

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

think the confusion stems from the await - if you await in an async function:

const x = await work()
doOtherWork()
const y = x

the main thread is free to continue on until the result of the await-ed promise is used (ie, const y = x). however, because we await here, i think we'll pause execution for 90s

Copy link
Contributor

Choose a reason for hiding this comment

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

@kishore03109 if you added await on test:

console.log("STARTING")
await test()
console.log("ENDED")

it will wait for promise to be done first

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@harishv7, for ease of mind, I have made the main caller (this.handleSiteLaunchResults) function non-blocking (ie, removing the await), since we technically can run this async after returning the 200 response. However, this means that any error thrown by this will not be caught by handleSiteLaunchFormRequest. Therefore, I am wrapping a try catch around and logging the error to any unintended effects of some unexpected error. Ideally, during an error, this would return something to the user, but I think this can be revisited when we are integrating the features with the frontend!

Copy link
Contributor

Choose a reason for hiding this comment

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

i think this is non-blocking - top-most caller is handleSiteLaunchFormRequest in formsgSiteLaunch and it calls this.handleSiteLaunchResults(formResponses, submissionId) without await (which calls this method later on)

@harishv7
Copy link
Contributor

I am concerned that future devs working around site launch feature might be confused why they are not able to create domain associations during development. Not too sure if there is a way to highlight clearly in code that the calls are mocks apart from the proposed solution of already naming them mockCreateDomainAssociationOutput and mockCreateDomainAssociationOutput in LaunchesClient. Open to feedback on this!

@kishore03109 maybe do you want to have an actual dev config like "MOCK_AMPLIFY_CALLS = true or false" which gives devs more "conscious" control over this? If you feel this could extend beyond in future as the launch service develops, you might want to have a great detail of specificity apart from isTestEnv().

@kishore03109
Copy link
Contributor Author

@harishv7

@kishore03109 maybe do you want to have an actual dev config like "MOCK_AMPLIFY_CALLS = true or false" which gives devs more "conscious" control over this? If you feel this could extend beyond in future as the launch service develops, you might want to have a great detail of specificity apart from isTestEnv().

Hey thanks for the suggestion, think this is a cleaner solution, updated to use an env var. Regarding greater detail of specificity, I think we can revisit this as the service develops, I think this is ok for now!

@kishore03109 kishore03109 requested a review from harishv7 April 14, 2023 10:10
@harishv7
Copy link
Contributor

@harishv7

@kishore03109 maybe do you want to have an actual dev config like "MOCK_AMPLIFY_CALLS = true or false" which gives devs more "conscious" control over this? If you feel this could extend beyond in future as the launch service develops, you might want to have a great detail of specificity apart from isTestEnv().

Hey thanks for the suggestion, think this is a cleaner solution, updated to use an env var. Regarding greater detail of specificity, I think we can revisit this as the service develops, I think this is ok for now!

Sounds good, actually MOCK_AMPLIFY_CREATE_DOMAIN_CALLS is very specific already, as it specifies exactly that only "Create Domain" calls are mocked.

If there are a lot of mocking calls required, then a more generic env like MOCK_ALL_AMPLIFY_CALLS may be required. You can revise this in future if required

Copy link
Contributor

@seaerchin seaerchin left a comment

Choose a reason for hiding this comment

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

generally when we mock, we want to preserve the same function contract to ensure that calling code behaves as intended. moreover, because of how we use DI, we are able to substitute out the inner client (amplifyClient) for a mocked one, which ensures that our calling code is never aware of the fact that it's calling a fake client.

in here, the API contract is changed, which might lead to callers not upholding the contract and test logic is intermingled w/ actual biz logic, which seems a little messy.

wdyt about creating a MOCK_AMPLIFY_CLIENT that returns const data and passing in the appropriate client based on teh result of isTest?

src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
* this directly within the Isomer CMS.
* todo: push this check into a queue-like system when integrating this with cms
*/
await new Promise((resolve) => setTimeout(resolve, 90000))
Copy link
Contributor

Choose a reason for hiding this comment

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

think the confusion stems from the await - if you await in an async function:

const x = await work()
doOtherWork()
const y = x

the main thread is free to continue on until the result of the await-ed promise is used (ie, const y = x). however, because we await here, i think we'll pause execution for 90s

src/services/infra/InfraService.ts Outdated Show resolved Hide resolved
@kishore03109
Copy link
Contributor Author

kishore03109 commented Apr 17, 2023

@seaerchin

generally when we mock, we want to preserve the same function contract to ensure that calling code behaves as intended. moreover, because of how we use DI, we are able to substitute out the inner client (amplifyClient) for a mocked one, which ensures that our calling code is never aware of the fact that it's calling a fake client.
in here, the API contract is changed, which might lead to callers not upholding the contract and test logic is intermingled w/ actual biz logic, which seems a little messy.
wdyt about creating a MOCK_AMPLIFY_CLIENT that returns const data and passing in the appropriate client based on teh result of isTest?

Yup, you are right to say that this is not ideal where test logic is intermingled with actual biz logic, and seems messy.

We primarily have two main calls to Amplify here:

  1. CreateDomainAssociation
  2. GetDomainAssociation

Originally, when actually calling Amplify, the getDomainAssociation only takes in the arguments domainName and appId, since the state of the application (specifically the SubDomainSettings[]) are stored in Amplify itself. To fully mock this behaviour, one must store this state, either in the database (which is def an over-engineered solution) or as a static variable in MockAmplifyClient. (Please let me know if there is another way that I missed :/ )

Say we store this state in some static variable AmplifyDomains in a new class MockAmplifyClient . To fully mock the GetDomainAssociation during development, one must continue to manipulate AmplifyDomains to fit the expected mocking values. I am foreseeing that this would make writing tests in the future harder, because it might not be immediately obvious to the developer that some variable in MockAmplifyClient needs to be modified in order for an accurate representation of what the Amplify Client would have returned if this was not mocked. This leads to an implicit dependency here, since the precondition for receiving an accurate mock from GetDomainAssociation is to set the static variable AmplifyDomains properly.

To be clear, changing the API contract here is a concession to give more power to the consumer to give a less ambiguous, clearer response of what the Amplify client will return if the values are not mocked. Does this reasoning make sense?

@kishore03109 kishore03109 requested a review from seaerchin April 17, 2023 06:11
@seaerchin
Copy link
Contributor

@seaerchin

generally when we mock, we want to preserve the same function contract to ensure that calling code behaves as intended. moreover, because of how we use DI, we are able to substitute out the inner client (amplifyClient) for a mocked one, which ensures that our calling code is never aware of the fact that it's calling a fake client.
in here, the API contract is changed, which might lead to callers not upholding the contract and test logic is intermingled w/ actual biz logic, which seems a little messy.
wdyt about creating a MOCK_AMPLIFY_CLIENT that returns const data and passing in the appropriate client based on teh result of isTest?

Yup, you are right to say that this is not ideal where test logic is intermingled with actual biz logic, and seems messy.

We primarily have two main calls to Amplify here:

  1. CreateDomainAssociation
  2. GetDomainAssociation

Originally, when actually calling Amplify, the getDomainAssociation only takes in the arguments domainName and appId, since the state of the application (specifically the SubDomainSettings[]) are stored in Amplify itself. To fully mock this behaviour, one must store this state, either in the database (which is def an over-engineered solution) or as a static variable in MockAmplifyClient. (Please let me know if there is another way that I missed :/ )

Say we store this state in some static variable AmplifyDomains in a new class MockAmplifyClient . To fully mock the GetDomainAssociation during development, one must continue to manipulate AmplifyDomains to fit the expected mocking values. I am foreseeing that this would make writing tests in the future harder, because it might not be immediately obvious to the developer that some variable in MockAmplifyClient needs to be modified in order for an accurate representation of what the Amplify Client would have returned if this was not mocked. This leads to an implicit dependency here, since the precondition for receiving an accurate mock from GetDomainAssociation is to set the static variable AmplifyDomains properly.

To be clear, changing the API contract here is a concession to give more power to the consumer to give a less ambiguous, clearer response of what the Amplify client will return if the values are not mocked. Does this reasoning make sense?

hmm i don't understand what you're trying to say here - this only applies if we ever hit the amplify endpoint, which we're not doing (by design). so why can't we just give a constant result of the expected shape back to the caller? we don't have to store anything/compute anything since the code in localdev won't ever hit amplify anyway, no?

@kishore03109
Copy link
Contributor Author

kishore03109 commented Apr 17, 2023

@seaerchin

hmm i don't understand what you're trying to say here - this only applies if we ever hit the amplify endpoint, which we're not doing (by design). so why can't we just give a constant result of the expected shape back to the caller? we don't have to store anything/compute anything since the code in localdev won't ever hit amplify anyway, no?

the issue stems from the fact that the result of GetDomainAssociationCall semantically differs depending on arguments given to CreateDomainAssociation to trigger the domain association process.

super high level code to demonstrate this:

when actually calling Amplify:

createDomainAssociation({appId: blah, domainName: 'test.gov.sg', subDomainSettings: 'www'})
// some code to wait 90 secs
const res = getDomainAssociation({appId: blah, domainName:'test.gov.sg'})
if (res.subDomains) {
  // lets call this node in the CFG as node A 
  handleSubdomains(res) 
}  else {
  // lets call this node in the CFG as node B 
  handleDomainsWithoutSubdomains(res)
}

When mocking getDomainAssociation in local dev, if we returned an object of
const res = {
subDomains: "www"
...otherProps
}, node A will never be run.

When mocking getDomainAssociation in local dev, if we returned an object of
const res = {
...otherProps
}, node B will never be run.

To give more flexibility to the caller, the suggested solution proposes adding the prop subDomainSettings to fine-tune the mocked result returned!
Concretely I can call
getDomainAssociation({appId: blah, domainName: 'test.gov.sg', subDomainSettings: 'www'}) or
getDomainAssociation({appId: blah, domainName: 'test.gov.sg'}) depending on whether or not I wish to receive a mocked Response from amplify with subdomains or no subdomains.

@kishore03109
Copy link
Contributor Author

@seaerchin as discussed offline, have modified to retain the original API contract. The state is now stored in LaunchesClient, who is the only one who is aware of any mocked values.

@kishore03109 kishore03109 force-pushed the IS-89-Backend-Mock-CreateDomainAssociation-calls branch from a1fa956 to cd97d22 Compare April 19, 2023 04:03
Copy link
Contributor

@seaerchin seaerchin left a comment

Choose a reason for hiding this comment

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

wrong method signature for InfraService.getDomainAssociationRecord call; minor comments otherwise

src/routes/formsgSiteLaunch.ts Show resolved Hide resolved
src/config/config.ts Show resolved Hide resolved
src/services/identity/LaunchClient.ts Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/identity/LaunchClient.ts Outdated Show resolved Hide resolved
src/services/infra/InfraService.ts Outdated Show resolved Hide resolved
* this directly within the Isomer CMS.
* todo: push this check into a queue-like system when integrating this with cms
*/
await new Promise((resolve) => setTimeout(resolve, 90000))
Copy link
Contributor

Choose a reason for hiding this comment

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

i think this is non-blocking - top-most caller is handleSiteLaunchFormRequest in formsgSiteLaunch and it calls this.handleSiteLaunchResults(formResponses, submissionId) without await (which calls this method later on)

@kishore03109 kishore03109 requested a review from seaerchin April 19, 2023 06:31
Copy link
Contributor

@seaerchin seaerchin left a comment

Choose a reason for hiding this comment

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

lgtm

@kishore03109 kishore03109 merged commit 408da99 into develop Apr 19, 2023
@mergify mergify bot deleted the IS-89-Backend-Mock-CreateDomainAssociation-calls branch April 19, 2023 08:21
@alexanderleegs alexanderleegs mentioned this pull request Apr 20, 2023
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.

3 participants