Implement the schema foundation for syncing products from POD providers like Printify. This includes encrypted credential storage, product/variant schemas, and an Oban worker for background sync. New modules: - Vault: AES-256-GCM encryption for API keys - Products context: CRUD and sync operations for products - Provider behaviour: abstraction for POD provider implementations - ProductSyncWorker: Oban job for async product sync Schemas: ProviderConnection, Product, ProductImage, ProductVariant Also reorganizes Printify client to lib/simpleshop_theme/clients/ and mockup generator to lib/simpleshop_theme/mockups/ for better structure. 134 tests added covering all new functionality. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
107 lines
3.0 KiB
Elixir
107 lines
3.0 KiB
Elixir
defmodule SimpleshopTheme.VaultTest do
|
|
use SimpleshopTheme.DataCase, async: true
|
|
|
|
alias SimpleshopTheme.Vault
|
|
|
|
describe "encrypt/1 and decrypt/1" do
|
|
test "round-trips a string successfully" do
|
|
plaintext = "my_secret_api_key_12345"
|
|
|
|
assert {:ok, ciphertext} = Vault.encrypt(plaintext)
|
|
assert is_binary(ciphertext)
|
|
assert ciphertext != plaintext
|
|
|
|
assert {:ok, decrypted} = Vault.decrypt(ciphertext)
|
|
assert decrypted == plaintext
|
|
end
|
|
|
|
test "produces different ciphertext for same plaintext (random IV)" do
|
|
plaintext = "same_secret"
|
|
|
|
{:ok, ciphertext1} = Vault.encrypt(plaintext)
|
|
{:ok, ciphertext2} = Vault.encrypt(plaintext)
|
|
|
|
assert ciphertext1 != ciphertext2
|
|
|
|
# Both should decrypt to same value
|
|
assert {:ok, ^plaintext} = Vault.decrypt(ciphertext1)
|
|
assert {:ok, ^plaintext} = Vault.decrypt(ciphertext2)
|
|
end
|
|
|
|
test "handles empty string" do
|
|
assert {:ok, ciphertext} = Vault.encrypt("")
|
|
assert {:ok, ""} = Vault.decrypt(ciphertext)
|
|
end
|
|
|
|
test "handles unicode characters" do
|
|
plaintext = "héllo wörld 你好 🎉"
|
|
|
|
{:ok, ciphertext} = Vault.encrypt(plaintext)
|
|
{:ok, decrypted} = Vault.decrypt(ciphertext)
|
|
|
|
assert decrypted == plaintext
|
|
end
|
|
|
|
test "handles long strings" do
|
|
plaintext = String.duplicate("a", 10_000)
|
|
|
|
{:ok, ciphertext} = Vault.encrypt(plaintext)
|
|
{:ok, decrypted} = Vault.decrypt(ciphertext)
|
|
|
|
assert decrypted == plaintext
|
|
end
|
|
|
|
test "encrypt/1 returns {:ok, nil} for nil input" do
|
|
assert {:ok, nil} = Vault.encrypt(nil)
|
|
end
|
|
|
|
test "decrypt/1 returns {:ok, nil} for nil input" do
|
|
assert {:ok, nil} = Vault.decrypt(nil)
|
|
end
|
|
|
|
test "decrypt/1 returns {:ok, empty} for empty string" do
|
|
assert {:ok, ""} = Vault.decrypt("")
|
|
end
|
|
|
|
test "decrypt/1 returns error for invalid ciphertext" do
|
|
assert {:error, :invalid_ciphertext} = Vault.decrypt("not_valid")
|
|
assert {:error, :invalid_ciphertext} = Vault.decrypt(<<1, 2, 3>>)
|
|
end
|
|
|
|
test "decrypt/1 returns error for tampered ciphertext" do
|
|
{:ok, ciphertext} = Vault.encrypt("secret")
|
|
|
|
# Tamper with the ciphertext (flip a bit in the middle)
|
|
tampered = :binary.bin_to_list(ciphertext)
|
|
middle = div(length(tampered), 2)
|
|
tampered = List.update_at(tampered, middle, &Bitwise.bxor(&1, 0xFF))
|
|
tampered = :binary.list_to_bin(tampered)
|
|
|
|
assert {:error, :decryption_failed} = Vault.decrypt(tampered)
|
|
end
|
|
end
|
|
|
|
describe "encrypt!/1 and decrypt!/1" do
|
|
test "round-trips successfully" do
|
|
plaintext = "test_secret"
|
|
|
|
ciphertext = Vault.encrypt!(plaintext)
|
|
decrypted = Vault.decrypt!(ciphertext)
|
|
|
|
assert decrypted == plaintext
|
|
end
|
|
|
|
test "encrypt!/1 raises on invalid input type" do
|
|
assert_raise FunctionClauseError, fn ->
|
|
Vault.encrypt!(123)
|
|
end
|
|
end
|
|
|
|
test "decrypt!/1 raises on invalid ciphertext" do
|
|
assert_raise RuntimeError, ~r/Decryption failed/, fn ->
|
|
Vault.decrypt!("invalid")
|
|
end
|
|
end
|
|
end
|
|
end
|