Jump Into Our API Testing Adventure – $250 Prize + Swag for first 15 submissions

june-community-challenge

My submission shows the simplicity of using the new packages feature instead of the old methods that leveraged sendRequest().

Using moment, as dates and time zones questions pop up quite frequently on the forum.

First of all, the old method..

if (!pm.globals.has("moment_js") || !pm.globals.has("moment_tz")) {
    pm.sendRequest("https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data-10-year-range.js", (mtzErr, mtzRes) => {
        pm.sendRequest("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js", (mjsErr, mjsRes) => {
            pm.globals.set("moment_js", mjsRes.text());
            // console.log(pm.globals.get("moment_js"));
            pm.globals.set("moment_tz", mtzRes.text());
            // console.log(pm.globals.get("moment_tz"));
        })
    })
}

(new Function(pm.globals.get("moment_js")))();
(new Function(pm.globals.get("moment_tz")))();

console.log(moment.tz("2024-06-26T16:55:00", "America/New_York").format("YYYY-MM-DDTHH:mm:ss ZZ"));
console.log(moment.tz("2024-12-26T16:55:00", "America/New_York").format("YYYY-MM-DDTHH:mm:ss ZZ"));

Then the new method using pm.require.

// Note: Using this node.js method, you don’t need to require/import the base moment library as well. Moment Timezone will automatically load and extend the moment module, then return the modified instance.

const moment = pm.require('npm:[email protected]');

// Using the examples from the https://momentjs.com/timezone/ website and logging them to the Postman Console

// Format Dates in Any Timezone

let jun = moment("2024-06-01T12:00:00Z");
let dec = moment("2024-12-01T12:00:00Z");

console.log(jun);
console.log(dec);

console.log(jun.tz('America/Los_Angeles').format('ha z'));  // 5am PDT
console.log(dec.tz('America/Los_Angeles').format('ha z'));  // 4am PST

console.log(jun.tz('America/New_York').format('ha z'));     // 8am EDT
console.log(dec.tz('America/New_York').format('ha z'));     // 7am EST

console.log(jun.tz('Asia/Tokyo').format('ha z'));           // 9pm JST
console.log(dec.tz('Asia/Tokyo').format('ha z'));           // 9pm JST

console.log(jun.tz('Australia/Sydney').format('ha z'));     // 10pm AEST
console.log(dec.tz('Australia/Sydney').format('ha z'));     // 11pm AEDT

// Convert Dates Between Timezones

let newYork    = moment.tz("2024-06-01 12:00", "America/New_York");
let losAngeles = newYork.clone().tz("America/Los_Angeles");
let london     = newYork.clone().tz("Europe/London");

console.log(newYork.format());    // 2024-06-01T12:00:00-04:00
console.log(losAngeles.format()); // 2024-06-01T09:00:00-07:00
console.log(london.format());     // 2024-06-01T17:00:00+01:00

All of the previous code for importing the external library has basically been replaced with a one liner.

Finally, those console logs to ensure that the data being returned is correct.

1 Like

Hey Folks :wave:,

Just a quick reminder: to be eligible for the top prize of $250 and to snag some amazing Postman swag, you need to fully complete the challenge.

Posting your solution in the Discourse thread is only part of the challenge.

  1. Complete the tasks in the Postman collection, forked from the Public Workspace.
  2. Share your awesome solution in this Discourse thread.
  3. Submit your entry on the form

Only participants who complete all steps will be entered into the prize draw. The first 15 completed submissions will win swag, and one standout entry will take home the $250 grand prize :money_bag:

Make sure your submission is complete to be in the running! :raising_hands:

Any issues with your submissions, please drop me a message! Always on hand to help :heart:

3 Likes

Hey Postman Community! :waving_hand:

I’m Shreya Prashant Langote, and this is my improved and final submission for the June API Testing Challenge! :hammer_and_wrench:

During my first submission, I received an email saying that some tasks were incomplete :cross_mark:—specifically in:

Folder 1: Get Started with API Testing
Folder 5: External Packages

So, I went back, re-read all instructions carefully, fixed the issues, and made sure everything was done as required. :white_check_mark:
Final Tasks Completed:

:white_check_mark: Folder 1: Completed all script validations and API chaining
:white_check_mark: Folder 2: Used full JSON Schema for response validation
:white_check_mark: Folder 3: Implemented “Refresh Results” and pm.test.skip
:white_check_mark: Folder 4: Used shared reusable scripts from the Package Library
:white_check_mark: Folder 5: Created a custom request using external NPM packages

