<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: meseret akalu</title>
    <description>The latest articles on DEV Community by meseret akalu (@meseret_akalu_1743b6f6aa5).</description>
    <link>https://dev.to/meseret_akalu_1743b6f6aa5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3898824%2F55647a30-a14a-45f1-ad17-d7d2379e876b.jpg</url>
      <title>DEV Community: meseret akalu</title>
      <link>https://dev.to/meseret_akalu_1743b6f6aa5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meseret_akalu_1743b6f6aa5"/>
    <language>en</language>
    <item>
      <title>API Automation (Zedu Platform)</title>
      <dc:creator>meseret akalu</dc:creator>
      <pubDate>Tue, 28 Apr 2026 07:27:56 +0000</pubDate>
      <link>https://dev.to/meseret_akalu_1743b6f6aa5/api-automation-zedu-platform-38ih</link>
      <guid>https://dev.to/meseret_akalu_1743b6f6aa5/api-automation-zedu-platform-38ih</guid>
      <description>&lt;h1&gt;
  
  
  HNG Stage 3 — API Test Automation (Zedu)
&lt;/h1&gt;

&lt;p&gt;This is my API automation project for HNG Stage 3. I tested the Zedu platform API using Python and Pytest. The suite covers login, registration, logout, and user profile endpoints — 30 tests in total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API being tested:&lt;/strong&gt; &lt;code&gt;https://api.zedu.chat/api/v1&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hng-stage3-qa/
├── tests/
│   ├── test_auth.py       # login and register tests
│   ├── test_users.py      # user profile tests
│   └── test_logout.py     # logout tests
├── utils/
│   └── auth.py            # handles login and token retrieval
├── conftest.py            # shared fixtures used across all tests
├── .env.example           # template for environment variables
├── requirements.txt       # dependencies
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10 or higher&lt;/li&gt;
&lt;li&gt;pip&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to set it up
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Clone the repo&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/meseretak/hng-stage3-qa.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hng-stage3-qa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create a virtual environment&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate      &lt;span class="c"&gt;# Mac/Linux&lt;/span&gt;
venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\a&lt;/span&gt;ctivate         &lt;span class="c"&gt;# Windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Install dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create your .env file&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;.env&lt;/code&gt; and fill in your details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://api.zedu.chat/api/v1&lt;/span&gt;
&lt;span class="py"&gt;TEST_EMAIL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_zedu_email@example.com&lt;/span&gt;
&lt;span class="py"&gt;TEST_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_zedu_password&lt;/span&gt;
&lt;span class="py"&gt;TEST_REGISTER_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;password_for_new_test_users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file is in &lt;code&gt;.gitignore&lt;/code&gt; so it won't be pushed to GitHub.&lt;/p&gt;




&lt;h2&gt;
  
  
  Running the tests
&lt;/h2&gt;

&lt;p&gt;Run everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run one file at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_auth.py &lt;span class="nt"&gt;-v&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_users.py &lt;span class="nt"&gt;-v&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pytest tests/test_logout.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What each file tests
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;test_auth.py&lt;/strong&gt; — 20 tests covering login and register. Includes happy path tests (valid login, successful registration), negative tests (wrong password, missing fields, invalid email format, SQL injection), and edge cases (null values, very long password, XSS in name field, whitespace in email).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;test_users.py&lt;/strong&gt; — 5 tests covering the user profile endpoints. Checks that authenticated requests work, that the response contains the right email, that passwords are never exposed, and that unauthenticated requests are properly rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;test_logout.py&lt;/strong&gt; — 5 tests covering logout. Verifies that logout works with a valid token, that the token stops working after logout, and that bad/missing tokens are rejected.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I handled authentication
&lt;/h2&gt;

&lt;p&gt;All tokens are fetched at runtime by calling the login API — nothing is hardcoded. The &lt;code&gt;utils/auth.py&lt;/code&gt; file has a single &lt;code&gt;get_token()&lt;/code&gt; function that everything uses. Tests that need a token get it through fixtures in &lt;code&gt;conftest.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For logout tests I use a separate &lt;code&gt;fresh_token&lt;/code&gt; fixture so each test gets its own token and doesn't affect the shared session.&lt;/p&gt;

&lt;p&gt;Meseret akalu&lt;/p&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>How I Built a Real-Time Anomaly Detection Engine for Nextcloud</title>
      <dc:creator>meseret akalu</dc:creator>
      <pubDate>Sun, 26 Apr 2026 13:47:55 +0000</pubDate>
      <link>https://dev.to/meseret_akalu_1743b6f6aa5/devops-track-3-4-20l2</link>
      <guid>https://dev.to/meseret_akalu_1743b6f6aa5/devops-track-3-4-20l2</guid>
      <description>&lt;p&gt;I built this for HNG Stage 3. The task was to watch Nginx traffic in real time, learn what normal looks like, and block anything that looks like an attack — automatically, without any manual intervention.&lt;/p&gt;

