File size: 9,706 Bytes
9e76c6a
 
 
 
 
 
ac645e6
9e76c6a
4e6b2a1
9e76c6a
 
 
 
821d359
9e76c6a
 
821d359
9e76c6a
821d359
9e76c6a
 
 
4e6b2a1
9e76c6a
 
 
95a10f9
 
 
 
 
 
 
821d359
 
 
9e76c6a
a0d9e71
9e76c6a
 
 
 
821d359
9e76c6a
 
 
 
 
 
 
 
 
a0d9e71
95a10f9
 
 
 
 
 
 
821d359
a0d9e71
5250b05
 
 
 
 
 
 
 
 
 
 
 
 
a0d9e71
9e76c6a
 
 
4e6b2a1
9e76c6a
4e6b2a1
9e76c6a
 
 
 
 
a0d9e71
9e76c6a
 
 
a0d9e71
9e76c6a
 
 
 
a0d9e71
9e76c6a
 
 
 
a0d9e71
9e76c6a
 
 
 
 
a0d9e71
9e76c6a
 
 
 
 
 
a0d9e71
9e76c6a
 
 
 
 
 
 
 
4e6b2a1
9e76c6a
 
77bae86
9e76c6a
 
 
a0d9e71
9e76c6a
a0d9e71
9e76c6a
 
 
 
 
 
 
 
 
 
5788ee2
5250b05
ac645e6
d8b5f3a
 
5250b05
 
 
 
8552c92
5250b05
8552c92
5250b05
 
 
 
 
1f02084
821d359
 
ac645e6
5788ee2
9e76c6a
5788ee2
a0d9e71
5788ee2
 
5250b05
a0d9e71
9e76c6a
 
5788ee2
9e76c6a
 
 
a0d9e71
 
 
 
5788ee2
a0d9e71
9e76c6a
 
4e6b2a1
5788ee2
 
 
 
95a10f9
9e76c6a
 
 
5250b05
9e76c6a
 
 
 
 
 
5788ee2
9e76c6a
 
 
 
5788ee2
4e6b2a1
 
 
 
 
 
ac645e6
4e6b2a1
 
 
63850a8
4e6b2a1
63850a8
4e6b2a1
9e76c6a
 
 
4e6b2a1
 
5788ee2
 
5250b05
5788ee2
4e6b2a1
5788ee2
5250b05
4e6b2a1
9e76c6a
 
 
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import gradio as gr
import pandas as pd
import requests
import json
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
import io, os

# Load semantic similarity model once
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')

def calculate_semantic_similarity(text1: str, text2: str) -> float:
    embeddings = model.encode([text1, text2])
    return float(cosine_similarity([embeddings[0]], [embeddings[1]])[0][0])

def is_empty_response(response):
    return response is None or (isinstance(response, str) and response.strip() in ['', 'null', 'undefined'])

def validate_json_response(expected, actual):
    try:
        expected_json = json.loads(expected) if isinstance(expected, str) else expected
        actual_json = json.loads(actual) if isinstance(actual, str) else actual
        return expected_json == actual_json, "Exact match" if expected_json == actual_json else "Mismatch"
    except Exception as e:
        return False, f"Invalid JSON: {str(e)}"

def check_connectivity(endpoint, headers):
    try:
        resp = requests.head(endpoint, headers=headers, timeout=5)
        return True, resp.status_code
    except requests.exceptions.RequestException as e:
        return False, str(e)

def run_single_test(endpoint, method, bearer_token,
                    expected_status, expected_body,
                    payload=None, test_name=None):
    result = {
        'TestName': test_name,
        'Endpoint': endpoint,
        'Method': method,
        'Expected Status': expected_status,
        'Actual Status': None,
        'Expected Body': expected_body[:100] + '...' if isinstance(expected_body, str) and len(expected_body) > 100 else expected_body,
        'Actual Body': None,
        'Outcome': 'FAIL',
        'Similarity': None,
        'Notes': ''
    }
    try:
        headers = {}
        if bearer_token and bearer_token.strip():
            headers['Authorization'] = f'Bearer {bearer_token.strip()}'

        # Connectivity pre-check
        reachable, info = check_connectivity(endpoint, headers)
        if not reachable:
            result['Outcome'] = 'UNREACHABLE'
            result['Notes'] = f'Cannot reach endpoint: {info}'
            return result

        data, json_payload = None, None
        if payload and isinstance(payload, str) and payload.strip():
            if payload.lower().startswith("header:"):
                try:
                    kv = payload.split(":",1)[1]
                    key, value = kv.split("=",1)
                    headers[key.strip()] = value.strip().strip('"')
                except Exception:
                    result['Notes'] = f'Invalid header format: {payload}'
                    return result
            else:
                try:
                    json_payload = json.loads(payload)
                except Exception:
                    data = payload

        if method.upper() == 'GET':
            response = requests.get(endpoint, headers=headers, timeout=10)
        elif method.upper() == 'POST':
            response = requests.post(endpoint, headers=headers, data=data, json=json_payload, timeout=10)
        elif method.upper() == 'PUT':
            response = requests.put(endpoint, headers=headers, data=data, json=json_payload, timeout=10)
        elif method.upper() == 'DELETE':
            response = requests.delete(endpoint, headers=headers, timeout=10)
        else:
            result['Notes'] = f'Unsupported method: {method}'
            return result

        result['Actual Status'] = response.status_code
        actual_body = response.text
        result['Actual Body'] = actual_body[:100] + '...' if len(actual_body) > 100 else actual_body

        if response.status_code != expected_status:
            result['Outcome'] = 'FAIL'
            result['Notes'] = f'Status mismatch: expected {expected_status}, got {response.status_code}'
            return result

        if not expected_body or is_empty_response(expected_body):
            result['Outcome'] = 'PASS'
            result['Notes'] = 'Status matches, no body validation'
            return result

        if is_empty_response(actual_body):
            result['Outcome'] = 'UNCERTAIN'
            result['Similarity'] = 0.0
            result['Notes'] = 'Response is empty (manual review needed)'
            return result

        json_match, _ = validate_json_response(expected_body, actual_body)
        if json_match:
            result['Outcome'] = 'PASS'
            result['Similarity'] = 1.0
            result['Notes'] = 'Exact JSON match'
            return result

        try:
            similarity = calculate_semantic_similarity(expected_body, actual_body)
            result['Similarity'] = round(similarity, 3)
            if similarity >= 0.75:
                result['Outcome'] = 'PASS'
                result['Notes'] = f'High semantic similarity: {similarity:.1%}'
            elif similarity >= 0.5:
                result['Outcome'] = 'UNCERTAIN'
                result['Notes'] = f'Medium similarity: {similarity:.1%} (review needed)'
            else:
                result['Outcome'] = 'UNCERTAIN'
                result['Notes'] = f'Low similarity: {similarity:.1%}'
        except Exception as sim_error:
            result['Outcome'] = 'UNCERTAIN'
            result['Notes'] = f'Cannot compute similarity: {str(sim_error)}'

        return result

    except requests.exceptions.Timeout:
        result['Notes'] = 'Request timeout'
        return result
    except requests.exceptions.ConnectionError:
        result['Notes'] = 'Connection error'
        return result
    except Exception as e:
        result['Notes'] = f'Error: {str(e)}'
        return result

