Possible to skip validation for an action? (or best practices for working with external credentials)

My app has a resource called Vendor for interacting with external vendors, so we store user credentials for those vendors encrypted in the resource. I've now added a validation when creating vendors, that validates that the credentials are actually valid (by talking to the external API). This works great! However, I have a test suite that generates lots of Vendor resources in the course of testing things, and 99% of them don't use valid credentials (nor should they!) so now all the tests are failing due to this new validation. I can see a couple of options: * In my test functions, skip validation when calling eg. Vendor.create(data). This would be the easiest way, but I can't see any way in Ash to do it? (I thought there might be a nice validate?: false opt like there's a authorize?: false opt but there isn't) * Use Mimic (https://github.com/edgurgel/mimic) (already used in the app) to globally stub out the new validation module, to bypass it. I can foresee running into issues when I want to actually test the validation though - I'd have to muck around with unstubbing/restubbing and I'm not sure if Mimic even supports it. * Create a new action in my resource that is the same as the create action but doesn't include the extra validation. This would be my least favorite option. * Actually my least favorite option (but still an option) would be to use valid credentials to test accounts in all of the tests, but I'm not even considering doing this. Have I missed something in the Ash docs on how to skip validations, or how would other people approach doing this?
14 Replies
Rebecca Le
Rebecca LeOP2y ago
I have just seen that its possible to skip_global_validations? from inside an action (I'm guessing that would be validations from a validations block on the resource?) so there might be a way to skip validations from outside, hidden somewhere
kernel
kernel2y ago
validation module config which always returns valid = true when run in test done via dev.exs, test.exs 🤷🏿‍♂️ even easier is swapping out the valid? function at compile time
ZachDaniel
ZachDaniel2y ago
Yeah, there are a couple options along those lines. You could also look for something like context.skip_credential_validation, and then set the context when creating things in test. Because validations and changes aren’t named, we can’t provide an elegant way to skip individual ones in test, and an option to skip all of them with an option when creating is only useful in very specific cases. We could add a name option to changes and validations, and then add skip: [:foo, :bar]?
Rebecca Le
Rebecca LeOP2y ago
Yeah I thought later maybe I could add an argument to the action to decide whether or not to run the external validation, defaulting to true, set to false in tests
ZachDaniel
ZachDaniel2y ago
You can also make it a private? argument if you don't want it to be settable as user input private arguments have to be set explicitly with Ash.Changeset.set_argument/2
Rebecca Le
Rebecca LeOP2y ago
that would be doable, given I only do it a few places in factory methods I think I'll take a look at doing this shortly and mark resolved if I can get it all working 🙂 I think I'm not understanding how to run the validation conditionally. I've tried:
create :create do
argument :run_external_validations, :boolean, default: true, private?: true

validate {Vendor.CredentialValidation, []},
only_when_valid?: true,
before_action?: true,
where: &Ash.Changeset.get_argument(&1, :run_external_validations)
create :create do
argument :run_external_validations, :boolean, default: true, private?: true

validate {Vendor.CredentialValidation, []},
only_when_valid?: true,
before_action?: true,
where: &Ash.Changeset.get_argument(&1, :run_external_validations)
but that seems to always skip it
ZachDaniel
ZachDaniel2y ago
interesting
Rebecca Le
Rebecca LeOP2y ago
the docs say
Validations that should pass in order for this validation to apply. These validations failing will not invalidate the changes, but will instead result in this validation being ignored. Accepts a module, module and opts, or a 1 argument function that takes the changeset. Defaults to [] .
so its not "return a boolean to run", but I don't know what it is
ZachDaniel
ZachDaniel2y ago
yeah, I feel like you should be getting an error The basic thing is that where is supposd to also be a validation and so should return :ok | {:error, error} which we use essentially as a boolean flag but allows composing validations.
Rebecca Le
Rebecca LeOP2y ago
okay so I'm definitely using it very wrong
ZachDaniel
ZachDaniel2y ago
there isn't an argument_equals builtin validation right now (we should add one though)
Rebecca Le
Rebecca LeOP2y ago
I can add one in my project and then contribute it back 👍
ZachDaniel
ZachDaniel2y ago
you can probably get a jump start by copying the built in attribute_equals.ex
Rebecca Le
Rebecca LeOP2y ago
I got this to work 😄 My resource code:
create :create do
primary? true
argument :run_external_validations, :boolean, default: true, private?: true

validate {Vendor.CredentialValidation, []},
only_when_valid?: true,
before_action?: true,
where: argument_equals(:run_external_validations, true)
create :create do
primary? true
argument :run_external_validations, :boolean, default: true, private?: true

validate {Vendor.CredentialValidation, []},
only_when_valid?: true,
before_action?: true,
where: argument_equals(:run_external_validations, true)
My test factory code:
defp insert_vendor_params!(params) do
Vendor
|> Changeset.new(params)
|> Changeset.set_argument(:run_external_validations, false)
|> Changeset.for_create(:create, authorize?: false)
|> MyApp.Api.create!()
end
defp insert_vendor_params!(params) do
Vendor
|> Changeset.new(params)
|> Changeset.set_argument(:run_external_validations, false)
|> Changeset.for_create(:create, authorize?: false)
|> MyApp.Api.create!()
end

Did you find this page helpful?