&lt;p&gt;The core idea is simple: instead of hardcoding a threshold like "block anyone over 100 req/s", the tool watches actual traffic and builds a picture of what normal looks like. Then it reacts when something deviates from that picture.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tails the Nginx JSON access log line by line as requests come in&lt;/li&gt;
&lt;li&gt;Tracks request rates per IP and globally using sliding windows (deques, last 60 seconds)&lt;/li&gt;
&lt;li&gt;Builds a rolling baseline from the last 30 minutes of traffic — recalculates mean and stddev every 60 seconds&lt;/li&gt;
&lt;li&gt;If the current hour has enough data, uses that instead of the full window (handles daily traffic patterns)&lt;/li&gt;
&lt;li&gt;Flags anything that spikes above 3x stddev or 5x the mean — whichever fires first&lt;/li&gt;
&lt;li&gt;If an IP has a high error rate (4xx/5xx), thresholds are cut in half automatically&lt;/li&gt;
&lt;li&gt;Blocks bad IPs with iptables DROP rules within 10 seconds&lt;/li&gt;
&lt;li&gt;Sends Slack alerts on ban and unban&lt;/li&gt;
&lt;li&gt;Releases bans on a backoff schedule: 10 min → 30 min → 2 hours → permanent&lt;/li&gt;
&lt;li&gt;Shows everything on a live web dashboard that refreshes every 3 seconds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How the sliding window works
&lt;/h2&gt;

&lt;p&gt;Each window is a &lt;code&gt;deque&lt;/code&gt; of &lt;code&gt;(timestamp, is_error)&lt;/code&gt; tuples. New requests go on the right. On every read, anything older than 60 seconds gets popped from the left. Rate = &lt;code&gt;len(deque) / 60&lt;/code&gt;. One deque per IP, one global. No libraries — just Python's built-in &lt;code&gt;collections.deque&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the baseline works
&lt;/h2&gt;

&lt;p&gt;Every second I flush the current request count into a rolling 30-minute deque. Every 60 seconds I recalculate mean and stddev from that window. I also keep per-hour slots — if the current hour has at least 10 samples I use that instead of the full window. Floor is 1.0 req/s so it doesn't go crazy on low traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo layout
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detector/
  main.py        wires everything together, runs the main loop
  monitor.py     tails and parses the Nginx JSON log
  window.py      sliding window per IP and global
  baseline.py    rolling baseline with hourly slots
  detector.py    detection logic — z-score and multiplier checks
  blocker.py     iptables DROP rules and ban state
  unbanner.py    background thread that checks for expired bans
  notifier.py    Slack webhook alerts
  dashboard.py   Flask live metrics UI
  audit.py       writes structured audit log entries
  config.py      loads config.yaml
  config.yaml    all thresholds and settings
  requirements.txt
  Dockerfile
nginx/
  nginx.conf
docs/
  architecture.png
  blog-post.md
  screenshots/
docker-compose.yml
.env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Setup from scratch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/meseretak/hng-stage3.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hng-stage3
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_slack_webhook_url&lt;/span&gt;
&lt;span class="py"&gt;SERVER_IP&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_server_ip&lt;/span&gt;
&lt;span class="py"&gt;NEXTCLOUD_ADMIN_USER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;
&lt;span class="py"&gt;NEXTCLOUD_ADMIN_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch iptables to legacy mode (needed for Docker + iptables to work together):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-alternatives &lt;span class="nt"&gt;--set&lt;/span&gt; iptables /usr/sbin/iptables-legacy
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-alternatives &lt;span class="nt"&gt;--set&lt;/span&gt; ip6tables /usr/sbin/ip6tables-legacy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it's running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
curl http://localhost/api/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Nextcloud shows "untrusted domain", add your IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 hng-stage3-nextcloud-1 php /var/www/html/occ config:system:set trusted_domains 0 &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_server_ip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Thresholds (all in config.yaml)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;zscore_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.0&lt;/span&gt;
&lt;span class="na"&gt;rate_multiplier_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5.0&lt;/span&gt;
&lt;span class="na"&gt;error_rate_multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.0&lt;/span&gt;
&lt;span class="na"&gt;unban_schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;10&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;30&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;120&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;baseline_window_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="na"&gt;baseline_floor_rps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/docs%2Fscreenshots%2FTool-running.png" alt="Tool running" width="800" height="400"&gt;&lt;/td&gt;
&lt;td&gt;Daemon running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/docs%2Fscreenshots%2FIptables-banned.png" alt="iptables" width="800" height="400"&gt;&lt;/td&gt;
&lt;td&gt;Blocked IP in iptables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/docs%2Fscreenshots%2FAudit-log.png" alt="Audit log" width="800" height="400"&gt;&lt;/td&gt;
&lt;td&gt;Audit log entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/docs%2Fscreenshots%2FBan-slack.png" alt="Ban slack" width="800" height="400"&gt;&lt;/td&gt;
&lt;td&gt;Slack ban alert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/docs%2Fscreenshots%2FUnban-slack.png" alt="Unban slack" width="800" height="400"&gt;&lt;/td&gt;
&lt;td&gt;Slack unban alert&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Blog post
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/meseret_akalu_1743b6f6aa5/devops-track-3-4-20l2"&gt;https://dev.to/meseret_akalu_1743b6f6aa5/devops-track-3-4-20l2&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/meseretak/hng-stage3" rel="noopener noreferrer"&gt;https://github.com/meseretak/hng-stage3&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
