QA Engineer · test automation and requirements analysis
I test B2B SaaS at BeamSec, focusing on tenant isolation and role-based access for accounts synced from Active Directory and LDAP. I automate those checks with Playwright and TypeScript.
Automated tenant-isolation and role-permission checks in Playwright (UI and API) and added them to release validation.
Root-caused failures from logs before filing: a report-generation crash traced to a null user field, a CI break traced to a JDK and library version mismatch.
Wrote development cards, functional requirements and acceptance criteria from product documents, so developers and QA worked from the same expected behavior.
2024 · Military service, Turkish Armed Forces
AUG 2023 – FEB 2024 Remote
Hypnotes
Test Automation Engineer
Built a Java / Rest Assured API framework and connected the Selenium regression suites to Jenkins, which took the regression cycle from days to hours. Built the Jira Xray dashboards used in release decisions.
APR – AUG 2023 Remote · USA · Contract
Hyrai
Software Test Engineer
Implemented a Java, Selenium and Cucumber framework around business rules. Contributed to requirements analysis early in the development lifecycle.
Earlier project · Library Test Automation, 2022–2023
Java BDD framework with Cucumber, Gherkin, Page Object Model and Maven. Covered UI and API flows with Rest Assured and Postman, plus SQL data validation managed through Jira / Xray.
Selected work
Work samples
A public framework built on the patterns I use at work, and work examples with product and client details removed.
Automation framework Public project
Playwright BDD Automation Framework
A public suite against two demo targets: SauceDemo for the UI and RESTful Booker for the API. The targets are simple on purpose; what matters is how the suite is built.
Design decisions
Authorization is checked where it is enforced: after a 403, the test reads the booking again to confirm nothing changed.
In this suite, the API client retries only token acquisition.
Page objects stop at readiness. Business assertions live in the step definitions, next to the scenario.
The sort test polls the displayed order because client-side sorting exposes no completion event.
@apiFeature:Booking API
CRUD lifecycle of a booking via RESTful Booker, including token-based auth
and what the API does when the token is missing.
# …# The token is the only thing standing between the public internet and someone# else's reservation, so its absence is asserted rather than assumed.Scenario:A write without a token is refusedGiven a booking exists with a total price of 150
When I try to update the booking without a token
Then the request should be refused as forbidden
And the booking total price should still be 150
import { expect } from'@playwright/test';
import { createBdd } from'playwright-bdd';
import { test, type World } from'../../fixtures/fixtures';
// …When('I try to update the booking without a token', async ({ booker, world }) => {
const id = fromWorld(world.bookingId, 'bookingId', 'Given a booking exists');
world.lastResponse = await booker.updateBookingUnauthenticated(id, { totalprice: 1 });
});
Then('the request should be refused as forbidden', async ({ world }) => {
const response = fromWorld(
world.lastResponse,
'lastResponse',
'When I try to update the booking without a token',
);
expect(response.status(), awaitresponseDetail(response)).toBe(403);
});
Then('the booking total price should still be {int}', async ({ booker, world }, price: number) => {
const id = fromWorld(world.bookingId, 'bookingId', 'Given a booking exists');
awaitexpectTotalPrice(booker, id, price);
});
// …async function expectTotalPrice(booker: BookerClient, id: number, price: number): Promise<void> {
const response = await booker.getBooking(id);
expect(response.status()).toBe(200);
const body = (await response.json()) as Booking;
expect(body.totalprice).toBe(price);
}
/** Puts the response body in the failure message, so a red run needs no re-run to diagnose. */async function responseDetail(response: {
status(): number;
text(): Promise<string>;
}): Promise<string> {
return`unexpected status ${response.status()}, body: ${(await response.text()).slice(0, 300)}`;
}
/** Typed wrapper over Playwright's `request`. Nothing here asserts, which is what makes the retry safe. */exportclass BookerClient {
constructor(privatereadonly request: APIRequestContext) {}
/**
* The only retried request in the suite. It is a precondition rather than a subject
* under test, and it is where a sandbox cold start turns a healthy run red.
*/asyncrequestToken(): Promise<string> {
const attempts = env.api.tokenRetries;
const failures: string[] = [];
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
const response = await this.request.post('/auth', {
data: env.api.credentials,
timeout: env.api.timeout,
});
if (response.ok()) {
const body = (await response.json()) as { token?: string; reason?: string };
if (body.token) return body.token;
failures.push(`attempt ${attempt}: 200 but no token (reason: ${body.reason ?? 'none'})`);
} else {
failures.push(`attempt ${attempt}: HTTP ${response.status()}`);
}
} catch (error) {
failures.push(`attempt ${attempt}: ${(error as Error).message}`);
}
if (attempt < attempts) {
// Linear backoff: a cold-starting dyno needs seconds, not milliseconds.awaitnewPromise((resolve) => setTimeout(resolve, attempt * 1_000));
}
}
thrownewError(
`Could not obtain an auth token from ${env.api.baseURL} after ${attempts} attempts:\n` +
failures.map((line) => ` ${line}`).join('\n'),
);
}
// …/**
* Its own method rather than an optional token argument: with an optional argument an
* undefined token would silently turn an authorized test into this one.
*/updateBookingUnauthenticated(id: number, patch: Partial<Booking>): Promise<APIResponse> {
return this.request.patch(`/booking/${id}`, { data: patch, timeout: env.api.timeout });
}
// …
}
Applied QA & analysis
BeamSec
Performance testing
Load testing before launch
The product expected 10,000–20,000 people to open the same public link at once.
JMeterLoad testing
What the test showed
In this test, some requests went unanswered above roughly 25,000 virtual users.
My work
I built a staged JMeter test up to 30,000 virtual users.
Outcome
I reported the findings to the team before the client launch.
AI workflow evaluation
Investigating agent failures
Context
An LLM agent that runs multi-step tasks was going in front of a client. Its answers read well, which made the wrong ones hard to spot.
My work
Ran a hallucination risk analysis on the conversation logs: checked each answer against the source data it should have been grounded in, and repeated the same prompts to see how consistent the agent stayed from run to run. Built a failure taxonomy from what came up, covering intent misclassification, context lost across turns, unsupported claims and risky actions taken on a wrong decision.
Result
Reported the failure patterns to developers ahead of the demo, grouped by cause and by how much risk each one carried.
Product documents described features in business terms and left edge cases open.
My work
Collected the open questions from the documents, clarified them with the product side, and wrote development cards with functional requirements and acceptance criteria.
Result
Developers and QA started from the same expected behavior, and open questions were settled before implementation instead of during testing.
There are six bugs in this signup form. Try the edge cases and see which ones you can reproduce. Your findings appear in the defect list as you go, and the test design behind each one is inside.