Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion controllers/domeBlog.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const { indexes } = require('../lib/indexes');
const utils = require('../lib/utils');
const config = require('../config');

const BLOG_EDITABLE_FIELDS = ['title', 'slug', 'featuredImage', 'metaDescription', 'excerpt', 'content', 'partyId', 'author', 'tags', 'date'];
const CONTENT_TYPES = ['blog', 'news', 'faq'];
const BLOG_EDITABLE_FIELDS = ['title', 'slug', 'featuredImage', 'metaDescription', 'excerpt', 'content', 'partyId', 'author', 'tags', 'date', 'contentType'];

const domeBlog = (function () {
const hasOwnProperty = function (obj, key) {
Expand Down Expand Up @@ -101,6 +102,20 @@ const domeBlog = (function () {
return parsedDate.toISOString();
};

const parseContentType = function (contentType) {
if (contentType === undefined) {
return undefined;
}

if (typeof contentType !== 'string' || !CONTENT_TYPES.includes(contentType)) {
const validationError = new Error(`contentType must be one of: ${CONTENT_TYPES.join(', ')}`);
validationError.statusCode = 400;
throw validationError;
}

return contentType;
};

const buildSlugBase = function (title) {
const normalizedTitle = typeof title === 'string' ? title.toLowerCase() : '';
const slugBase = normalizedTitle
Expand Down Expand Up @@ -203,6 +218,26 @@ const domeBlog = (function () {
return blog;
};

const ensureBlogContentType = function (blog) {
if (!blog) {
return blog;
}

if (!blog.contentType && blog.type) {
blog.contentType = blog.type;
}

if (!blog.contentType) {
blog.contentType = 'blog';
}

if (hasOwnProperty(blog, 'type')) {
delete blog.type;
}

return blog;
};

const create = async function (req, res) {
if (!utils.hasRole(req.user, config.roles.admin)) {
res.status(403).send('Only administrators can create entries');
Expand All @@ -223,6 +258,10 @@ const domeBlog = (function () {
mongoBlog.date = parseDate(mongoBlog.date);
}

if (hasOwnProperty(mongoBlog, 'contentType')) {
mongoBlog.contentType = parseContentType(mongoBlog.contentType);
}

if (!mongoBlog.slug) {
mongoBlog.slug = await generateUniqueSlug(mongoBlog.title);
} else if (!(await isSlugAvailable(mongoBlog.slug))) {
Expand All @@ -233,12 +272,17 @@ const domeBlog = (function () {
mongoBlog.date = new Date().toISOString();
}

if (!hasOwnProperty(mongoBlog, 'contentType')) {
mongoBlog.contentType = 'blog';
}

const blog = new Blog({
...mongoBlog
});

await blog.save();
ensureBlogTags(blog);
ensureBlogContentType(blog);

indexes.indexDocument('blog', uuidv4(), mongoBlog);

Expand All @@ -256,6 +300,7 @@ const domeBlog = (function () {
for (const blog of blogs) {
await ensureBlogSlug(blog);
ensureBlogTags(blog);
ensureBlogContentType(blog);
}

res.json(blogs);
Expand All @@ -276,6 +321,7 @@ const domeBlog = (function () {

await ensureBlogSlug(blog);
ensureBlogTags(blog);
ensureBlogContentType(blog);

res.json(blog);
} catch (err) {
Expand Down Expand Up @@ -322,6 +368,10 @@ const domeBlog = (function () {
updates.date = parseDate(updates.date);
}

if (hasOwnProperty(updates, 'contentType')) {
updates.contentType = parseContentType(updates.contentType);
}

if (!updates || Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No update fields provided' });
}
Expand Down Expand Up @@ -356,6 +406,7 @@ const domeBlog = (function () {
Object.assign(blog, updates);
const patchedBlog = await blog.save();
ensureBlogTags(patchedBlog);
ensureBlogContentType(patchedBlog);

res.json({ message: 'Blog entry patched successfully', patchedBlog });
} catch (err) {
Expand Down
20 changes: 20 additions & 0 deletions db/schemas/blogModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const mongoose = require('mongoose');

const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
const MAX_TAG_LENGTH = 50;
const CONTENT_TYPES = ['blog', 'news', 'faq'];

const normalizeOptionalString = function (value) {
if (typeof value !== 'string') {
Expand Down Expand Up @@ -37,6 +38,15 @@ const normalizeTags = function (value) {

const blogSchema = new mongoose.Schema({
title: { type: String, required: true, trim: true },
contentType: {
type: String,
required: true,
enum: {
values: CONTENT_TYPES,
message: 'contentType must be one of: blog, news, faq'
},
default: 'blog'
},
slug: {
type: String,
trim: true,
Expand Down Expand Up @@ -83,6 +93,16 @@ const blogSchema = new mongoose.Schema({
content: { type: String, required: true, trim: true }
});

blogSchema.pre('init', function (data) {
if (data && data.contentType === undefined && data.type !== undefined) {
data.contentType = data.type;
}

if (data && data.type !== undefined) {
delete data.type;
}
});

blogSchema.pre('validate', function (next) {
if (typeof this.slug === 'string' && this.slug.length > 0) {
this.slugNormalized = this.slug.toLowerCase();
Expand Down
1 change: 1 addition & 0 deletions portal/bae-frontend/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@
"legalTitle": "Legal",
"resourcesTitle": "Resources",
"documentation": "Documentation",
"news": "News & Events",
"support": "Support",
"faqs": "FAQs",
"follow-us": "Follow us",
Expand Down
1 change: 1 addition & 0 deletions portal/bae-frontend/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@
"legalTitle": "Legal",
"resourcesTitle": "Resources",
"documentation": "Documentation",
"news": "News & Events",
"support": "Support",
"faqs": "FAQs",
"follow-us": "Follow us",
Expand Down
4 changes: 2 additions & 2 deletions portal/bae-frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
document.documentElement.classList.remove('dark')
}
</script>
<style>*,:before,:after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(63 131 248 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}html{font-family:Blinker,system-ui,sans-serif}.min-h-screen{min-height:100vh}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}:root{--blockquote-border-color:#ccc;--blockquote-bg-color:#dde6f6;--blockquote-text-color:black}@media (prefers-color-scheme: dark){:root{--blockquote-border-color:#171717;--blockquote-bg-color:#0c1c38;--blockquote-text-color:#eee}}:root{--autofill-bg-light:#f0f0f0;--autofill-text-light:#000000}</style><link rel="stylesheet" href="styles.699e3f8aafc4dbf4.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.699e3f8aafc4dbf4.css"></noscript></head>
<style>*,:before,:after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(63 131 248 / .5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}html{font-family:Blinker,system-ui,sans-serif}.min-h-screen{min-height:100vh}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}:root{--blockquote-border-color:#ccc;--blockquote-bg-color:#dde6f6;--blockquote-text-color:black}@media (prefers-color-scheme: dark){:root{--blockquote-border-color:#171717;--blockquote-bg-color:#0c1c38;--blockquote-text-color:#eee}}:root{--autofill-bg-light:#f0f0f0;--autofill-text-light:#000000}</style><link rel="stylesheet" href="styles.2bff7a030cf9697a.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.2bff7a030cf9697a.css"></noscript></head>
<body class="min-h-screen bg-white dark:bg-tertiary-50"> <!-- class="dark:bg-gray-900" style="background-image: url('assets/logos/dome-logo-element-colour.png');"-->
<app-root></app-root>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.8.1/flowbite.min.js"></script>-->
<script src="runtime.0992c3ee06235509.js" type="module"></script><script src="polyfills.c596680e6f68f5f2.js" type="module"></script><script src="main.3b4171ad02bc15e6.js" type="module"></script></body>
<script src="runtime.0992c3ee06235509.js" type="module"></script><script src="polyfills.c596680e6f68f5f2.js" type="module"></script><script src="main.a173bf4e6d368bba.js" type="module"></script></body>
</html>
1 change: 0 additions & 1 deletion portal/bae-frontend/main.3b4171ad02bc15e6.js

This file was deleted.

1 change: 1 addition & 0 deletions portal/bae-frontend/main.a173bf4e6d368bba.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions portal/bae-frontend/styles.2bff7a030cf9697a.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion portal/bae-frontend/styles.699e3f8aafc4dbf4.css

This file was deleted.

69 changes: 64 additions & 5 deletions test/controllers/domeBlog.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ describe('DomeBlog Controller', () => {
content: '# Heading',
partyId: 'party-1',
author: 'Admin',
tags: [' Data Space ', 'AI', '', ' ']
tags: [' Data Space ', 'AI', '', ' '],
contentType: 'news'
})
};
const res = makeResponse();
Expand All @@ -127,7 +128,8 @@ describe('DomeBlog Controller', () => {
content: '# Heading',
partyId: 'party-1',
author: 'Admin',
tags: ['Data Space', 'AI']
tags: ['Data Space', 'AI'],
contentType: 'news'
}));
expect(indexesMock.indexDocument).toHaveBeenCalledWith(
'blog',
Expand All @@ -137,7 +139,8 @@ describe('DomeBlog Controller', () => {
featuredImage: 'https://example.com/post.png',
metaDescription: 'Small description',
excerpt: 'Small excerpt',
tags: ['Data Space', 'AI']
tags: ['Data Space', 'AI'],
contentType: 'news'
})
);
expect(res.status).toHaveBeenCalledWith(201);
Expand Down Expand Up @@ -182,7 +185,8 @@ describe('DomeBlog Controller', () => {
expect(BlogMock).toHaveBeenCalledWith(jasmine.objectContaining({
title: 'Legacy Post Title',
slug: 'legacy-post-title',
content: 'Body'
content: 'Body',
contentType: 'blog'
}));
expect(res.status).toHaveBeenCalledWith(201);
});
Expand Down Expand Up @@ -243,6 +247,26 @@ describe('DomeBlog Controller', () => {
});
});

it('should return 400 when contentType is invalid on create', async () => {
const req = {
user: { roles: [{ name: 'provider' }] },
body: JSON.stringify({
title: 'Invalid content type',
content: 'Body',
contentType: 'case-study'
})
};
const res = makeResponse();

await controller.create(req, res);

expect(BlogMock).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
error: 'contentType must be one of: blog, news, faq'
});
});

it('should return 403 when a non-admin tries to update a post', async () => {
utilsMock.hasRole.and.returnValue(false);

Expand Down Expand Up @@ -281,7 +305,8 @@ describe('DomeBlog Controller', () => {
featuredImage: 'https://example.com/new-image.jpg',
metaDescription: 'Updated description',
excerpt: 'Updated excerpt',
author: 'Updated Author'
author: 'Updated Author',
contentType: 'faq'
})
};
const res = makeResponse();
Expand All @@ -298,6 +323,7 @@ describe('DomeBlog Controller', () => {
expect(existingBlog.metaDescription).toBe('Updated description');
expect(existingBlog.excerpt).toBe('Updated excerpt');
expect(existingBlog.author).toBe('Updated Author');
expect(existingBlog.contentType).toBe('faq');
expect(existingBlog.save).toHaveBeenCalled();
expect(res.json).toHaveBeenCalledWith({
message: 'Blog entry patched successfully',
Expand Down Expand Up @@ -539,6 +565,36 @@ describe('DomeBlog Controller', () => {
}));
});

it('should return 400 when contentType is invalid on patch', async () => {
const existingBlog = {
_id: 'blog-id-1',
title: 'Legacy title',
slug: 'legacy-title',
contentType: 'blog',
content: '# Existing',
save: jasmine.createSpy('save').and.callFake(async function() { return this; })
};
BlogMock.findById.and.returnValue(Promise.resolve(existingBlog));

const req = {
user: { roles: [{ name: 'provider' }] },
params: { id: 'blog-id-1' },
body: JSON.stringify({
contentType: 'guide'
})
};
const res = makeResponse();

await controller.updateById(req, res);

expect(BlogMock.findById).not.toHaveBeenCalled();
expect(existingBlog.save).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
error: 'contentType must be one of: blog, news, faq'
});
});

it('should auto-generate and persist slug for legacy blogs in list response', async () => {
const blogs = [{
_id: 'blog-id-1',
Expand All @@ -557,6 +613,7 @@ describe('DomeBlog Controller', () => {
expect(blogs[0].save).toHaveBeenCalled();
const responsePayload = res.json.calls.mostRecent().args[0];
expect(responsePayload[0].tags).toEqual([]);
expect(responsePayload[0].contentType).toBe('blog');
});

it('should return 409 when slug already exists (case-insensitive)', async () => {
Expand Down Expand Up @@ -590,6 +647,7 @@ describe('DomeBlog Controller', () => {
featuredImage: 'https://example.com/image.png',
metaDescription: 'SEO description',
excerpt: 'Summary',
contentType: 'faq',
content: '# Content',
save: jasmine.createSpy('save')
}];
Expand All @@ -606,6 +664,7 @@ describe('DomeBlog Controller', () => {
expect(responsePayload[0].metaDescription).toBe('SEO description');
expect(responsePayload[0].excerpt).toBe('Summary');
expect(responsePayload[0].tags).toEqual([]);
expect(responsePayload[0].contentType).toBe('faq');
});

it('should return 500 when listing blog entries fails', async () => {
Expand Down
34 changes: 34 additions & 0 deletions test/db/schemas/blogModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('Blog Model', () => {
expect(blog.author).toBe('Author name');
expect(blog.partyId).toBe('party-123');
expect(blog.tags).toEqual([]);
expect(blog.contentType).toBe('blog');
});

it('should trim tags and remove empty values', () => {
Expand Down Expand Up @@ -119,4 +120,37 @@ describe('Blog Model', () => {
expect(validationError.errors.metaDescription).toBeDefined();
expect(validationError.errors.excerpt).toBeDefined();
});

it('should validate contentType values', () => {
const blog = new Blog({
...basePayload,
contentType: 'news'
});

const validationError = blog.validateSync();

expect(validationError).toBeUndefined();
expect(blog.contentType).toBe('news');
});

it('should reject invalid contentType values', () => {
const blog = new Blog({
...basePayload,
contentType: 'case-study'
});

const validationError = blog.validateSync();

expect(validationError).toBeDefined();
expect(validationError.errors.contentType).toBeDefined();
});

it('should hydrate legacy type as contentType', () => {
const blog = Blog.hydrate({
...basePayload,
type: 'faq'
});

expect(blog.contentType).toBe('faq');
});
});
Loading