fraiseql-server 2.9.0

HTTP server for FraiseQL v2 GraphQL engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
//! RBAC Management API tests
//!
//! Tests for role and permission management REST endpoints

// ============================================================================
// Test 1: Role Management Endpoints
// ============================================================================

/// Test creating a new role
#[test]
fn test_create_role_success() {
    // POST /api/roles with valid CreateRoleRequest should return 201 Created
    // Response should include role ID, name, permissions, created_at timestamp
}

/// Test creating role with empty name should fail
#[test]
fn test_create_role_empty_name() {
    // POST /api/roles with empty name should return 400 Bad Request
}

/// Test creating duplicate role should fail
#[test]
fn test_create_role_duplicate() {
    // Creating role with same name twice should return 409 Conflict
}

/// Test listing all roles
#[test]
fn test_list_roles_success() {
    // GET /api/roles should return 200 OK with array of RoleDto
    // Should include pagination support (limit, offset)
}

/// Test listing roles with pagination
#[test]
fn test_list_roles_with_pagination() {
    // GET /api/roles?limit=10&offset=5 should respect pagination parameters
}

/// Test getting role by ID
#[test]
fn test_get_role_success() {
    // GET /api/roles/{role_id} should return 200 OK with RoleDto
}

/// Test getting non-existent role
#[test]
fn test_get_role_not_found() {
    // GET /api/roles/{non_existent_id} should return 404 Not Found
}

/// Test updating role details
#[test]
fn test_update_role_success() {
    // PUT /api/roles/{role_id} with updated name/description should return 200
    // Should update updated_at timestamp
}

/// Test updating role preserves permissions unless explicitly changed
#[test]
fn test_update_role_preserves_permissions() {
    // PUT /api/roles/{role_id} without permissions field should keep existing permissions
}

/// Test deleting a role
#[test]
fn test_delete_role_success() {
    // DELETE /api/roles/{role_id} should return 204 No Content
}

/// Test deleting role in use should fail
#[test]
fn test_delete_role_in_use() {
    // DELETE /api/roles/{role_id} when users have this role should return 409 Conflict
    // With message about active assignments
}

// ============================================================================
// Test 2: Permission Management Endpoints
// ============================================================================

/// Test creating a new permission
#[test]
fn test_create_permission_success() {
    // POST /api/permissions with valid CreatePermissionRequest should return 201 Created
    // Response should include permission ID, resource, action, created_at
}

/// Test creating permission with invalid resource format
#[test]
fn test_create_permission_invalid_resource() {
    // Resource should follow format "resource:action" or similar
    // Invalid format should return 400 Bad Request
}

/// Test creating duplicate permission should fail
#[test]
fn test_create_permission_duplicate() {
    // Creating permission with same resource:action twice should return 409 Conflict
}

/// Test listing all permissions
#[test]
fn test_list_permissions_success() {
    // GET /api/permissions should return 200 OK with array of PermissionDto
}

/// Test filtering permissions by resource
#[test]
fn test_list_permissions_filter_by_resource() {
    // GET /api/permissions?resource=query should return only query permissions
}

/// Test getting permission by ID
#[test]
fn test_get_permission_success() {
    // GET /api/permissions/{permission_id} should return 200 OK with PermissionDto
}

/// Test getting non-existent permission
#[test]
fn test_get_permission_not_found() {
    // GET /api/permissions/{non_existent_id} should return 404 Not Found
}

/// Test deleting a permission
#[test]
fn test_delete_permission_success() {
    // DELETE /api/permissions/{permission_id} should return 204 No Content
}

/// Test deleting permission in use should fail
#[test]
fn test_delete_permission_in_use() {
    // DELETE /api/permissions/{permission_id} when roles use it should return 409 Conflict
}

// ============================================================================
// Test 3: User-Role Assignment Endpoints
// ============================================================================

/// Test assigning a role to a user
#[test]
fn test_assign_role_success() {
    // POST /api/user-roles with valid AssignRoleRequest should return 201 Created
    // Should create UserRoleDto with assigned_at timestamp
}

/// Test assigning non-existent role should fail
#[test]
fn test_assign_role_not_found() {
    // POST /api/user-roles with invalid role_id should return 404 Not Found
}

