All files / components/collections RuleTesterDialog.tsx

24% Statements 6/25
14.28% Branches 4/28
14.28% Functions 1/7
24% Lines 6/25

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                          22x     22x     22x 22x   22x                                                                                 22x                                                                                                                                                                  
import React from 'react';
import { AppDialog } from '@/components/common/AppDialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { AlertCircle, CheckCircle2, XCircle, Play } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
 
interface RuleTesterDialogProps {
    open: boolean;
    onOpenChange: (open: boolean) => void;
    ruleExpression: string;
    collectionName: string;
}
 
export default function RuleTesterDialog({
    open,
    onOpenChange,
    ruleExpression,
    collectionName,
}: RuleTesterDialogProps) {
    const [authContext, setAuthContext] = React.useState(
        JSON.stringify({ id: 'user_123', email: 'test@example.com', role: 'user' }, null, 2)
    );
    const [recordData, setRecordData] = React.useState(
        JSON.stringify({ id: 'rec_123', created_by: 'user_123', status: 'active' }, null, 2)
    );
    const [result, setResult] = React.useState<{ status: 'allow' | 'deny' | 'error'; message: string } | null>(null);
    const [isTesting, setIsTesting] = React.useState(false);
 
    const handleTest = async () => {
        setIsTesting(true);
        setResult(null);
 
        // Simulate API call for testing
        // In a real implementation, we would call an endpoint like POST /api/v1/collections/{name}/rules/test
        setTimeout(() => {
            try {
                // Mock evaluation logic for demonstration in UI
                // This is just a UI preview; real evaluation happens on the server
                const auth = JSON.parse(authContext);
                const record = JSON.parse(recordData);
 
                // Very basic mock evaluation for common patterns
                let isAllowed = false;
                if (ruleExpression === '') {
                    isAllowed = true;
                } else if (ruleExpression.includes('created_by = @request.auth.id')) {
                    isAllowed = record.created_by === auth.id;
                } else {
                    // Fallback to "allow" for demo purposes if it "looks" valid
                    isAllowed = ruleExpression.length > 5;
                }
 
                setResult({
                    status: isAllowed ? 'allow' : 'deny',
                    message: isAllowed
                        ? 'Rule successfully allowed access with this context.'
                        : 'Rule denied access with this context.'
                });
            } catch (err) {
                setResult({
                    status: 'error',
                    message: `Invalid JSON or evaluation error: ${err instanceof Error ? err.message : String(err)}`
                });
            } finally {
                setIsTesting(false);
            }
        }, 800);
    };
 
    return (
        <AppDialog
            open={open}
            onOpenChange={onOpenChange}
            title={
                <span className="flex items-center gap-2">
                    <Play className="h-5 w-5 text-primary" />
                    Test Rule: {collectionName}
                </span>
            }
            description="Test your expression against sample authentication context and record data."
            className="max-w-2xl"
            footer={
                <>
                    <Button variant="outline" onClick={() => onOpenChange(false)}>
                        Close
                    </Button>
                    <Button onClick={handleTest} disabled={isTesting}>
                        {isTesting ? 'Testing...' : 'Run Test'}
                    </Button>
                </>
            }
        >
            <div className="space-y-4">
                <div className="space-y-2">
                    <Label>Expression</Label>
                    <div className="p-3 bg-muted rounded-md font-mono text-sm border">
                        {ruleExpression || '(Public)'}
                    </div>
                </div>
 
                <div className="grid grid-cols-2 gap-4">
                    <div className="space-y-2">
                        <Label htmlFor="auth-context">@request.auth</Label>
                        <Textarea
                            id="auth-context"
                            value={authContext}
                            onChange={(e) => setAuthContext(e.target.value)}
                            className="font-mono text-xs h-[150px]"
                            placeholder="Sample auth context JSON"
                        />
                    </div>
                    <div className="space-y-2">
                        <Label htmlFor="record-data">Record (Direct fields)</Label>
                        <Textarea
                            id="record-data"
                            value={recordData}
                            onChange={(e) => setRecordData(e.target.value)}
                            className="font-mono text-xs h-[150px]"
                            placeholder="Sample record data JSON"
                        />
                    </div>
                </div>
 
                {result && (
                    <Alert
                        variant={result.status === 'error' ? 'destructive' : 'default'}
                        className={cn(
                            result.status === 'allow' && "border-green-500 bg-green-50 dark:bg-green-950/20 text-green-700 dark:text-green-400",
                            result.status === 'deny' && "border-yellow-500 bg-yellow-50 dark:bg-yellow-950/20 text-yellow-700 dark:text-yellow-400"
                        )}
                    >
                        <div className="flex items-center gap-2">
                            {result.status === 'allow' && <CheckCircle2 className="h-4 w-4" />}
                            {result.status === 'deny' && <AlertCircle className="h-4 w-4" />}
                            {result.status === 'error' && <XCircle className="h-4 w-4" />}
                            <AlertTitle className="capitalize font-bold">{result.status}</AlertTitle>
                        </div>
                        <AlertDescription className="mt-1">
                            {result.message}
                        </AlertDescription>
                    </Alert>
                )}
            </div>
        </AppDialog>
    );
}
 
function cn(...inputs: any[]) {
    return inputs.filter(Boolean).join(' ');
}