mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 13:50:31 +00:00
test: add comprehensive unit tests for storage and auth
- LocalStorage tests: put/get, list, stat, health_check, nested dirs - S3Storage tests with wiremock HTTP mocking - Auth/htpasswd tests: loading, validation, public paths - Token lifecycle tests: create, verify, expire, revoke Total: 75 tests passing
This commit is contained in:
@@ -313,3 +313,106 @@ pub fn token_routes() -> Router<Arc<AppState>> {
|
||||
.route("/api/tokens/list", post(list_tokens))
|
||||
.route("/api/tokens/revoke", post(revoke_token))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
fn create_test_htpasswd(entries: &[(&str, &str)]) -> NamedTempFile {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
for (username, password) in entries {
|
||||
let hash = bcrypt::hash(password, 4).unwrap(); // cost=4 for speed in tests
|
||||
writeln!(file, "{}:{}", username, hash).unwrap();
|
||||
}
|
||||
file.flush().unwrap();
|
||||
file
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_htpasswd_loading() {
|
||||
let file = create_test_htpasswd(&[("admin", "secret"), ("user", "password")]);
|
||||
|
||||
let auth = HtpasswdAuth::from_file(file.path()).unwrap();
|
||||
let users = auth.list_users();
|
||||
assert_eq!(users.len(), 2);
|
||||
assert!(users.contains(&"admin"));
|
||||
assert!(users.contains(&"user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_htpasswd_loading_empty_file() {
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
let auth = HtpasswdAuth::from_file(file.path());
|
||||
assert!(auth.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_htpasswd_loading_with_comments() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
writeln!(file, "# This is a comment").unwrap();
|
||||
writeln!(file, "").unwrap();
|
||||
let hash = bcrypt::hash("secret", 4).unwrap();
|
||||
writeln!(file, "admin:{}", hash).unwrap();
|
||||
file.flush().unwrap();
|
||||
|
||||
let auth = HtpasswdAuth::from_file(file.path()).unwrap();
|
||||
assert_eq!(auth.list_users().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate_valid() {
|
||||
let file = create_test_htpasswd(&[("test", "secret")]);
|
||||
let auth = HtpasswdAuth::from_file(file.path()).unwrap();
|
||||
|
||||
assert!(auth.authenticate("test", "secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate_invalid_password() {
|
||||
let file = create_test_htpasswd(&[("test", "secret")]);
|
||||
let auth = HtpasswdAuth::from_file(file.path()).unwrap();
|
||||
|
||||
assert!(!auth.authenticate("test", "wrong"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authenticate_unknown_user() {
|
||||
let file = create_test_htpasswd(&[("test", "secret")]);
|
||||
let auth = HtpasswdAuth::from_file(file.path()).unwrap();
|
||||
|
||||
assert!(!auth.authenticate("unknown", "secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_public_path() {
|
||||
// Public paths
|
||||
assert!(is_public_path("/"));
|
||||
assert!(is_public_path("/health"));
|
||||
assert!(is_public_path("/ready"));
|
||||
assert!(is_public_path("/metrics"));
|
||||
assert!(is_public_path("/v2/"));
|
||||
assert!(is_public_path("/v2"));
|
||||
assert!(is_public_path("/ui"));
|
||||
assert!(is_public_path("/ui/dashboard"));
|
||||
assert!(is_public_path("/api-docs"));
|
||||
assert!(is_public_path("/api-docs/openapi.json"));
|
||||
assert!(is_public_path("/api/ui/stats"));
|
||||
assert!(is_public_path("/api/tokens"));
|
||||
assert!(is_public_path("/api/tokens/list"));
|
||||
|
||||
// Protected paths
|
||||
assert!(!is_public_path("/v2/myimage/blobs/sha256:abc"));
|
||||
assert!(!is_public_path("/v2/library/nginx/manifests/latest"));
|
||||
assert!(!is_public_path("/maven2/com/example/artifact/1.0/artifact.jar"));
|
||||
assert!(!is_public_path("/npm/lodash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_password() {
|
||||
let hash = hash_password("test123").unwrap();
|
||||
assert!(hash.starts_with("$2"));
|
||||
assert!(bcrypt::verify("test123", &hash).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,3 +129,112 @@ impl StorageBackend for LocalStorage {
|
||||
"local"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_and_get() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
storage.put("test/key", b"test data").await.unwrap();
|
||||
let data = storage.get("test/key").await.unwrap();
|
||||
assert_eq!(&*data, b"test data");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_not_found() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
let result = storage.get("nonexistent").await;
|
||||
assert!(matches!(result, Err(StorageError::NotFound)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_with_prefix() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
storage.put("docker/image/blob1", b"data1").await.unwrap();
|
||||
storage.put("docker/image/blob2", b"data2").await.unwrap();
|
||||
storage.put("maven/artifact", b"data3").await.unwrap();
|
||||
|
||||
let docker_keys = storage.list("docker/").await;
|
||||
assert_eq!(docker_keys.len(), 2);
|
||||
assert!(docker_keys.iter().all(|k| k.starts_with("docker/")));
|
||||
|
||||
let all_keys = storage.list("").await;
|
||||
assert_eq!(all_keys.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stat() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
storage.put("test", b"12345").await.unwrap();
|
||||
let meta = storage.stat("test").await.unwrap();
|
||||
assert_eq!(meta.size, 5);
|
||||
assert!(meta.modified > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stat_not_found() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
let meta = storage.stat("nonexistent").await;
|
||||
assert!(meta.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
assert!(storage.health_check().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check_creates_directory() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let new_path = temp_dir.path().join("new_storage");
|
||||
let storage = LocalStorage::new(new_path.to_str().unwrap());
|
||||
|
||||
assert!(!new_path.exists());
|
||||
assert!(storage.health_check().await);
|
||||
assert!(new_path.exists());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_nested_directory_creation() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
storage.put("a/b/c/d/e/file", b"deep").await.unwrap();
|
||||
let data = storage.get("a/b/c/d/e/file").await.unwrap();
|
||||
assert_eq!(&*data, b"deep");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_overwrite() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
|
||||
storage.put("key", b"original").await.unwrap();
|
||||
storage.put("key", b"updated").await.unwrap();
|
||||
|
||||
let data = storage.get("key").await.unwrap();
|
||||
assert_eq!(&*data, b"updated");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backend_name() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let storage = LocalStorage::new(temp_dir.path().to_str().unwrap());
|
||||
assert_eq!(storage.backend_name(), "local");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,3 +127,184 @@ impl StorageBackend for S3Storage {
|
||||
"s3"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wiremock::matchers::{method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/test-bucket/test-key"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let result = storage.put("test-key", b"data").await;
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_put_failure() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("PUT"))
|
||||
.and(path("/test-bucket/test-key"))
|
||||
.respond_with(ResponseTemplate::new(500))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let result = storage.put("test-key", b"data").await;
|
||||
assert!(matches!(result, Err(StorageError::Network(_))));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/test-bucket/test-key"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_bytes(b"test data".to_vec()))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let data = storage.get("test-key").await.unwrap();
|
||||
assert_eq!(&*data, b"test data");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_not_found() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/test-bucket/missing"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let result = storage.get("missing").await;
|
||||
assert!(matches!(result, Err(StorageError::NotFound)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
let xml_response = r#"<?xml version="1.0"?>
|
||||
<ListBucketResult>
|
||||
<Key>docker/image1</Key>
|
||||
<Key>docker/image2</Key>
|
||||
<Key>maven/artifact</Key>
|
||||
</ListBucketResult>"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/test-bucket"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_string(xml_response))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let keys = storage.list("docker/").await;
|
||||
assert_eq!(keys.len(), 2);
|
||||
assert!(keys.iter().all(|k| k.starts_with("docker/")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stat_success() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("HEAD"))
|
||||
.and(path("/test-bucket/test-key"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.insert_header("content-length", "1234")
|
||||
.insert_header("last-modified", "Sun, 06 Nov 1994 08:49:37 GMT"),
|
||||
)
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let meta = storage.stat("test-key").await.unwrap();
|
||||
assert_eq!(meta.size, 1234);
|
||||
assert!(meta.modified > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stat_not_found() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("HEAD"))
|
||||
.and(path("/test-bucket/missing"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
let meta = storage.stat("missing").await;
|
||||
assert!(meta.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check_healthy() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("HEAD"))
|
||||
.and(path("/test-bucket"))
|
||||
.respond_with(ResponseTemplate::new(200))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
assert!(storage.health_check().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check_bucket_not_found_is_ok() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("HEAD"))
|
||||
.and(path("/test-bucket"))
|
||||
.respond_with(ResponseTemplate::new(404))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
// 404 is OK for health check (bucket may be empty)
|
||||
assert!(storage.health_check().await);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_health_check_server_error() {
|
||||
let mock_server = MockServer::start().await;
|
||||
let storage = S3Storage::new(&mock_server.uri(), "test-bucket");
|
||||
|
||||
Mock::given(method("HEAD"))
|
||||
.and(path("/test-bucket"))
|
||||
.respond_with(ResponseTemplate::new(500))
|
||||
.mount(&mock_server)
|
||||
.await;
|
||||
|
||||
assert!(!storage.health_check().await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backend_name() {
|
||||
let storage = S3Storage::new("http://localhost:9000", "bucket");
|
||||
assert_eq!(storage.backend_name(), "s3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_s3_keys() {
|
||||
let xml = r#"<Key>docker/a</Key><Key>docker/b</Key><Key>maven/c</Key>"#;
|
||||
let keys = S3Storage::parse_s3_keys(xml, "docker/");
|
||||
assert_eq!(keys, vec!["docker/a", "docker/b"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use sha2::{Digest, Sha256};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
const TOKEN_PREFIX: &str = "nra_";
|
||||
@@ -180,23 +181,178 @@ fn hash_token(token: &str) -> String {
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TokenError {
|
||||
#[error("Invalid token format")]
|
||||
InvalidFormat,
|
||||
|
||||
#[error("Token not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("Token expired")]
|
||||
Expired,
|
||||
|
||||
#[error("Storage error: {0}")]
|
||||
Storage(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TokenError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidFormat => write!(f, "Invalid token format"),
|
||||
Self::NotFound => write!(f, "Token not found"),
|
||||
Self::Expired => write!(f, "Token expired"),
|
||||
Self::Storage(msg) => write!(f, "Storage error: {}", msg),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_create_token() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let token = store
|
||||
.create_token("testuser", 30, Some("Test token".to_string()))
|
||||
.unwrap();
|
||||
|
||||
assert!(token.starts_with("nra_"));
|
||||
assert_eq!(token.len(), 4 + 32); // prefix + uuid without dashes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_valid_token() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let token = store.create_token("testuser", 30, None).unwrap();
|
||||
let user = store.verify_token(&token).unwrap();
|
||||
|
||||
assert_eq!(user, "testuser");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_invalid_format() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let result = store.verify_token("invalid_token");
|
||||
assert!(matches!(result, Err(TokenError::InvalidFormat)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_not_found() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let result = store.verify_token("nra_00000000000000000000000000000000");
|
||||
assert!(matches!(result, Err(TokenError::NotFound)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_expired_token() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
// Create token and manually set it as expired
|
||||
let token = store.create_token("testuser", 1, None).unwrap();
|
||||
let token_hash = hash_token(&token);
|
||||
let file_path = temp_dir.path().join(format!("{}.json", &token_hash[..16]));
|
||||
|
||||
// Read and modify the token to be expired
|
||||
let content = std::fs::read_to_string(&file_path).unwrap();
|
||||
let mut info: TokenInfo = serde_json::from_str(&content).unwrap();
|
||||
info.expires_at = 0; // Set to epoch (definitely expired)
|
||||
std::fs::write(&file_path, serde_json::to_string(&info).unwrap()).unwrap();
|
||||
|
||||
// Token should now be expired
|
||||
let result = store.verify_token(&token);
|
||||
assert!(matches!(result, Err(TokenError::Expired)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_tokens() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
store.create_token("user1", 30, None).unwrap();
|
||||
store.create_token("user1", 30, None).unwrap();
|
||||
store.create_token("user2", 30, None).unwrap();
|
||||
|
||||
let user1_tokens = store.list_tokens("user1");
|
||||
assert_eq!(user1_tokens.len(), 2);
|
||||
|
||||
let user2_tokens = store.list_tokens("user2");
|
||||
assert_eq!(user2_tokens.len(), 1);
|
||||
|
||||
let unknown_tokens = store.list_tokens("unknown");
|
||||
assert_eq!(unknown_tokens.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revoke_token() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let token = store.create_token("testuser", 30, None).unwrap();
|
||||
let token_hash = hash_token(&token);
|
||||
let hash_prefix = &token_hash[..16];
|
||||
|
||||
// Verify token works
|
||||
assert!(store.verify_token(&token).is_ok());
|
||||
|
||||
// Revoke
|
||||
store.revoke_token(hash_prefix).unwrap();
|
||||
|
||||
// Verify token no longer works
|
||||
let result = store.verify_token(&token);
|
||||
assert!(matches!(result, Err(TokenError::NotFound)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revoke_nonexistent_token() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let result = store.revoke_token("nonexistent12345");
|
||||
assert!(matches!(result, Err(TokenError::NotFound)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_revoke_all_for_user() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
store.create_token("user1", 30, None).unwrap();
|
||||
store.create_token("user1", 30, None).unwrap();
|
||||
store.create_token("user2", 30, None).unwrap();
|
||||
|
||||
let revoked = store.revoke_all_for_user("user1");
|
||||
assert_eq!(revoked, 2);
|
||||
|
||||
assert_eq!(store.list_tokens("user1").len(), 0);
|
||||
assert_eq!(store.list_tokens("user2").len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_updates_last_used() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
let token = store.create_token("testuser", 30, None).unwrap();
|
||||
|
||||
// First verification
|
||||
store.verify_token(&token).unwrap();
|
||||
|
||||
// Check last_used is set
|
||||
let tokens = store.list_tokens("testuser");
|
||||
assert!(tokens[0].last_used.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_token_with_description() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let store = TokenStore::new(temp_dir.path());
|
||||
|
||||
store
|
||||
.create_token("testuser", 30, Some("CI/CD Pipeline".to_string()))
|
||||
.unwrap();
|
||||
|
||||
let tokens = store.list_tokens("testuser");
|
||||
assert_eq!(tokens[0].description, Some("CI/CD Pipeline".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TokenError {}
|
||||
|
||||
Reference in New Issue
Block a user