mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-08 00:16:54 +02:00
added API Sender test variables
Signed-off-by: RonniSkansing <rskansing@gmail.com>
This commit is contained in:
6 files changed
+384
-32
No files matched your search
@@ -182,10 +182,16 @@ func (a *APISender) SendTest(g *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// parse the optional test values, an empty body means use the defaults
|
||||
var req model.APISenderTestRequest
|
||||
if ok := a.handleParseOptionalRequest(g, &req); !ok {
|
||||
return
|
||||
}
|
||||
data, err := a.APISenderService.SendTest(
|
||||
g.Request.Context(),
|
||||
session,
|
||||
id,
|
||||
&req,
|
||||
)
|
||||
// output the error
|
||||
if err != nil {
|
||||
|
||||
@@ -83,6 +83,26 @@ func (c *Common) handleGlobalAuthorization(
|
||||
func (c *Common) handleParseRequest(
|
||||
g *gin.Context,
|
||||
req any,
|
||||
) bool {
|
||||
return c.parseRequest(g, req, false)
|
||||
}
|
||||
|
||||
// handleParseOptionalRequest parses a request body like handleParseRequest but
|
||||
// treats an empty body as no input, leaving req at its zero value. Use it for
|
||||
// endpoints where every field is optional.
|
||||
func (c *Common) handleParseOptionalRequest(
|
||||
g *gin.Context,
|
||||
req any,
|
||||
) bool {
|
||||
return c.parseRequest(g, req, true)
|
||||
}
|
||||
|
||||
// parseRequest reads and unmarshals the request body. When allowEmpty is true an
|
||||
// empty body is accepted and leaves req untouched.
|
||||
func (c *Common) parseRequest(
|
||||
g *gin.Context,
|
||||
req any,
|
||||
allowEmpty bool,
|
||||
) bool {
|
||||
body, err := io.ReadAll(g.Request.Body)
|
||||
if err != nil {
|
||||
@@ -92,6 +112,9 @@ func (c *Common) handleParseRequest(
|
||||
c.Response.BadRequest(g)
|
||||
return false
|
||||
}
|
||||
if allowEmpty && len(bytes.TrimSpace(body)) == 0 {
|
||||
return true
|
||||
}
|
||||
if err := utils.Unmarshal(body, &req); err != nil {
|
||||
c.Logger.Debugw("failed to parse request",
|
||||
"error", err,
|
||||
|
||||
@@ -37,6 +37,25 @@ type APISender struct {
|
||||
ExpectedResponseBody nullable.Nullable[vo.OptionalString1MB] `json:"expectedResponseBody"`
|
||||
}
|
||||
|
||||
// APISenderTestRequest holds optional values the user sets when running a
|
||||
// test send. A nil or empty field falls back to the built in default.
|
||||
type APISenderTestRequest struct {
|
||||
Email *string `json:"email"`
|
||||
FirstName *string `json:"firstName"`
|
||||
LastName *string `json:"lastName"`
|
||||
Subject *string `json:"subject"`
|
||||
Content *string `json:"content"`
|
||||
FromEmail *string `json:"fromEmail"`
|
||||
FromName *string `json:"fromName"`
|
||||
Phone *string `json:"phone"`
|
||||
ExtraIdentifier *string `json:"extraIdentifier"`
|
||||
Position *string `json:"position"`
|
||||
Department *string `json:"department"`
|
||||
City *string `json:"city"`
|
||||
Country *string `json:"country"`
|
||||
Misc *string `json:"misc"`
|
||||
}
|
||||
|
||||
// Validate checks if the API sender has a valid state
|
||||
func (a *APISender) Validate() error {
|
||||
if err := validate.NullableFieldRequired("name", a.Name); err != nil {
|
||||
|
||||
+150
-18
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
@@ -400,6 +401,7 @@ func (a *APISender) SendTest(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
id *uuid.UUID,
|
||||
req *model.APISenderTestRequest,
|
||||
) (*APISenderTestResponse, error) {
|
||||
ae := NewAuditEvent("ApiSender.SendTest", session)
|
||||
ae.Details["id"] = id.String()
|
||||
@@ -436,26 +438,156 @@ func (a *APISender) SendTest(
|
||||
oauthAccessToken = token
|
||||
a.Logger.Debugw("got oauth access token for api test request", "oauthProviderID", oauthProviderID)
|
||||
}
|
||||
emailRaw := "bob@enterprise.test"
|
||||
email := *vo.NewEmailMust(emailRaw)
|
||||
// resolve the test values, a nil or empty override falls back to the default
|
||||
if req == nil {
|
||||
req = &model.APISenderTestRequest{}
|
||||
}
|
||||
const (
|
||||
defFromEmail = "bob@enterprise.test"
|
||||
defFromName = "Bob"
|
||||
defEmail = "bob@enterprise.test"
|
||||
defFirstName = "Bob"
|
||||
defLastName = "Test"
|
||||
defSubject = "Test Email Subject"
|
||||
defContent = "Hi {{.FirstName}},\n\nThis is a test email.\n\nBest,\nBob"
|
||||
defPhone = "+1234567890"
|
||||
defExtraID = "extra-test-identifier"
|
||||
defPosition = "Lead API Tester"
|
||||
defDept = "Research and Development"
|
||||
defCity = "Odin"
|
||||
defCountry = "Denmark"
|
||||
defMisc = "This is a test recipient"
|
||||
)
|
||||
pick := func(p *string, def string) string {
|
||||
if p != nil && *p != "" {
|
||||
return *p
|
||||
}
|
||||
return def
|
||||
}
|
||||
// trim the from fields, they are composed into one header below where
|
||||
// surrounding whitespace would otherwise fail validation
|
||||
fromEmailRaw := strings.TrimSpace(pick(req.FromEmail, defFromEmail))
|
||||
fromNameRaw := strings.TrimSpace(pick(req.FromName, defFromName))
|
||||
recipientEmailRaw := pick(req.Email, defEmail)
|
||||
firstNameRaw := pick(req.FirstName, defFirstName)
|
||||
lastNameRaw := pick(req.LastName, defLastName)
|
||||
subjectRaw := pick(req.Subject, defSubject)
|
||||
contentRaw := pick(req.Content, defContent)
|
||||
phoneRaw := pick(req.Phone, defPhone)
|
||||
extraIdentifierRaw := pick(req.ExtraIdentifier, defExtraID)
|
||||
positionRaw := pick(req.Position, defPosition)
|
||||
departmentRaw := pick(req.Department, defDept)
|
||||
cityRaw := pick(req.City, defCity)
|
||||
countryRaw := pick(req.Country, defCountry)
|
||||
miscRaw := pick(req.Misc, defMisc)
|
||||
|
||||
recipientEmail, err := vo.NewEmail(recipientEmailRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("email: %w", err))
|
||||
}
|
||||
envelopeFrom, err := vo.NewMailEnvelopeFrom(fromEmailRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("from email: %w", err))
|
||||
}
|
||||
// build the header via mail.Address so a display name with a comma or other
|
||||
// special character is quoted correctly, fromEmail is already validated above
|
||||
headerFromRaw := (&mail.Address{Name: fromNameRaw, Address: fromEmailRaw}).String()
|
||||
headerFrom, err := vo.NewEmail(headerFromRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("from name or from email: %w", err))
|
||||
}
|
||||
subject, err := vo.NewOptionalString255(subjectRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("subject: %w", err))
|
||||
}
|
||||
content, err := vo.NewOptionalString1MB(contentRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("content: %w", err))
|
||||
}
|
||||
firstName, err := vo.NewOptionalString127(firstNameRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("first name: %w", err))
|
||||
}
|
||||
lastName, err := vo.NewOptionalString127(lastNameRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("last name: %w", err))
|
||||
}
|
||||
phone, err := vo.NewOptionalString127(phoneRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("phone: %w", err))
|
||||
}
|
||||
extraIdentifier, err := vo.NewOptionalString127(extraIdentifierRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("extra identifier: %w", err))
|
||||
}
|
||||
position, err := vo.NewOptionalString127(positionRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("position: %w", err))
|
||||
}
|
||||
department, err := vo.NewOptionalString127(departmentRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("department: %w", err))
|
||||
}
|
||||
city, err := vo.NewOptionalString127(cityRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("city: %w", err))
|
||||
}
|
||||
country, err := vo.NewOptionalString127(countryRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("country: %w", err))
|
||||
}
|
||||
misc, err := vo.NewOptionalString127(miscRaw)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(fmt.Errorf("misc: %w", err))
|
||||
}
|
||||
|
||||
// record which values differ from the default, names only to avoid logging content
|
||||
isOverride := func(p *string, def string) bool {
|
||||
return p != nil && strings.TrimSpace(*p) != "" && strings.TrimSpace(*p) != def
|
||||
}
|
||||
overridden := []string{}
|
||||
for _, f := range []struct {
|
||||
name string
|
||||
p *string
|
||||
def string
|
||||
}{
|
||||
{"fromEmail", req.FromEmail, defFromEmail},
|
||||
{"fromName", req.FromName, defFromName},
|
||||
{"email", req.Email, defEmail},
|
||||
{"firstName", req.FirstName, defFirstName},
|
||||
{"lastName", req.LastName, defLastName},
|
||||
{"subject", req.Subject, defSubject},
|
||||
{"content", req.Content, defContent},
|
||||
{"phone", req.Phone, defPhone},
|
||||
{"extraIdentifier", req.ExtraIdentifier, defExtraID},
|
||||
{"position", req.Position, defPosition},
|
||||
{"department", req.Department, defDept},
|
||||
{"city", req.City, defCity},
|
||||
{"country", req.Country, defCountry},
|
||||
{"misc", req.Misc, defMisc},
|
||||
} {
|
||||
if isOverride(f.p, f.def) {
|
||||
overridden = append(overridden, f.name)
|
||||
}
|
||||
}
|
||||
ae.Details["overridden"] = strings.Join(overridden, ",")
|
||||
|
||||
cid := nullable.NewNullableWithValue(uuid.New())
|
||||
testEmail := &model.Email{
|
||||
Name: nullable.NewNullableWithValue(
|
||||
*vo.NewString64Must("Test Email"),
|
||||
),
|
||||
MailEnvelopeFrom: nullable.NewNullableWithValue(
|
||||
*vo.NewMailEnvelopeFromMust(emailRaw),
|
||||
*envelopeFrom,
|
||||
),
|
||||
MailHeaderFrom: nullable.NewNullableWithValue(
|
||||
*vo.NewEmailMust(
|
||||
fmt.Sprintf("Bob <%s>", emailRaw),
|
||||
),
|
||||
*headerFrom,
|
||||
),
|
||||
MailHeaderSubject: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString255Must("Test Email Subject"),
|
||||
*subject,
|
||||
),
|
||||
Content: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString1MBMust("Hi {{.FirstName}},\n\nThis is a test email.\n\nBest,\nBob"),
|
||||
*content,
|
||||
),
|
||||
AddTrackingPixel: nullable.NewNullableWithValue(false),
|
||||
}
|
||||
@@ -464,34 +596,34 @@ func (a *APISender) SendTest(
|
||||
Recipient: &model.Recipient{
|
||||
ID: cid,
|
||||
Email: nullable.NewNullableWithValue(
|
||||
email,
|
||||
*recipientEmail,
|
||||
),
|
||||
Phone: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("+1234567890"),
|
||||
*phone,
|
||||
),
|
||||
ExtraIdentifier: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("extra-test-identifier"),
|
||||
*extraIdentifier,
|
||||
),
|
||||
FirstName: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Bob"),
|
||||
*firstName,
|
||||
),
|
||||
LastName: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Test"),
|
||||
*lastName,
|
||||
),
|
||||
Position: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Lead API Tester"),
|
||||
*position,
|
||||
),
|
||||
Department: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Research and Development"),
|
||||
*department,
|
||||
),
|
||||
City: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Odin"),
|
||||
*city,
|
||||
),
|
||||
Country: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("Denmark"),
|
||||
*country,
|
||||
),
|
||||
Misc: nullable.NewNullableWithValue(
|
||||
*vo.NewOptionalString127Must("This is a test recipient"),
|
||||
*misc,
|
||||
),
|
||||
Company: &model.Company{
|
||||
Name: nullable.NewNullableWithValue(
|
||||
|
||||
@@ -3203,10 +3203,11 @@ export class API {
|
||||
/**
|
||||
* Send a test request to an API sender.
|
||||
* @param {string} id
|
||||
* @param {object} [values] optional test values, empty fields use defaults
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
test: async (id) => {
|
||||
return await postJSON(this.getPath(`/api-sender/${id}/test`));
|
||||
test: async (id, values = {}) => {
|
||||
return await postJSON(this.getPath(`/api-sender/${id}/test`), values);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
import { fetchAllRows } from '$lib/utils/api-utils';
|
||||
import TextFieldSelect from '$lib/components/TextFieldSelect.svelte';
|
||||
import VimToggle from '$lib/components/editor/VimToggle.svelte';
|
||||
import FormButton from '$lib/components/FormButton.svelte';
|
||||
|
||||
// services
|
||||
const appStateService = AppStateService.instance;
|
||||
@@ -107,6 +108,68 @@
|
||||
let testResponse = {};
|
||||
|
||||
let isTestModalVisible = false;
|
||||
let isTestSubmitting = false;
|
||||
let selectedTestID = null;
|
||||
|
||||
// defaults mirror the backend test defaults, empty fields fall back to them
|
||||
const testValuesDefault = () => ({
|
||||
fromEmail: 'bob@enterprise.test',
|
||||
fromName: 'Bob',
|
||||
email: 'bob@enterprise.test',
|
||||
firstName: 'Bob',
|
||||
lastName: 'Test',
|
||||
subject: 'Test Email Subject',
|
||||
content: 'Hi {{.FirstName}},\n\nThis is a test email.\n\nBest,\nBob'
|
||||
});
|
||||
let testValues = testValuesDefault();
|
||||
|
||||
// optional recipient variables the user can add to a test on demand
|
||||
const extraVariableDefs = [
|
||||
{ key: 'phone', label: 'Phone', variable: '{{.Phone}}', value: '+1234567890' },
|
||||
{
|
||||
key: 'extraIdentifier',
|
||||
label: 'Extra Identifier',
|
||||
variable: '{{.ExtraIdentifier}}',
|
||||
value: 'extra-test-identifier'
|
||||
},
|
||||
{ key: 'position', label: 'Position', variable: '{{.Position}}', value: 'Lead API Tester' },
|
||||
{
|
||||
key: 'department',
|
||||
label: 'Department',
|
||||
variable: '{{.Department}}',
|
||||
value: 'Research and Development'
|
||||
},
|
||||
{ key: 'city', label: 'City', variable: '{{.City}}', value: 'Odin' },
|
||||
{ key: 'country', label: 'Country', variable: '{{.Country}}', value: 'Denmark' },
|
||||
{ key: 'misc', label: 'Misc', variable: '{{.Misc}}', value: 'This is a test recipient' }
|
||||
];
|
||||
let extraOverrides = [];
|
||||
let selectedExtra = '';
|
||||
// remembers an edited value so a removed variable keeps it when added again
|
||||
let extraValueCache = {};
|
||||
$: availableExtras = extraVariableDefs
|
||||
.filter((d) => !extraOverrides.some((o) => o.key === d.key))
|
||||
.map((d) => ({ value: d.key, label: d.label }));
|
||||
|
||||
/** @param {string} key */
|
||||
const addExtra = (key) => {
|
||||
const def = extraVariableDefs.find((d) => d.key === key);
|
||||
if (!def || extraOverrides.some((o) => o.key === key)) {
|
||||
return;
|
||||
}
|
||||
const value = key in extraValueCache ? extraValueCache[key] : def.value;
|
||||
extraOverrides = [...extraOverrides, { ...def, value }];
|
||||
selectedExtra = '';
|
||||
};
|
||||
|
||||
/** @param {string} key */
|
||||
const removeExtra = (key) => {
|
||||
const existing = extraOverrides.find((o) => o.key === key);
|
||||
if (existing) {
|
||||
extraValueCache[key] = existing.value;
|
||||
}
|
||||
extraOverrides = extraOverrides.filter((o) => o.key !== key);
|
||||
};
|
||||
|
||||
$: {
|
||||
modalText = getModalText('api sender', modalMode);
|
||||
@@ -318,32 +381,44 @@
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
const openTestModal = async (id) => {
|
||||
const openTestModal = (id) => {
|
||||
selectedTestID = id;
|
||||
testResponse = {};
|
||||
testValues = testValuesDefault();
|
||||
extraOverrides = [];
|
||||
extraValueCache = {};
|
||||
selectedExtra = '';
|
||||
isTestModalVisible = true;
|
||||
};
|
||||
|
||||
const runTest = async () => {
|
||||
try {
|
||||
showIsLoading();
|
||||
const res = await api.apiSender.test(id);
|
||||
isTestSubmitting = true;
|
||||
const payload = { ...testValues };
|
||||
for (const o of extraOverrides) {
|
||||
payload[o.key] = o.value;
|
||||
}
|
||||
const res = await api.apiSender.test(selectedTestID, payload);
|
||||
if (!res.success) {
|
||||
const res2 = await api.apiSender.getByID(id);
|
||||
const res2 = await api.apiSender.getByID(selectedTestID);
|
||||
if (!res2.success) {
|
||||
throw res2.error;
|
||||
}
|
||||
testResponse.apiSender = res2.data;
|
||||
testResponse.error = res.error;
|
||||
isTestModalVisible = true;
|
||||
testResponse = { apiSender: res2.data, error: res.error };
|
||||
return;
|
||||
}
|
||||
testResponse = res.data;
|
||||
isTestModalVisible = true;
|
||||
} catch (e) {
|
||||
addToast('Failed to test API sender', 'Error');
|
||||
console.error('failed to test API sender:', e);
|
||||
} finally {
|
||||
hideIsLoading();
|
||||
isTestSubmitting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closeTestModal = () => {
|
||||
testResponse = {};
|
||||
selectedTestID = null;
|
||||
isTestModalVisible = false;
|
||||
};
|
||||
|
||||
@@ -664,11 +739,106 @@ X-Custom-Header: Hello Friend"
|
||||
</FormGrid>
|
||||
</Modal>
|
||||
|
||||
<Modal headerText="API Sender Test Results" visible={isTestModalVisible} onClose={closeTestModal}>
|
||||
<Modal headerText="API Sender Test" visible={isTestModalVisible} onClose={closeTestModal}>
|
||||
<div
|
||||
class="col-span-3 w-full overflow-y-auto px-6 py-4 space-y-6 select-text overflow-x-hidden"
|
||||
class="col-span-3 w-[800px] max-w-full overflow-y-auto px-6 py-4 space-y-6 select-text overflow-x-hidden"
|
||||
>
|
||||
{#if !testResponse.error}
|
||||
<!-- Test values, empty fields fall back to the defaults -->
|
||||
<form on:submit|preventDefault={runTest}>
|
||||
<div class="pt-4 pb-2 w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">Test values</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<TextField
|
||||
type="email"
|
||||
width="full"
|
||||
maxLength={254}
|
||||
bind:value={testValues.fromEmail}
|
||||
toolTipText={'Sent as {{.FromEmail}}'}
|
||||
placeholder="bob@enterprise.test">From Email</TextField
|
||||
>
|
||||
<TextField
|
||||
width="full"
|
||||
maxLength={128}
|
||||
bind:value={testValues.fromName}
|
||||
toolTipText={'Sent as {{.FromName}}'}
|
||||
placeholder="Bob">From Name</TextField
|
||||
>
|
||||
<TextField
|
||||
type="email"
|
||||
width="full"
|
||||
maxLength={254}
|
||||
bind:value={testValues.email}
|
||||
toolTipText={'Sent as {{.Email}}'}
|
||||
placeholder="bob@enterprise.test">Email</TextField
|
||||
>
|
||||
<TextField
|
||||
width="full"
|
||||
maxLength={127}
|
||||
bind:value={testValues.firstName}
|
||||
toolTipText={'Sent as {{.FirstName}}'}
|
||||
placeholder="Bob">First Name</TextField
|
||||
>
|
||||
<TextField
|
||||
width="full"
|
||||
maxLength={127}
|
||||
bind:value={testValues.lastName}
|
||||
toolTipText={'Sent as {{.LastName}}'}
|
||||
placeholder="Test">Last Name</TextField
|
||||
>
|
||||
<TextField
|
||||
width="full"
|
||||
maxLength={255}
|
||||
bind:value={testValues.subject}
|
||||
toolTipText={'Sent as {{.Subject}}'}
|
||||
placeholder="Test Email Subject">Subject</TextField
|
||||
>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<TextareaField
|
||||
fullWidth
|
||||
height={'medium'}
|
||||
bind:value={testValues.content}
|
||||
toolTipText={'Sent as {{.Content}}'}>Content</TextareaField
|
||||
>
|
||||
</div>
|
||||
<!-- optional recipient variables added on demand -->
|
||||
<div class="mt-4 space-y-3">
|
||||
{#each extraOverrides as extra (extra.key)}
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex-1">
|
||||
<TextField
|
||||
width="full"
|
||||
maxLength={127}
|
||||
bind:value={extra.value}
|
||||
toolTipText={'Sent as ' + extra.variable}>{extra.label}</TextField
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="mb-3 px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-red-600"
|
||||
on:click={() => removeExtra(extra.key)}
|
||||
aria-label={'Remove ' + extra.label}>Remove</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
{#if availableExtras.length > 0}
|
||||
<TextFieldSelect
|
||||
id="addTestVariable"
|
||||
placeholder="Add another variable..."
|
||||
bind:value={selectedExtra}
|
||||
options={availableExtras}
|
||||
optional={true}
|
||||
onSelect={addExtra}>Add variable</TextFieldSelect
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<FormButton isSubmitting={isTestSubmitting}>Run test</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{#if testResponse.request || testResponse.error}
|
||||
{#if !testResponse.error}
|
||||
<!-- Successful Test -->
|
||||
<div class="mb-6 pt-4 pb-2 border-b border-gray-200 dark:border-gray-600 w-full">
|
||||
<h3 class="text-base font-medium text-pc-darkblue dark:text-white mb-3">
|
||||
@@ -819,6 +989,7 @@ X-Custom-Header: Hello Friend"
|
||||
<div class="text-red-600 whitespace-pre-wrap break-words">{testResponse.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Footer with Close Button -->
|
||||
|
||||
Reference in new issue
Block a user