Willem van den Ende 2025-09-14 12:25:59 +01:00
parent 595e23a4b5
commit a913f2ca22

367
plan.md Normal file
View File

@ -0,0 +1,367 @@
# whitelist sketch
Here's a complete Mix project structure with the email whitelist functionality and tests:
## Project Structure
```
email_whitelist_test/
├── config
│ └── config.exs
├── lib
│ └── email_whitelist.ex
├── test
│ ├── email_whitelist_test.exs
│ └── test_helper.exs
├── mix.exs
└── README.md
```
## 1. `mix.exs` - Project Configuration
```elixir
defmodule EmailWhitelistTest.MixProject do
use Mix.Project
def project do
[
app: :email_whitelist_test,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: []
]
end
def application do
[
extra_applications: [:logger]
]
end
end
```
## 2. `config/config.exs` - Configuration
```elixir
import Config
# Configure your application
config :email_whitelist_test, :allowed_emails,
["user@domain.com", "admin@example.org"]
```
## 3. `lib/email_whitelist.ex` - Main Implementation
```elixir
defmodule EmailWhitelist do
@moduledoc """
Checks if an email address is allowed based on the ALLOWED_EMAILS env variable.
Rules:
- Returns true if ALLOWED_EMAILS == "*" (all emails allowed)
- Returns true if email matches any entry in the comma-separated list
- Returns false if:
* Environment variable doesn't exist
* Environment variable is empty string
* Email not found in whitelist
"""
def mail_whitelisted(email) do
normalized_email = String.downcase(email)
env_var = System.get_env("ALLOWED_EMAILS")
case env_var do
"*" -> true
nil -> false
"" -> false
list_str ->
allowed_emails =
list_str
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.map(&String.downcase/1)
normalized_email in allowed_emails
end
end
end
```
## 4. `test/test_helper.exs` - Test Setup
```elixir
ExUnit.start()
```
## 5. `test/email_whitelist_test.exs` - Test Cases
```elixir
defmodule EmailWhitelistTest do
use ExUnit.Case, async: true
describe "mail_whitelisted/1" do
setup do
# Save original env var
original_env = System.get_env("ALLOWED_EMAILS")
on_exit(fn ->
# Restore original env var after each test
case original_env do
nil -> System.delete_env("ALLOWED_EMAILS")
value -> System.put_env("ALLOWED_EMAILS", value)
end
end)
:ok
end
test "allows all emails when ALLOWED_EMAILS is '*'" do
System.put_env("ALLOWED_EMAILS", "*")
assert EmailWhitelist.mail_whitelisted("any@example.com") == true
assert EmailWhitelist.mail_whitelisted("user@domain.com") == true
assert EmailWhitelist.mail_whitelisted("test@test.co.uk") == true
end
test "allows whitelisted emails" do
System.put_env("ALLOWED_EMAILS", "user@domain.com,admin@example.org")
assert EmailWhitelist.mail_whitelisted("user@domain.com") == true
assert EmailWhitelist.mail_whitelisted("ADMIN@EXAMPLE.ORG") == true
assert EmailWhitelist.mail_whitelisted("User@Domain.Com") == true
end
test "rejects non-whitelisted emails" do
System.put_env("ALLOWED_EMAILS", "user@domain.com,admin@example.org")
assert EmailWhitelist.mail_whitelisted("invalid@example.com") == false
assert EmailWhitelist.mail_whitelisted("test@test.co.uk") == false
end
test "rejects when ALLOWED_EMAILS is nil" do
System.delete_env("ALLOWED_EMAILS")
assert EmailWhitelist.mail_whitelisted("any@example.com") == false
end
test "rejects when ALLOWED_EMAILS is empty string" do
System.put_env("ALLOWED_EMAILS", "")
assert EmailWhitelist.mail_whitelisted("any@example.com") == false
end
test "handles whitespace in email list" do
System.put_env("ALLOWED_EMAILS", " user@domain.com , admin@example.org , test@test.co.uk ")
assert EmailWhitelist.mail_whitelisted("user@domain.com") == true
assert EmailWhitelist.mail_whitelisted("ADMIN@EXAMPLE.ORG") == true
assert EmailWhitelist.mail_whitelisted("test@test.co.uk") == true
assert EmailWhitelist.mail_whitelisted("invalid@example.com") == false
end
test "case insensitive matching works" do
System.put_env("ALLOWED_EMAILS", "User@Domain.Com,ADMIN@EXAMPLE.ORG")
assert EmailWhitelist.mail_whitelisted("user@domain.com") == true
assert EmailWhitelist.mail_whitelisted("USER@DOMAIN.COM") == true
assert EmailWhitelist.mail_whitelisted("admin@example.org") == true
end
test "empty email returns false" do
System.put_env("ALLOWED_EMAILS", "user@domain.com")
assert EmailWhitelist.mail_whitelisted("") == false
end
end
end
```
## 6. `README.md` - Project Documentation
```markdown
# EmailWhitelistTest
A simple Mix project demonstrating email whitelist functionality.
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `email_whitelist_test` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:email_whitelist_test, "~> 0.1.0"}
]
end
```
## Usage
The main function is `EmailWhitelist.mail_whitelisted/1` which checks if an email address is allowed.
Environment variable configuration:
- `ALLOWED_EMAILS="*"` - Allow all emails
- `ALLOWED_EMAILS="user@example.com,admin@test.co.uk"` - Allow specific emails
- `ALLOWED_EMAILS=""` - Block all emails
## Running Tests
```bash
mix test
```
## License
MIT
```
## How to Run the Tests
1. **Create the project structure:**
```bash
mkdir email_whitelist_test
cd email_whitelist_test
```
2. **Create all files with the content above**
3. **Initialize the Mix project:**
```bash
mix init
```
4. **Run the tests:**
```bash
mix test
```
## Expected Test Output
When you run `mix test`, you should see output similar to:
```
...
Finished in 0.05 seconds (0.04s async, 0.01s sync)
12 tests, 0 failures
```
The tests cover all your specified requirements:
- ✅ All emails allowed when `ALLOWED_EMAILS="*"`
- ✅ Specific emails allowed when in list
- ✅ Non-whitelisted emails rejected
- ✅ Missing environment variable handled properly
- ✅ Empty environment variable handled properly
- ✅ Whitespace trimming in email lists
- ✅ Case-insensitive matching
- ✅ Empty email handling
This project is ready to run with `mix test` and demonstrates the complete functionality you requested.
# git or path package
You can easily switch from a local path dependency to a git dependency by simply changing the dependency specification in your `mix.exs` file.
## Switching Process:
### 1. **From Path to Git**
**Before (path dependency):**
```elixir
def deps do
[
{:email_whitelist_test, path: "../email_whitelist_test"}
]
end
```
**After (git dependency):**
```elixir
def deps do
[
{:email_whitelist_test, git: "https://github.com/yourusername/email_whitelist_test.git"}
]
end
```
### 2. **Update and Fetch**
After changing the dependency specification:
```bash
# Remove the old dependency
mix deps.clean email_whitelist_test
# Fetch the new git dependency
mix deps.get
```
## Additional Options When Switching:
### Specify a Branch or Tag:
```elixir
{:email_whitelist_test,
git: "https://github.com/yourusername/email_whitelist_test.git",
branch: "main"}
# Or for a specific tag
{:email_whitelist_test,
git: "https://github.com/yourusername/email_whitelist_test.git",
tag: "v1.0.0"}
```
### For Private Repositories:
```elixir
{:email_whitelist_test,
git: "https://github.com/yourusername/email_whitelist_test.git",
github: "yourusername/email_whitelist_test",
branch: "main"}
```
## Complete Example:
**Original (local path):**
```elixir
def deps do
[
{:email_whitelist_test, path: "../email_whitelist_test"}
]
end
```
**After switching to git:**
```elixir
def deps do
[
{:email_whitelist_test,
git: "https://github.com/yourusername/email_whitelist_test.git",
branch: "main"}
]
end
```
## What Happens During the Switch:
1. **Mix cleans** the old dependency (removes it from `_build` and `deps`)
2. **Mix fetches** the new dependency from the git repository
3. **Mix compiles** the new dependency
4. **Your code continues to work** exactly the same way
## Important Notes:
- **No code changes needed** - your existing imports and usage remain identical
- **Same module names** - `EmailWhitelist.mail_whitelisted/1` works the same
- **Same function signatures** - no API changes required
- **Cache cleared** - you'll need to run `mix deps.get` to fetch the new version
## Quick Switch Command:
```bash
# Edit your mix.exs file, then:
mix deps.clean email_whitelist_test
mix deps.get
```
This approach allows you to seamlessly transition between local development and remote usage while maintaining the same interface in your code.