Lifts the auth-essential users table from fewo-webapp into oatpp-authkit in temporal form per Option B from the issue body. The previous shape (id INTEGER autoinc + is_active flag) is replaced with the entity_id + valid_from/valid_until triple; soft-delete via valid_until = now() instead of toggling is_active. New files (all in oatpp-authkit): - dto/UserDto.hpp — auth-essential columns only: id, entity_id, username, password_hash, role, tls_cert_dn, valid_from, valid_until. Registered as temporal so TemporalRepository composes cleanly. Application- specific columns (email, profile data) belong on a consumer-side DTO + parallel SchemaContract that contributes additional columns to the same users table. - db/UserDb.hpp — DbClient with login-path queries (findLiveByUsername, findLiveByTlsCertDn) plus generic CRUD. UserSchema declares the schema: TEXT id, entity_id, username, password_hash, role, tls_cert_dn, with natural-key UNIQUE on (username, valid_until) so no two live rows can share a username while historical rows for the same username are allowed. - repo/ConcreteUserRepository.hpp — Repository<UserDto> adapter + makeUserRepository factory wrapping in TemporalRepository. - test/test_user_schema.cpp — verifies SchemaBuilder<UserSchema, TemporalRepository<UserDto>>::create produces the expected 5 DDL statements; specifically asserts is_active and created_at are NOT present in the temporal shape (Option B replacement). 13 of 13 tests pass. Bumped 0.11.0 → 0.12.0. Per owner directive on authkit#14: password_hash rides the temporal row. A separate security follow-up issue tracks the redaction policy for historical password hashes (likely blank the hash but keep the row so change-history is auditable). The migration of an existing non-temporal users table to this shape is documented in db/UserDb.hpp: Atlas-generated migration handles the structural conversion + backfill (each existing row becomes its own entity with entity_id = CAST(id AS TEXT)). Sessions/certificates FKs that referenced users.id (INTEGER) need rewiring to reference users.entity_id — that's a consumer-side rewire, separate PR. Closes #14 — the four migration sub-PRs (PR 1 role_templates, PRs 2+3 permissions, PR 4 users) are now landed; the umbrella issue can close. Follow-ups (security hash redaction, fewo-webapp consumer migration, Atlas CI integration) get their own issues. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.1 KiB
C++
88 lines
3.1 KiB
C++
// Tests for authkit#14 PR 4 — temporal users schema.
|
|
|
|
#include "oatpp-authkit/db/UserDb.hpp"
|
|
#include "oatpp-authkit/dto/UserDto.hpp"
|
|
#include "oatpp-authkit/repo/ConcreteUserRepository.hpp"
|
|
#include "oatpp-authkit/repo/SchemaContract.hpp"
|
|
#include "oatpp-authkit/repo/TemporalRepository.hpp"
|
|
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#define REQUIRE(cond) do { \
|
|
if (!(cond)) { std::fprintf(stderr, "REQUIRE failed: %s @ %s:%d\n", \
|
|
#cond, __FILE__, __LINE__); std::abort(); } } while (0)
|
|
|
|
namespace {
|
|
|
|
bool contains(const std::string& haystack, const std::string& needle) {
|
|
return haystack.find(needle) != std::string::npos;
|
|
}
|
|
|
|
void test_users_temporal_create() {
|
|
using namespace oatpp_authkit::repo;
|
|
using namespace oatpp_authkit::db;
|
|
using namespace oatpp_authkit::dto;
|
|
|
|
std::vector<std::string> sqls;
|
|
SqlExec exec = [&](const std::string& sql) { sqls.push_back(sql); };
|
|
|
|
SchemaBuilder<
|
|
UserSchema,
|
|
TemporalRepository<UserDto>>::create("users", exec);
|
|
|
|
// 1 entity table + 3 schema-side indexes + 1 temporal composite index = 5
|
|
REQUIRE(sqls.size() == 5);
|
|
|
|
REQUIRE(contains(sqls[0], "CREATE TABLE IF NOT EXISTS users"));
|
|
REQUIRE(contains(sqls[0], "id TEXT PRIMARY KEY"));
|
|
REQUIRE(contains(sqls[0], "entity_id TEXT NOT NULL"));
|
|
REQUIRE(contains(sqls[0], "username TEXT NOT NULL"));
|
|
REQUIRE(contains(sqls[0], "password_hash TEXT"));
|
|
REQUIRE(contains(sqls[0], "role TEXT NOT NULL DEFAULT 'editor'"));
|
|
REQUIRE(contains(sqls[0], "valid_from TEXT NOT NULL DEFAULT ''"));
|
|
REQUIRE(contains(sqls[0], "valid_until TEXT NOT NULL DEFAULT '9999-12-31T23:59:59Z'"));
|
|
// is_active and created_at must NOT appear — those are dropped in
|
|
// the temporal shape (Option B).
|
|
REQUIRE(!contains(sqls[0], "is_active"));
|
|
REQUIRE(!contains(sqls[0], "created_at"));
|
|
|
|
REQUIRE(contains(sqls[1], "ix_users_entity_id"));
|
|
REQUIRE(contains(sqls[2], "ux_users_username_until"));
|
|
REQUIRE(contains(sqls[2], "(username, valid_until)"));
|
|
REQUIRE(contains(sqls[3], "ix_users_tls_cert_dn"));
|
|
REQUIRE(contains(sqls[4], "ux_users_entity_valid_until"));
|
|
REQUIRE(contains(sqls[4], "(entity_id, valid_until)"));
|
|
}
|
|
|
|
// ConcreteUserRepository contributes nothing; ensure SchemaBuilder is
|
|
// idempotent w.r.t. its presence in the parameter pack.
|
|
void test_concrete_user_repo_no_schema() {
|
|
using namespace oatpp_authkit::repo;
|
|
using namespace oatpp_authkit::db;
|
|
using namespace oatpp_authkit::dto;
|
|
|
|
std::vector<std::string> with_repo;
|
|
std::vector<std::string> without_repo;
|
|
|
|
SchemaBuilder<
|
|
UserSchema, ConcreteUserRepository, TemporalRepository<UserDto>>::create(
|
|
"users", [&](const std::string& s){ with_repo.push_back(s); });
|
|
|
|
SchemaBuilder<
|
|
UserSchema, TemporalRepository<UserDto>>::create(
|
|
"users", [&](const std::string& s){ without_repo.push_back(s); });
|
|
|
|
REQUIRE(with_repo == without_repo);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_users_temporal_create();
|
|
test_concrete_user_repo_no_schema();
|
|
std::printf("test_user_schema: OK\n");
|
|
return 0;
|
|
}
|