def normalize_to_csv(file):
    filename = file if isinstance(file, str) else getattr(file, "name", None)
    ext = os.path.splitext(filename)[1].lower()
    try:
        if ext in [".xlsx", ".xlsm", ".xls"]:
            df = pd.read_excel(file, engine="openpyxl")
        elif ext == ".csv":
            df = pd.read_csv(file, encoding="utf-8")
        elif ext in [".tsv", ".txt"]:
            try:
                df = pd.read_csv(file, sep="\t", encoding="utf-8")
            except UnicodeDecodeError:
                df = pd.read_csv(file, sep="\t", encoding="latin1")
        else:
            raise ValueError(f"Unsupported file type: {ext}")
    except Exception as e:
        raise RuntimeError(f"Error reading file: {e}")

    df.columns = [c.strip() for c in df.columns]
    return df

def process_file(file, bearer_token: str):
    try:
        df = normalize_to_csv(file)
        required_columns = ['Endpoint', 'Method', 'ExpectedStatus', 'ExpectedBody']
        missing = [c for c in required_columns if c not in df.columns]
        if missing:
            return pd.DataFrame([{"Outcome":"ERROR","Notes":f"Missing columns: {', '.join(missing)}"}]), f"❌ Missing columns: {', '.join(missing)}"

        results = []
        for _, row in df.iterrows():
            results.append(run_single_test(
                endpoint=str(row['Endpoint']),
                method=str(row['Method']),
                bearer_token=bearer_token,
                expected_status=int(row['ExpectedStatus']),
                expected_body=str(row['ExpectedBody']),
                payload=row.get('Payload', None),
                test_name=row.get('TestName', None)
            ))

        results_df = pd.DataFrame(results)
        summary = f"""
        ### πŸ“Š Test Summary
        - Total Tests: {len(results_df)}
        - βœ… Passed: {len(results_df[results_df['Outcome']=="PASS"])}
        - ❌ Failed: {len(results_df[results_df['Outcome']=="FAIL"])}
        - ⚠️ Uncertain: {len(results_df[results_df['Outcome']=="UNCERTAIN"])}
        - 🚫 Unreachable: {len(results_df[results_df['Outcome']=="UNREACHABLE"])}
        """
        return results_df, summary
    except Exception as e:
        return pd.DataFrame([{"Outcome":"ERROR","Notes":str(e)}]), f"❌ Error processing file: {str(e)}"

def download_results(df):
    if df is None or df.empty:
        return None
    output = io.BytesIO()
    with pd.ExcelWriter(output, engine='openpyxl') as writer:
        df.to_excel(writer, index=False, sheet_name="Test Results")
    output.seek(0)
    return output

# Gradio UI
with gr.Blocks(css="* { font-family: 'Times New Roman', serif; }") as app:
    gr.Markdown("## πŸš€ API Test Runner with AI-Powered Validation")

    token_state = gr.State("")

    with gr.Tab("Run Tests"):
        with gr.Group():
            file_input = gr.File(label="πŸ“‚ Upload Test Cases", file_types=['.xlsx', '.xls', '.csv', '.tsv', '.txt'])
            token_input = gr.Textbox(label="πŸ”‘ Bearer Token", type="password")
            run_button = gr.Button("▢️ Run Tests", variant="primary")

        with gr.Tab("Results"):
        summary_output = gr.Markdown()
        results_output = gr.Dataframe(
            headers=['TestName','Endpoint','Method','Expected Status','Actual Status','Outcome','Similarity','Notes'],
            interactive=False
        )
        download_button = gr.Button("πŸ’Ύ Download Results")
        download_output = gr.File()

    def run_with_feedback(file, token):
        df, summary = process_file(file, token)
        return df, summary

    token_input.change(fn=lambda t: t, inputs=token_input, outputs=token_state)
    run_button.click(fn=run_with_feedback, inputs=[file_input, token_state],
                     outputs=[results_output, summary_output])
    download_button.click(fn=download_results, inputs=[results_output], outputs=[download_output])

if __name__ == "__main__":
    app.launch()