- POST /api/listings - Create new listing
- PUT /api/listings/{id} - Update own listing
- DELETE /api/listings/{id} - Soft delete own listing
- Docker Desktop installed and running
docker compose up -d$loginBody = @{
email = "alice@acme.test"
password = "password123"
tenant_slug = "acme"
} | ConvertTo-Json
$loginResponse = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -Body $loginBody -ContentType "application/json"
$token = $loginResponse.access_token
Write-Host "Token: $token"Expected: 201 Created with listing data
$createBody = @{
title = "Test Listing from Phase 3"
description = "A test listing created during Phase 3 testing"
type = "offer"
status = "active"
location = "Downtown"
estimated_hours = 2.5
} | ConvertTo-Json
$headers = @{ Authorization = "Bearer $token" }
$response = Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method POST -Body $createBody -ContentType "application/json" -Headers $headers
$response | ConvertTo-Json -Depth 5
$newListingId = $response.id
Write-Host "Created listing ID: $newListingId"Expected: 201 Created with status = "draft"
$draftBody = @{
title = "Draft Listing"
description = "This is a draft listing"
type = "request"
status = "draft"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method POST -Body $draftBody -ContentType "application/json" -Headers $headers
$response | ConvertTo-Json -Depth 5
$draftListingId = $response.idExpected: 400 Bad Request
$noTitleBody = @{
description = "No title"
type = "offer"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method POST -Body $noTitleBody -ContentType "application/json" -Headers $headers
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Expected: 400 Bad Request
$longTitle = "A" * 300
$longTitleBody = @{
title = $longTitle
type = "offer"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method POST -Body $longTitleBody -ContentType "application/json" -Headers $headers
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Expected: 400 Bad Request
$invalidTypeBody = @{
title = "Invalid Type Listing"
type = "invalid"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method POST -Body $invalidTypeBody -ContentType "application/json" -Headers $headers
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Expected: 200 OK with updated data
$updateBody = @{
title = "Updated Listing Title"
description = "Updated description"
status = "fulfilled"
} | ConvertTo-Json
$response = Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$newListingId" -Method PUT -Body $updateBody -ContentType "application/json" -Headers $headers
$response | ConvertTo-Json -Depth 5Expected: 404 Not Found
$updateBody = @{
title = "Should Fail"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/99999" -Method PUT -Body $updateBody -ContentType "application/json" -Headers $headers
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Expected: 403 Forbidden (if listing exists but owned by another user)
First, let's get Bob's token and try to update Alice's listing:
# Login as Bob (same tenant, different user)
$bobLoginBody = @{
email = "bob@acme.test"
password = "password123"
tenant_slug = "acme"
} | ConvertTo-Json
$bobLoginResponse = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -Body $bobLoginBody -ContentType "application/json"
$bobToken = $bobLoginResponse.access_token
$bobHeaders = @{ Authorization = "Bearer $bobToken" }
# Try to update Alice's listing
$updateBody = @{
title = "Bob trying to update Alice's listing"
} | ConvertTo-Json
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$newListingId" -Method PUT -Body $updateBody -ContentType "application/json" -Headers $bobHeaders
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Expected: 204 No Content
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$draftListingId" -Method DELETE -Headers $headers
Write-Host "Deleted listing $draftListingId"Expected: Deleted listing not in results
$listings = Invoke-RestMethod -Uri "http://localhost:5080/api/listings" -Method GET -Headers $headers
$deletedListing = $listings.data | Where-Object { $_.id -eq $draftListingId }
if ($deletedListing) {
Write-Host "FAIL: Deleted listing still visible"
} else {
Write-Host "PASS: Deleted listing not visible"
}Expected: 403 Forbidden
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$newListingId" -Method DELETE -Headers $bobHeaders
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
$reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
$reader.ReadToEnd()
}Login as a user from tenant 2 and try to access tenant 1's listing:
# Login as user from tenant 2
$tenant2LoginBody = @{
email = "carol@globex.test"
password = "password123"
tenant_slug = "globex"
} | ConvertTo-Json
$tenant2Response = Invoke-RestMethod -Uri "http://localhost:5080/api/auth/login" -Method POST -Body $tenant2LoginBody -ContentType "application/json"
$tenant2Token = $tenant2Response.access_token
$tenant2Headers = @{ Authorization = "Bearer $tenant2Token" }
# Try to access tenant 1's listing
try {
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$newListingId" -Method GET -Headers $tenant2Headers
Write-Host "FAIL: Should not be able to see cross-tenant listing"
} catch {
Write-Host "Status: $($_.Exception.Response.StatusCode.value__)"
Write-Host "PASS: Cross-tenant access correctly blocked"
}Delete the test listing we created:
Invoke-RestMethod -Uri "http://localhost:5080/api/listings/$newListingId" -Method DELETE -Headers $headers
Write-Host "Cleaned up test listing $newListingId"| Test | Description | Expected Status |
|---|---|---|
| 1 | Create listing | 201 Created |
| 2 | Create draft listing | 201 Created |
| 3 | Missing title | 400 Bad Request |
| 4 | Title too long | 400 Bad Request |
| 5 | Invalid type | 400 Bad Request |
| 6 | Update own listing | 200 OK |
| 7 | Update non-existent | 404 Not Found |
| 8 | Update another's listing | 403 Forbidden |
| 9 | Delete own listing | 204 No Content |
| 10 | Verify soft delete | Listing not in list |
| 11 | Delete another's listing | 403 Forbidden |
| 12 | Cross-tenant access | 404 Not Found |
- All write operations require authentication (JWT token)
- Owner checks prevent users from modifying others' listings
- Soft delete sets
deleted_attimestamp instead of removing from DB - Global query filter automatically hides soft-deleted listings
- Cross-tenant access returns 404 (not 403) for security