Used Chance to generate dynamic test data
Used Day.js to manipulate and calculate time differences
Stored values in collection variables and validated them
This challenge helped me understand not just how to test APIs but also how to improve and debug my scripts when something fails. :brain::sparkles:

Thank you Postman Team for the feedback and this awesome learning opportunity! :yellow_heart:

1 Like

Hi everyone! :waving_hand:

For this challenge, I created a JWT Inspector Toolkit, plug-and-play script that helps developers decode, validate and inspect JSON Web Tokens directly within Postman. It’s not just for me but it’s meant to be used by anyone working with JWTs. It performs:

  • :white_check_mark: Signature checks
  • :white_check_mark: Schema validation (built-in or custom)
  • :white_check_mark: Expiration insights
  • :white_check_mark: Optional issuer (iss) verification
  • :white_check_mark: Also comes with a documentation.

:package: External Packages Used

  1. jsonwebtoken – for decoding and signature verification
  2. joi – for schema-based payload validation
  3. moment– for human-readable expiration insight All logic is encapsulated in a single request

Everything runs inside one request using test scripts and environment variables, no setup required.


:counterclockwise_arrows_button: Update: Added JWT Visualizer :artist_palette:

After seeing @danny-dainton clean UI using Postman Visualizer, I decided to bring that to my JWT Inspector and of course, sprinkle in some CSS magic of my own to make it a playful and colorful visualization :sparkles:
I structured the data like this:

const visualizationData = {
  header: decoded?.header || {},
  payload: payload || {},
  validation: validation || { valid: false },
  schemaType: schemaType,
  timestamp: new Date().toISOString(),
  expInfo: payload?.exp ? TestUtils.getExpirationInfo(payload.exp) : null
};

pm.visualizer.set(htmlTemplate, visualizationData);

:magnifying_glass_tilted_left: Preview:
Screencast from 2025-06-26 15-37-24

:page_facing_up: Full HTML and CSS Code used for the visualizer


const htmlTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JWT Inspector Dashboard</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
            animation: backgroundShift 10s ease-in-out infinite alternate;
        }
        
        @keyframes backgroundShift {
            0% { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
            50% { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
            100% { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background: rgba(255, 255, 255, 0.1);
            backdrop-filter: blur(20px);
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        
        .header {
            text-align: center;
            margin-bottom: 40px;
            animation: slideDown 0.8s ease-out;
        }
        
        @keyframes slideDown {
            from { transform: translateY(-50px); opacity: 0; }
            to { transform: translateY(0); opacity: 1; }
        }
        
        .title {
            font-size: 3rem;
            background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
            background-size: 400% 400%;
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
            animation: gradientShift 3s ease-in-out infinite;
            margin-bottom: 10px;
            font-weight: bold;
        }
        
        @keyframes gradientShift {
            0%, 100% { background-position: 0% 50%; }
            50% { background-position: 100% 50%; }
        }
        
        .subtitle {
            color: rgba(255, 255, 255, 0.8);
            font-size: 1.2rem;
            margin-bottom: 20px;
        }
        
        .status-badge {
            display: inline-block;
            padding: 10px 20px;
            border-radius: 25px;
            font-weight: bold;
            text-transform: uppercase;
            letter-spacing: 1px;
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.05); }
        }
        
        .valid {
            background: linear-gradient(45deg, #56ab2f, #a8e6cf);
            color: white;
            box-shadow: 0 4px 15px rgba(86, 171, 47, 0.4);
        }
        
        .invalid {
            background: linear-gradient(45deg, #ff416c, #ff4b2b);
            color: white;
            box-shadow: 0 4px 15px rgba(255, 65, 108, 0.4);
        }
        
        .dashboard {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 25px;
            margin-top: 30px;
        }
        
        .card {
            background: rgba(255, 255, 255, 0.15);
            backdrop-filter: blur(10px);
            border-radius: 15px;
            padding: 25px;
            border: 1px solid rgba(255, 255, 255, 0.2);
            transition: all 0.3s ease;
            animation: fadeInUp 0.6s ease-out;
        }
        
        .card:hover {
            transform: translateY(-5px);
            box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
        }
        
        @keyframes fadeInUp {
            from { transform: translateY(30px); opacity: 0; }
            to { transform: translateY(0); opacity: 1; }
        }
        
        .card-header {
            display: flex;
            align-items: center;
            margin-bottom: 20px;
        }
        
        .card-icon {
            font-size: 2rem;
            margin-right: 15px;
            animation: rotate 4s linear infinite;
        }
        
        @keyframes rotate {
            from { transform: rotate(0deg); }
            to { transform: rotate(360deg); }
        }
        
        .card-title {
            font-size: 1.3rem;
            font-weight: bold;
            color: white;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
        }
        
        .card-content {
            color: rgba(255, 255, 255, 0.9);
            line-height: 1.6;
        }
        
        .property {
            display: flex;
            justify-content: space-between;
            margin-bottom: 12px;
            padding: 8px 12px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 8px;
            transition: background 0.3s ease;
        }
        
        .property:hover {
            background: rgba(255, 255, 255, 0.2);
        }
        
        .property-key {
            font-weight: bold;
            color: #ffd700;
        }
        
        .property-value {
            color: #e0e0e0;
            text-align: right;
            word-break: break-all;
        }
        
        .expiration-info {
            text-align: center;
            padding: 20px;
            border-radius: 10px;
            margin-top: 15px;
        }
        
        .expires-soon {
            background: linear-gradient(45deg, #ff9a9e, #fecfef);
            color: #d63384;
        }
        
        .expires-good {
            background: linear-gradient(45deg, #a8edea, #fed6e3);
            color: #198754;
        }
        
        .expired {
            background: linear-gradient(45deg, #ff6b6b, #ee5a24);
            color: white;
        }
        
        .footer {
            text-align: center;
            margin-top: 40px;
            padding: 20px;
            border-top: 1px solid rgba(255, 255, 255, 0.2);
            color: rgba(255, 255, 255, 0.7);
        }
        
        .timestamp {
            font-size: 0.9rem;
            font-style: italic;
        }
        
        .floating-elements {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: -1;
        }
        
        .floating-element {
            position: absolute;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 50%;
            animation: float 6s ease-in-out infinite;
        }
        
        @keyframes float {
            0%, 100% { transform: translateY(0px) rotate(0deg); }
            50% { transform: translateY(-20px) rotate(180deg); }
        }
        
        .json-viewer {
            background: rgba(0, 0, 0, 0.3);
            border-radius: 8px;
            padding: 15px;
            margin-top: 15px;
            font-family: 'Courier New', monospace;
            font-size: 0.9rem;
            overflow-x: auto;
            border-left: 4px solid #4ecdc4;
        }
        
        .json-key {
            color: #ffd700;
        }
        
        .json-string {
            color: #98fb98;
        }
        
        .json-number {
            color: #87ceeb;
        }
        
        .json-boolean {
            color: #dda0dd;
        }
    </style>
</head>
<body>
    <div class="floating-elements">
        <div class="floating-element" style="top: 10%; left: 10%; width: 60px; height: 60px; animation-delay: 0s;"></div>
        <div class="floating-element" style="top: 70%; left: 80%; width: 80px; height: 80px; animation-delay: 2s;"></div>
        <div class="floating-element" style="top: 30%; left: 70%; width: 40px; height: 40px; animation-delay: 4s;"></div>
        <div class="floating-element" style="top: 80%; left: 20%; width: 100px; height: 100px; animation-delay: 1s;"></div>
    </div>
    
    <div class="container">
        <div class="header">
            <h1 class="title">🔐 JWT Inspector</h1>
            <p class="subtitle">Advanced Token Analysis Dashboard</p>
            <div class="status-badge ${visualizationData.validation.valid ? 'valid' : 'invalid'}">
                ${visualizationData.validation.valid ? '✅ Valid Token' : '❌ Invalid Token'}
            </div>
        </div>
        
        <div class="dashboard">
            <!-- Token Header Card -->
            <div class="card">
                <div class="card-header">
                    <div class="card-icon">🎯</div>
                    <div class="card-title">Token Header</div>
                </div>
                <div class="card-content">
                    ${Object.entries(visualizationData.header).map(([key, value]) => `
                        <div class="property">
                            <span class="property-key">${key}:</span>
                            <span class="property-value">${value}</span>
                        </div>
                    `).join('')}
                </div>
            </div>
            
            <!-- Token Payload Card -->
            <div class="card">
                <div class="card-header">
                    <div class="card-icon">📦</div>
                    <div class="card-title">Token Payload</div>
                </div>
                <div class="card-content">
                    ${Object.entries(visualizationData.payload).map(([key, value]) => `
                        <div class="property">
                            <span class="property-key">${key}:</span>
                            <span class="property-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
                        </div>
                    `).join('')}
                </div>
            </div>
            
            <!-- Validation Status Card -->
            <div class="card">
                <div class="card-header">
                    <div class="card-icon">${visualizationData.validation.valid ? '✅' : '❌'}</div>
                    <div class="card-title">Validation Status</div>
                </div>
                <div class="card-content">
                    <div class="property">
                        <span class="property-key">Schema Type:</span>
                        <span class="property-value">${visualizationData.schemaType}</span>
                    </div>
                    <div class="property">
                        <span class="property-key">Status:</span>
                        <span class="property-value">${visualizationData.validation.valid ? 'PASSED' : 'FAILED'}</span>
                    </div>
                    ${!visualizationData.validation.valid && visualizationData.validation.error ? `
                        <div class="json-viewer">
                            <strong>Validation Errors:</strong><br>
                            ${visualizationData.validation.error.map(err => `• ${err.message}`).join('<br>')}
                        </div>
                    ` : ''}
                </div>
            </div>
            
            <!-- Expiration Info Card -->
            ${visualizationData.expInfo ? `
            <div class="card">
                <div class="card-header">
                    <div class="card-icon">⏰</div>
                    <div class="card-title">Expiration Info</div>
                </div>
                <div class="card-content">
                    <div class="property">
                        <span class="property-key">Expires At:</span>
                        <span class="property-value">${visualizationData.expInfo.expTime}</span>
                    </div>
                    <div class="property">
                        <span class="property-key">Time Left:</span>
                        <span class="property-value">${visualizationData.expInfo.timeLeft}</span>
                    </div>
                    <div class="expiration-info ${visualizationData.expInfo.expired ? 'expired' : visualizationData.expInfo.minutesLeft <= 60 ? 'expires-soon' : 'expires-good'}">
                        ${visualizationData.expInfo.expired ? 
                            '🚨 TOKEN EXPIRED!' : 
                            visualizationData.expInfo.minutesLeft <= 60 ? 
                                '⚠️ Expires Soon!' : 
                                '✅ Token Valid'
                        }
                    </div>
                </div>
            </div>
            ` : ''}
            
            <!-- Raw JSON Card -->
            <div class="card" style="grid-column: 1 / -1;">
                <div class="card-header">
                    <div class="card-icon">📄</div>
                    <div class="card-title">Raw JSON Data</div>
                </div>
                <div class="card-content">
                    <div class="json-viewer">
                        <pre>${JSON.stringify(visualizationData, null, 2)}</pre>
                    </div>
                </div>
            </div>
        </div>
        
        <div class="footer">
            <div class="timestamp">
                🕐 Generated: ${visualizationData.timestamp}
            </div>
            <div style="margin-top: 10px;">
                <strong>JWT Inspector Toolkit</strong> - Postman Test Script Visualizer
            </div>
        </div>
    </div>
    
    <script>
        // Add some interactive effects
        document.addEventListener('DOMContentLoaded', function() {
            // Animate cards on scroll
            const cards = document.querySelectorAll('.card');
            cards.forEach((card, index) => {
                card.style.animationDelay = (index * 0.1) + 's';
            });
            
            // Add click effects to properties
            const properties = document.querySelectorAll('.property');
            properties.forEach(property => {
                property.addEventListener('click', function() {
                    this.style.transform = 'scale(1.02)';
                    setTimeout(() => {
                        this.style.transform = 'scale(1)';
                    }, 200);
                });
            });
            
            // Status badge animation
            const statusBadge = document.querySelector('.status-badge');
            if (statusBadge) {
                setInterval(() => {
                    statusBadge.style.boxShadow = statusBadge.classList.contains('valid') ? 
                        '0 4px 20px rgba(86, 171, 47, 0.6)' : 
                        '0 4px 20px rgba(255, 65, 108, 0.6)';
                    setTimeout(() => {
                        statusBadge.style.boxShadow = statusBadge.classList.contains('valid') ? 
                            '0 4px 15px rgba(86, 171, 47, 0.4)' : 
                            '0 4px 15px rgba(255, 65, 108, 0.4)';
                    }, 1000);
                }, 2000);
            }
        });
    </script>
</body>
</html>
`;

pm.visualizer.set(htmlTemplate, visualizationData);


:light_bulb: What I Learned

  • I challenged myself to build something reusable and helpful not just for this challenge, but for other developers who regularly work with JWTs.
  • I learned how far you can push Postman’s testing environment using external packages like jsonwebtoken, joi, and moment, even with sandbox limitations.
  • Creating dynamic schema validation and clean expiration tracking helped me better understand how to design test scripts for real-world team workflows.
  • I learned how to create beautiful visualization in my test script, its amazing what Postman can do
  • Most importantly, I learned how small tools like this, when made modular and flexible can save time and add clarity across projects and teams.

Screenshots




1 Like

Hey Postman Community :postman: :waving_hand:

Although I don’t get a chance to be in the hat for the $250 prize, I can still create random scripts using External Packages…no one can stop me doing that!!

I wanted to bring the visualizer feature into the Challenge, like a few people have done already in their submissions - It’s such an awesome feature and pairing that with External Packages is :heart_eyes:


I’m just using the very trusty jsonplaceholder API to return some data, on this occasion, it’s a list of todos.

The structure of the todos data is very simple, it’s an array of objects. Each object has 4 data points so there isn’t a lot to play around with here.

[
    {
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
    }
]

Basically, I wanted to make it look a :rainbow: had thrown up on the screen… :joy: but also wanted to add a dashboard style feel, to track the todo progress by individual user - I’ve used a couple of packages that I haven’t used before like @iarna/toml and randomcolor but sprinkle in some lodash magic into the mix!

This is just a random little project but you can start to see the power that External Packages bring to an already established feature like the Visualizer. You can do so much with only a idea and a little bit of code! :trophy:

pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

// Dependencies 
const toml = pm.require('npm:@iarna/[email protected]'),
    randomColor = pm.require('npm:[email protected]'),
    lodash = pm.require('npm:[email protected]');

const todos = pm.response.json(),
    completed = todos.filter(t => t.completed),
    incomplete = todos.filter(t => !t.completed),
    total = todos.length;

function generateProgressBar(percent) {
    const full = Math.round(percent / 5);
    return '🟩'.repeat(full) + '⬜'.repeat(20 - full);
}

function styledTodoItem(todo) {
    const bg = randomColor({ luminosity: 'light' });
    return `
    <li style="padding: 8px; margin: 6px 0; background-color: ${bg}; border-radius: 5px;">
      ${todo.completed ? '✅' : '🔲'} <strong>#${todo.id}</strong>: ${lodash.startCase(todo.title)}
    </li>
  `;
}

const groupedByUser = lodash.groupBy(todos, 'userId');

const userDashboard = Object.entries(groupedByUser).map(([userId, userTodos]) => {
    const done = userTodos.filter(t => t.completed).length;
    const total = userTodos.length;
    const percent = (done / total) * 100;
    const color = randomColor({ luminosity: 'light', seed: userId });

    return `
        <div style="background: ${color}; padding: 12px; border-radius: 10px; margin-bottom: 10px;">
            <strong>User #${userId}</strong><br>
            <div style="font-size: 18px;">${generateProgressBar(percent)}</div>
            <div style="font-size: 12px;">${done}/${total} completed (${percent.toFixed(1)}%)</div>
        </div>
    `;
}).join('');

const template = `
   <div style="font-family: monospace; text-align: center; padding: 20px;">
    <h2>🚀 Todo Tracker</h2>
    
    <p style="font-size: 18px;">
      ✅ Completed: <strong>${completed.length}</strong> / ${total}<br>
      ❌ Incomplete: <strong>${incomplete.length}</strong> / ${total}
    </p>
    
    <div style="margin: 20px 0;">
      <div style="font-size: 16px; margin-bottom: 5px;">Overall Progress</div>
      <div style="font-size: 24px;">${generateProgressBar((completed.length / total) * 100)}</div>
      <div style="margin-top: 5px; font-size: 14px;">${((completed.length / total) * 100).toFixed(1)}% complete</div>
    </div>

    <hr style="margin: 30px 0;">
    
    <div style="text-align:left; max-height: 300px; overflow-y: auto; padding: 10px; background: #eef6ff; border-radius: 8px;">
      <h3>👥 User Progress Overview</h3>
      ${userDashboard}
    </div>

    <hr style="margin: 30px 0;">

    <div style="text-align:left; max-height: 700px; overflow-y: auto; padding: 10px; background: #f5f5f5; border-radius: 8px;">
      <strong>📝 Grouped Todo List:</strong>
      <ul style="list-style: none; padding-left: 0;">
        ${lodash.map(lodash.groupBy(todos, 'completed'), (tasks, status) => `
          <li style="margin-bottom: 10px;">
            <div style="font-weight: bold; margin: 10px 0;">${status === 'true' ? '✅ Completed' : '🔲 Incomplete'}</div>
            <ul style="padding-left: 10px; list-style: none;">
              ${tasks.map(styledTodoItem).join('')}
            </ul>
          </li>
        `).join('')}
      </ul>
    </div>
  </div>
`;

pm.visualizer.set(template, {});

Here is a slightly different version of rendering the same response data, not sure which I prefer :thinking:

const _ = pm.require("npm:lodash");
const randomColor = pm.require("npm:randomcolor");

const todos = pm.response.json();

function generateProgressBar(percent) {
  const full = Math.round(percent / 5);
  return '🟩'.repeat(full) + '⬜'.repeat(20 - full);
}

function formatTodo(todo) {
  return `${todo.completed ? '✅' : '🔲'} <strong>#${todo.id}</strong>: ${_.startCase(todo.title)}`;
}

const grouped = _.groupBy(todos, 'userId');

const userSections = Object.entries(grouped).map(([userId, tasks]) => {
  const completed = tasks.filter(t => t.completed).length;
  const total = tasks.length;
  const percent = (completed / total) * 100;
  const bg = randomColor({ luminosity: 'light', seed: userId });

  return `
    <div style="background: ${bg}; padding: 16px; border-radius: 12px; margin-bottom: 20px;">
      <h3>User #${userId}</h3>
      <div style="font-size: 16px; margin: 8px 0;">
        Progress: ${generateProgressBar(percent)}
        <br><small>${completed} of ${total} completed (${percent.toFixed(1)}%)</small>
      </div>
      <ul style="list-style: none; padding-left: 0; font-size: 14px; text-align: left;">
        ${tasks.map(task => `<li>${formatTodo(task)}</li>`).join('')}
      </ul>
    </div>
  `;
}).join('');

const template = `
  <div style="font-family: monospace; text-align: center; padding: 20px;">
    <h2>👥 Todo Progress by User</h2>
    ${userSections}
  </div>
`;

pm.visualizer.set(template, {});

Please keep the submissions coming, it’s be so cool to see them getting posted and I’m hoping that there will be many more of the next few days!!

3 Likes

A Smart Phishing Domain Detector Using @nlpjs/similarity

// Pre-request Script
pm.environment.set('knownDomains', JSON.stringify(['amazon.com', 'facebook.com', 'google.com']));
pm.environment.set('inputDomains', JSON.stringify([
    'faceb00k-login.net', 
    'amaz0n.com', 
    'goog1e.com', 
]));

// Test Script 
const { similarity } = pm.require('npm:@nlpjs/similarity');

const knownDomains = JSON.parse(pm.environment.get('knownDomains'));
const inputDomains = JSON.parse(pm.environment.get('inputDomains'));

const threshold = 0.7; // similarity threshold
const flaggedDomains = [];

inputDomains.forEach(domain => {
    let bestMatch = { target: null, score: 0 };

    knownDomains.forEach(known => {
        // Use normalized similarity for better matching (case, accents, etc)
        const score = similarity(domain, known, true);
        if (score > bestMatch.score) {
            bestMatch = { target: known, score };
        }
    });

    if (bestMatch.score > threshold && domain !== bestMatch.target) {
        console.warn(`🚨 Possible spoof detected: "${domain}" mimics "${bestMatch.target}" (score: ${bestMatch.score.toFixed(2)})`);
        flaggedDomains.push({
            domain,
            mimics: bestMatch.target,
            score: bestMatch.score.toFixed(2)
        });
    }
});

pm.test("Detect possible phishing domains", () => {
    pm.expect(flaggedDomains.length).to.be.above(0);
    if (flaggedDomains.length) {
        console.log("Flagged domains:", flaggedDomains);
    } else {
        console.log("No suspicious domains detected.");
    }
});


1 Like