TestAuditLogFormat is both a regression test for the 3.17.0 bug where AuthService/Login (and Signup/ExchangeToken) silently dropped audit entries, AND a contract test for the shape of audit log entries returned by AuditLogService/SearchAuditLogs. Downstream consumers (SIEMs, compliance tooling, `doc
(t *testing.T)
| 21 | // tooling, `docker logs | grep log_type:audit`) depend on this shape being |
| 22 | // stable across releases — changes here are user-visible breaking changes. |
| 23 | func TestAuditLogFormat(t *testing.T) { |
| 24 | a := require.New(t) |
| 25 | ctx := context.Background() |
| 26 | ctl := &controller{} |
| 27 | ctx, err := ctl.StartServerWithExternalPg(ctx) |
| 28 | a.NoError(err) |
| 29 | defer ctl.Close(ctx) |
| 30 | |
| 31 | // --- Part 1: Login (workspace-scoped, allow_without_credential) --- |
| 32 | // |
| 33 | // Clear the token so the Login call runs without credentials — the exact |
| 34 | // path that regressed in 3.17.0 (Resources is empty, so the audit loop |
| 35 | // never fires unless the handler hands us the workspace). |
| 36 | adminToken := ctl.authInterceptor.token |
| 37 | ctl.authInterceptor.token = "" |
| 38 | |
| 39 | loginResp, err := ctl.authServiceClient.Login(ctx, connect.NewRequest(&v1pb.LoginRequest{ |
| 40 | Email: "demo@example.com", |
| 41 | Password: "1024bytebase", |
| 42 | })) |
| 43 | a.NoError(err) |
| 44 | workspace := loginResp.Msg.GetUser().GetWorkspace() |
| 45 | a.NotEmpty(workspace, "login response should carry the user's workspace") |
| 46 | |
| 47 | // Restore the token so the SearchAuditLogs call below authenticates. |
| 48 | ctl.authInterceptor.token = adminToken |
| 49 | |
| 50 | loginAuditLogs, err := ctl.auditLogServiceClient.SearchAuditLogs(ctx, connect.NewRequest(&v1pb.SearchAuditLogsRequest{ |
| 51 | Parent: workspace, |
| 52 | Filter: `method == "/bytebase.v1.AuthService/Login"`, |
| 53 | OrderBy: "create_time desc", |
| 54 | })) |
| 55 | a.NoError(err) |
| 56 | a.NotEmpty(loginAuditLogs.Msg.AuditLogs, "Login must produce an audit entry under the caller's workspace (regression guard for 3.17.0)") |
| 57 | |
| 58 | entry := loginAuditLogs.Msg.AuditLogs[0] |
| 59 | // Name: "workspaces/{id}/auditLogs/{uid}" — the resource name format is |
| 60 | // part of the API contract and also the parent users filter on. |
| 61 | a.Regexp(regexp.MustCompile(`^workspaces/[^/]+/auditLogs/[^/]+$`), entry.Name, |
| 62 | "audit log name must match workspaces/{id}/auditLogs/{uid}") |
| 63 | a.True(strings.HasPrefix(entry.Name, workspace+"/auditLogs/"), |
| 64 | "audit log must be parented under the login workspace") |
| 65 | a.NotNil(entry.CreateTime, "CreateTime must be set") |
| 66 | a.Equal("users/demo@example.com", entry.User, "User must be users/{email}") |
| 67 | a.Equal("/bytebase.v1.AuthService/Login", entry.Method, |
| 68 | "Method is part of the filter API contract and must be the full procedure name") |
| 69 | a.Equal(v1pb.AuditLog_INFO, entry.Severity, "successful Login is INFO severity") |
| 70 | a.Equal("demo@example.com", entry.Resource, "Login's Resource is the login email") |
| 71 | a.Nil(entry.Status, "successful Login has nil Status (code 0)") |
| 72 | a.NotNil(entry.Latency, "Latency must be recorded") |
| 73 | |
| 74 | // Request JSON must round-trip back to LoginRequest, have the caller's |
| 75 | // email, and must NOT contain the plaintext password. |
| 76 | gotReq := &v1pb.LoginRequest{} |
| 77 | a.NoError(common.ProtojsonUnmarshaler.Unmarshal([]byte(entry.Request), gotReq), |
| 78 | "Request JSON must be valid protojson for LoginRequest") |
| 79 | a.Equal("demo@example.com", gotReq.Email) |
| 80 | a.Empty(gotReq.Password, "password must be redacted in the Request payload") |
nothing calls this directly
no test coverage detected