|
1 | | -import { extractTemplateVariables, validateVariables } from '../../src/utils/template-utils'; |
| 1 | +import { |
| 2 | + extractTemplateVariables, |
| 3 | + validateVariables, |
| 4 | + validateTemplate, |
| 5 | + compilePrompt, |
| 6 | +} from '../../src/utils/template-utils'; |
2 | 7 | import { Database } from '@/app/__generated__/supabase.types'; |
3 | 8 |
|
4 | 9 | type PromptVariable = Database['public']['Tables']['prompt_variables']['Row']; |
@@ -825,3 +830,139 @@ describe('validateVariables', () => { |
825 | 830 | expect(variablesWithDefaults).toEqual({ opt1: 'a', opt2: '1' }); |
826 | 831 | }); |
827 | 832 | }); |
| 833 | + |
| 834 | +describe('validateTemplate', () => { |
| 835 | + it('should return isValid: true for a valid template', () => { |
| 836 | + const template = 'Hello {{ name }}!'; |
| 837 | + const result = validateTemplate(template); |
| 838 | + expect(result.isValid).toBe(true); |
| 839 | + expect(result.error).toBeUndefined(); |
| 840 | + }); |
| 841 | + |
| 842 | + it("should return isValid: true for a template using 'global'", () => { |
| 843 | + const template = 'Version: {{ global.version }}'; |
| 844 | + const result = validateTemplate(template); |
| 845 | + expect(result.isValid).toBe(true); |
| 846 | + expect(result.error).toBeUndefined(); |
| 847 | + }); |
| 848 | + |
| 849 | + it('should return isValid: false for Nunjucks syntax errors', () => { |
| 850 | + const template = 'Hello {{ name !'; // Invalid Nunjucks syntax |
| 851 | + const result = validateTemplate(template); |
| 852 | + expect(result.isValid).toBe(false); |
| 853 | + expect(result.error).toMatch(/expected variable end/i); |
| 854 | + }); |
| 855 | + |
| 856 | + it('should return isValid: false for disallowed identifiers', () => { |
| 857 | + const template = 'Data: {{ process.env.SECRET }}'; |
| 858 | + const result = validateTemplate(template); |
| 859 | + expect(result.isValid).toBe(false); |
| 860 | + expect(result.error).toBe("Security: Disallowed identifier 'process' found."); |
| 861 | + }); |
| 862 | + |
| 863 | + it('should return isValid: false for another disallowed identifier (eval)', () => { |
| 864 | + const template = '{{ eval("alert(1)") }}'; |
| 865 | + const result = validateTemplate(template); |
| 866 | + expect(result.isValid).toBe(false); |
| 867 | + expect(result.error).toBe("Security: Disallowed identifier 'eval' found."); |
| 868 | + }); |
| 869 | + |
| 870 | + it('should return isValid: false for disallowed property lookups', () => { |
| 871 | + const template = 'Access: {{ myObj.__proto__ }}'; |
| 872 | + const result = validateTemplate(template); |
| 873 | + expect(result.isValid).toBe(false); |
| 874 | + expect(result.error).toBe("Security: Disallowed property lookup '__proto__' found."); |
| 875 | + }); |
| 876 | + |
| 877 | + it('should return isValid: false for disallowed property lookups via string literal', () => { |
| 878 | + const template = "Access: {{ myObj['constructor'] }}"; |
| 879 | + const result = validateTemplate(template); |
| 880 | + expect(result.isValid).toBe(false); |
| 881 | + expect(result.error).toBe("Security: Disallowed property lookup 'constructor' found."); |
| 882 | + }); |
| 883 | + |
| 884 | + it('should allow legitimate property lookups that are not disallowed', () => { |
| 885 | + const template = 'Value: {{ myObj.legitProperty }}'; |
| 886 | + const result = validateTemplate(template); |
| 887 | + expect(result.isValid).toBe(true); |
| 888 | + expect(result.error).toBeUndefined(); |
| 889 | + }); |
| 890 | + |
| 891 | + it('should handle complex but valid templates correctly', () => { |
| 892 | + const template = ` |
| 893 | + {% if user.isAdmin %} |
| 894 | + Admin: {{ user.name }} |
| 895 | + {% for item in user.items %} |
| 896 | + {{ item.id }} - {{ item.value }} |
| 897 | + {% endfor %} |
| 898 | + {% else %} |
| 899 | + User: {{ user.name }} |
| 900 | + {% endif %} |
| 901 | + `; |
| 902 | + const result = validateTemplate(template); |
| 903 | + expect(result.isValid).toBe(true); |
| 904 | + expect(result.error).toBeUndefined(); |
| 905 | + }); |
| 906 | +}); |
| 907 | + |
| 908 | +describe('compilePrompt', () => { |
| 909 | + const baseMockVariables = { |
| 910 | + name: 'Tester', |
| 911 | + myObj: { legitProperty: 'hello' }, |
| 912 | + global: { version: '1.0' }, |
| 913 | + }; |
| 914 | + |
| 915 | + it('should compile a valid prompt', () => { |
| 916 | + const template = 'Hello {{ name }}!'; |
| 917 | + expect(() => compilePrompt(template, baseMockVariables)).not.toThrow(); |
| 918 | + expect(compilePrompt(template, baseMockVariables)).toBe('Hello Tester!'); |
| 919 | + }); |
| 920 | + |
| 921 | + it("should compile a prompt using 'global' as it was removed from blacklist by user", () => { |
| 922 | + const template = 'Version: {{ global.version }}'; |
| 923 | + expect(() => compilePrompt(template, baseMockVariables)).not.toThrow(); |
| 924 | + // Ensure the output matches the global.version from baseMockVariables |
| 925 | + expect(compilePrompt(template, baseMockVariables)).toBe('Version: 1.0'); |
| 926 | + }); |
| 927 | + |
| 928 | + it('should throw error for disallowed identifiers', () => { |
| 929 | + const template = 'Data: {{ process.env.SECRET }}'; |
| 930 | + expect(() => compilePrompt(template, baseMockVariables)).toThrow( |
| 931 | + "Security: Disallowed identifier 'process' found.", |
| 932 | + ); |
| 933 | + }); |
| 934 | + |
| 935 | + it('should throw error for disallowed property lookups', () => { |
| 936 | + const template = 'Access: {{ myObj.__proto__ }}'; |
| 937 | + expect(() => compilePrompt(template, baseMockVariables)).toThrow( |
| 938 | + "Security: Disallowed property lookup '__proto__' found.", |
| 939 | + ); |
| 940 | + }); |
| 941 | + |
| 942 | + it('should throw error for Nunjucks syntax errors which cause parse fail in ensureSecureAst', () => { |
| 943 | + const template = 'Hello {{ name !'; // Invalid Nunjucks syntax |
| 944 | + expect(() => compilePrompt(template, baseMockVariables)).toThrow( |
| 945 | + 'Template parsing failed: expected variable end', |
| 946 | + ); |
| 947 | + }); |
| 948 | + |
| 949 | + it("should allow mixed-case identifiers like 'Process' if not explicitly blacklisted (current check is case-sensitive)", () => { |
| 950 | + const template = 'Data: {{ Process.env.SECRET }}'; // 'Process' is not in DISALLOWED_IDENTIFIERS |
| 951 | + const result = validateTemplate(template); |
| 952 | + expect(result.isValid).toBe(true); |
| 953 | + expect(result.error).toBeUndefined(); |
| 954 | + |
| 955 | + const specificMockVariables = { |
| 956 | + ...baseMockVariables, |
| 957 | + Process: { env: { SECRET: 'oops' } }, |
| 958 | + }; |
| 959 | + expect(() => compilePrompt(template, specificMockVariables)).not.toThrow(); |
| 960 | + expect(compilePrompt(template, specificMockVariables)).toBe('Data: oops'); |
| 961 | + }); |
| 962 | + |
| 963 | + it('should allow deeply nested valid lookups', () => { |
| 964 | + const template = 'Deep: {{ a.b.c.d }}'; |
| 965 | + const vars = { ...baseMockVariables, a: { b: { c: { d: 'value' } } } }; |
| 966 | + expect(compilePrompt(template, vars)).toBe('Deep: value'); |
| 967 | + }); |
| 968 | +}); |
0 commit comments