Mock API
GLUT starts a mock GitLab API server for each test. Jobs receive
CI_API_V4_URL that points to this server. The server records each request so
assert.api can check it after the pipeline run.
The mock API is not a full GitLab clone. It supports the endpoints that CI components commonly need in tests: project data, token data, common project resources, and a few special endpoints.
How Jobs Call It
Use the normal GitLab CI variable in the job:
curl \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
"$CI_API_V4_URL/projects/1"
GLUT sets CI_API_V4_URL to the local mock server. The test decides token and
project behavior in setup.api.
.glut:
setup:
api:
token:
valid: true
scopes:
- "api"
project:
path: "group/project"
default_branch: "main"
setup.api Reference
| Field | Type | Meaning |
|---|---|---|
token |
object | Token state used by auth checks and token self response. |
project |
object | Project data returned by the project endpoint. |
seed |
object | Initial API resources available before the job starts. |
token
| Field | Type | Default | Meaning |
|---|---|---|---|
valid |
boolean | true |
When false, protected endpoints return 401 Unauthorized. |
expires_at |
string | empty | Value returned by token self endpoint. |
scopes |
list of strings | empty | When set, API reads need api or read_api. API writes need api. |
setup:
api:
token:
valid: true
expires_at: "2030-01-01T00:00:00Z"
scopes:
- "api"
Use an invalid token to test failure paths:
setup:
api:
token:
valid: false
Use read-only scope to reject writes:
setup:
api:
token:
valid: true
scopes:
- "read_api"
project
| Field | Type | Default | Meaning |
|---|---|---|---|
path |
string | test-group/test-project |
Project path with namespace. |
default_branch |
string | main |
Project default branch. |
setup:
api:
project:
path: "platform/components"
default_branch: "trunk"
The mock accepts two project ids:
1- the URL-escaped project path, such as
platform%2Fcomponents
seed
seed prepares resource data before the job starts.
| Field | Type | Meaning |
|---|---|---|
releases |
list | Initial release objects. |
merge_requests |
list | Initial merge request objects. |
labels |
list | Initial label objects. |
setup:
api:
seed:
releases:
- tag_name: "v1.0.0"
name: "Old release"
merge_requests:
- iid: 7
title: "Update image"
state: "opened"
labels:
- id: 1
name: "ready"
color: "#00ff00"
Seeded data uses the same store as data created during the test. A job can read, update, or delete seeded objects.
Auth Rules
Most endpoints need one of these headers:
PRIVATE-TOKEN: test-token
Authorization: Bearer test-token
Auth behavior:
| Case | Result |
|---|---|
| No token header | 401 Unauthorized |
setup.api.token.valid: false |
401 Unauthorized |
Read request with api or read_api |
Allowed |
Write request with read_api |
403 Forbidden |
API request with only write_repository |
401 Unauthorized |
| Request with no scopes set | Allowed |
GET /api/v4/version |
Allowed without auth |
Write methods are POST, PUT, DELETE, and PATCH.
Built-in Endpoints
| Method | Path | Auth | Response |
|---|---|---|---|
GET |
/api/v4/version |
no | GitLab version object. |
GET |
/api/v4/personal_access_tokens/self |
yes | Token state from setup.api.token. |
GET |
/api/v4/projects/:id |
yes | Project state from setup.api.project. |
Version
curl "$CI_API_V4_URL/version"
Example response:
{
"version": "16.11.0",
"revision": "mock"
}
Token Self
curl \
-H "PRIVATE-TOKEN: test-token" \
"$CI_API_V4_URL/personal_access_tokens/self"
Example response:
{
"id": 1,
"name": "glut token",
"active": true,
"revoked": false,
"scopes": ["api"],
"expires_at": "2030-01-01T00:00:00Z"
}
Project
curl \
-H "PRIVATE-TOKEN: test-token" \
"$CI_API_V4_URL/projects/1"
Example response:
{
"id": 1,
"path_with_namespace": "test-group/test-project",
"name": "test-project",
"default_branch": "main"
}
Standard Project Resources
Standard project resources use common create, read, update, and delete behavior
under /api/v4/projects/:id. These endpoints are useful for GitLab objects
that behave like stored records.
| Action | Method | Path | Status |
|---|---|---|---|
| List | GET |
/api/v4/projects/:id/<resource> |
200 OK |
| Create | POST |
/api/v4/projects/:id/<resource> |
201 Created |
| Get one | GET |
/api/v4/projects/:id/<resource>/<identifier> |
200 OK or 404 Not Found |
| Update | PUT |
/api/v4/projects/:id/<resource>/<identifier> |
200 OK or 404 Not Found |
| Delete | DELETE |
/api/v4/projects/:id/<resource>/<identifier> |
200 OK or 404 Not Found |
Supported resources:
| Resource | Path | Identifier | Default fields |
|---|---|---|---|
| Releases | /releases |
tag_name |
tag_name, name, description |
| Merge requests | /merge_requests |
iid |
iid, title, state, labels |
| Tags | /repository/tags |
name |
name, message |
| Branches | /repository/branches |
name |
name, protected |
| Labels | /labels |
id |
id, name, color |
| Milestones | /milestones |
id |
id |
| Issues | /issues |
iid |
iid |
| Hooks | /hooks |
id |
id |
| Variables | /variables |
key |
key, value |
| Deployments | /deployments |
id |
id |
| Environments | /environments |
id |
id |
| Pipelines | /pipelines |
id |
id |
Create A Release
Job script:
curl \
-H "PRIVATE-TOKEN: test-token" \
-H "Content-Type: application/json" \
-d '{"tag_name":"v1.2.0","name":"v1.2.0"}' \
"$CI_API_V4_URL/projects/1/releases"
Assert:
assert:
api:
"POST /api/v4/projects/*/releases":
called: true
times: 1
body:
tag_name: "v1.2.0"
name: "v1.2.0"
Read A Seeded Release
Setup:
setup:
api:
seed:
releases:
- tag_name: "v1.2.0"
name: "v1.2.0"
Job script:
curl \
-H "PRIVATE-TOKEN: test-token" \
"$CI_API_V4_URL/projects/1/releases/v1.2.0"
Assert:
assert:
api:
"GET /api/v4/projects/*/releases/*":
called: true
Special Project Endpoints
Special project endpoints have custom behavior. They are still recorded for
assert.api, but they do more than store and return records.
| Method | Path | Behavior |
|---|---|---|
POST |
/api/v4/projects/:id/repository/commits |
Returns a mock commit response. |
POST |
/api/v4/projects/:id/merge_requests/:iid/notes |
Returns a mock note response. |
POST |
/api/v4/projects/:id/merge_requests/:iid/approve |
Returns an approve response. |
Create Commit
Request:
curl \
-H "PRIVATE-TOKEN: test-token" \
-H "Content-Type: application/json" \
-d '{"branch":"main","commit_message":"update manifest","actions":[]}' \
"$CI_API_V4_URL/projects/1/repository/commits"
Response fields:
| Field | Value |
|---|---|
id |
mock-commit-sha |
short_id |
mock-com |
title |
commit_message from the request body |
message |
commit_message from the request body |
committed_date |
Current time in RFC3339 format |
Assert:
assert:
api:
"POST /api/v4/projects/*/repository/commits":
called: true
body:
commit_message:
contain-substring: "update manifest"
Add Merge Request Note
Request:
curl \
-H "PRIVATE-TOKEN: test-token" \
-H "Content-Type: application/json" \
-d '{"body":"release is ready"}' \
"$CI_API_V4_URL/projects/1/merge_requests/7/notes"
Response:
{
"id": 1,
"body": "release is ready"
}
Assert:
assert:
api:
"POST /api/v4/projects/*/merge_requests/*/notes":
called: true
body:
body:
contain-substring: "ready"
Approve Merge Request
Request:
curl \
-X POST \
-H "PRIVATE-TOKEN: test-token" \
"$CI_API_V4_URL/projects/1/merge_requests/7/approve"
Response:
{
"approved": true
}
Assert:
assert:
api:
"POST /api/v4/projects/*/merge_requests/*/approve":
called: true
times: 1
Recorded Calls
The recorder stores:
| Field | Meaning |
|---|---|
| Method | HTTP method. |
| Path | Escaped request path. |
| Request body | Raw request body bytes. |
| Status code | Response status code. |
| Timestamp | Time when the request was recorded. |
Use assert.api to check recorded calls. Prefer * for project ids so tests do
not depend on 1 or an escaped path.
assert:
api:
"POST /api/v4/projects/*/releases":
called: true
times:
ge: 1
Body checks use the same matcher syntax as other asserts.
assert:
api:
"POST /api/v4/projects/*/releases":
body:
gjson:
"assets.links.#":
ge: 1
"assets.links.0.name":
have-suffix: ".tar.gz"
Common Test Patterns
Test A Missing Token Path
Setup:
setup:
api:
token:
valid: false
Assert that the release was not created:
assert:
api:
"POST /api/v4/projects/*/releases":
called: true
times: 1
Also assert the job failed or printed the expected error:
assert:
job:
release:
exit-status: 1
stderr:
- "401 Unauthorized"
Test A Read-only Token
Setup:
setup:
api:
token:
valid: true
scopes:
- "read_api"
Assert that a write was attempted:
assert:
api:
"POST /api/v4/projects/*/releases":
called: true
Then assert the job handled the 403 Forbidden response.
Test Seeded Data
Setup:
setup:
api:
seed:
releases:
- tag_name: "v1.0.0"
name: "Existing release"
Assert that the job looked up the release:
assert:
api:
"GET /api/v4/projects/*/releases/*":
called: true
Test No Destructive Call
assert:
api:
"DELETE /api/v4/projects/*/releases/*":
called: false
Limits
The mock API is made for component tests. It gives stable, local behavior for common GitLab API use. It does not validate every GitLab field and it does not try to match all GitLab server rules.
Add new HTTP behavior in internal/mockserver. Keep request recording there so
assert.api can see the calls.