/// Test assigning role twice should fail
#[test]
fn test_assign_role_duplicate() {
    // Assigning same role to same user twice should return 409 Conflict
}

/// Test listing user-role assignments
#[test]
fn test_list_user_roles_success() {
    // GET /api/user-roles should return 200 OK with array of UserRoleDto
}

/// Test filtering user-roles by `user_id`
#[test]
fn test_list_user_roles_filter_by_user() {
    // GET /api/user-roles?user_id={user_id} should return only that user's roles
}

/// Test filtering user-roles by `role_id`
#[test]
fn test_list_user_roles_filter_by_role() {
    // GET /api/user-roles?role_id={role_id} should return only that role's users
}

/// Test revoking a role from a user
#[test]
fn test_revoke_role_success() {
    // DELETE /api/user-roles/{user_id}/{role_id} should return 204 No Content
}

/// Test revoking non-existent assignment should fail
#[test]
fn test_revoke_role_not_found() {
    // DELETE /api/user-roles/{user_id}/{role_id} when assignment doesn't exist should return 404
}

// ============================================================================
// Test 4: Audit Endpoints
// ============================================================================

/// Test querying permission access audit logs
#[test]
fn test_query_permission_audit_success() {
    // GET /api/audit/permissions should return 200 OK with array of audit events
}

/// Test filtering audit logs by `user_id`
#[test]
fn test_query_permission_audit_filter_by_user() {
    // GET /api/audit/permissions?user_id={user_id} should return only that user's accesses
}

/// Test filtering audit logs by permission
#[test]
fn test_query_permission_audit_filter_by_permission() {
    // GET /api/audit/permissions?permission_id={perm_id} should return only that permission's
    // accesses
}

/// Test filtering audit logs by time range
#[test]
fn test_query_permission_audit_filter_by_time() {
    // GET /api/audit/permissions?start_time={iso}&end_time={iso} should return events in range
}

/// Test filtering audit logs by status
#[test]
fn test_query_permission_audit_filter_by_status() {
    // GET /api/audit/permissions?status=denied should return only denied accesses
}

/// Test audit pagination
#[test]
fn test_query_permission_audit_pagination() {
    // GET /api/audit/permissions?limit=20&offset=40 should respect pagination
}

// ============================================================================
// Test 5: Authorization & Access Control
// ============================================================================

/// Test creating role requires admin permission
#[test]
fn test_create_role_requires_admin() {
    // POST /api/roles without admin:write permission should return 403 Forbidden
}

/// Test listing roles doesn't require special permission
#[test]
fn test_list_roles_no_special_permission() {
    // GET /api/roles should work for authenticated users
}

/// Test modifying role requires admin permission
#[test]
fn test_update_role_requires_admin() {
    // PUT /api/roles/{role_id} without admin:write should return 403 Forbidden
}

/// Test deleting role requires admin permission
#[test]
fn test_delete_role_requires_admin() {
    // DELETE /api/roles/{role_id} without admin:write should return 403 Forbidden
}

/// Test querying audit logs requires audit:read permission
#[test]
fn test_query_audit_requires_permission() {
    // GET /api/audit/permissions without audit:read should return 403 Forbidden
}

// ============================================================================
// Test 6: Multi-Tenancy
// ============================================================================

/// Test creating role respects `tenant_id`
#[test]
fn test_create_role_respects_tenant() {
    // Role created in tenant A should not be visible to tenant B
}

/// Test listing roles filters by tenant
#[test]
fn test_list_roles_filters_by_tenant() {
    // GET /api/roles should only return roles for current tenant
}

/// Test user-role assignment respects tenant
#[test]
fn test_assign_role_respects_tenant() {
    // Cannot assign role from tenant A to user in tenant B
}

/// Test audit logs filter by tenant
#[test]
fn test_audit_logs_filter_by_tenant() {
    // GET /api/audit/permissions should only return events for current tenant
}

// ============================================================================
// Test 7: Error Handling & Validation
// ============================================================================

/// Test invalid JSON in request body returns 400
#[test]
fn test_invalid_json_request() {
    // POST /api/roles with malformed JSON should return 400 Bad Request
}

/// Test missing required fields returns 400
#[test]
fn test_missing_required_fields() {
    // POST /api/roles without required 'name' field should return 400
}

/// Test invalid permission resource format returns 400
#[test]
fn test_invalid_permission_format() {
    // POST /api/permissions with malformed resource:action should return 400
}

/// Test concurrent role creation handles race conditions
#[test]
fn test_concurrent_role_creation() {
    // Multiple simultaneous creates of same role should fail gracefully
    // One should succeed, others should return 409 Conflict
}

/// Test cascade behavior when deleting with dependents
#[test]
fn test_cascade_delete_protection() {
    // DELETE /api/roles/{role_id} with active users should refuse
    // Suggest cascade delete or revoking assignments first
}

// ============================================================================
// Test 8: API Consistency
// ============================================================================

/// Test all endpoints return consistent error format
#[test]
fn test_consistent_error_format() {
    // All error responses should have structure: {error: "message", code: "ERROR_CODE"}
}

/// Test all endpoints return consistent timestamp format (ISO 8601)
#[test]
fn test_consistent_timestamp_format() {
    // All created_at, updated_at, assigned_at should be ISO 8601 UTC
}

/// Test all list endpoints support pagination
#[test]
fn test_all_list_endpoints_support_pagination() {
    // GET /api/roles, /api/permissions, /api/user-roles should all support limit/offset
}

/// Test all create endpoints return the created resource
#[test]
fn test_create_endpoints_return_resource() {
    // POST /api/roles, /api/permissions, /api/user-roles should return full DTO
}

// ── db_backend_tests ──────────────────────────────────────────────────────────

mod db_backend_tests {
    #![allow(clippy::unwrap_used)] // Reason: test code, panics acceptable
    #![allow(clippy::cast_precision_loss)] // Reason: test metrics reporting
    #![allow(clippy::cast_sign_loss)] // Reason: test data uses small positive integers
    #![allow(clippy::cast_possible_truncation)] // Reason: test data values are bounded
    #![allow(clippy::cast_possible_wrap)] // Reason: test data values are bounded
    #![allow(clippy::missing_panics_doc)] // Reason: test helpers
    #![allow(clippy::missing_errors_doc)] // Reason: test helpers
    #![allow(missing_docs)] // Reason: test code
    #![allow(clippy::items_after_statements)] // Reason: test helpers defined near use site

    use super::super::db_backend::*;

    #[test]
    fn test_parse_permission_valid() {
        let (resource, action) = parse_permission("content:write").unwrap();
        assert_eq!(resource, "content");
        assert_eq!(action, "write");
    }

    #[test]
    fn test_parse_permission_wildcard() {
        let (resource, action) = parse_permission("*:*").unwrap();
        assert_eq!(resource, "*");
        assert_eq!(action, "*");
    }

    #[test]
    fn test_parse_permission_invalid() {
        assert!(
            matches!(parse_permission("no_colon"), Err(RbacDbError::QueryError(_))),
            "expected QueryError for permission without colon, got: {:?}",
            parse_permission("no_colon")
        );
    }

    #[test]
    fn test_rbac_db_error_display() {
        assert_eq!(format!("{}", RbacDbError::RoleNotFound), "Role not found");
        assert_eq!(format!("{}", RbacDbError::RoleDuplicate), "Role already exists");
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)] // Reason: test code; pool ctor errors must panic to surface test setup failures
mod router_construction {
    //! See `crates/fraiseql-server/src/observers/routes.rs::tests` for context:
    //! axum validates path-capture syntax inside `Router::route`, so any
    //! lingering `:param` literal panics here at build time (issue #316).

    use std::sync::Arc;

    use sqlx::PgPool;

    use crate::api::rbac_management::{
        RbacManagementState, db_backend::RbacDbBackend, rbac_management_router,
    };

    fn lazy_pool() -> PgPool {
        PgPool::connect_lazy("postgres://test:test@localhost/test").unwrap()
    }

    #[tokio::test]
    async fn rbac_router_constructs() {
        let state = RbacManagementState {
            db: Arc::new(RbacDbBackend::new(lazy_pool())),
        };
        let _ = rbac_management_router(state);
    }
}